安卓端自行实现工信部要求的隐私合规检测一(教你手写Xposed模块代码)

前言

友情提示:文章较长,源码及相关使用教程都在文尾。

之所以写这篇文章,是因为不久前,我们公司上架的app被打回来了。信通院那边出了个报告,里面说我们app未经授权就自动获取了手机的mac地址。当时其实是有点懵逼的,因为合规措施其实是已经做过了的,为什么还会出现这种情况呢?仔细看了一眼报告,发现了端倪:

安卓端自行实现工信部要求的隐私合规检测一(教你手写Xposed模块代码)_第1张图片
出问题的是getHardwareAddress的调用。然后调用者是:cn.jiguang
这货不就是极光推送吗,那么应该是极光推送的调用部分出了问题。后来排查到JPushInterface.getRegistrationID这个方法在调用的时候就会走初始化那步操作,进而调用获取一堆用户隐私信息的方法。
问题发现了,也找到了源头。但是被工信部打回来,其实是很耗费时间的。来回检测上架少说也有小半个月了。那么就必须要找到一种相对稳定的方法来预防。最初我们打算的是用网上的检测公司来进行检测。但是一听到我们公司是大型企业后,要价都很高。淘宝上只给检测报告不提供解决方法的单次最低都要1800元,网上的大型检测公司给出的价格是30000元/次,包年20万。上级认为这笔开销比较大,而且检测只要第一次通过,后面其实没啥大改的。所以想让我自行研究出一套检测方案。
经过调研,目前开源的检测方案当属使用xposed框架加上自定义的xposed检测模块进行检测最为有效。那么,本篇文章就来讲解一下如何编写xposed模块进行合规检测。

关于Xposed

安卓端自行实现工信部要求的隐私合规检测一(教你手写Xposed模块代码)_第2张图片

在进入正篇之前,我们有必要了解一下我们的主角,Xposed到底是个什么东西?
首先,这个框架在github上面是完全开源的,作者是rovo89,看名字这老哥是89年生的,而且他的主页很简洁,都是关于xposed的(包括头像)。目前我前段佩服的大神共有3人,rovo89算一个。还有两个分别是vue的作者尤雨溪和ButterKnife的作者JakeWharton(不服来辩)
百度百科的解释是:

Xposed框架(Xposed Framework)是一套开源的、在Android高权限模式下运行的框架服务,
可以在不修改APK文件的情况下影响程序运行(修改系统)的框架服务,
基于它可以制作出许多功能强大的模块,且在功能不冲突的情况下同时运作。

官方解释一般都是听得云里雾里。还好网上有大神透彻得研究过这个框架,请看这里。
框架核心就是将java的方法映射为JNI方法。JNI这个东西,搞过安卓的同学应该很熟悉了。用于链接C的底层交互接口。当然这个讨论下去就又可以出一篇大文章了(当然本人对这块不是很熟悉,有兴趣的朋友可以自行度娘)。
话说回来上面的介绍都还太专业(难懂)了。我用我自己的总结来说就是,以AOP的思想对想要监测的方法进行代理,从而实现自己想要做的事情。实际上,xposed本身就是个AOP框架(对于AOP不太懂得同学也可以自行度娘)。在这篇文章的话,大家只需要清楚,这个东西可以改系统的默认行为就行了。
安卓上要安装Xposed框架就要用到Xposed Installer。由于Xposed修改了系统文件,那么势必是需要手机root的。但是随着安卓版本的不断升级,对root的要求也越来越苛刻。而且root的一个前提就是需要BL解锁码。如果手机厂商提供这个码,那还好说,要是不提供,那就悲剧了。于是乎,除了正统的Xposed,聪明的猿猿们还开发出了一些魔改版本:

魔改版本 是否需要root 描述
Dexposed 阿里巴巴根据Xposed源码进行修改的无需root就能使用的Xposed框架
EdXposed Xposed的安卓高版本系统兼容框架,支持安卓8~10
VirtualXposed 以安装虚拟机的方式安装Xposed框架,真机本身不需要root

本文将会讲解如何在原生Xposed和VirtualXposed两种模式下进行合规检测。

编写Xposed模块

了解完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类

编写代码

假如我们需要监测的方法是:
安卓端自行实现工信部要求的隐私合规检测一(教你手写Xposed模块代码)_第3张图片
那么,我们的初始方法就可以写成这个样子:

public class HookTrack implements IXposedHookLoadPackage {
    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) {
        
    }
}

handleLoadPackage中,调用XposedHelpers类的findMethodHook来进行,在写代码的时候,我们发现其实有两个方式可以选用:
安卓端自行实现工信部要求的隐私合规检测一(教你手写Xposed模块代码)_第4张图片
区别在于第一个方法传入的是class本体,然后源码那边使用的classLoader就是class.getClassLoader;第二种不需要class本体,只需要指定这个class的名字,然后再指定加载这个class的classLoader。从便捷上来说,第一种无疑是便捷的。但是第二种的灵活度比第一种高。假如有一些类是第三方SDK里面的,而这个SDK没在你源码里面,是以插件形式在你app安装完后才加进来的。这时候,你在编码阶段是没有办法得到这个class本体的,所以第二种方法可以看作是能hook运行时的class,并且官方注释还给出了第二种的使用模式:
安卓端自行实现工信部要求的隐私合规检测一(教你手写Xposed模块代码)_第5张图片
因此,按照官方提供的思路,我们可以这样写:

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,
安卓端自行实现工信部要求的隐私合规检测一(教你手写Xposed模块代码)_第6张图片
首先,这是一个抽象类,不是接口。beforeHookMethodafterHookMethod从字面意思也能看出是在hook前后的调用回调。然后其构造函数有两个,有一个是带int类型的,传入的是一个设置hook优先级的数字。
安卓端自行实现工信部要求的隐私合规检测一(教你手写Xposed模块代码)_第7张图片
从方法注释上看,这个priority会影响后面beforeHookMethodafterHookMethod的调用顺序。优先级越高的Hook,其beforeHook方法会越先执行,然后其afterHook方法会在最后执行。如果存在hook多个方法,且所有的priority都相同,会依次此执行完这个方法的before和after在执行下一个方法的before和after,以此类推。
而采用无参构造的,其priority是一个系统默认值50:
安卓端自行实现工信部要求的隐私合规检测一(教你手写Xposed模块代码)_第8张图片
假如我们Hook了3个方法A,B,C。在priority相同和不同时的调用关系可以参考下图:

安卓端自行实现工信部要求的隐私合规检测一(教你手写Xposed模块代码)_第9张图片
知道了上面的原理后,我们就应该选用默认或者相同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 {
                        //在这里写调用方法栈过程
                    }
                }
        );

日志打印的话自然还是用到XposedBridgelog方法。由于我们需要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信息


<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管理器上面:
安卓端自行实现工信部要求的隐私合规检测一(教你手写Xposed模块代码)_第10张图片
注意:填写meta信息是标记我们这个apk是个xposed模块的关键,否则xposed installer不会识别。

2、在项目asset文件夹下面新建xposed_init文件
安卓端自行实现工信部要求的隐私合规检测一(教你手写Xposed模块代码)_第11张图片
在里面写上我们实现IXposedHookLoadPackage那个类的包名+类名

com.cjs.hegui30.HookTrack

这样我们就写好了自定义的xposed模块。Xposed在加载的时候会从这个文件里面读取需要初始化的类。
至此,我们的所有代码就编写完成了,此时装在手机后,可以在xposed installer里面识别激活了。

下载

源码已上传到:github和gitee
github被墙的同学可以到站内下载
源码同时捆绑了一个快速测试的demo和相关的apk文件,demo可以单独编译成apk,记得切换
安卓端自行实现工信部要求的隐私合规检测一(教你手写Xposed模块代码)_第12张图片
安卓端自行实现工信部要求的隐私合规检测一(教你手写Xposed模块代码)_第13张图片

如何检测

参考《安卓端自行实现工信部要求的隐私合规检测二(使用Xposed/VirtualXposed进行监测)》

Bug修复&Dmo更新

2021-09-29

有一些小伙伴评论说requestLocationUpdates这个方法监听失败。后面经过测试发现我在之前的案例中把,该方法的参数个数搞错了,你们在AS面能看见7个重写方法:
安卓端自行实现工信部要求的隐私合规检测一(教你手写Xposed模块代码)_第14张图片
我的demo里只写了一个String类型的,自然会找不到。
另外需要注意的是,如果参数是基础类型,不要使用封装类型的class,否则找不到方法:

	XposedHelpers.findAndHookMethod(
                LocationManager.class.getName(),
                lpparam.classLoader,
                "requestLocationUpdates",
                String.class,
                long.class,//注意,如果是基础类型,不要使用其对应的包装类,否则会找不到这个方法
                float.class,
                LocationListener.class,
                new DumpMethodHook() {
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) {
                        XposedBridge.log(lpparam.packageName + "调用requestLocationUpdates获取了GPS地址");
                    }
                }
        );

   XposedHelpers.findAndHookMethod(
           LocationManager.class.getName(),
           lpparam.classLoader,
           "requestLocationUpdates",
           String.class,
           long.class,
           float.class,
           LocationListener.class,
           Looper.class,
           new DumpMethodHook() {
               @Override
               protected void beforeHookedMethod(MethodHookParam param) {
                   XposedBridge.log(lpparam.packageName + "调用requestLocationUpdates获取了GPS地址");
               }
           }

源码及实例部分也已经更新,可在github下载体验,大家可以继续放心食用,也欢迎多多在评论区留言。

你可能感兴趣的:(安卓,android,xposed,virtual,xposed,合规检测)