Xposed 简介 Xposed 框架是 Android 平台上一个非常著名且强大的开源框架,使用它能够对系统进程内运行的方法进行 hook,所以可以用它来做一些系统层面的工作,它拥有无限可能的灵活性,目前市面上基于 Xposed 框架下开发 Xposed 子模块已经数不胜数了。
原理简析 Android 系统运行的核心和起点是 Zygote 进程,所有应用都是从它 fork 子进程产生的,当系统开始运行时由 init.rc
脚本启动, 使用 /system/bin/app_process
程序完成启动,它加载所需的类并调用初始化方法。
Xposed 框架将在这个地方发挥作用,当 Xposed 框架被安装时,一个被扩展的 app_process
程序将被复制到 /system/bin/
中,这个扩展的 app_process
将向类的路径附加一个 jar
文件,并在某些位置调用其方法,可能是虚拟机创建之后,或者在 Zygote 进程的 main 方法之前。在这个方法里,我们可以在其上下文中做插桩。
环境配置 Xposed 框架会替换系统的关键文件,所以需要 root 权限,获取 root 权限之后,安装 Xposed 框架。
Xposed 下载地址: http://repo.xposed.info/module/de.robv.android.xposed.installer
安装 Xposed 框架的 APK 后,进入并点击 INSTALL/UPDATE
下面的版本号,即可开始安装。
提示:安装可能会导致设备无限重启或变砖,所以一定要确认适合自己的设备后再安装。
模块开发 Xposed 框架安装完毕后即可进行 Xposed 模块的开发,我们自定义的功能都是在 Xposed 模块中实现的。
它是一个普通的 APK,包含一些实现 Xposed 依赖库提供的特定接口的类,当它被安装到设备后,被 Xposed 框架调起发挥作用,下面开始进行 Xposed 模块的开发。
项目依赖 首先添加项目依赖。
确认根 Project 或 Module 添加了 jcenter 的仓库渠道。
1 2 3 repositories { jcenter(); }
添加 Xposed 库的编译支持。
Android Studio 3.0+ 的版本
1 2 3 dependencies { compileOnly 'de.robv.android.xposed:api:82' }
低于 3.0 的版本
1 2 3 dependencies { provided 'de.robv.android.xposed:api:82' }
注意:这里的选项是指定只参与编译,不需要真正的导入它的类 ,因为系统的 Xposed 框架内已经提供了这些类,所以不要用 implementation(Studio 3.0+)
和 complie(lower than 3.0)
。
方法钩子 方法钩子是 Xposed 的核心功能,一般通过对 APK 反编译后进行修改的方式,可以在任何位置插入和更改代码,但是必须重新编译打包整个包。使用 Xposed 可以放置方法钩子,不能修改方法代码,不过可以在方法调用的前后插入代码。
XposedBridge
中有一个私有的 hookMethod
本地方法,它在扩展后的 app_process
中实现,它可以将方法更改为本地方法,并链接到自己的泛型本地方法,每次调用钩子方法时,都将调用泛型方法,但是调用方无需知晓,在这个方法中,会调用 XposedBridge
的 handleHookedMethod
方法,handleHookedMethod
则会调用注册的回调方法。这里可以更改调用的参数、更改实例或静态变量、调用其他方法或对结果执行某些操作…或者跳过任何内容。
下面在 Android Studio 中创建一个 Module,在 AndroidManifet.xml
的 application
标签中添加 3 个 meta-data
标签。
1 2 3 4 5 6 7 8 9 10 11 12 13 <application android:icon ="@drawable/ic_launcher" android:label ="@string/app_name" > <meta-data android:name ="xposedmodule" android:value ="true" /> <meta-data android:name ="xposeddescription" android:value ="Easy example which makes the status bar clock red and adds a smiley" /> <meta-data android:name ="xposedminversion" android:value ="82" /> </application >
xposedmodule
是固定的配置,说明自己是一个 Xposed 模块。
xposeddescription
是 Xposed 模块的简要描述,在 Xposed 框架应用中会展示出来。
xposedminversion
是 Xposed 模块的 API 版本,通常情况下,和使用的 Xposed API 版本应该一致。
实现 IXposedHookLoadPackage
接口,这个是放置方法钩子的入口点,如果需要实现资源替换,需要实现另外的接口,当 Android 系统启动时,每一个应用加载启动时都会回调这个接口。
1 2 3 4 5 6 7 8 package io.l0neman.xposedproject;public class MyXposedStub implements IXposedHookLoadPackage { @Override public void handleLoadPackage (final XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { XposedBridge.log("Loading app: " + lpparam.packageName); }
最后需要配置入口点,在 assets 目录下,建立一个 xposed_init
文件,它的每行都可以指定一个入口点的全类名。
1 io.l0neman.xposedproject.MyXposedStub
现在安装 APK,然后打开 Xposed 框架,勾选这个模块,重启手机,就能看到日志。
1 2 3 4 5 6 Loading Xposed (for Zygote)... Loading modules from /data/app/io.l0neman.xposedproject-1.apk Loading class io.l0neman.xposedproject Loaded app: com.android.systemui Loaded app: com.android.settings ...
上面的基础工作做完了,现在就可以使用 XposedHelpers 的辅助方法来获取方法钩子了,下面对一个应用的 Application 类进行 hook。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class MyXposedClient implements IXposedHookLoadPackage { @Override public void handleLoadPackage (final XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { XposedBridge.log("Loading app: " + lpparam.packageName); if (!lpparam.packageName.equales("com.android.systemui" )) { return ; } XposedHelpers.findAndHookMethod(Application.class, "attach" , Context.class, new XC_MethodHook() { @Override protected void beforeHookedMethod (MethodHookParam param) throws Throwable { } @Override protected void afterHookedMethod (MethodHookParam param) throws Throwable { Context context = (Context) param.args[0 ]; } }); }
除了以上的方法 hook 之外,也可对应用内的任意类型进行反射获取。
资源替换 目前还没有使用资源替换的方法,后续会补充。
避免重启的方法 每次更改 Xposed 模块的代码后,都需要从新启动 Android 设备,非常不利于调试,所以可以用一种方法只在第一次重启,以后都不用重启了。
原理就是,首先编写一个 XposedStub APK,然后在其中通过包名的形式寻找另一个实现 Xposed 逻辑的 APK,通过动态加载的方式加载调用 APK 中的类和方法,这时只需要每次更改实现逻辑的 APK 即可,无需重启刷新 XposedStub 这个 APK 里面的逻辑了,相当于搭了一个桥梁。
首先实现一个实现逻辑的 APK,这里我需要一个能够 hook 类的方法,创建一个参数接收的类。
1 2 3 4 5 6 7 8 9 10 11 package io.l0neman.xposedproject;import android.util.Log;import de.robv.android.xposed.callbacks.XC_LoadPackage;public class MyXposedClient { private static final String TAG = "MyXposedClient" ; public static void handle (String seflApkPath, XC_LoadPackage.LoadPackageParam llparam) { Log.d(TAG, "accept ok: " + llparam.packageName); } }
然后编写 XposedStub APK,它将作为一个和 Xposed 框架通信的中间角色,接收 Xposed 框架的回调结果,并转交给目标 APK,下面是具体逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 public class MyXposedStub implements IXposedHookLoadPackage { private static final String TAG = "MyXposedStub" ; private static final String TARGET_PACKAGE_NAME = "io.l0neman.xposedproject" ; private static final String TARGET_INJECT_CLASS_NAME = "MyXposedClient" ; private static final String TARGET_INJECT_METHOD = "handleLoadPackage" ; @Override public void handleLoadPackage (final XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { log("loading app: " + lpparam.packageName); XposedHelpers.findAndHookMethod(Application.class, "attach" , Context.class, new XC_MethodHook() { @Override protected void afterHookedMethod (MethodHookParam param) throws Throwable { Context context = (Context) param.args[0 ]; callTarget(context, lpparam); } }); } private void callTarget (Context context, XC_LoadPackage.LoadPackageParam lpparam) throws Exception { File apkPath = findApkPath(context, TARGET_PACKAGE_NAME); if (apkPath == null ) { return ; } log("hit apk: " + apkPath); PathClassLoader pathClassLoader = new PathClassLoader( apkPath.getAbsolutePath(), ClassLoader.getSystemClassLoader() ); Class<?> targetClass = Class.forName(TARGET_INJECT_CLASS_NAME, true , pathClassLoader); Method handle = targetClass.getMethod(TARGET_INJECT_METHOD, String.class, XC_LoadPackage.LoadPackageParam.class); handle.invoke(null , apkPath.getPath(), lpparam); log("inject ok" ); } private static File findApkPath (Context context, String packaegName) { if (context == null ) { throw new AssertionError("context is null" ); } try { Context targetContext = context.createPackageContext(packaegName, Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY ); String apkPath = targetContext.getPackageCodePath(); return new File(apkPath); } catch (PackageManager.NameNotFoundException ignore) {} return null ; } private void log (String log) { Log.d(TAG, log); } }
提示 编写 Xposed 模块时需要清楚的知道自己的代码处于什么环境,当 handleLoadPackage
被回调时,说明一个新的代码包(常见为一个新的应用)被加载起来了,此时下面的代码将运行在目标应用的进程中,当模块具有 UI 界面时,模块的代码将分别运行在本身的应用进程和注入的目标应用进程中,如果需要进行设配置,可能需要进行进程间通信。
由于一些原因,Xposed 框架的作者不再提供 Android 9 系统之上的版本,如果需要在 Android 9 平台或以上版本使用 Xposed,推荐使用基于 Magisk(面具框架)的 EdXposed 替代,目前它的最新版本具有即时更新模块的功能,不用再重启设备了。
Magisk 地址:https://github.com/topjohnwu/Magisk
Edxposed 地址:https://github.com/ElderDrivers/EdXposed
参考