Xposed 框架的使用

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 模块的开发。

项目依赖

首先添加项目依赖。

  1. 确认根 Project 或 Module 添加了 jcenter 的仓库渠道。
1
2
3
repositories {
jcenter();
}
  1. 添加 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 中实现,它可以将方法更改为本地方法,并链接到自己的泛型本地方法,每次调用钩子方法时,都将调用泛型方法,但是调用方无需知晓,在这个方法中,会调用 XposedBridgehandleHookedMethod 方法,handleHookedMethod 则会调用注册的回调方法。这里可以更改调用的参数、更改实例或静态变量、调用其他方法或对结果执行某些操作…或者跳过任何内容。

  • 下面在 Android Studio 中创建一个 Module,在 AndroidManifet.xmlapplication 标签中添加 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>
  1. xposedmodule 是固定的配置,说明自己是一个 Xposed 模块。
  2. xposeddescription 是 Xposed 模块的简要描述,在 Xposed 框架应用中会展示出来。
  3. 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 {
/* 激活模块后,使用 Xposed 作为标签过滤即可看到每个加载的应用包名 */
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 {
/* 激活模块后,使用 Xposed 作为标签过滤即可看到每个加载的应用包名 */
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 {
/* 可以更改 hook 方法的行为 */
// param.setResult(null);
// param.setThrowable(new Throwable());
}

@Override protected void afterHookedMethod(MethodHookParam param) throws Throwable {
/* 获取到了 attach 方法的 context 参数 */
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";
/* 目标 APK 包名 */
private static final String TARGET_PACKAGE_NAME = "io.l0neman.xposedproject";
/* 目标 APK 内的类型 */
private static final String TARGET_INJECT_CLASS_NAME =
"MyXposedClient";
/* 目标 APK 类的方法 */
private static final String TARGET_INJECT_METHOD = "handleLoadPackage";

@Override
public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
log("loading app: " + lpparam.packageName);
/* hook Application 类,为了获取 Context 对象 */
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);
}
});
}

/* 使用 context ,寻找目标 APK 的安装包并调用目标方法 */
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");
}

/* 返回 APK 的安装包文件路径,每个 APK 安装后都会有,data/app/com.xxx.xxx/base_x.apk */
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

参考

作者

l0neman

发布于

2020-06-03

更新于

2020-06-03

许可协议

评论