Hegui3.0
了解完 Xposed 框架的相关知识后,我们还要编写一些模块代码,才能实现我们的监测操作。 首先在 gradle 里面依赖一下 xposed 的 api:
compileOnly 'de.robv.android.xposed:api:82'
compileOnly 'de.robv.android.xposed:api:82:sources'
在进行 Xposed 模块开发之前,我们有必要了解一下Xposed API。完成一个模块的开发至少有两步要做:
1、编写一个 java 类并实现**IXposedHookLoadPackage**接口,实现**handleLoadPackage**方法进行自定义的监测操作
2、注册这个 java 类
编写代码
假如我们需要监测的方法是:
那么,我们的初始方法就可以写成这个样子:
public class HookTrack implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) {
}
}
在handleLoadPackage中,调用XposedHelpers类的findMethodHook来进行,在写代码的时候,我们发现其实有两个方式可以选用:
区别在于第一个方法传入的是 class 本体,然后源码那边使用的 classLoader 就是class.getClassLoader;第二种不需要 class 本体,只需要指定这个 class 的名字,然后再指定加载这个 class 的 classLoader。从便捷上来说,第一种无疑是便捷的。但是第二种的灵活度比第一种高。假如有一些类是第三方 SDK 里面的,而这个 SDK 没在你源码里面,是以插件形式在你 app 安装完后才加进来的。这时候,你在编码阶段是没有办法得到这个 class 本体的,所以第二种方法可以看作是能 hook 运行时的 class,并且官方注释还给出了第二种的使用模式:
因此,按照官方提供的思路,我们可以这样写:
XposedHelpers.findAndHookMethod(
android.telephony.TelephonyManager.class.getName(),
lpparam.classLoader,
"getDeviceId",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(lpparam.packageName + "调用 getDeviceId()获取了 imei");
}
}
);
注意到我们最后的那个回调函数XC_methodHook,
首先,这是一个抽象类,不是接口。beforeHookMethod和afterHookMethod从字面意思也能看出是在 hook 前后的调用回调。然后其构造函数有两个,有一个是带 int 类型的,传入的是一个设置 hook 优先级的数字。
从方法注释上看,这个 priority 会影响后面beforeHookMethod和afterHookMethod的调用顺序。优先级越高的 Hook,其 beforeHook 方法会越先执行,然后其 afterHook 方法会在最后执行。如果存在 hook 多个方法,且所有的 priority 都相同,会依次此执行完这个方法的 before 和 after 在执行下一个方法的 before 和 after,以此类推。
而采用无参构造的,其 priority 是一个系统默认值 50:
假如我们 Hook 了 3 个方法 A,B,C。在 priority 相同和不同时的调用关系可以参考下图:
知道了上面的原理后,我们就应该选用默认或者相同 priority 的方式来进行 hook。
扯了这么多,大家也别嫌麻烦,工欲善其事,必先利其器。现在再回到之前的代码。我们在 beforeHookMethod 里面调用了
XposedBridge.log(lpparam.packageName + "调用 getDeviceId()获取了 imei");
XposedBridge 也是 rovo89 开发的一个 Xposed 的辅助库,调用其 log 方法后可以在手机端的 Xposed 管理器里面显示相关信息,这一步的意思表示我们监测了 app 调用 android.telephony.TelephonyManager 这个类的 getDeviceId 方法
打印方法调用栈
上面的所有操作知识标记了调没调用指定的方法。但是如果调用了,是谁调用的,其实我们时不清楚的。这样其实不利于我们查找问题的根源。回看本文的第一张信通院的图,发现他们检测时,其实给了方法调用栈。那么我们现在就来模拟一下这种操作。 我们需要打印的是整个 hook 期间的方法栈,那么这个操作就应该放在 afterHookMethod 里面,于是,我们可以写成这样:
XposedHelpers.findAndHookMethod(
android.telephony.TelephonyManager.class.getName(),
lpparam.classLoader,
"getDeviceId",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(lpparam.packageName + "调用 getDeviceId()获取了 imei");
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
//在这里写调用方法栈过程
}
}
);
日志打印的话自然还是用到XposedBridge的log方法。由于我们需要 hook 的方法不止一个,而我们打印方法调用栈又是一样的操作,于是乎我们可以自己写一个抽象类继承XC_MethodHook,只实现afterMethodHook方法,在里面做统一的方法栈追踪操作。因此,我们先自定义一个DumpMethodHook的类,代码如下:
public abstract class DumpMethodHook extends XC_MethodHook {
/**
* 该方法会在 Hook 了指定方法后调用
* @param param
*/
@Override
protected void afterHookedMethod(MethodHookParam param) {
//在这里,我们 dump 一下调用的方法栈信息
dump2();
}
/**
* dump 模式一:根据线程进行过滤
*/
private static void dump() {
for (Map.Entry<Thread, StackTraceElement[]> stackTrace : Thread.getAllStackTraces().entrySet()) {
Thread thread = (Thread) stackTrace.getKey();
StackTraceElement[] stack = (StackTraceElement[]) stackTrace.getValue();
// 进行过滤
if (thread.equals(Thread.currentThread())) {
continue;
}
XposedBridge.log("[Dump Stack]" + "**********线程名字:" + thread.getName() + "**********");
int index = 0;
for (StackTraceElement stackTraceElement : stack) {
XposedBridge.log("[Dump Stack]" + index + ": " + stackTraceElement.getClassName()
+ "----" + stackTraceElement.getFileName()
+ "----" + stackTraceElement.getLineNumber()
+ "----" + stackTraceElement.getMethodName());
}
// 增加序列号
index++;
}
XposedBridge.log("[Dump Stack]" + "********************* over **********************");
}
/**
* dump 模式 2:类信通院报告模式
*/
private static void dump2(){
XposedBridge.log("Dump Stack: "+"---------------start----------------");
Throwable ex = new Throwable();
StackTraceElement[] stackElements = ex.getStackTrace();
if (stackElements != null) {
for (int i= 0; i < stackElements.length; i++) {
StringBuilder sb=new StringBuilder("[方法栈调用]");
sb.append(i);
XposedBridge.log("[Dump Stack]"+i+": "+ stackElements[i].getClassName()
+"----"+stackElements[i].getFileName()
+"----" + stackElements[i].getLineNumber()
+"----" +stackElements[i].getMethodName());
}
}
XposedBridge.log("Dump Stack: "+ "---------------over----------------");
}
}
通过查询资料,我写了两种方法栈打印的操作。第一种打印得比较细一些,但是实际测试要卡顿一点。第二种就和信通院报告差不多了,而且没有明显卡顿。 写好了自定义的回调,这时我们只需要将前面的 XC_MethodHook 替换为 DumpMethodHook 即可:
XposedHelpers.findAndHookMethod(
android.telephony.TelephonyManager.class.getName(),
lpparam.classLoader,
"getDeviceId",
new DumpMethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(lpparam.packageName + "调用 getDeviceId()获取了 imei");
}
}
);
需要监测的方法
既然合规这件事情是工信部搞出来的,那么我们自然要看一下当时的这份红头文件——工信部信管函「164 号文」 下面是我目前整理出来的需要 hook 的一些方法: |方法名字|所属包名 |作用| |--|--|--| |getDeviceId| android.telephony.TelephonyManager| 获取设备号| |getDeviceId(int)| android.telephony.TelephonyManager| getDeviceId 的带参版本| |getImei| android.telephony.TelephonyManager| 安卓 8 增加的获取 IMEI 的方法| |getImei(int)| android.telephony.TelephonyManager| getImei 的带参版本| |getSubscriberId| android.telephony.TelephonyManager| 获取 IMSI| |getMacAddress| android.net.wifi.WifiInfo| 获取 MAC 地址| |getHardwareAddress| java.net.NetworkInterface| 获取 MAC 地址| |getString| android.provider.Settings.Secure| 获取系统相关信息字符来拼接 deviceId| |getLastKnownLocation| LocationManager| 获取 GPS 定位信息| |requestLocationUpdates| LocationManager| 位置、时间发生改变的时候获取定位信息| 上面的方法信息可能不全,如果大家有更好的意见可以留言。我看网上很多资料是没有对requestLocationUpdates和安卓 8 的新增方法getImei进行监控的,这里我加了进来。
对 Hook 的 APP 进行过滤,设置白名单
一般来讲,你的手机安装的不止一个 app。如果用上面的代码去监测,实际会监测你手机上所有的 app。这就导致日志会很杂乱,我们其实只关心指定的 app。因此我们需要设置一个白名单进行过滤:
/**
* 需要 Hook 的包名白名单
*/
private static final String[] whiteList = {
"com.cjs.drv",
"com.cjs.hegui30.demo"
};
里面填写的就是你需要监测的 app 的包名。 然后我们在 HandleLoadPackage 方法的最开始,写一段过滤的操作:
/*判断 hook 的包名*/
boolean res = false;
for (String pkgName : whiteList) {
if (pkgName.equals(lpparam.packageName)) {
res = true;
break;
}
}
if (!res) {
Log.e(TAG, "不符合的包:" + lpparam.packageName);
return;
}
最终,贴上一个成品的代码:
public class HookTrack implements IXposedHookLoadPackage {
private static final String TAG = "HookTrack";
/**
* 需要 Hook 的包名白名单
*/
private static final String[] whiteList = {
"com.cjs.drv",
"com.bw30.zsch",
"com.bw30.zsch.magic",
"com.cjs.hegui30.demo"
};
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) {
if (lpparam == null) {
return;
}
Log.e(TAG, "开始加载 package:" + lpparam.packageName);
/*判断 hook 的包名*/
boolean res = false;
for (String pkgName : whiteList) {
if (pkgName.equals(lpparam.packageName)) {
res = true;
break;
}
}
if (!res) {
Log.e(TAG, "不符合的包:" + lpparam.packageName);
return;
}
//固定格式
XposedHelpers.findAndHookMethod(
android.telephony.TelephonyManager.class.getName(), // 需要 hook 的方法所在类的完整类名
lpparam.classLoader, // 类加载器,固定这么写就行了
"getDeviceId", // 需要 hook 的方法名
new DumpMethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(lpparam.packageName + "调用 getDeviceId()获取了 imei");
}
}
);
XposedHelpers.findAndHookMethod(
android.telephony.TelephonyManager.class.getName(),
lpparam.classLoader,
"getDeviceId",
int.class,
new DumpMethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(lpparam.packageName + "调用 getDeviceId(int)获取了 imei");
}
}
);
XposedHelpers.findAndHookMethod(
android.telephony.TelephonyManager.class.getName(),
lpparam.classLoader,
"getSubscriberId",
int.class,
new DumpMethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(lpparam.packageName + "调用 getSubscriberId 获取了 imsi");
}
}
);
XposedHelpers.findAndHookMethod(
android.telephony.TelephonyManager.class.getName(),
lpparam.classLoader,
"getImei",
new DumpMethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(lpparam.packageName + "调用 getImei 获取了 imei");
}
}
);
XposedHelpers.findAndHookMethod(
android.telephony.TelephonyManager.class.getName(),
lpparam.classLoader,
"getImei",
int.class,
new DumpMethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(lpparam.packageName + "调用 getImei(int)获取了 imei");
}
}
);
XposedHelpers.findAndHookMethod(
android.net.wifi.WifiInfo.class.getName(),
lpparam.classLoader,
"getMacAddress",
new DumpMethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(lpparam.packageName + "调用 getMacAddress()获取了 mac 地址");
}
}
);
XposedHelpers.findAndHookMethod(
java.net.NetworkInterface.class.getName(),
lpparam.classLoader,
"getHardwareAddress",
new DumpMethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(lpparam.packageName + "调用 getHardwareAddress()获取了 mac 地址");
}
}
);
XposedHelpers.findAndHookMethod(
android.provider.Settings.Secure.class.getName(),
lpparam.classLoader,
"getString",
ContentResolver.class,
String.class,
new DumpMethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(lpparam.packageName + "调用 Settings.Secure.getstring 获取了" + param.args[1]);
}
}
);
XposedHelpers.findAndHookMethod(
LocationManager.class.getName(),
lpparam.classLoader,
"getLastKnownLocation",
String.class,
new DumpMethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(lpparam.packageName + "调用 getLastKnownLocation 获取了 GPS 地址");
}
}
);
XposedHelpers.findAndHookMethod(
LocationManager.class.getName(),
lpparam.classLoader,
"requestLocationUpdates",
String.class,
new DumpMethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log(lpparam.packageName + "调用 requestLocationUpdates 获取了 GPS 地址");
}
}
);
}
}
注册模块代码
上面的操作到目前为止也只是在你的安卓项目中添加了一个 java 类。如何让 xposed 识别到我们写的代码是个 xposed 模块呢?这就需要注册一下这个类。 注册分两步操作: 1、在 AndroidManifest.xml 中编写 meta 信息
<!-- 标志该 apk 为一个 Xposed 模块,供 Xposed 框架识别-->
<meta-data
android:name="xposedmodule"
android:value="true" />
<!--模块说明,一般为模块的功能描述-->
<meta-data
android:name="xposeddescription"
android:value="这个模块是用来检测用户隐私合规的,在用户未授权同意前,调用接口获取信息属于违规" />
<!--模块兼容版本-->
<meta-data
android:name="xposedminversion"
android:value="54" />
在 application 节点里面加上这三个 meta 信息。那个说明会最终显示在 xposed 管理器上面:
注意:填写 meta 信息是标记我们这个 apk 是个 xposed 模块的关键,否则 xposed installer 不会识别。
2、在项目asset文件夹下面新建xposed_init文件
在里面写上我们实现 IXposedHookLoadPackage 那个类的包名+类名
com.cjs.hegui30.HookTrack
这样我们就写好了自定义的 xposed 模块。Xposed 在加载的时候会从这个文件里面读取需要初始化的类。 至此,我们的所有代码就编写完成了,此时装在手机后,可以在 xposed installer 里面识别激活了。
其他
源码同时捆绑了一个快速测试的 demo 和相关的 apk 文件,demo 可以单独编译成 apk,记得切换

操作手册
一、准备条件
1、编译合规检测的 Xposed 模块源码
下载源码,修改设置白名单,编译成 apk,安装到手机 相关操作参考《安卓端自行实现工信部要求的隐私合规检测一(教你手写 Xposed 模块代码)》
注意:源码里面包含了各种安装包及 demo
2、已经 root 的手机可以下载 Xposed.apk
Xposed 在 github 上面开源,可以自己下载XposedInstaller的源码进行编译,也可以直接下载已经编译好的 apk。
需要注意的是,安卓 5 以上和以下的安装版本是不一样的 支持安卓 5 及以上的 XposedInstaller 支持安卓 5 以下的 XposedInstaller
3、没有 root 的手机可以下载 VirtualXposed.apk
VirtualXposed 在 github 上面有专门的release页面,其作者在 0.20.x 的版本的时候放弃了对 32 位应用的支持,理由是谷歌商店未来只允许 64 位的 app 上架,不想花更多精力维护 32 位的开发。如果你的 app 为 32 位应用,因此不能用 0.20.x 及之后版本的 VirtualXposed
0.20.3 版本(支持 64 位应用) 0.18.2 版本(支持 32 位应用) 注意:由于 0.20.x 版本更换了包名,所以它和 0.18.x 的版本能同时安装
4、下载合规检测测试程序
该项非必须,仅作为合规快速校验
> 下载地址:合规检测测试程序 apk
二、具体操作
这里以对“合规检测测试程序“的校验进行说明。
1、安装
- 已 root 的用户
1. 安装 xposed.apk
在 root 手机上安装 xposed 框架的操作可能会比较麻烦,详情问问度娘
2. 安装合规检测 xposed 模块.apk
3. 安装合规检测测试程序.apk
- 未 root 用户
1. 先安装 virtual-xposed.apk
2. 后续操作有两种方案:
第一种可以和已 root 用户一样,直接将合规检测 xposed 模块.apk 和合规检测测试程序.apk 安装在手机真机上,然后在 virtual-xposed 里面克隆这两个 app;第二种的话就是不在真机装,直接在 virtual-xposed 里面装,具体操作如下:
打开virtual-xposed,点击底部的菜单按钮,进入到 virtual-xposed 的菜单界面
在菜单界面有两个关键点需要注意,一个是顶部的添加应用,一个是底部的重启。后者在激活模块的时候需要使用。这里我们先点击添加应用。
添加应用界面默认的 tab 项就是克隆 APP,如果使用克隆的方式的话,就要先找到安装的合规检测 xposed 模块和合规检测测试程序,并勾选,然后选择底部的安装;如果选择的是虚拟机安装,就直接点击右下角的加号按钮,选择本地的 apk 安装包进行安装。
在克隆 APP 中,会弹出是否使用太极安装,我们还是选择virtual-xposed 安装
安装完 app 后,下一步就是激活模块。我们返回到1中的界面,上滑,进入到 app 列表界面,在这里可以看见我们刚刚安装的 3 个 app
最右边的那边 Xposed Installer 就是我们的 xposed 控制界面 ,点击进入。大大的绿勾表示我们的 xposed 框架已经激活。点击左上角三杠。
在弹出来的菜单界面中有两个项要注意:模块用于安装/卸载xposed 模块,日志用于查看合规检测的结果,后面会用到。这里我们点击模块。
可以看见有一个合规检测的 xposed 模块,勾上它。勾上后需要重启系统。如果是 root 用户在真机下操作 xposed 框架,就需要真机重启。但是我们用的是 virtual-xposed,所以只需要重启虚拟机就行了。这时候,返回到2的界面,点击重启。
重启成功的话,底部会有提示语展示
2、如何检测
- 先打开我们安装的测试 app
里面有四个按钮,分别对应四种不同的检测条件,我们以第一种模拟获取 IMSI来说明,先别点击任何按钮。
![]()
- 查看日志
回到我们在安装讲解中的第7步界面,里面有个日志选项,点击它。该界面右上角有保存按钮,还有个三个点的菜单按钮,点击菜单按钮后有两个项需要注意:
- 立即清理日志会清空当前界面的所有日志,我们先点击清空一下。
- 重新载入会刷新最新的日志进界面。因为日志的显示不是自动的,要想看到最新的结果,就要手动刷新。
![]()
![]()
接下来回到我们的测试 app 界面,点击模拟获取 IMSI,然后再返回到日志界面,并且重新载入,接着就能看见如下的日志记录:

上面的截图主要针对 virtual-xposed 来讲的。对于 xposed 而言,操作大同小异。最大的区别就是 xposed 需要真实地重启手机,这点的话要麻烦一点。
