Xposed 学习之路

2018年4月18日 · 2916 字 · 6 分钟 · Android Xposed

Xposed 简介

Xposed 框架是一款可以在不修改 APK 的情况下影响程序运行(修改系统)的框架服务。

官网: http://repo.xposed.info/

Xposed 有以下几个部分组成:

  • XposedInstaller (Xposed 的插件管理和功能控制 APP)
  • Xposed (属于 Xposed 框架,其实是做了一套 xposed 版的 zygote,替换掉系统原生的 zygote。由 XposedInstaller 在 root 之后放到 /system/bin 下。)
  • XposedBridge (Xposed 框架的 Java 部分,编译出来是一个 XposedBridge.jar)
  • XposedTools (帮助编译 Xposed 和 XposedBridge,而且还有一些定制化的东西)

所以 Xposed 的原理就是通过替换 /system/bin/app_process 程序控制 zygote 进程,使得 app_process 在启动过程中会加载 XposedBridge.jar 这个 jar 包,从而完成对 Zygote 进程及其创建的 Dalvik 虚拟机的劫持。

安装环境

使用 Xposed 插件的前提需要 root 手机并刷入 XposedInstaller。

具体步骤请参考 Android 刷机

如果你不想或者不能刷入 Xposed,可以使用 VirtualXposed,VirtualXposed 是基于 VirtualApp 和 epic 在非 ROOT 环境下运行 Xposed 模块的实现(支持 5.0~8.1)。

开发流程

1. Gradle 配置

provided 'de.robv.android.xposed:api:82'
//如果需要引入文档,方便查看的话
provided 'de.robv.android.xposed:api:82:sources'

2. AndroidManifest 配置

<?xml version="1.0" encoding="utf-8"?>
<manifest xxx

    <application xxx>

        <!-- 1、标识自己是否为一个Xposed模块 -->
        <meta-data
            android:name="xposedmodule"
            android:value="true"/>

        <!-- 2、Xposed模块的描述信息 -->
        <meta-data
            android:name="xposeddescription"
            android:value="a sample for xposed"/>

        <!-- 3、支持Xposed框架的最低版本 -->
        <meta-data
            android:name="xposedminversion"
            android:value="53"/>

    </application>

</manifest>

3. 编写 Module

public class RedClock implements IXposedHookLoadPackage {
    public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable {
        if (!lpparam.packageName.equals("com.android.systemui"))
            return;

        findAndHookMethod("com.android.systemui.statusbar.policy.Clock", lpparam.classLoader, "updateClock", new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                TextView tv = (TextView) param.thisObject;
                String text = tv.getText().toString();
                tv.setText(text + " :)");
                tv.setTextColor(Color.RED);
            }
        });
    }
}

4. 添加入口

在 assets 目录下创建 xposed_init 文件并添加 Module 文件所在完整目录,如

de.robv.android.xposed.examples.redclock.RedClock

5. 运行

完成以上步骤之后,重启手机再次运行 app 即可看到实际效果。

开发经验

1. 务必关闭 Instant Run

如果 apk 是采用动态加载的话,xposed 是不能 hook 住动态加载的代码的,所以采用 instant runn 方式安装的 xposed 插件可能不生效,所以务必要关闭。

2. HotXposed

每次写完 Xposed 都要重启,真是浪费时间,所以这里推荐一下 @wingHotXposed,主要原理就是直接动态加载替换目标 app 的 dex。

具体使用方式可以参看 HotXposed

3. Hook 操作

常用的方法包括以下两种:

  • XposedHelpers.findAndHookMethod (只知道方法的参数和类型)
  • XposedBridge.hookAllMethods (不知道方法的参数,只知道名称)
  • XposedHelpers.findAndHookConstructor (hook 构造函数)

构造函数同理。

4. 使用 XC_MethodHook 和 XC_MethodReplacement

在 XC_MethodHook 和 XC_MethodReplacement 的回调中,都有 param 参数:

  • 通过这个 param.args 可以拿到方法的各个参数的值,也可以去它们的值
  • 通过 param.getResult() 可以拿到返回值
  • 通过 setResult 可以修改返回值

另外在 XC_MethodHook 可以在方法执行前和执行后执行你插入的代码,非常简单,但是有一个问题,多次 hook 后,你插入的代码重复执行多次,这时可以用 XC_MethodReplacement 来解决这个问题:

public class TestReplacementHook extends XC_MethodReplacement {
    @Override
        protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
            // 如果不想影响结果,记得返回XposedBridge.invokeOriginalMethod的执行结果
            return XposedBridge.invokeOriginalMethod(param.method, param.thisObject, param.args);
        }
}

5. 使用 Xposed 框架提供的反射方法

Xposed 的 XposedHelper 中提供了大量反射工具类,要合理利用。

Class<?> findClass(String className, ClassLoader classLoader)
...
Object getObjectField(Object obj, String fieldName)
float getFloatField(Object obj, String fieldName)
...
void setObjectField(Object obj, String fieldName, Object value)
void setFloatField(Object obj, String fieldName, float value)
...
Object callMethod(Object obj, String methodName, Object... args)
...

6. 获取 Context

  • 在 Hook 程序的 Application 或者 Activity 的 onCreate 方法,通过 param.thisObject 转化成 Context
  • 直接使用系统自带的 AndroidAppHelper.currentApplication() 得到 Application

7. 自动 Root。

用 am start 命令打开一些 Activity 需要 Root 权限,这时可以 hook 授权对话框的允许按钮,实现 Root 的自动化。

8. 在 Application 中注册广播

如果需要在被 Hook 的 app 里接收到信息,可以在目标 app 的 application 初始化时注册广播,通过广播接收数据。

9. 跨进程通信

因为 hook 的代码是执行在目标程序的进程中,所以需要和开发的 app 通信就成了跨进程的,使用 ContentProvider 是一个相对比较舒服的选择。

  • 使用 ContentProvider 本身可以增删改数据,可以直接数据操作。
  • ContentProvider 中的 call 方法是一种更简单的行为调用,也可以传递数据,非常不错。
public class MyAppProvider extends ContentProvider {
    private static final String TAG = "MyAppProvider";
    public static final String AUTHORITY = "com.test.provider";
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        return null;
    }
    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }
    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        return null;
    }
    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }
    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs){
        return 0;
    }

    // 可以用method定义行为,用arg, extra传递参数
    @Override
    public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
        return super.call(method, arg, extras);
    }
}

10. 动态加载

有些动态加载的类,用默认的 loadPackageParam.classLoader 怎么都拦截不到,这是正常的,因为它就不在这个 classloader 里。

但是可以曲线救国,先 hook 动态加载的类在系统中的父类,通过这个父类找到真正的 classloader,再用这个真正的 classloader 去 hook 动态加载的类。

if (loadPackageParam.packageName.equals("com.test.xxx")) {
    // 拦截onCreate方法,得到 Fragment, 根据当前动态加载的 fragment 去获取它真正的 classloader
    Class fragmentClazz = XposedHelpers.findClass("android.support.v4.app.Fragment", loadPackageParam.classLoader);
    XposedBridge.hookAllMethods(fragmentClazz, "onCreate", new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                super.afterHookedMethod(param);
                Object fragment = param.thisObject;
                String fragmentName = fragment.getClass().getName();
                Log.d(TAG, "fragment: " + fragmentName);
                if (fragmentName.equals("com.xx.yy")) {
                    // 注意这里的 fragment.getClass().getClassLoader() 才是正确的那个动态加载的 dex 的 classloader
                    Class clazz = XposedHelpers.findClass("a.b.c", fragment.getClass().getClassLoader());
                    XposedBridge.hookAllConstructors(clazz, new XC_MethodReplacement() {
                        @Override
                        protected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {
                            return XposedBridge.invokeOriginalMethod(methodHookParam.method, methodHookParam.thisObject, methodHookParam.args);
                        }
                    });
                }
            }
    });
}

工具

这部分其实主要是为了更方便找到 Hook Point

1. Android Profiler 分析器

CPU 分析器可帮助您实时检查应用程序的 CPU 使用情况和线程活动,并记录方法跟踪,以便可以优化和调试应用程序的代码。

在安装了 XDebuggable 之后,选择目标 app 的进程,然后点击 cpu 一栏,录制目标 app 的操作就可以查看其调用堆栈信息。

获取相关调用信息之后配合反编译目标 app 的代码可以帮你得到更多信息。

2. 日志工具类

打印类的方法和字段、View 的视图结构、Bundle 参数、堆栈信息。

object PrintHelper : AnkoLogger {
    fun log(msg: String) {
        XposedBridge.log("Bot:\t" + msg + "\tts=" + System.currentTimeMillis())
    }

    fun log(e: Throwable) {
        XposedBridge.log(e)
    }

    fun printBundle(bundle: Bundle) {
        for (key in bundle.keySet()) {
            debug { "bundle.key: " + key + ", value: " + bundle.get(key) }
        }
    }

    fun printTreeView(activity: Activity) {
        val rootView = activity.window.decorView
        printTreeView(rootView)
    }

    fun printTreeView(rootView: View) {
        if (rootView is ViewGroup) {
            val parentView = rootView
            for (i in 0 until parentView.childCount) {
                printTreeView(parentView.getChildAt(i))
            }
        } else {
            debug { "view: " + rootView.id + ", class: " + rootView.javaClass.name }
            // any view if you want something different
            if (rootView is EditText) {
                debug {
                    "edit:" + rootView.getTag() + ", hint: " + rootView.hint
                }
            } else if (rootView is TextView) {
                debug { "text:" + rootView.text.toString() }
            }
        }
    }

    fun printMethods(clazz: Class<*>) {
        for (method in clazz.declaredMethods) {
            debug { "" + method }
        }
    }

    fun printFields(clazz: Class<*>) {
        for (field in clazz.fields) {
            debug { "" + field }
        }
    }

    fun logStackTraces(methodCount: Int = 15, methodOffset: Int = 3) {
        val trace = Thread.currentThread().stackTrace
        var level = ""
        log("---------logStackTraces start----------")
        for (i in methodCount downTo 1) {
            val stackIndex = i + methodOffset
            if (stackIndex >= trace.size) {
                continue
            }
            val builder = StringBuilder()
            builder.append("|")
                    .append(' ')
                    .append(level)
                    .append(trace[stackIndex].className)
                    .append(".")
                    .append(trace[stackIndex].methodName)
                    .append(" ")
                    .append(" (")
                    .append(trace[stackIndex].fileName)
                    .append(":")
                    .append(trace[stackIndex].lineNumber)
                    .append(")")
            level += "   "
            log(builder.toString())
        }
        log("---------logStackTraces end----------")
    }
}

3. adb

# 获取当前安装的app列表
adb shell pm list packages -f

# 导出apk
adb pull /data/app/com.tencent.mm-1/base.apk

# 获取当前的Activity
adb shell dumpsys activity | grep mFocusedActivity

# 查询AMS服务相关信息
adb shell dumpsys activity

# 当前界面app状态
adb shell dumpsys activity top

# 查询某个App所有的Activity状态
adb shell dumpsys activity a com.tencent.mm

# 查询WMS服务相关信息
adb shell dumpsys window

4. 动态调试

常规手段是通过 apktool 反编译 apk, 在 AndroidManifest.xml 文件的 application 节点中设置属性 android:debuggable=“true”; 之后回编译、签名后安装。

上面的方法比较麻烦,并且如果 apk 有反二次打包机制,上面的方法就行不通了,这时可以通过 xposed 安装 XDebuggable 实现。

具体操作请参考 动态调试 Android APPsmali 动态调试

此外还可以使用 JEB 或者 IDA,这两个工具我还没用过,不过据说很强大。

5. 动态分析

Inspeckage 对动态分析很多常用的功能进行了汇总并且内建一个 webserver。

整个分析操作可以在友好的界面环境中进行。

具体使用参考 安卓分析辅助工具 Inspeckage 介绍

但在使用过程中发现在 web 端数据偶尔不能正常显示出来,也有人提了 issue,但是没有回复和解决方案,只能先在 AS 中可以看到日志使用了。

另外对于 Https 的请求也抓取不到,所以这种方式只能作为辅助手段使用。

6. 脱壳

现在大部分的 app 都会使用加固的手段进行防护,所以脱壳也是需要掌握的。

可以使用 dumpDex,或者 FDex2

实例

推荐插件及项目

更多

其他

在编写 Java 层 hook 插件的时候使用 Xposed 非常方便好用,除了 Xposed 还有别的方式如使用 Cydia Substrate 或者 Frida,具体的使用我暂时还没有研究,等后续研究之后再更新。

参考