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用法详解