2015-11-28

安卓注入框架Xposed用法详解

    之前安卓注入框架Xposed分析与简单应用只是简单的了解了一下xposed框架,知道如何hook函数,并没有深入去使用,也不知道这个框架能用到哪种程度,最近详细的总结了下,简单来说就是,没有hook不到的地方,只有你想不到的地方。

接管系统所有广播包

    在Android四大组件中,Broadcast是一种广泛运用的在应用程序之间传输信息的机制。而BroadcastReceiver 是对发送出来的Broadcast进行过滤接受并响应的一类组件。应用通过BroadcastReceiver对一个外部的事件做出响应,这是非常有意思的,当开机,锁屏等外部事件到来的时候,可以利用BroadcastReceiver进行相应的处理。如果你能接管所有的广播包,基本上就接管了整个系统的信息传输过程。广播包也是频繁唤醒手机的一个重要原因,通过管理这些广播包,可以达到省电效果。

通过阅读源代码中 IntentFirewall这个类,126行代码开始有一些以check开头的方法,说明如下:

This is called from ActivityManager to check if a start activity intent should be allowed. It is assumed the caller is already holding the global ActivityManagerService lock.

说明很清晰的告诉我们,所有activity启动时,会到这里做相应的检测是否被允许,因此我们hook掉checkBroadcast方法,就可以控制所有的广播包走向。checkBroadcast代码如下:


public boolean checkBroadcast(Intent intent, int callerUid, int callerPid,
    String resolvedType, int receivingUid) {
    return checkIntent(mBroadcastResolver, intent.getComponent(), TYPE_BROADCAST, intent, callerUid, callerPid, resolvedType, receivingUid);
}

这里,我通过hook该方法并获取到第一个参数Intent intent,然后就可以获取到广播类型,同时也可以获取到该广播的发送者(callerUid)和接受者(receivingUid),hook代码如下:

hook_method("com.android.server.firewall.IntentFirewall",
                lpparam.classLoader,
                "checkBroadcast",
                Intent.class,   // intent
                int.class,  // callerUid
                int.class,  // callerPid
                String.class,   // resolvedType
                int.class,  // receivingUid
                new XC_MethodHook() {

            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                int callerUid = (int) param.args[1];
                int receivingUid = (int) param.args[4];
                XposedBridge.log("hook IntentFirewall.checkBroadcast : " + "broadcast from " + callerUid + " to " + receivingUid);

                Intent intent = (Intent) param.args[0];
                String action = intent.getAction();

                if (action == null)
                    return;
                if (action.equals("android.intent.action.SCREEN_OFF"))
                    XposedBridge.log("hook IntentFirewall.checkBroadcast : " + "screen off");
                if (action.equals("android.intent.action.SCREEN_ON"))
                    XposedBridge.log("hook IntentFirewall.checkBroadcast : " + "screen on");
            }
        });
        

打印锁屏和亮屏的广播事件,结果如下所示: 过滤日志adb logcat | grep checkBroadcast

checkBroadcast

嵌套hook监控前台应用

    在编写代码时候,要实现实时监控前台应用是相当棘手的一件事情,并且在5.11以后的版本获取所有运行app都已经受到限制,stackoverflow中给出了一种方法,Get Running Apps on M with no permissions & get the foreground app on Android 5.1.1+,测试了获取前台应用的代码相当耗性能。这里基于xposed框架给出另外一种方法。

同样通过阅读源代码中的NetworkPolicyManagerService这个类,找到回调函数onForegroundActivitiesChanged


private IProcessObserver mProcessObserver = new IProcessObserver.Stub() {

 @Override
   public void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {
   }

   @Override
   public void onProcessStateChanged(int pid, int uid, int procState) {
  synchronized (mRulesLock) {
   
  }
 }
}

ActivityManager服务中IProcessObserver有个回调函数onForegroundActivitiesChanged。而动态设置网络连接规则的时候,NetworkPolicyManagerService服务通过检测系统发出的一些相关事件(在NetworkPolicyManagerService的启动systemReady函数中注册),其中会调用ActivityManager服务中IProcessObserver的onForegroundActivitiesChanged及onProcessDied回调事件,因此,我们通过hook服务类NetworkPolicyManagerService中的onForegroundActivitiesChanged回调函数来监控前台应用,但是这个过程必须保证在systemReady函数已启动注册了该服务,因此需要嵌套hook。具体过程如下:

  1. hook systemReady函数
  2. 通过param.thisObject获取hook方法所在类的实例, 即NetworkPolicyManagerService.class
  3. 通过getObjectField获取类中的对象mProcessObserver
  4. hook对象mProcessObserver中的onForegroundActivitiesChanged方法

完整代码如下所示:

hook_method("com.android.server.net.NetworkPolicyManagerService",
                lpparam.classLoader,
                "systemReady",
                new XC_MethodHook() {

            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                XposedBridge.log("hook NetworkPolicyManagerService.systemReady");
                XposedBridge.log("hook NetworkPolicyManagerService.systemReady : " + param.thisObject.getClass());

                Object mProcessObserverClass = XposedHelpers.getObjectField(param.thisObject, "mProcessObserver");
                XposedBridge.log("hook NetworkPolicyManagerService.systemReady : " + mProcessObserverClass.getClass());

                hook_method(mProcessObserverClass.getClass(),
                        "onForegroundActivitiesChanged",
                        int.class,  // pid
                        int.class,  // uid
                        boolean.class, // foregroundActivities
                        new XC_MethodHook() {
                    @Override
                    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                        if ((boolean) param.args[2])
                            XposedBridge.log("hook NetworkPolicyManagerService.onForegroundActivitiesChanged : foreground uid = " + param.args[1]);
                    }
                });
            }
        });

打印前台应用,结果如下所示: 过滤日志adb logcat | grep onForegroundActivitiesChanged

onForegroundActivitiesChanged

xposed进程读取文件

    有时候需要xposed进程根据我们自身进程的设置来执行不同的逻辑功能,因此需要xposed进程读取我们进程的配置文件。xposed框架中提供了读取data/data/package name/shared_prefs目录下的xml配置文件功能类XSharedPreferences,该类继承了系统类SharedPreferences并提供类额外的reload方法,当我们配置文件更新后,可以调用该方法重新加载。

    需要注意的是,该类只支持读操作,如果你尝试去执行写操作会抛异常。至于为什么作者没有添加写权限,作者也给出了详细的解释,详见:Cannot Write into a Shared Preferences File

    我在使用的过程中,需要读取files目录下的json文件内容,使用时发现该类只能读取xml文件,作为jar导入包又不适合修改,因此仿照XSharedPreferences写了一个可以读取任何文件内容的XFile类,其实可以逐渐完善读更多文件类型的内容,包括数据库等。


package com.example.idhyt.xposedExtend;

import android.os.Environment;
import android.util.Log;


import org.json.JSONException;
import org.json.JSONObject;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

import de.robv.android.xposed.SELinuxHelper;
import de.robv.android.xposed.services.FileResult;


/**
 *
 * Created by idhyt on 2015/12/10.
 *
 * This class is used to read file from data/data/xxx/files directory,
 * same as XSharedPreferences, read-only and without listeners support.
 */


public class XFiles {
    private static final String TAG = "XFiles";
    private final File mFile;
    private final String mFilename;
    private ByteArrayOutputStream mFileOutputStream;
    private boolean mLoaded = false;
    private long mLastModified;
    private long mFileSize;

    /**
     * Read settings from the specified file.
     * @param file The file to read.
     */
    public XFiles(File file) {
        mFile = file;
        mFilename = mFile.getAbsolutePath();
        startLoadFromDisk();
    }

    /**
     *
     * @param packageName The package name.
     */
    public XFiles(String packageName) {
        this(packageName, packageName + "_files");
    }

    /**
     *
     * @param packageName The package name.
     * @param fileName The file name with suffix (.txt, .json, etc..)
     */
    public XFiles(String packageName, String fileName) {
        mFile = new File(Environment.getDataDirectory(), "data/" + packageName + "/files/" + fileName);
        mFilename = mFile.getAbsolutePath();
        startLoadFromDisk();
    }

    /**
     * Tries to make the files file world-readable.
     *
     * This will only work if executed as root (e.g. {@code initZygote()}) and only if SELinux is disabled.
     *
     * @return {@code true} in case the file could be made world-readable.
     */
    public boolean makeWorldReadable() {
        if (!SELinuxHelper.getAppDataFileService().hasDirectFileAccess())
            return false; // It doesn't make much sense to make the file readable if we wouldn't be able to access it anyway.

        if (!mFile.exists()) // Just in case - the file should never be created if it doesn't exist.
            return false;

        return mFile.setReadable(true, false);
    }

    /**
     * Returns the file that is backing these preferences.
     *
     * <p><strong>Warning:</strong> The file might not be accessible directly.
     */
    public File getFile() {
        return mFile;
    }

    private void startLoadFromDisk() {
        synchronized (this) {
            mLoaded = false;
        }
        new Thread("XFiles-load") {
            @Override
            public void run() {
                synchronized (XFiles.this) {
                    loadFromDiskLocked();
                }
            }
        }.start();
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    private void loadFromDiskLocked() {
        if (mLoaded) {
            return;
        }

        ByteArrayOutputStream fileOutputStream = null;
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        FileResult fileResult = null;

        try {
            fileResult = SELinuxHelper.getAppDataFileService().getFileInputStream(mFilename, mFileSize, mLastModified);
            InputStream inputStream = fileResult.stream;
            if (inputStream != null) {
                int length = inputStream.available();
                byte [] buffer = new byte[length];

                int readLength;
                while ((readLength = inputStream.read(buffer)) != -1) {
                    byteArrayOutputStream.write(buffer, 0, readLength);
                }
                fileOutputStream = byteArrayOutputStream;
            }

        } catch (FileNotFoundException ignored) {
            // SharedPreferencesImpl has a canRead() check, so it doesn't log anything in case the file doesn't exist
        } catch (IOException e) {
            Log.w(TAG, "getSharedPreferences", e);
        } finally {
            if (fileResult != null && fileResult.stream != null) {
                try {
                    fileResult.stream.close();
                } catch (RuntimeException rethrown) {
                    throw rethrown;
                } catch (Exception ignored) {
                }
            }
        }

        mLoaded = true;
        if (fileOutputStream != null) {
            mFileOutputStream = fileOutputStream;
            mLastModified = fileResult.mtime;
            mFileSize = fileResult.size;
        } else {
            mFileOutputStream = new ByteArrayOutputStream();
        }
        notifyAll();
    }

    /**
     * Reload the settings from file if they have changed.
     *
     * <p><strong>Warning:</strong> With enforcing SELinux, this call might be quite expensive.
     *
     * @return true if execute reload;
     */
    public synchronized boolean reload() {
        if (hasFileChanged()) {
            startLoadFromDisk();
            return true;
        }
        return false;
    }

    /**
     * Check whether the file has changed since the last time it has been loaded.
     *
     * <p><strong>Warning:</strong> With enforcing SELinux, this call might be quite expensive.
     */
    public synchronized boolean hasFileChanged() {
        try {
            FileResult result = SELinuxHelper.getAppDataFileService().statFile(mFilename);
            return mLastModified != result.mtime || mFileSize != result.size;
        } catch (FileNotFoundException ignored) {
            // SharedPreferencesImpl doesn't log anything in case the file doesn't exist
            return true;
        } catch (IOException e) {
            Log.w(TAG, "hasFileChanged", e);
            return true;
        }
    }

    private void awaitLoadedLocked() {
        while (!mLoaded) {
            try {
                wait();
            } catch (InterruptedException unused) {
            }
        }
    }

    public String getFileContent() {
        synchronized (this) {
            awaitLoadedLocked();
            return mFileOutputStream.toString();
        }
    }

    public JSONObject getJsonFileContent() throws JSONException {
        return new JSONObject(getFileContent());
    }
}

判断xposed框架是否生效

    正如上边所说,xposed框架中对文件只有读权限,因此获取信息就变成了单项了,xposed进程只能根据我们的配置文件进行相应的逻辑操作。由于应用进程和xposed进程是不同的进程,如果应用进程要判断xposed框架是否启用,就需要进程间数据通信,如果仅仅只是判断xposed是否启动而去启动一个服务处理数据,感觉有点小题大做了。这里给出一种简单有效的方法。

1.首先我们生成一个配置文件setting.xml 2.定义一个函数


public void setXposedStatus(boolean bStatus) {
    this.getSharedPreferences("setting", Context.MODE_WORLD_READABLE).edit().putBoolean("xposed_enabled", bStatus).apply();
}
    

3.应用每次启动时候调用函数setXposedStatus 4.在xposed中hook函数setXposedStatus调用前,并将参数改为true

这样我们要想知道xposed框架是否生效可用,读取setting.xmlxposed_enabled字段值即可。

总结

    xposed框架不仅能够hook任意你想hook的函数,并且自身也有一个很大的特点,就是一旦用户安装并启用,你所有的操作都不需要任何权限就可实现。基于xposed框架的优秀应用也特别多,比如著名的省电应用绿色守护。再如有一些app对敏感数据进行加密,加密算法又很复杂,那么通过hook解密函数就可以轻松的拿到明文。xposed框架的奇妙之处远远不仅于此,国外也有人对其进行二次封装作出了更神奇的用法,例如XClasses,如果你有什么奇淫技巧,我只想对你说四个字,请带上我!

2015-09-25

安卓注入框架Xposed分析与简单应用

概述

Xposed是针对Android平台的动态劫持项目,源码github

主要模块说明如下:

Xposed:Xposed的C++部分,主要是用来替换/system/bin/app_process,并为XposedBridge提供JNI方法。

XposedInstaller:Xposed的安装包,负责配置Xposed工作的环境并且提供对基于Xposed框架的Modules的管理。在安装XposedInstaller之后,app_process与XposedBridge.jar放置在了/data/data/de.robv.android.xposed.installer。

XposedBridge.jar:XposedBridge.jar是Xposed提供的jar文件,负责在Native层与FrameWork层进行交互。/system/bin/app_process进程启动过程中会加载该jar包,其它的Modules的开发与运行都是基于该jar包的。注意:XposedBridge.jar文件本质上是由XposedBridge生成的APK文件更名而来,查看该工程目录下的install.bat

    cd /d %~dp0
    dir bin\XposedBridge.apk
    adb push bin\XposedBridge.apk /data/data/de.robv.android.xposed.installer/bin/XposedBridge.jar.newversion
    pause

XposedMods:使用Xposed开发的一些Modules,其中AppSettings是一个可以进行权限动态管理的应用

Xposed框架的基本运行环境如下:

Configuration RequireMent
Root Access 因为Xposed工作原理是在/system/bin目录下替换文件,在install的时候需要root权限,但是运行时不需要root权限
版本要求 需要在Android 4.0以上版本的机器中

源码分析

XposedInstaller

通过/XposedInstaller/AndroidManifest.xml看出主activity为WelcomeActivity, 主要实现了界面加载,添加功能图标和描述信息Item,如Install, Modules, Download等。 选择Framework后进入界面 android-xposed-1

我们直接找到install代码,install代码定位的简单方法: 先点击install进入安装界面,然后在输入adb命令查看当前最顶层activity,就可以定位到代码。

adb 查看最上层成activity名字 linux: adb shell dumpsys activity | grep "mFocusedActivity" windows: adb shell dumpsys activity | findstr "mFocusedActivity"

最后定位到install代码位于InstallerFragment中,代码及注释如下:

// 该代码的功能就是替换了app_process文件
private boolean install() {
    // 获取安装的方式,直接写入 or 使用 recovery进行安装
    final int installMode = getInstallMode();

    // 检查获取Root权限
    if (!startShell())
        return false;

    List<String> messages = new LinkedList<String>();
    boolean showAlert = true;
    try {
        messages.add(getString(R.string.sdcard_location, XposedApp.getInstance().getExternalFilesDir(null)));
        messages.add("");

        messages.add(getString(R.string.file_copying, "Xposed-Disabler-Recovery.zip"));

        // 把Xposed-Disabler-Recovery.zip文件 从asset copy到sdcard中
        if (AssetUtil.writeAssetToSdcardFile("Xposed-Disabler-Recovery.zip", 00644) == null) {
            messages.add("");
            messages.add(getString(R.string.file_extract_failed, "Xposed-Disabler-Recovery.zip"));
            return false;
        }

        // 将编译后的app_process二进制文件,从asset文件夹中,copy到/data/data/de.robv.android.xposed.installer/bin/app_process
        File appProcessFile = AssetUtil.writeAssetToFile(APP_PROCESS_NAME, new File(XposedApp.BASE_DIR + "bin/app_process"), 00700);
        if (appProcessFile == null) {
            showAlert(getString(R.string.file_extract_failed, "app_process"));
            return false;
        }

        if (installMode == INSTALL_MODE_NORMAL) {
            // 普通安装模式
            // 重新挂载/system为rw模式
            // Normal installation
            messages.add(getString(R.string.file_mounting_writable, "/system"));
            if (mRootUtil.executeWithBusybox("mount -o remount,rw /system", messages) != 0) {
                messages.add(getString(R.string.file_mount_writable_failed, "/system"));
                messages.add(getString(R.string.file_trying_to_continue));
            }

            // 查看原有的app_process文件是否已经备份,如果没有备份,现将原有的app_process文件备份一下
            if (new File("/system/bin/app_process.orig").exists()) {
                messages.add(getString(R.string.file_backup_already_exists, "/system/bin/app_process.orig"));
            } else {
                if (mRootUtil.executeWithBusybox("cp -a /system/bin/app_process /system/bin/app_process.orig", messages) != 0) {
                    messages.add("");
                    messages.add(getString(R.string.file_backup_failed, "/system/bin/app_process"));
                    return false;
                } else {
                    messages.add(getString(R.string.file_backup_successful, "/system/bin/app_process.orig"));
                }

                mRootUtil.executeWithBusybox("sync", messages);
            }

            // 将项目中的自定义app_process文件copy覆盖系统的app_process,修改权限
            messages.add(getString(R.string.file_copying, "app_process"));
            if (mRootUtil.executeWithBusybox("cp -a " + appProcessFile.getAbsolutePath() + " /system/bin/app_process", messages) != 0) {
                messages.add("");
                messages.add(getString(R.string.file_copy_failed, "app_process", "/system/bin"));
                return false;
            }
            if (mRootUtil.executeWithBusybox("chmod 755 /system/bin/app_process", messages) != 0) {
                messages.add("");
                messages.add(getString(R.string.file_set_perms_failed, "/system/bin/app_process"));
                return false;
            }
            if (mRootUtil.executeWithBusybox("chown root:shell /system/bin/app_process", messages) != 0) {
                messages.add("");
                messages.add(getString(R.string.file_set_owner_failed, "/system/bin/app_process"));
                return false;
            }

        } else if (installMode == INSTALL_MODE_RECOVERY_AUTO) {
            // 自动进入Recovery
            if (!prepareAutoFlash(messages, "Xposed-Installer-Recovery.zip"))
                return false;

        } else if (installMode == INSTALL_MODE_RECOVERY_MANUAL) {
            // 手动进入Recovery
            if (!prepareManualFlash(messages, "Xposed-Installer-Recovery.zip"))
                return false;
        }

        File blocker = new File(XposedApp.BASE_DIR + "conf/disabled");
        if (blocker.exists()) {
            messages.add(getString(R.string.file_removing, blocker.getAbsolutePath()));
            if (mRootUtil.executeWithBusybox("rm " + blocker.getAbsolutePath(), messages) != 0) {
                messages.add("");
                messages.add(getString(R.string.file_remove_failed, blocker.getAbsolutePath()));
                return false;
            }
        }

        // copy XposedBridge.jar 到私有目录,XposedBridge.jar是Xposed提供的jar文件,负责在Native层与FrameWork层进行交互
        messages.add(getString(R.string.file_copying, "XposedBridge.jar"));
        File jarFile = AssetUtil.writeAssetToFile("XposedBridge.jar", new File(JAR_PATH_NEWVERSION), 00644);
        if (jarFile == null) {
            messages.add("");
            messages.add(getString(R.string.file_extract_failed, "XposedBridge.jar"));
            return false;
        }
       // 将有关文件系统的存储器常驻信息送入物理介质内,在暂停系统之前,比如要重新启动机器,一定要去执行sync命令
        mRootUtil.executeWithBusybox("sync", messages);

        showAlert = false;
        messages.add("");
        if (installMode == INSTALL_MODE_NORMAL)
            offerReboot(messages);
        else
            offerRebootToRecovery(messages, "Xposed-Installer-Recovery.zip", installMode);

        return true;

    } finally {
        // 删除busybox 工具库
        AssetUtil.removeBusybox();

        if (showAlert)
            showAlert(TextUtils.join("\n", messages).trim());
    }
}

为什么要替换app_process

在android的源码中的init.rc文件可以看到

service zygote /system/bin/app_process -Xzygote /system/bin –zygote –start-system-server
socket zygote stream 666 
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd

appprocess是andriod app的启动程序,该注入框架通过替换/system/bin/appprocess程序控制zygote进程,使得app_process在启动过程中会加载XposedBridge.jar这个jar包,从而完成对Zygote进程及其创建的Dalvik虚拟机的劫持。与采取传统的Inhook方式Dynamic Dalvik Instrumentation相比,Xposed在开机的时候完成对所有的Hook Function的劫持,在原Function执行的前后加上自定义代码。

为什么要控制zygote进程

在Android系统中,应用程序进程都是由Zygote进程孵化出来的,而Zygote进程是由Init进程启动的。Zygote进程在启动时会创建一个Dalvik虚拟机实例,每当它孵化一个新的应用程序进程时,都会将这个Dalvik虚拟机实例复制到新的应用程序进程里面去,从而使得每一个应用程序进程都有一个独立的Dalvik虚拟机实例。所以如果选择对Zygote进程Hook,则能够达到针对系统上所有的应用程序进程Hook,即一个全局Hook。这也是Xposed选择替换app_process的原因。如下图所示: hook-before-after

Zygote进程在启动的过程中,除了会创建一个Dalvik虚拟机实例之外,还会将Java运行时库加载到进程中来,以及注册一些Android核心类的JNI方法来前面创建的Dalvik虚拟机实例中去。注意,一个应用程序进程被Zygote进程孵化出来的时候,不仅会获得Zygote进程中的Dalvik虚拟机实例拷贝,还会与Zygote一起共享Java运行时库。这也就是可以将XposedBridge这个jar包加载到每一个Android应用程序中的原因。XposedBridge有一个私有的Native(JNI)方法hookMethodNative,这个方法也在app_process中使用。这个函数提供一个方法对象利用Java的Reflection机制来对内置方法覆写。

Hook/Replace

Xposed 框架中真正起作用的是对方法的hook。在Repackage技术中,如果要对APK做修改,则需要修改Smali代码中的指令。而另一种动态修改指令的技术需要在程序运行时基于匹配搜索来替换smali代码,但因为方法声明的多样性与复杂性,这种方法也比较复杂。

总结Hook运行过程如下:

  1. 在Android系统启动的时候,zygote进程加载XposedBridge将所有需要替换的Method通过JNI方法hookMethodNative指向Native方法xposedCallHandler,xposedCallHandler在转入handleHookedMethod这个Java方法执行用户规定的Hook Func。

  2. 在hookMethodNative的实现中,会调用XposedBridge中的handleHookedMethod这个方法来传递参数; XposedBridge这个jar包含有一个私有的本地方法:hookMethodNative,该方法在附加的app_process程序中也得到了实现。它将一个方法对象作为输入参数(你可以使用Java的反射机制来获取这个方法)并且改变Dalvik虚拟机中对于该方法的定义。它将该方法的类型改变为native并且将这个方法的实现链接到它的本地的通用类的方法。换言之,当调用那个被hook的方法时候,通用的类方法会被调用而不会对调用者有任何的影响。

  3. handleHookedMethod这个方法类似于一个统一调度的Dispatch例程,其对应的底层的C++函数是xposedCallHandler;

  4. handleHookedMethod实现里面会根据一个全局结构hookedMethodCallbacks来选择相应的hook函数,并调用他们的before,after函数;

  5. 当多模块同时Hook一个方法的时候,Xposed会自动根据Module的优先级来排序,调用顺序如下: A.before -> B.before -> original method -> B.after -> A.after

Xposed代码分析

详见Android Hook框架Xposed原理与源代码分析

Xposed插件编写

创建一个插件XposedExample,hook掉系统handleLoadPackage,在系统调用之前打印出加载app的package name,工程用eclipse编写,android studio略有不同。

1.创建一个Blank Activity的android项目

2.工程目录新建lib文件夹,添加文件XposedBridgeApi-54.jar,然后右键-Build path。

3.修改AndroidManifest.xml,插入xposed元数据。

<meta-data
            android:name="xposedmodule"
            android:value="true" />
        <meta-data
            android:name="xposeddescription"
            android:value="Xposed Example" />
        <meta-data
            android:name="xposedminversion"
            android:value="54" />

如果不想让插件在系统中显示图标,就把activity给注释掉。

4.完整的的AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.idhyt.xposedexample"
    android:versionCode="1"
    android:versionName="1.0" >
    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="21" />
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        
         <activity
            android:name=".XposedExample"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
      
        <meta-data
            android:name="xposedmodule"
            android:value="true" />
        <meta-data
            android:name="xposeddescription"
            android:value="Xposed Example" />
        <meta-data
            android:name="xposedminversion"
            android:value="54" /> //和XposedBridgeApi-54.jar版本对应
        
    </application>
</manifest>

5.编写插件代码

package com.idhyt.xposedexample;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
public class XposedExample implements IXposedHookLoadPackage {
    public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
        XposedBridge.log("Loaded app: " + lpparam.packageName);
        XposedBridge.log("this is a test by idhyt");
    }
}  

6.在assets文件夹(如果不存在就创建)下新建file写入插件启动入口: com.idhyt.xposedexample.XposedExample

整个工程目录如下所示: project-dir

7.运行之后 会在虚拟机中看到我们的插件已经安装到xposed模块里边,然后勾选重启系统。 install-ok

8.重启过后,加入过滤规则“loaded app”,在logcat窗口会看到我们的输出日志信息,你打开应用也会显示加载的应用包名。 logcat-out

xposed用法详解

移步:安卓注入框架Xposed用法详解

Android Root Zap Framework

‎ 1. Warning 请遵守GPL开源协议, 请遵守法律法规, 本项目仅供学习和交流, 请勿用于非法用途! 道路千万条, 安全第一条, 行车不规范, 亲人两行泪. 2. Android Root Zap Frame...