文章

自定义XPosed模块

自定义XPosed模块

自定义 Xposed 模块

什么是 Xposed 框架?

  • Xposed 框架是适用于 Android 的一个工具,它允许开发者在不修改 APK 文件的情况下对 APP 和系统行为进行修改或定制。
  • 通过 Hook 技术,Xposed 可以在运行时对应用程序的方法进行拦截、修改,甚至替换,从而实现对系统和应用程序的深度定制。
  • 注意: 使用 Xposed 需要获取 Root 权限或修改系统,可能对设备的稳定性和安全性有影响,且某些应用可能检测到 Xposed 的存在而限制功能。

Xposed 模块的结构

一个典型的 Xposed 模块是一个普通的 Android 应用程序(APK),但它包含了一些特定的配置和代码,以便 Xposed 框架能够正确加载和执行。

Xposed 模块组成

  • 基本组成:
    • AndroidManifest.xml 声明应用程序的基本信息。
    • 模块代码 包含了实际的 Hook 逻辑,通常实现了 Xposed 的接口。
    • assets 目录下的 xposed_init 文件 指示 Xposed 框架入口类的位置(实现了 IXposedHookLoadPackage 接口的全路径)。

xposed_init 的作用

  • 定义:
    • xposed_init 是存放在模块的 assets 目录中的一个纯文本文件,文件名固定为 xposed_init,没有扩展名。
  • 作用:
    • 告知 Xposed 框架哪些类包含模块的入口代码,即哪些类需要在系统启动时被加载。
    • 框架在启动时会读取每个已激活的 Xposed 模块的 xposed_init 文件,获取要加载的类名,然后使用这些类初始化模块。
  • 内容:
    • 文件中包含一个或多个类的完整限定名(即包名 + 类名),每行一个类名。 20250212161740
1
me.hacket.hello_world.HelloWorldXPosed

Xposed 资源梳理

  • XposedBridge.jar:XposedBridge.jar 是 Xposed 提供的 jar 文件,负责在 Native 层与 FrameWork 层进行交互。/system/bin/app_process 进程启动过程中会加载该 jar 包,其它的 Modules 的开发与运行都是基于该 jar 包的。 注意:XposedBridge.jar 文件本质上是由 XposedBridge 生成的 APK 文件更名而来。
  • 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
  • XposedMods:使用 Xposed 开发的一些 Modules,其中 AppSettings 是一个可以进行权限动态管理的应用

Xposed 模块的初始化流程

  1. Xposed 框架启动:
    • 当设备开机或系统启动时,Xposed 框架被加载。
  2. 加载已激活的模块:
    • Xposed 框架遍历所有已激活的模块,读取它们的 xposed_init 文件。
  3. 加载入口类:
    • 对于每个模块,Xposed 会使用模块的 ClassLoader 加载 xposed_init 文件中指定的类。
  4. 调用入口类的方法:
    • 加载类后,Xposed 框架会调用该类的 init 方法(通常是实现的接口方法),在适当的时机执行模块的初始化逻辑。

自定义 Xposed 步骤

本质

从本质上来讲,Xposed 模块也是一个 Android 程序。但与普通程序不同的是,想要让写出的 Android 程序成为一个 Xposed 模块,要额外多完成以下四个步骤:

  1. 让手机上的 xposed 框架知道我们安装的这个程序是个 xposed 模块。
  2. 模块里要包含有 xposed 的 API 的 jar 包,以实现下一步的 hook 操作。
  3. 这个模块里面要有对目标程序进行 hook 操作的方法。
  4. 要让手机上的 xposed 框架知道,我们编写的 xposed 模块中,哪一个方法是实现 hook 操作的。

对应上面的四个步骤我们需要做的修改有:

  1. AndroidManifest.xml
  2. XposedBridgeApi-xx.jarbuild.gradle
  3. 实现 hook 操作的具体代码
  4. src/main/assets/xposed_init

目标

  • hook 应用包名为:me.hacket.demos
  • me.hacket.sample.MainActivity 的 onCreate 弹出一个 toast

添加 Gradle 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
repositories {
    jcenter()
}

dependencies {
    compileOnly 'de.robv.android.xposed:api:82'
    // compileOnly 'de.robv.android.xposed:api:82:sources'
}

// settings.gradle.kts
dependencyResolutionManagement {  
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)  
    repositories {  
        google()  
        mavenCentral()
        maven("https://api.xposed.info/")  
    }  
}

更改 AndroidManifest.xml 文件

添加模块的元数据,让 Xposed 框架识别该模块。放到 application 节点下

1
2
3
4
5
6
7
8
9
<meta-data
	android:name="xposedmodule"
	android:value="true" />
<meta-data
	android:name="xposeddescription"
	android:value="这是一个Xposed模块" />
<meta-data
	android:name="xposedminversion"
	android:value="30" />

创建入口类

实现接口:IXposedHookLoadPackage

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
class HelloWorldXPosed : IXposedHookLoadPackage {  
    override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {  
        val packageName = lpparam.packageName  
        XposedBridge.log("----------Hello World, handleLoadPackage! packageName: $packageName")  
        if (!packageName.equals("me.hacket.demos")) {  
            return  
        }  
  
        XposedBridge.log("--------Hello World, XPosed! packageName: $packageName")  
  
        XposedHelpers.findAndHookMethod("me.hacket.sample.MainActivity",  
            lpparam.classLoader,  
            "onCreate",  
            Bundle::class.java,  
            object : XC_MethodHook() {  
                @Throws(Throwable::class)  
                override fun afterHookedMethod(param: MethodHookParam) {  
                    val activity = param.thisObject as? Activity  
  
                    // 在 onCreate 方法中添加你的方法  
                    myMethod(activity)  
                }  
            })  
    }  
  
    private fun myMethod(activity: Activity?) {  
        // 你的方法实现  
        Log.e("hacket.hook", "hooked method called")  
        Handler(Looper.getMainLooper()).postDelayed({  
            Toast.makeText(activity, "hooked method called", Toast.LENGTH_SHORT).show()  
        }, 4000)  
    }  
}

配置 xposed_init

告诉 Xposed 框架,你的 hook 代码的入口点在哪里,以便框架能够在合适的时机调用你的 hook 代码。在 app/src/main 目录下创建一个名为 assets 的文件夹,并在其中创建一个名为 xposed_init 的文件(没有扩展名)。在这个文件中,写入你的 IXposedHookLoadPackage 实现类的全限定名:

1
me.hacket.hello_world.HelloWorldXPosed

20250212161740

进行编译签名打包

不用用 AS→Run,这种方式可能找不到模块的入口

激活模块

  • 打开设备上的 Xposed 框架管理应用(Xposed Installer)LSPosed
  • 在模块列表中找到您的模块,勾选启用。
  • 重启设备,使模块生效。

20250212162628

测试

安装好之后在 LSPosed 中启用该 XPosed 模块,勾选应用的 app,然后重启手机应用/重启系统更改

  • 启动测试 APP,进入到 MainActivity,可以查看到 Logs 有输出 20250212163006

  • 进入 APP 等待 5 秒会弹出一个 hook 的 toast 提示

其他

LSPosed 作用于应用 自动勾选

LSPosed 模块自动勾选应用模块作用域 LSPosed 推荐的应用。

Edxposed 的模块作用域和 LSPosed 需要用户手动勾选应用,但是有些模块可以自动勾选应用。

  • 添加 array:在 res -> values 目录下添加 array.xml
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="xposed_scope">
        <!--这里是填入需要自动勾选的应用的包名,多个就是多个item-->
        <item>com.eg.android.AlipayGphone</item> 
    </string-array>
</resources>

  • 添加 Scope: 在清单文件 Androidmanifest.xml 的 application 标签下添加 xposedscope 的 meta-data,跟 xposedmodule 是同级的,如下
1
2
3
<meta-data
	android:name="xposedscope"
	android:resource="@array/xposed_scope" />
  • LSposed 的 Modules 对应的模块会自动勾选该包名对应的 APP

示例

Hook JobSchedulerService 修改 15 min 限制

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
public class ModifyJobScheduler implements IXposedHookLoadPackage {

    @Override
    public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) {
        String packageName = lpparam.packageName;
        if (!packageName.equals("android") && !packageName.equals("system")
        ) {
            XposedBridge.log("Init error, packageName:" + packageName + ",not android or system, ignore.");
            return;
        } else {
            XposedBridge.log("Init start , hook packageName:" + packageName);
        }

        try {
            Class<?> jobSchedulerServiceClass = XposedHelpers.findClass(
                    "com.android.server.job.JobSchedulerService",
//                    lpparam.classLoader // failed to load class com.android.server.job.JobSchedulerService
                    ClassLoader.getSystemClassLoader() //  修改为使用 ClassLoader.getSystemClassLoader()
            );

            // Hook onStart() 确保mConstants已初始化
            XposedHelpers.findAndHookMethod(jobSchedulerServiceClass, "onStart",
                    new XC_MethodHook() {
                        @Override
                        protected void afterHookedMethod(MethodHookParam param) {
                            Object service = param.thisObject;
                            Object constants = XposedHelpers.getObjectField(service, "mConstants");

                            try {
                                Class<?> constantsClass = constants.getClass();

                                // 修改MIN_PERIOD_MS(移除final修饰符)
                                Field minPeriodField = constantsClass.getDeclaredField("MIN_PERIOD_MS");
                                minPeriodField.setAccessible(true);
                                Field modifiersField = Field.class.getDeclaredField("modifiers");
                                modifiersField.setAccessible(true);
                                modifiersField.setInt(minPeriodField, minPeriodField.getModifiers() & ~Modifier.FINAL);
                                minPeriodField.setLong(constants, 60_000L);

                                // 类似修改MIN_FLEX_MS...
                                XposedBridge.log("MIN_PERIOD_MS updated to 1 minute");
                            } catch (Exception e) {
                                XposedBridge.log("Error: " + e);
                            }
                        }
                    });
        } catch (Throwable t) {
            XposedBridge.log("Init error: " + t);
        }
    }
}
  • IXposedHookLoadPackage:一个接口,允许开发者实现应用加载时的 hook 逻辑。通过实现这个接口,模块可以在特定应用被加载时执行自定义代码。
  • XposedBridge:Xposed 框架的核心类,提供了多种方法来进行 hook 和日志记录等操作。
  • XC_LoadPackage.LoadPackageParam:一个类,包含了关于加载的应用程序的信息,例如包名、类加载器等。
  • handleLoadPackage 是接口 IXposedHookLoadPackage 中定义的方法。当一个应用被加载时,Xposed 框架会调用这个方法。
  • LoadPackageParam lpparam 参数包含了被加载应用的相关信息。
  • XposedBridge.log(…) 方法用于记录日志,这里记录了加载的应用的包名。通过查看 Xposed 的日志,开发者可以看到哪些应用被加载了。

测试代码:

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
private fun scheduleJob() {  
    val serviceName = ComponentName(packageName, TestJobService::class.java.name)  
    val builder = JobInfo.Builder(JOB_ID, serviceName)  
        .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) // 可选:设置网络类型  
        .setPeriodic(1 * 30 * 1000)  // 设置周期为 1 分 30 秒 (90秒) -  您可以调整这个时间  
        .setPersisted(true) // 设备重启后任务仍然存在  
  
    val scheduler = getSystemService(JOB_SCHEDULER_SERVICE) as JobScheduler  
  
    val result = scheduler.schedule(builder.build())  
    if (result == JobScheduler.RESULT_SUCCESS) {  
        Log.d(TAG, "Job scheduled successfully")  
        Toast.makeText(this, "Job scheduled successfully", Toast.LENGTH_SHORT).show()  
    } else {  
        Log.e(TAG, "Job scheduling failed")  
        Toast.makeText(this, "Job scheduling failed", Toast.LENGTH_SHORT).show()  
    }  
}

class TestJobService : JobService() {
    private val TAG = "hacket.JobSchedulerTest"
    override fun onStartJob(params: JobParameters?): Boolean {
        Log.d(TAG, "TestJobService onStartJob() executed at: " + System.currentTimeMillis())
        // 这里可以添加您要执行的周期性任务的代码
        jobFinished(params, false) //  false 表示不需要重新调度,因为是周期性任务
        return true //  true 表示任务是异步的,onStopJob 需要被调用
    }
    override fun onStopJob(params: JobParameters?): Boolean {
        Log.d(TAG, "TestJobService onStopJob()")
        return true // true 表示如果任务被取消,需要稍后重新调度
    }
}

问题: failed to load class com.android.server.job.JobSchedulerService

问题

Hook 不到类报错以及 Xposed 的 Hook 方法

failed to load class me.hacket.sample.MainActivity

解决 AndroidStudio 修改代码运行没变化没生效的问题 -Xposed 插件重载

是因为 AndroidStudio 对 Android11 之后部署优化的问题,在 Run 处的位置 EditConfigruations, 勾选 Always install with package manager 即可。

202502220019323

Ref

开源的 XPosed 模块

QAuxiliary

QAuxiliary 是一个基于 QNotified 的开源 Xposed 模块

YukiHookAPI (XPosed API 封装)

GitHub - HighCapable/YukiHookAPI: ⛱️ An efficient Hook API and Xposed Module solution built in Kotlin.

本文由作者按照 CC BY 4.0 进行授权