Hegui3.0

Project Url: ChenJunsen/Hegui3.0
Introduction: 工信部合规检测 Xposed 模块源码
More: Author   ReportBugs   
Tags:

了解完 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 里面装,具体操作如下:

  1. 打开virtual-xposed,点击底部的菜单按钮,进入到 virtual-xposed 的菜单界面

    图 1
  2. 在菜单界面有两个关键点需要注意,一个是顶部的添加应用,一个是底部的重启。后者在激活模块的时候需要使用。这里我们先点击添加应用。

    图 2
  3. 添加应用界面默认的 tab 项就是克隆 APP,如果使用克隆的方式的话,就要先找到安装的合规检测 xposed 模块和合规检测测试程序,并勾选,然后选择底部的安装;如果选择的是虚拟机安装,就直接点击右下角的加号按钮,选择本地的 apk 安装包进行安装。

    图 3
  4. 在克隆 APP 中,会弹出是否使用太极安装,我们还是选择virtual-xposed 安装

    图 4
  5. 安装完 app 后,下一步就是激活模块。我们返回到1中的界面,上滑,进入到 app 列表界面,在这里可以看见我们刚刚安装的 3 个 app

    图 5
  6. 最右边的那边 Xposed Installer 就是我们的 xposed 控制界面 ,点击进入。大大的绿勾表示我们的 xposed 框架已经激活。点击左上角三杠。

    图 6
  7. 在弹出来的菜单界面中有两个项要注意:模块用于安装/卸载xposed 模块,日志用于查看合规检测的结果,后面会用到。这里我们点击模块。

    图 7
  8. 可以看见有一个合规检测的 xposed 模块,勾上它。勾上后需要重启系统。如果是 root 用户在真机下操作 xposed 框架,就需要真机重启。但是我们用的是 virtual-xposed,所以只需要重启虚拟机就行了。这时候,返回到2的界面,点击重启。

    图 8
  9. 重启成功的话,底部会有提示语展示

    图 9-1 图 9-2

2、如何检测

  1. 先打开我们安装的测试 app

里面有四个按钮,分别对应四种不同的检测条件,我们以第一种模拟获取 IMSI来说明,先别点击任何按钮。

图 10
  1. 查看日志

回到我们在安装讲解中的第7步界面,里面有个日志选项,点击它。该界面右上角有保存按钮,还有个三个点的菜单按钮,点击菜单按钮后有两个项需要注意:

  • 立即清理日志会清空当前界面的所有日志,我们先点击清空一下。
  • 重新载入会刷新最新的日志进界面。因为日志的显示不是自动的,要想看到最新的结果,就要手动刷新。
    图 11-1 图 11-2

接下来回到我们的测试 app 界面,点击模拟获取 IMSI,然后再返回到日志界面,并且重新载入,接着就能看见如下的日志记录: 图 12

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