PrivacySentry

Introduction: Android 隐私合规检测,基于 Runtime-hook 的检测方案
More: Author   ReportBugs   
Tags:
android 隐私合规检测工具,可规避应用市场上架合规检测的大部分问题

群二维码

加作者个人微信,备注来意 PrivacySentry, 进社区群 image

更新日志

2023-09-18(1.3.4.2)
    1. 修复 Service 自启动的尝试
    2. 修复异常情况下,产物替换文件缺失的问题

2023-09-18(1.3.4)
    1. 修复内存缓存数据转换问题
    2. 增加部分 demo
    3. 修复 getSimState 闪退:https://github.com/allenymt/PrivacySentry/issues/116

2023-08-22(1.3.3-灰度版本)
    1. 重构 plugin 部分,引入 Boost, 适配 Agp 和 Gradle 高版本,支持 AGP7.0
    2. 尝试解决小米照明弹自启动的问题

2023-07-12(1.3.2)
    1. 对于 hook 的方法,内部不再 try catch

2023-04-18(1.3.1)
    1. 新增 wifiinfo.getIPAddress 代理
    2. 支持粘性数据,即使 sdk 初始化时机较晚,api 的调用记录也可以写入文件
    3. android 系统库不再 hook

 2023-04-14(1.2.9)
    1. ip 地址只做代理,不再拦截
    2. contentResolver 的方法,只做代理,不再拦截
    3. 兼容剪贴板写入空异常
    4. 修复同意隐私协议之前 ,代理的数据没有写入到文件的 bug issues/103

2023-02-21(1.2.8)
    1. 放开 package 信息读取
    2. 放开 bssid ,ssdid 的缓存,这个会导致腾讯定位出问题
    3. 修复地理位置缓存问题
    4. 放开 wifiEnable 缓存,不再走缓存判断

2023-01-06(1.2.7)
    1. 修复拦截 ip 地址时,主线程异常问题
    2. 默认关闭 debug 模式
    3. 优化部分逻辑
    4. 注意尽可能在 attachBaseContext 里第一个调用,因为 attachBaseContext 之后才能反射拿到 ActivityThread 的 application,所以如果是在 attachBaseContext 中,


2022-12-06(1.2.6.1)
    重构缓存模块,修复部分问题

2022-12-05(1.2.6)
    修复值转换的问题

2022-12-05(1.2.4)
    1. 修复 ClipboardManager.hasPrimaryClip 和 WifiManager.isWifiEnabled 拦截失败的问题
    2. 增加注解 PrivacyClassBlack,用于标记类不需要拦截

2022-11-15(1.2.3)
    1. 升级 asm 至 9.1 版本
    2. 支持类替换,主要是为了拦截构造函数的入参,比如对 File 的访问,这个功能还是试验期,增加了开关 hookConstructor
       详细的配置方法请参考 privacy-replace 这个 lib

2022-11-15(1.2.2)
    1. 放开 support androidx 目录下的类 hook
    2. 支持权限请求 hook(requestPermissions) https://github.com/allenymt/PrivacySentry/issues/75
    3. 修复部分多线程引起的数据同步问题
    4. 支持关闭插件的 hook 功能(感谢 runforprogram)

2022-11-02(1.2.1)
    更新的东西有点多,尽量测试和自测
    1. androidId 等不能只做内存缓存,还要磁盘缓存 
    2. 传感器信息加入到进程级别缓存
    3. 增加三种缓存,分别是内存缓存,时间单位的磁盘缓存,永久的磁盘缓存
    4. 设备名加入到不可变字段缓存,类似于 Android—id 一样
    5. 扩展存储 api,比如位置信息等,wifi 参数等,增加拦截 sim 卡状态,sim 卡操作码
    6. 增加剪切板读取开关,对应到合规库加一个全局开关
    7. 修复 SHA-256 digest error 问题, https://github.com/allenymt/PrivacySentry/issues/29
    8. 修复问题多线程写入问题:https://github.com/allenymt/PrivacySentry/issues/84


2022-08-30(1.1.0)
    1. 变量 hook 支持通过注解配置
    2. 修复不引入 privacy-proxy 引起的问题
2022-07-29(1.0.9)
    1. 删除多余的 aar 引用
2022-07-26(1.0.8)
    1. 优化 log 输出,未初始化也能有 log 输出
    2. 优化初始化方式
2022-06-24(1.0.7)
    1. 新增 hook 传感器方法
    2. 新增静态扫描,支持产出敏感函数 hook 列表
2022-06-16(1.0.5)
    1. 修复 Settings.System 获取 Android_id,未拦截到的问题
    2. 支持业务方配置同类型的 hook 函数覆盖自带的 hook 函数
    3. 新增 MIT 开源协议
2022-04-22(1.0.4)
    1. 对 imei、imsi、mac、android_id、meid、serial 等不可变字段,单进程内只读取一次
    2. 精简堆栈,删除重复部分
    3. 修复 Android_id 拦截问题
2022-03-04(1.0.3)
    支持变量 hook,主要是 Build.SERIAL
2022-1-18(1.0.2)
    1. 编译期注解+hook 方案
    2. 支持业务方自定义配置拦截
2021-12-26(1.0.0)
    1. Asm 修改字节码,hook 敏感函数
2021-12-02(0.0.7)
    1. 支持多进程
    2. 日志加上时间戳,方便阅读
    3. 优化文件分时段写入
    4. pms 增加部分 hook 方法

TODO

  1. 有其他问题欢迎提 issue
  2. 项目里如果有引入高德地图 or openInstall,先加黑 blackList = ["com.loc","com.amap.api","io.openinstall.sdk"]
  3. 动态加载加载的代码无法拦截(热修复,插件化)

如何使用

    1. 在根目录的 build.gralde 下添加
    allprojects {
        repositories {
            ...
            maven { url 'https://jitpack.io' }
        }
    }

    buildscript {
         dependencies {
             // 添加插件依赖
             classpath 'com.github.allenymt.PrivacySentry:plugin-sentry:1.3.4.2'
         }
    }

    allprojects {
        repositories {
            maven { url 'https://jitpack.io' }
        }
    }
    2. 在项目中的 build.gralde 下添加
        // 在主项目里添加插件依赖
        apply plugin: 'privacy-sentry-plugin'

        dependencies {
            // aar 依赖
            def privacyVersion = "1.3.4.2"
            implementation "com.github.allenymt.PrivacySentry:hook-sentry:$privacyVersion"
            implementation "com.github.allenymt.PrivacySentry:privacy-annotation:$privacyVersion"

             // 代理类的库,如果自己没有代理类,那么必须引用这个 aar!!
             // 如果不想使用库中本身的代理方法,可以不引入这个 aar,但是自己必须实现代理类!!
             // 引入 privacy-proxy,也可以自定义类代理方法,优先以业务方定义的为准
            implementation "com.github.allenymt.PrivacySentry:privacy-proxy:$privacyVersion"
            // 1.2.3 新增类替换,主要是为了 hook 构造函数的参数,按业务方需求自己决定
            implementation "com.github.allenymt.PrivacySentry:privacy-replace:$privacyVersion"
        }

        // 黑名单配置,可以设置这部分包名不会被修改字节码
        // 项目里如果有引入高德地图,先加黑 blackList = ["com.loc","com.amap.api"], asm 的版本有冲突
        // 如果需要生成静态扫描文件, 默认名是 replace.json
       privacy {
            // 设置免 hook 的名单
            blackList = []
            // 开关 PrivacySentry 插件功能
            enablePrivacy = true
            // 开启 hook 反射的方法
            hookReflex = true
            // 开启 hook 替换类,目前支持 file
            hookConstructor = true
            // 是否开启 hook 变量,默认为 false,建议弃用
            hookField = true


            // 以下是为了解决小米照明弹自启动问题的尝试, 如果没有自启动的需求,这里关闭即可
            // hook Service 的部分代码,修复在 MIUI 上的自启动问题
            // 部分 Service 把自己的 Priority 设置为 1000,这里开启代理功能,可以代理成 0
            enableReplacePriority = true
            replacePriority = 1

            // 支持关闭 Service 的 Export 功能,默认为 false,注意部分厂商通道之类的 push(xiaomi、vivo、huawei 等厂商的 pushService),不能关闭
            enableCloseServiceExport = true
            // Export 白名单 Service, 这里根据厂商的名称设置了白名单
            serviceExportPkgWhiteList = ["xiaomi","vivo","honor","meizu","oppo","Oppo","Hms","huawei","stp","Honor"]
            // 修改 Service 的 onStartCommand 返回值修改为 START_NOT_STICKY
            enableHookServiceStartCommand = true
        }
    初始化方法最好在 attachBaseContext 中第一个调用!!!(1.3.1 开始不需要了,可以晚点初始化,不影响检测结果)
    完成功能的初始化
    PrivacySentryBuilder builder = new PrivacySentryBuilder()
                        // 自定义文件结果的输出名
                        .configResultFileName("buyer_privacy")
    `        //  debug 打开,可以看到 logcat 的堆栈日志
            .syncDebug(true)
                        // 配置写入文件日志 , 线上包这个开关不要打开!!!!,true 打开文件输入,false 关闭文件输入
                        .enableFileResult(true)
                        // 持续写入文件 30 分钟
                        .configWatchTime(30 * 60 * 1000)
                        // 文件输出后的回调
                        .configResultCallBack(new PrivacyResultCallBack() {

                            @Override
                            public void onResultCallBack(@NonNull String s) {

                            }
                        });
    // 添加默认结果输出,包含 log 输出和文件输出
    PrivacySentry.Privacy.INSTANCE.init(application, builder);
    如果在日志中发现 check!!! 还未展示隐私协议,Illegal print,说明此时还未同意隐私协议,调用了敏感或者违规的 api
    所以在隐私协议确认的时候调用,这一步非常重要!,一定要加,这一步是告知 SDK,APP 已经同意隐私协议了
    kotlin:PrivacySentry.Privacy.updatePrivacyShow()
    java:PrivacySentry.Privacy.INSTANCE.updatePrivacyShow();
    支持自定义配置 hook 函数
    /**
 * @author yulun
 * @since 2022-01-13 17:57
 * 主要是两个注解 PrivacyClassProxy 和 PrivacyMethodProxy,PrivacyClassProxy 代表要解析的类,PrivacyMethodProxy 代表要 hook 的方法配置
 */
@Keep
open class PrivacyProxyResolver {

    // kotlin 里实际解析的是这个 PrivacyProxyCall$Proxy 内部类
    @PrivacyClassProxy
    @Keep
    object Proxy {

        // 查询
        @SuppressLint("MissingPermission")
        @PrivacyMethodProxy(
            originalClass = ContentResolver::class,   // hook 的方法所在的类名
            originalMethod = "query",   // hook 的方法名
            originalOpcode = MethodInvokeOpcode.INVOKEVIRTUAL //hook 的方法调用,一般是静态调用和实例调用
        )
        @JvmStatic
        fun query(
            contentResolver: ContentResolver?, //实例调用的方法需要把声明调用对象,我们默认把对象参数放在第一位
            uri: Uri,
            projection: Array<String?>?, selection: String?,
            selectionArgs: Array<String?>?, sortOrder: String?
        ): Cursor? {
            doFilePrinter("query", "查询服务: ${uriToLog(uri)}") // 输入日志到文件
            return contentResolver?.query(uri, projection, selection, selectionArgs, sortOrder)
        }

        @RequiresApi(Build.VERSION_CODES.O)
        @PrivacyMethodProxy(
            originalClass = android.os.Build::class,
            originalMethod = "getSerial",
            originalOpcode = MethodInvokeOpcode.INVOKESTATIC //静态调用
        )
        @JvmStatic
        fun getSerial(): String? {
            var result = ""
            try {
                doFilePrinter("getSerial", "读取 Serial")
                if (PrivacySentry.Privacy.getBuilder()?.isVisitorModel() == true) {
                return ""
                }
            result = Build.getSerial()
            } catch (e: Exception) {
                e.printStackTrace()
            }
        return result
        }
    }
}
    支持多进程,多进程产出的文件名前缀默认增加进程名
    如何配置替换一个类
    可以参考源码中 PrivacyFile 的配置,使用 PrivacyClassReplace 注解,originClass 代表你要替换的类,注意要继承 originClass 的所有构造函数
    可以配置 hookConstructor = false 关闭这个功能
/**
 * @author yulun
 * @since 2022-11-18 15:01
 * 代理 File 的构造方法,如果是自定义的 file 类,需要业务方单独配置自行处理
 */
@PrivacyClassReplace(originClass = File.class)
public class PrivacyFile extends File {

    public PrivacyFile(@NonNull String pathname) {
        super(pathname);
        record(pathname);
    }

    public PrivacyFile(@Nullable String parent, @NonNull String child) {
        super(parent, child);
        record(parent + child);
    }

    public PrivacyFile(@Nullable File parent, @NonNull String child) {
        super(parent, child);
        record(parent.getPath() + child);
    }

    public PrivacyFile(@NonNull URI uri) {
        super(uri);
        record(uri.toString());
    }

    private void record(String path) {
        PrivacyProxyUtil.Util.INSTANCE.doFilePrinter("PrivacyFile", "访问文件", "path is " + path, PrivacySentry.Privacy.INSTANCE.getBuilder().isVisitorModel(), false);
    }
}

隐私方法调用结果产出

  • 支持 hook 调用堆栈至文件,默认的时间为 1 分钟,支持自定义设置时间。
  • 排查结果可参考目录下的 demo_result.xls,排查结果支持两个维度查看,第一是结合隐私协议的展示时机和敏感方法的调用时机,第二是统计所有敏感函数的调用次数
  • 排查结果可观察日志,结果文件会在 /storage/emulated/0/Android/data/yourPackgeName/files/xx.xls,需要手动执行下 adb pull
  • logcat 日志查看:TAG 名为 PrivacyOfficer

基本原理

  • 编译期注解+hook 方案,第一个 transform 收集需要拦截的敏感函数,第二个 transform 替换敏感函数,运行期收集日志
  • 为什么不用 xposed 等框架? 因为想做本地自动化定期排查,第三方 hook 框架外部依赖性太大
  • 为什么不搞基于 lint 的排查方式? 工信部对于运行期 敏感函数的调用时机和次数都有限制,代码扫描解决不了这些问题

支持的 hook 函数列表

支持 hook 以下功能函数:

  • 支持敏感字段缓存(磁盘缓存、带有时间限制的磁盘缓存、内存缓存)

  • hook 替换类 (构造函数)

  • 当前运行进程和任务

  • 系统剪贴板服务

  • 读取设备应用列表

  • 读取 Android SN(Serial,包括方法和变量),系统设备号

  • 读写联系人、日历、本机号码

  • 获取定位、基站信息、wifi 信息

  • Mac 地址、IP 地址

  • 读取 IMEI(DeviceId)、MEID、IMSI、ADID(AndroidID)

  • 手机可用传感器,传感器注册,传感器列表

  • 权限请求

常见的合规字段整理

IMEI、MAC 地址、MEID、IMSI、SN、ICCID 等设备唯一标识符,Android ID、WiFi(WiFi 名称、WiFi MAC 地址以及设备扫描到的所有 WiFi 信息),SIM 卡信息(IMSI、SIM 卡序列号 ICCID、手机号、运营商信息),应用安装列表(设备所有已安装应用的包名和应用名),传感器(传感器列表、加速度传感器、温度传感器等),蓝牙信息(设备蓝牙地址和设备扫描到的蓝牙设备信息),基站定位、GPS(用户地理位置信息),账户(各类应用注册的不同账号信息)、剪切板、IP 地址、硬件序列号、SDCard 信息(公有目录)

结语

整体代码很简单,有问题可以直接提~ (兄弟们,走过路过请给个 star~~~)
Apps
About Me
GitHub: Trinea
Facebook: Dev Tools