Android Hook与简单的xposed模块开发实例

Hook是一种在特定事件或操作发生时插入自定义代码的编程技术。在前端开发中,例如Vue和Angular的生命周期钩子,体现了Hook的机制,允许开发者在组件的不同阶段执行代码,提升代码的模块化和可重用性。

Android Hook与此类似,允许开发者在Android应用程序运行时修改或扩展现有功能。通过拦截方法调用、修改参数或返回值,Hook可用于调试、测试和逆向工程,例如监控应用程序行为、捕获API调用,或在不修改源代码的情况下添加新功能,以及替换so模块以实现获取验证码或广告拦截等功能。

在快写完这篇博客的时候,我才发现我想要实现的功能不需要Android Hook即可实现…不过不耽误我对这方面做一些了解,所以接着写完了。

补充知识

前几天刷机的时候基本算是闷头跟着教程走,中间还险些变砖。虽然过程中我边操作边了解,但也是比较粗糙,这里再补充点知识并做做记录。

Magisk工具

现在的刷机教程一般都会引导读者在root后安装Magisk,乍看上去是个工具包,集合了很多功能,那这个“面具”具体是什么,有什么用呢?

Magisk是一款流行的Android系统级root工具,由XDA开发者topjohnwu创建和维护。与传统的root方法不同,Magisk采用了一种“无系统”的方式,这意味着它不会直接修改系统分区中的文件,而是通过修改boot镜像来实现root权限。这种方法的好处在于它能够更好地兼容系统更新,避免root权限导致的系统不稳定或应用崩溃。Magisk的“无系统”特性也使得它能够更好地隐藏root权限,从而绕过一些应用的root检测机制,例如Google的SafetyNet。

Magisk的核心功能是提供root权限,允许用户访问和修改系统文件,以及安装各种Magisk模块来扩展系统功能。这些模块可以实现各种各样的功能,例如自定义系统UI、增强系统性能、拦截广告、修改系统设置等等。Magisk还提供了一个Magisk Manager应用,方便用户管理root权限和Magisk模块。Magisk的持续更新和强大的社区支持,使其成为Android用户中非常受欢迎的root工具。 Magisk的出现,很大程度上简化了root流程,并提升了root后的系统稳定性和安全性。

Xposed框架

Xposed框架是一个Android模块化框架,允许开发者通过模块修改系统和应用行为,无需修改APK文件。它由rovo89开发,最初用于调试和测试,通过替换系统核心文件实现功能,并允许开发者使用Java代码hook方法。Xposed功能强大,可修改系统UI、性能、广告等,但安装复杂,存在风险,需要root权限。

Magisk和Xposed都是Android root工具,但Magisk采用“无系统”方式,更安全稳定,兼容性更好;Xposed功能更强大,但修改系统文件,风险更高。一些用户会结合使用两者,先用Magisk获得root权限,再安装Xposed,以兼顾安全性和功能性,但需要注意模块兼容性。

与之相对的还有CydiaSubstrate框架,不过xposed是开源项目,所以培养了更加庞大的开发者社区,网上基于Xposed框架的模块插件非常多。

Xposed Hook实现原理及缺陷

Xposed虽好,但也存在缺陷,比较致命的就是兼容性差。

Xposed框架的运作依赖于Android系统的Zygote进程和app_process,但其机制并非简单的替换。准确地说,Xposed通过替换系统默认的/system/bin/app_process为一个修改过的版本来实现其功能,这个修改后的app_process包含了Xposed框架的核心代码,以及方法Hook机制。

系统启动时,init.rc脚本启动Zygote进程,使用的是这个被Xposed替换后的/system/bin/app_process。因此,Zygote进程本身就加载了Xposed框架。所有应用进程都是Zygote的子进程,因此它们继承了Xposed框架的代码。

开发者编写的Xposed模块定义了Hook规则,这些模块会被Xposed框架加载,并根据规则执行Hook操作。Xposed框架会管理这些模块,并确保它们能够正确地与Zygote进程和应用进程交互。Xposed的Hook操作并非在应用运行时动态进行,而是在Zygote进程启动的早期阶段就完成了,这使得Hook操作能够影响所有从Zygote fork出的应用进程。这与在应用运行时动态Hook方法相比,效率更高,也更稳定。

听起来很不错,但问题也就出在这里。Android系统的更新迭代频繁,且不说Android本身的各种大小版本,各家手机厂商也会在原本的基础上进行魔改,这就导致Android系统版本繁多。每次系统更新都可能改变app_process的结构或行为。Xposed框架需要针对每次系统更新进行适配,才能保证其正常工作。如果Xposed框架没有及时更新以适应新的app_process,那么它就可能无法正常工作,甚至导致系统崩溃或bootloop(无限重启)。知名项目可以依赖庞大的贡献者群体及时更新,小项目跟不上版本就会慢慢废弃。

EdXposed框架

GitHub - ElderDrivers/EdXposed: Elder driver Xposed Framework.

EdXposed是Xposed框架的改进版,主要解决了Xposed在高版本Android系统上的兼容性问题。Xposed通过替换核心文件app_process工作,这种方式在新的Android系统中容易冲突,导致不稳定甚至崩溃。而EdXposed则基于Riru项目,通过注入代码到zygote进程,避免了直接修改系统文件,从而提升了兼容性和稳定性。 两者功能相似,都允许安装模块扩展系统功能,但EdXposed在兼容性和稳定性方面显著优于Xposed。

LSPosed框架

LSPosed是EdXposed的进一步发展,旨在提供更高的兼容性和用户体验。与EdXposed类似,LSPosed也基于Riru项目,通过注入代码到Zygote进程来实现功能扩展,避免了直接修改系统文件的问题,从而提升了稳定性。

LSPosed在模块管理方面进行了优化,提供了更直观的用户界面,使得用户可以更方便地安装和管理模块。此外,LSPosed支持无root模式,降低了使用门槛,这个特点降低了用户的使用门槛,可以拉更多人入坑。

LSPosed仍然需要针对不同Android版本进行适配。

Zygote进程

上文中反复提到Zygote进程,这个进程是什么呢?

Zygote进程是Android系统中至关重要的一个进程,它是所有Android应用程序的父进程。它在系统启动时被初始化,预加载所有应用可能用到的核心类和资源到内存中,从而在需要启动新应用时,通过fork自身快速创建新进程,继承Zygote进程的内存空间,实现高效的应用启动。这利用了Linux内核的写时复制技术,提升效率并节省内存。

Zygote进程还会显式启动System Server进程,后者负责初始化和管理各种系统服务。 由于Android系统版本差异和厂商定制,app_process的结构和行为可能变化,这影响了需要修改app_process的框架(如Xposed)的稳定性。EdXposed通过Riru项目注入zygote进程,避免直接修改app_process,从而提升了兼容性和稳定性。

Riru

先贴仓库链接:GitHub - RikkaApps/Riru: Inject into zygote process,不过两年前就归档了。

Riru是一个Magisk模块,它允许将代码注入zygote进程,而不会直接修改系统文件。这使得它在不同Android版本和厂商定制系统上具有更好的兼容性。

Riru与Android的关系在于它利用Android系统的zygote进程机制运行代码,通过系统接口或漏洞注入代码,影响zygote进程及其子进程(所有应用)的行为。Riru和Xposed都可修改应用行为,但实现方式不同:Xposed替换app_process,而Riru注入zygote进程,后者更安全、兼容性更好。

前面说到的EdXposed框架则依赖Riru实现功能,利用Riru提供的机制注入zygote进程,避免直接修改系统文件,从而提高稳定性和兼容性。EdXposed可视为基于Riru构建的Xposed框架替代方案,Riru提供底层机制,EdXposed在其上构建更易用的框架。

ok,先介绍到这里,更进一步的实现原理可以看这篇文档,想必会收获颇丰:riru-docs/riru模块解析.md at main · AlienwareHe/riru-docs · GitHub

安装LSPosed

LSPosed仓库release地址:Releases · LSPosed/LSPosed,安装LSPosed-Zygisk模块即可,下载zip推送到设备,然后在Magisk中作为模块安装。安装后重启。

我前面已经配了基于Zygisk的Shamiko模块,如果切换为Riru会与Zygisk冲突,所以这里直接选择LSPosed-Zygisk模块。

重启后,通知栏会弹出“LsPosed已加载”,点击即可进入管理界面(如果没显示,可以通过拨号键输入 *#*#5776733#*#* 进入LSPosed)。

进入LSPosed App,设置 – 创建快捷方式 – 关闭 状态通知 – 显示已激活,代表已成功刷入LSPosed框架。

image.png

编写Xposed模块

刚开始找到的教程很老,各种奇奇怪怪问题一堆,后面边翻文档边修,也算是磕磕绊绊写出来第一个cposed模块,实现的功能很简单,就是监测应用的启动,检测到应用启动就打印一条日志。

晚上的很多类似的教程,但有一点不好,就是不解释为什么要怎么写,我比较呆,写完之后会搜搜为什么要这么做,这里也一并附上,所以后面的内容会有点啰嗦。

AndroidStudio版本为2024.2,设备版本为Oneplus Ace2(Android14),项目选择的是Kotlin DSL

开发xposed模块,本质上和开发android模块是一样的,区别在于:

  • 让LSPosed知道我们安装的这个程序是个xposed模块;
  • 模块里要包含有xposed的API的jar包,以实现下一步的hook操作;
  • 这个模块里面要有对目标程序进行hook操作的方法;
  • 要让手机上的xposed框架知道,我们编写的xposed模块中,哪一个方法是实现hook操作的,也就是hook类的入口。

Android Studio新建项目

Android Studio新建空项目,操作流程见我的上一篇文章:浅试Android开发,不同的是开发app可以选择empty activity,开发xposed模块推荐选择no activity(至少这个例子是这样的)。

选择 “Empty Activity” 模板时,Android Studio 会为你创建一个包含基本活动(Activity)和布局文件的项目,而对下面这个xposed模块来说,这些都是不必要的,所以此处选择no activity即可,干净的项目结构也方便操作。

settings.gradle添加xposed框架依赖

项目根目录下的settings.gradle中添加一行

1
2
3
4
5
6
7
8
dependencyResolutionManagement {  
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url 'https://api.xposed.info/' } // 添加这一行即可
}
}

Xposed模块需要依赖Xposed框架的库,而这些库不在默认的Maven仓库,通过添加 Xposed 的Maven仓库,确保Gradle能够找到并下载所需的Xposed相关依赖。

模块级build.gradle添加Xposed Framework API

将Xposed框架所需的API库添加到项目中,以便你的模块能够使用Xposed提供的功能,例如hook。在app/build.gradle文件的dependencies段中添加以下代码:

1
2
3
4
dependencies {
compileOnly("de.robv.android.xposed:api:82")
compileOnly("de.robv.android.xposed:api:82:sources")
}

其中complieOnly表示只在编译时使用这些库,最终生成的APK文件中不会包含这些库,从而减小APK大小;de.robv.android.xposed:api:82是Xposed API库,82代表版本号,你需要根据你使用的EdXposed版本选择合适的版本号,如果版本号不匹配,模块可能无法正常工作;de.robv.android.xposed:api:82:sources包含Xposed API库的源代码,方便调试和理解API的实现细节。

这里可以不必理会版本号,直接cv即可。

arrays.xml添加模块作用域

路径为app/src/main/res/values/,在该目录下创建arrays.xml文件,内容如下:

1
2
3
4
5
6
<resources>  
<string-array name="xposedscope" >
<!-- 这里填写模块的作用域应用的包名,可以填多个。 -->
<item>com.xposed.demo</item>
</string-array>
</resources>

模块作用域的主要目的是告诉Xposed框架,哪些应用程序是该模块可以影响的目标。通过在arrays.xml中指定应用的包名,Xposed框架能够在这些应用启动时加载你的模块,从而实现对这些应用的hook操作。

AndroidManifest.xml中添加meta信息

目的是告诉LSPosed框架,这个应用程序是一个Xposed模块,并提供模块的描述信息和最低API版本要求。在AndroidManifest.xml文件的<application>标签内添加以下元数据:

1
2
3
4
5
6
7
8
<!-- 是否是xposed模块 -->
<meta-data android:name="xposedmodule" android:value="true" />
<!-- 模块描述 -->
<meta-data android:name="xposeddescription" android:value="这是一个lsxposed demo" />
<!-- 最低xposed版本号 -->
<meta-data android:name="xposedminversion" android:value="82" />
<!-- 模块作用域 -->
<meta-data android:name="xposedscope" android:resource="@array/xposedscope"/>
  • xposedmodule="true"声明这是一个Xposed模块。
  • xposeddescription模块的描述,会在Xposed Installer中显示。
  • xposedminversion模块所需的最低Xposed API版本。 这个版本号应该与你设备上安装的EdXposed版本兼容,或者低于该版本。
  • 作用域在上一小标题解释过了。

编写Hook代码

在MainActivity同级目录创建一个名为HookDemo.kt的类,实现IXposedHookLoadPackage接口:

1
2
3
4
5
6
7
8
9
10
11
package com.xposed.demo.hookdemo;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class MainHook implements IXposedHookLoadPackage {
public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable {
XposedBridge.log("Loaded app: " + lpparam.packageName);
}
}

默认读者有kotlin基础,薄弱的话可以看看官方文档:基本语法 · Kotlin 官方文档 中文版,这里主要介绍引用的模块,首次接触会比较陌生。

  • IXposedHookLoadPackage:一个接口,允许开发者实现应用加载时的hook逻辑。通过实现这个接口,模块可以在特定应用被加载时执行自定义代码。
  • XposedBridge:Xposed框架的核心类,提供了多种方法来进行hook和日志记录等操作。
  • XC_LoadPackage.LoadPackageParam:一个类,包含了关于加载的应用程序的信息,例如包名、类加载器等。

然后是MainHook类的介绍,其实有上文的包介绍就差不多清楚了:

  • handleLoadPackage是接口IXposedHookLoadPackage中定义的方法。当一个应用被加载时,Xposed框架会调用这个方法。
  • LoadPackageParam lpparam参数包含了被加载应用的相关信息。
  • XposedBridge.log(...)方法用于记录日志,这里记录了加载的应用的包名。通过查看Xposed的日志,开发者可以看到哪些应用被加载了。

指定Hook入口

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

1
com.xposed.demo.hookdemo.MainHook

安装并测试

以上内容全部搞定之后就可以build项目为apk安装包了,打包出来后可以先使用debug版本的安装包,可以使用adb install 绝对路径直接安装到设备上

1
adb install C:\Users\{YOUR_USERNAME}\AndroidStudioProjects\HookDemo\app\build\outputs\apk\debug\app-debug.apk

安装好之后在LSPosed中启用该HookDemo模块,勾选应用的app,然后重启手机应用个更改,然后使用数据线连接设备和电脑后,在电脑命令行中监听带有特征值的日志信息(其实就是拼接的特殊字符串),我们的Hook代码中是Loaded app

1
adb logcat | Select-String "Loaded app"

开始监听后在移动设备上启动对应app,即可观察到日志信息。


我本来是想做个移动端智能助手,读系统消息以及检测部分应用的使用情况,喂给预设好的AI再给出一些反馈什么的,结果写xposed模块的时候发现普通android app就可以实现读取系统通知栏里通知的功能,hook反而没必要了😭。

不过也好,有个契机了解了点Android Hook的知识。如果智能助手的项目能接着往下推的话,Hook相关知识也不是完全用不上,比如当我点开某些应用时可以给出反馈,也不错~