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

前言

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

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

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

关于Xposed

在这里插入图片描述

在进入正篇之前,我们有必要了解一下我们的主角,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框架
EdXposedXposed的安卓高版本系统兼容框架,支持安卓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类

编写代码

假如我们需要监测的方法是:
在这里插入图片描述
那么,我们的初始方法就可以写成这个样子:

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,
在这里插入图片描述
首先,这是一个抽象类,不是接口。beforeHookMethodafterHookMethod从字面意思也能看出是在hook前后的调用回调。然后其构造函数有两个,有一个是带int类型的,传入的是一个设置hook优先级的数字。
在这里插入图片描述
从方法注释上看,这个priority会影响后面beforeHookMethodafterHookMethod的调用顺序。优先级越高的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 {
                        //在这里写调用方法栈过程
                    }
                }
        );

日志打印的话自然还是用到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的一些方法:

方法名字所属包名作用
getDeviceIdandroid.telephony.TelephonyManager获取设备号
getDeviceId(int)android.telephony.TelephonyManagergetDeviceId的带参版本
getImeiandroid.telephony.TelephonyManager安卓8增加的获取IMEI的方法
getImei(int)android.telephony.TelephonyManagergetImei的带参版本
getSubscriberIdandroid.telephony.TelephonyManager获取IMSI
getMacAddressandroid.net.wifi.WifiInfo获取MAC地址
getHardwareAddressjava.net.NetworkInterface获取MAC地址
getStringandroid.provider.Settings.Secure获取系统相关信息字符来拼接deviceId
getLastKnownLocationLocationManager获取GPS定位信息
requestLocationUpdatesLocationManager位置、时间发生改变的时候获取定位信息
上面的方法信息可能不全,如果大家有更好的意见可以留言。我看网上很多资料是没有对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里面识别激活了。

下载

源码已上传到:githubgitee
github被墙的同学可以到站内下载
源码同时捆绑了一个快速测试的demo和相关的apk文件,demo可以单独编译成apk,记得切换
在这里插入图片描述
在这里插入图片描述

如何检测

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

Bug修复&Dmo更新

2021-09-29

有一些小伙伴评论说requestLocationUpdates这个方法监听失败。后面经过测试发现我在之前的案例中把,该方法的参数个数搞错了,你们在AS面能看见7个重写方法:
requestLocation
我的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下载体验,大家可以继续放心食用,也欢迎多多在评论区留言。

评论 59
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值