allenymt/PrivacySentry

Android隐私合规整改检测工具,注解+Asm修改字节码的检测方案

Kotlin

2,289

225 commits

updated Dec 30, 2025

See the code

README

PrivacySentry

License API

Android 隐私合规检测工具,可规避应用市场上架合规检测的大部分问题

✨ 核心特性

  • ✅ 编译期字节码插桩:基于 ASM + Booster 框架,零运行时性能损耗
  • 🎯 完整的拦截方案:支持 60+ 敏感 API 拦截,覆盖工信部合规要求
  • 📊 自动化检测报告:生成 Excel 格式的详细调用记录和统计分析
  • 🔄 智能缓存机制:内存/磁盘多级缓存,优化性能
  • 🌐 多进程支持:自动处理多进程场景,独立输出日志
  • 🔧 灵活可扩展:支持自定义拦截规则,黑名单配置

📚 文档导航

📱 社区支持

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

image

🚀 快速开始

版本要求

组件版本要求
AGP8.0+ (推荐 8.2.0)
Gradle8.0+
Kotlin1.8.10+
minSdk19+
compileSdk34+

注意:AGP 8.0 以下版本请使用 1.3.6 版本

Step 1: 添加插件依赖

在项目根目录的 build.gradle 中添加:

buildscript {
    repositories {
        maven { url 'https://jitpack.io' }
        mavenCentral()
        google()
    }

    dependencies {
        classpath 'com.github.allenymt.PrivacySentry:plugin-sentry:1.3.7_v820_beta4'
    }
}

allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
        mavenCentral()
        google()
    }
}

Step 2: 应用插件和依赖

在 app 模块的 build.gradle 中:

// 应用插件
apply plugin: 'privacy-sentry-plugin'

dependencies {
    def privacyVersion = "1.3.7_v820_beta4"

    // 核心库(必须)
    implementation "com.github.allenymt.PrivacySentry:hook-sentry:$privacyVersion"
    implementation "com.github.allenymt.PrivacySentry:privacy-annotation:$privacyVersion"

    // 预置拦截实现(强烈推荐)
    implementation "com.github.allenymt.PrivacySentry:privacy-proxy:$privacyVersion"

    // 类替换功能(已废弃,不推荐使用)
    // implementation "com.github.allenymt.PrivacySentry:privacy-replace:$privacyVersion"
}

Step 3: 插件配置

在 app 模块的 build.gradle 中添加 privacy 配置块:

privacy {
    // ========== 核心配置 ==========

    /**
     * 插件功能总开关
     * 类型:Boolean
     * 默认值:true
     * 说明:控制 PrivacySentry 插件是否生效
     *      - true: 启用插件,执行字节码转换
     *      - false: 禁用插件,相当于未集成
     */
    enablePrivacy = true

    /**
     * 黑名单配置
     * 类型:Set<String>
     * 默认值:null (空列表)
     * 说明:指定不进行字节码修改的包名列表
     *      - 适用场景:
     *        1. 使用了其他 ASM 字节码修改工具的三方库(如高德地图)
     *        2. 已知会导致冲突的 SDK
     *        3. 不需要监控的系统库或三方库
     *      - 注意:blackList 中的包不会被插件修改,也无法拦截其中的敏感 API
     */
    blackList = []

    // 常见黑名单配置示例:
    // blackList = [
    //     "com.loc",              // 高德地图(ASM 版本冲突)
    //     "com.amap.api",         // 高德地图 API
    //     "io.openinstall.sdk",   // OpenInstall SDK
    //     "com.google.android",   // Google 服务(可选)
    //     "androidx"              // AndroidX 库(可选)
    // ]

    /**
     * 静态扫描结果文件名
     * 类型:String
     * 默认值:"privacy_hook.json"
     * 说明:记录所有被代理的方法名和类名的文件名
     *      - 文件位置:项目根目录
     *      - 文件格式:JSON
     *      - 内容包含:
     *        1. hookServiceList: 被 hook 的 Service 列表
     *        2. replaceMethodMap: 被替换的方法映射表
     *      - 设置为 null 或空字符串则不生成文件
     */
    replaceFileName = "privacy_hook.json"

    // ========== 反射 Hook 配置(可选)==========

    /**
     * 反射方法 Hook 开关
     * 类型:Boolean
     * 默认值:false
     * 说明:是否拦截通过反射调用的敏感方法
     *      - true: 拦截反射调用(需配合 reflexMap 使用)
     *      - false: 不拦截反射调用
     *      - 适用场景:
     *        1. 三方 SDK 通过反射获取设备信息(如小米 OAID)
     *        2. 极光推送、个推、穿山甲等 SDK 的设备标识获取
     *      - 性能影响:轻微(仅影响 LDC 指令的匹配)
     */
    hookReflex = false

    /**
     * 反射拦截配置映射
     * 类型:Map<String, List<String>>
     * 默认值:null
     * 说明:配置需要拦截的反射调用
     *      - Key: 类的全限定名
     *      - Value: 该类中需要拦截的方法名列表
     *      - 只有 hookReflex = true 时才生效
     *      - 匹配原理:检测字节码中的 LDC 指令加载的字符串常量
     */
    reflexMap = [:]

    // 反射拦截配置示例:
    // reflexMap = [
    //     // 小米设备标识服务
    //     "com.android.id.impl.IdProviderImpl": [
    //         "getOAID",   // 开放匿名设备标识符
    //         "getAAID",   // 应用匿名设备标识符
    //         "getVAID"    // 开发者匿名设备标识符
    //     ],
    //     // 自定义类的反射方法
    //     "com.example.utils.DeviceUtils": [
    //         "getDeviceId",
    //         "getIMEI"
    //     ]
    // ]

    // ========== 已废弃功能(不推荐使用,仅供参考)==========

    /**
     * 字段 Hook 开关(已废弃)
     * 类型:Boolean
     * 默认值:false
     * 废弃原因:几乎没有业务场景,功能不稳定
     * 说明:是否 hook 字段访问(如 Build.SERIAL)
     *      - 不推荐使用,请使用方法 hook 代替
     */
    // hookField = false

    /**
     * 构造函数 Hook 开关(已废弃)
     * 类型:Boolean
     * 默认值:false
     * 废弃原因:实现复杂,稳定性差,已停止维护
     * 说明:是否 hook 构造函数(主要用于拦截 File 构造函数参数)
     *      - 相关模块:privacy-replace
     *      - 不推荐使用
     */
    // hookConstructor = false

    /**
     * Manifest 处理开关(已废弃)
     * 类型:Boolean
     * 默认值:false
     * 废弃原因:功能边界不清晰,已停止维护
     * 说明:是否处理 AndroidManifest.xml 文件
     *      - 主要用于处理 Service 的 Priority 和 Export
     *      - 不推荐使用
     */
    // enableProcessManifest = false

    /**
     * Service Priority 替换开关(已废弃)
     * 类型:Boolean
     * 默认值:false
     * 废弃原因:针对 MIUI 自启动问题的特殊方案,已停止维护
     * 说明:是否替换 Service 的 Priority 值
     *      - 部分 Service 设置 Priority = 1000 导致自启动
     *      - 开启后会将 Priority 替换为 replacePriority 的值
     *      - 不推荐使用
     */
    // enableReplacePriority = false

    /**
     * Service Priority 替换值(已废弃)
     * 类型:Int
     * 默认值:0
     * 说明:替换后的 Priority 值
     *      - 配合 enableReplacePriority 使用
     *      - 不推荐使用
     */
    // replacePriority = 0

    /**
     * Service Export 关闭开关(已废弃)
     * 类型:Boolean
     * 默认值:false
     * 废弃原因:可能影响厂商推送功能,已停止维护
     * 说明:是否关闭 Service 的 Export 功能
     *      - 注意:部分厂商推送(小米、VIVO、华为)的 PushService 不能关闭
     *      - 配合 serviceExportPkgWhiteList 使用
     *      - 不推荐使用
     */
    // enableCloseServiceExport = false

    /**
     * Service Export 白名单(已废弃)
     * 类型:Set<String>
     * 默认值:null
     * 说明:允许保持 Export 的 Service 包名列表
     *      - 配合 enableCloseServiceExport 使用
     *      - 不推荐使用
     */
    // serviceExportPkgWhiteList = []

    /**
     * Service StartCommand Hook 开关(已废弃)
     * 类型:Boolean
     * 默认值:false
     * 废弃原因:针对 MIUI 自启动问题的特殊方案,已停止维护
     * 说明:是否 hook Service 的 startCommand 方法
     *      - 不推荐使用
     */
    // enableHookServiceStartCommand = false
}

配置优先级说明:

  1. 必须配置:enablePrivacy(总开关)
  2. 强烈推荐:blackList(避免冲突)
  3. 按需配置:hookReflex + reflexMap(反射拦截)
  4. 可选配置:replaceFileName(静态扫描文件)
  5. 不推荐使用:所有标记为 @Deprecated 的配置项

Step 4: SDK 初始化

在 Application 中初始化 SDK:

Kotlin 示例:

class MyApplication : Application() {

    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)

        // ⚠️ 重要:尽可能在 attachBaseContext 中第一个调用
        // 这样可以确保捕获所有敏感 API 调用
        initPrivacySentry()
    }

    override fun onCreate() {
        super.onCreate()

        // 展示隐私协议弹窗
        showPrivacyDialog {
            // 用户同意后,必须调用此方法
            PrivacySentry.Privacy.updatePrivacyShow()
        }
    }

    private fun initPrivacySentry() {
        val builder = PrivacySentryBuilder()
            // 自定义输出文件名
            .configResultFileName("privacy_result")

            // 开启 debug 模式(可在 logcat 查看日志)
            .syncDebug(BuildConfig.DEBUG)

            // 开启文件输出(⚠️ 线上版本请关闭)
            .enableFileResult(BuildConfig.DEBUG)

            // 监控时长(30 分钟)
            .configWatchTime(30 * 60 * 1000)

            // 文件输出完成回调
            .configResultCallBack(object : PrivacyResultCallBack {
                override fun onResultCallBack(filePath: String) {
                    Log.i("PrivacySentry", "结果文件:$filePath")
                }
            })

        PrivacySentry.Privacy.init(this, builder)
    }
}

Java 示例:

public class MyApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        // ⚠️ 重要:尽可能在 attachBaseContext 中第一个调用
        initPrivacySentry();
    }

    @Override
    public void onCreate() {
        super.onCreate();

        // 展示隐私协议弹窗
        showPrivacyDialog(() -> {
            // 用户同意后,必须调用此方法
            PrivacySentry.Privacy.INSTANCE.updatePrivacyShow();
        });
    }

    private void initPrivacySentry() {
        PrivacySentryBuilder builder = new PrivacySentryBuilder()
                .configResultFileName("privacy_result")
                .syncDebug(BuildConfig.DEBUG)
                .enableFileResult(BuildConfig.DEBUG)
                .configWatchTime(30 * 60 * 1000)
                .configResultCallBack(new PrivacyResultCallBack() {
                    @Override
                    public void onResultCallBack(@NonNull String filePath) {
                        Log.i("PrivacySentry", "结果文件:" + filePath);
                    }
                });

        PrivacySentry.Privacy.INSTANCE.init(this, builder);
    }
}

Step 5: 查看检测结果

方式 1:Logcat 日志

# 实时查看日志
adb logcat | grep "PrivacyOfficer"

方式 2:Excel 文件

# 拉取结果文件
adb pull /storage/emulated/0/Android/data/{your.package.name}/files/privacy/

# 文件名格式
# 主进程:privacy_result.xls
# 子进程:{进程名}_privacy_result.xls

Excel 文件包含两个 Sheet:

  1. Sheet 1 - 隐私合规明细:按时间倒序记录所有敏感 API 调用

    • 调用时间
    • 方法别名
    • 函数名
    • 完整调用堆栈
  2. Sheet 2 - 调用次数统计:按堆栈聚合统计调用次数

    • 方法别名
    • 函数名
    • 调用堆栈
    • 调用次数

⚙️ 插件配置详解

核心配置项完整说明

配置项类型默认值说明
enablePrivacyBooleantrue插件功能总开关
- true: 启用插件,执行字节码转换
- false: 禁用插件
blackListSet<String>null黑名单配置
指定不进行字节码修改的包名列表
- 适用场景:ASM 冲突的三方库
- 注意:黑名单中的包无法拦截敏感 API
replaceFileNameString"privacy_hook.json"静态扫描结果文件名
记录所有被代理的方法和类
- 文件位置:项目根目录
- 文件格式:JSON
- 设置为 null 则不生成
hookReflexBooleanfalse反射方法 Hook 开关
是否拦截通过反射调用的敏感方法
- 需配合 reflexMap 使用
- 适用:三方 SDK 反射获取设备信息
reflexMapMap<String, List<String>>null反射拦截配置
- Key: 类的全限定名
- Value: 需要拦截的方法名列表
- 只有 hookReflex=true 时生效

已废弃配置项

配置项类型默认值废弃原因
hookFieldBooleanfalse几乎没有业务场景,功能不稳定
hookConstructorBooleanfalse实现复杂,稳定性差,已停止维护
enableProcessManifestBooleanfalse功能边界不清晰,已停止维护
enableReplacePriorityBooleanfalse针对 MIUI 特殊问题,已停止维护
replacePriorityInt0配合 enableReplacePriority 使用
enableCloseServiceExportBooleanfalse可能影响厂商推送,已停止维护
serviceExportPkgWhiteListSet<String>null配合 enableCloseServiceExport 使用
enableHookServiceStartCommandBooleanfalse针对 MIUI 特殊问题,已停止维护

⚠️ 重要提示:所有标记为"已废弃"的配置项均不推荐使用,可能在未来版本中移除。

黑名单配置说明

什么情况需要配置黑名单?

  1. ASM 版本冲突

    • 使用高德地图 SDK(ASM 9.1 vs 其他版本)
    • 使用其他字节码修改工具的三方库
  2. 已知冲突的 SDK

    • OpenInstall SDK
    • 部分混淆工具
  3. 不需要监控的库

    • 系统库(可选)
    • 不涉及隐私的三方库

黑名单工作原理

  • 插件在 Transform 阶段会跳过黑名单中的包
  • 黑名单采用前缀匹配规则
  • 例如:"com.loc" 会匹配 com.loc.* 下的所有类

配置示例

privacy {
    blackList = [
        // 高德地图(ASM 版本冲突)
        "com.loc",
        "com.amap.api",

        // OpenInstall SDK
        "io.openinstall.sdk",

        // Google 服务(可选)
        "com.google.android",

        // AndroidX 库(可选)
        "androidx",

        // 自定义不需要监控的包
        "com.example.thirdparty"
    ]
}

黑名单注意事项

  • ✅ 推荐:只添加确实冲突的包
  • ❌ 不推荐:盲目添加大量包到黑名单
  • ⚠️ 影响:黑名单中的包无法拦截敏感 API 调用

反射 Hook 配置详解

使用场景

反射 Hook 用于拦截通过反射方式调用的敏感方法,常见场景:

  1. 设备标识获取

    • 小米设备 OAID/AAID/VAID
    • 华为设备标识
    • OPPO/VIVO 设备标识
  2. 三方 SDK

    • 极光推送(JPush)
    • 个推(GeTui)
    • 穿山甲广告 SDK
    • 友盟统计
  3. 自定义反射调用

    • 项目中通过反射获取的敏感信息

工作原理

// 原始代码(反射调用)
Class.forName("com.android.id.impl.IdProviderImpl")
    .getMethod("getOAID")
    .invoke(obj)

// 字节码层面
LDC "com.android.id.impl.IdProviderImpl"  // ← hookReflex 检测这里
LDC "getOAID"                              // ← reflexMap 匹配方法名
INVOKEVIRTUAL Method.invoke()              // ← 替换为代理方法

配置示例

场景 1:小米设备标识

privacy {
    hookReflex = true

    reflexMap = [
        "com.android.id.impl.IdProviderImpl": [
            "getOAID",   // 开放匿名设备标识符
            "getAAID",   // 应用匿名设备标识符
            "getVAID"    // 开发者匿名设备标识符
        ]
    ]
}

场景 2:多个 SDK 配置

privacy {
    hookReflex = true

    reflexMap = [
        // 小米设备标识
        "com.android.id.impl.IdProviderImpl": [
            "getOAID", "getAAID", "getVAID"
        ],

        // 华为设备标识
        "com.huawei.hms.ads.identifier.AdvertisingIdClient": [
            "getAdvertisingIdInfo"
        ],

        // 自定义工具类
        "com.example.utils.DeviceUtils": [
            "getDeviceId",
            "getIMEI",
            "getAndroidId"
        ]
    ]
}

场景 3:极光推送/个推配置

privacy {
    hookReflex = true

    reflexMap = [
        // 极光推送反射获取设备信息
        "cn.jpush.android.api.JCoreInterface": [
            "getDeviceId",
            "getRegistrationID"
        ],

        // 个推反射获取设备信息
        "com.igexin.sdk.PushManager": [
            "getClientid"
        ]
    ]
}

反射 Hook 注意事项

  • ✅ 精确匹配:类名和方法名必须完全匹配
  • ✅ 性能影响:轻微(仅影响 LDC 指令匹配)
  • ⚠️ 必须启用:hookReflex = true 才生效
  • ⚠️ 无法拦截:动态生成的类名或方法名

静态扫描文件说明

replaceFileName 配置

privacy {
    // 生成静态扫描文件
    replaceFileName = "privacy_hook.json"

    // 不生成文件
    // replaceFileName = null
}

文件内容示例

文件位置:项目根目录 /privacy_hook.json

{
    "hookServiceList": [
        "com.example.TestService",
        "com.example.BackgroundService"
    ],

    "replaceMethodMap": {
        "android.app.ActivityManager.getRunningTasks": {
            "count": 5,
            "originMethodList": [
                {
                    "originClassName": "com.example.MainActivity",
                    "originMethodName": "checkRunningTasks"
                },
                {
                    "originClassName": "com.example.utils.AppUtils",
                    "originMethodName": "getRunningApps"
                }
            ]
        },
        "android.telephony.TelephonyManager.getDeviceId": {
            "count": 2,
            "originMethodList": [
                {
                    "originClassName": "com.example.DeviceManager",
                    "originMethodName": "getIMEI"
                }
            ]
        }
    }
}

文件用途

  1. 静态分析:离线分析敏感 API 调用情况
  2. 合规检查:提供给安全团队审查
  3. 调试参考:确认插件是否正确拦截了目标方法
  4. 版本对比:对比不同版本的 API 调用变化

配置优先级建议

优先级配置项建议值说明
⭐⭐⭐ 必须enablePrivacytrue插件总开关
⭐⭐⭐ 强烈推荐blackList根据实际情况避免 ASM 冲突
⭐⭐ 推荐replaceFileName"privacy_hook.json"生成静态扫描文件
⭐ 按需配置hookReflexfalse 或 true根据是否有反射调用
⭐ 按需配置reflexMap[:] 或配置配合 hookReflex 使用
❌ 不推荐所有废弃配置-已停止维护

🔧 SDK 初始化

PrivacySentryBuilder 配置项

方法参数类型默认值说明
configResultFileName(String)String自动生成自定义输出文件名
syncDebug(Boolean)Booleanfalse开启 debug 日志
enableFileResult(Boolean)Booleantrue是否输出到文件
configWatchTime(Long)Long (毫秒)180000 (3分钟)监控时长
configResultCallBack(PrivacyResultCallBack)PrivacyResultCallBacknull文件输出完成回调
enableReadClipBoard(Boolean)Booleantrue是否允许读取剪贴板

最佳实践

1. 初始化时机

class MyApplication : Application() {
    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)

        // ✅ 推荐:第一个调用
        PrivacySentry.Privacy.init(this, builder)

        // ❌ 不推荐:在其他初始化之后
        // MultiDex.install(this)
        // PrivacySentry.Privacy.init(this, builder)
    }
}

原因:attachBaseContext 之后才能通过反射获取 ActivityThread 的 application,如果初始化太晚,可能无法捕获早期的敏感 API 调用。

2. Debug vs Release 配置

private fun initPrivacySentry() {
    val builder = PrivacySentryBuilder()
        .syncDebug(BuildConfig.DEBUG)           // Debug 开启日志
        .enableFileResult(BuildConfig.DEBUG)    // Release 关闭文件输出
        .configWatchTime(
            if (BuildConfig.DEBUG) 30 * 60 * 1000  // Debug: 30分钟
            else 3 * 60 * 1000                      // Release: 3分钟(谨慎)
        )

    PrivacySentry.Privacy.init(this, builder)
}

3. 隐私协议状态管理

override fun onCreate() {
    super.onCreate()

    // 检查是否已同意隐私协议
    if (!hasAgreedPrivacyPolicy()) {
        showPrivacyDialog {
            // 用户同意后保存状态
            savePrivacyAgreement()

            // ⚠️ 必须调用,告知 SDK 用户已同意
            PrivacySentry.Privacy.updatePrivacyShow()
        }
    } else {
        // 已同意,直接告知 SDK
        PrivacySentry.Privacy.updatePrivacyShow()
    }
}

重要提示:

  • ✅ 用户同意隐私协议后,必须调用 updatePrivacyShow()
  • ✅ 调用前的敏感 API 会返回空数据并标记 check!!!
  • ✅ 调用后的敏感 API 返回真实数据并记录日志

4. 手动停止监控

// 手动停止监控和文件写入
PrivacySentry.Privacy.stop()

🎯 自定义拦截

创建自定义代理类

import androidx.annotation.Keep
import com.yl.lib.privacy_annotation.MethodInvokeOpcode
import com.yl.lib.privacy_annotation.PrivacyClassProxy
import com.yl.lib.privacy_annotation.PrivacyMethodProxy
import com.yl.lib.sentry.hook.PrivacySentry
import com.yl.lib.sentry.hook.util.PrivacyProxyUtil.Util.doFilePrinter

@Keep
@PrivacyClassProxy
object MyCustomProxy {

    /**
     * 示例 1:拦截实例方法
     */
    @PrivacyMethodProxy(
        originalClass = YourClass::class,
        originalMethod = "getSensitiveData",
        originalOpcode = MethodInvokeOpcode.INVOKEVIRTUAL
    )
    @JvmStatic
    fun getSensitiveData(
        instance: YourClass,  // 第一个参数是实例对象
        param1: String
    ): String {
        // 记录日志
        doFilePrinter("getSensitiveData", "获取敏感数据: $param1")

        // 检查隐私协议状态
        if (PrivacySentry.Privacy.inDangerousState()) {
            return ""  // 未同意返回空
        }

        // 调用原始方法
        return instance.getSensitiveData(param1)
    }

    /**
     * 示例 2:拦截静态方法
     */
    @PrivacyMethodProxy(
        originalClass = YourUtilClass::class,
        originalMethod = "getDeviceId",
        originalOpcode = MethodInvokeOpcode.INVOKESTATIC
    )
    @JvmStatic
    fun getDeviceId(): String {
        doFilePrinter("getDeviceId", "获取设备ID")

        if (PrivacySentry.Privacy.inDangerousState()) {
            return ""
        }

        return YourUtilClass.getDeviceId()
    }

    /**
     * 示例 3:拦截接口方法
     */
    @PrivacyMethodProxy(
        originalClass = YourInterface::class,
        originalMethod = "getData",
        originalOpcode = MethodInvokeOpcode.INVOKEINTERFACE
    )
    @JvmStatic
    fun getData(instance: YourInterface): String {
        doFilePrinter("getData", "接口方法调用")
        return instance.getData()
    }
}

Java 自定义代理示例

import androidx.annotation.Keep;
import com.yl.lib.privacy_annotation.MethodInvokeOpcode;
import com.yl.lib.privacy_annotation.PrivacyClassProxy;
import com.yl.lib.privacy_annotation.PrivacyMethodProxy;
import com.yl.lib.sentry.hook.PrivacySentry;
import static com.yl.lib.sentry.hook.util.PrivacyProxyUtil.Util.doFilePrinter;

@Keep
@PrivacyClassProxy
public class MyCustomProxyJava {

    @PrivacyMethodProxy(
        originalClass = YourClass.class,
        originalMethod = "getSensitiveData",
        originalOpcode = MethodInvokeOpcode.INVOKEVIRTUAL
    )
    public static String getSensitiveData(YourClass instance, String param) {
        doFilePrinter("getSensitiveData", "获取敏感数据: " + param, false);

        if (PrivacySentry.Privacy.INSTANCE.inDangerousState()) {
            return "";
        }

        return instance.getSensitiveData(param);
    }
}

方法签名规则

实例方法 (INVOKEVIRTUAL)

// 原始方法
class TargetClass {
    fun method(param1: String, param2: Int): String
}

// 代理方法:第一个参数是实例对象
@JvmStatic
fun method(
    instance: TargetClass,  // ⬅️ 额外的第一个参数
    param1: String,
    param2: Int
): String

静态方法 (INVOKESTATIC)

// 原始方法
object TargetClass {
    fun method(param1: String): String
}

// 代理方法:参数完全相同
@JvmStatic
fun method(param1: String): String

接口方法 (INVOKEINTERFACE)

// 原始接口
interface TargetInterface {
    fun method(param: String): String
}

// 代理方法:第一个参数是接口实例
@JvmStatic
fun method(
    instance: TargetInterface,  // ⬅️ 接口实例
    param: String
): String

📋 支持的敏感 API

设备标识

  • ✅ IMEI / DeviceId (TelephonyManager.getDeviceId())
  • ✅ IMSI (TelephonyManager.getSubscriberId())
  • ✅ MEID (TelephonyManager.getMeid())
  • ✅ Android ID (Settings.Secure.getAndroidId())
  • ✅ Serial (Build.getSerial(), Build.SERIAL)
  • ✅ MAC 地址 (WifiInfo.getMacAddress())
  • ✅ ICCID (TelephonyManager.getSimSerialNumber())

网络信息

  • ✅ WiFi 信息 (WifiManager.getConnectionInfo())
  • ✅ WiFi 扫描结果 (WifiManager.getScanResults())
  • ✅ IP 地址 (NetworkInterface, WifiInfo.getIpAddress())
  • ✅ DHCP 信息 (WifiManager.getDhcpInfo())

位置信息

  • ✅ GPS 定位 (LocationManager.getLastKnownLocation())
  • ✅ 基站信息 (TelephonyManager.getAllCellInfo())
  • ✅ 位置监听 (LocationManager.requestLocationUpdates())

应用信息

  • ✅ 已安装应用列表 (PackageManager.getInstalledPackages())
  • ✅ 运行中任务 (ActivityManager.getRunningTasks())
  • ✅ 运行中进程 (ActivityManager.getRunningAppProcesses())
  • ✅ 最近任务 (ActivityManager.getRecentTasks())

联系人和日历

  • ✅ 联系人查询 (ContentResolver.query())
  • ✅ 联系人插入 (ContentResolver.insert())
  • ✅ 日历事件 (Calendar Provider)

传感器

  • ✅ 传感器列表 (SensorManager.getSensorList())
  • ✅ 传感器注册 (SensorManager.registerListener())

其他

  • ✅ 剪贴板 (ClipboardManager.getPrimaryClip())
  • ✅ 蓝牙 (BluetoothAdapter.getAddress())
  • ✅ 权限请求 (requestPermissions())
  • ✅ SIM 卡信息 (TelephonyManager.getSimOperator())

完整列表请参考 privacy-proxy 模块

🔍 检测结果说明

日志格式

[PrivacyOfficer] getDeviceId-线程名: main | 读取IMEI | com.example.MainActivity.onCreate(MainActivity.kt:42)
                                                           ↑ 调用堆栈

危险状态标记

如果在日志中看到 check!!! 标记:

check!!! 还未展示隐私协议,Illegal print

说明:此时还未同意隐私协议,调用了敏感 API

解决方法:

  1. 检查是否在隐私协议同意后调用了 updatePrivacyShow()
  2. 优化代码,避免在隐私协议同意前调用敏感 API

🛠️ 常见问题

Q1: 为什么某些 API 没有被拦截?

可能原因:

  1. 该 API 未在 privacy-proxy 中实现
  2. 包名在黑名单中
  3. 使用动态加载的代码(热修复、插件化)

解决方法:

  1. 查看 privacy_hook.json 确认是否包含该 API
  2. 检查黑名单配置
  3. 自定义拦截规则

Q2: 编译失败或运行时崩溃

可能原因:

  1. ASM 版本冲突(特别是高德地图)
  2. AGP 版本不兼容

解决方法:

  1. 添加冲突库到黑名单
  2. 升级到 AGP 8.0+
  3. 检查 Gradle 和 Kotlin 版本

Q3: 如何在线上环境使用?

建议配置:

PrivacySentryBuilder()
    .syncDebug(false)              // 关闭 debug 日志
    .enableFileResult(false)       // 关闭文件输出
    .configWatchTime(3 * 60 * 1000) // 缩短监控时间

注意:

  • ❌ 线上版本不要开启 enableFileResult,避免隐私数据泄露
  • ✅ 可以通过 configResultCallBack 上报统计数据

Q4: 多进程如何处理?

SDK 自动支持多进程,会为不同进程生成独立的日志文件:

主进程:privacy_result.xls
子进程:com.example.service_privacy_result.xls

Q5: 性能影响如何?

  • 编译时间:增加 < 2 秒
  • APK 体积:增加 ~200KB
  • 运行时性能:零额外开销(字节码已修改)
  • 内存占用:缓存数据占用,可通过 watchTime 控制

📖 更新日志

1.3.7_v820_beta4 (2025-05-18)

  • ✅ 支持 AGP 8.0+
  • ❌ 不兼容 AGP 8.0 以下版本(请使用 1.3.6)

1.3.6 (2024-11-01)

  • 修复 T.(args..):T 函数 hook 失败的问题

1.3.5 (2024-05-08)

  • 修复读取小米系统 OAID 反射代理失败的问题
  • 修复 SHA-256 digest error 问题

1.3.4 (2023-09-18)

  • 修复内存缓存数据转换问题
  • 修复 getSimState 闪退问题

1.3.3 (2023-08-22)

  • 重构 plugin 部分,引入 Booster
  • 适配 AGP 和 Gradle 高版本
  • 支持 AGP 7.0+

完整更新日志请查看 CHANGELOG

🤝 贡献指南

欢迎提交 Issue 和 Pull Request!

  • Bug 报告:请详细描述问题和复现步骤
  • 功能建议:欢迎提出改进建议
  • Pull Request:请先创建 Issue 讨论

📄 许可证

本项目采用 MIT License

💰 打赏支持

如果这个项目对你有帮助,欢迎打赏支持!

image image

🌟 Star History

如果觉得有用,请给个 Star ⭭!

🔗 相关资源


注意事项:

  1. ⚠️ 线上版本请关闭 enableFileResult
  2. ⚠️ 尽可能在 attachBaseContext 中第一个调用初始化
  3. ⚠️ 用户同意隐私协议后必须调用 updatePrivacyShow()
  4. ⚠️ 使用高德地图等三方库需配置黑名单
  5. ⚠️ 动态加载的代码无法被拦截

如有问题,请提 Issue

兄弟们,走过路过请给个 Star ⭭~~~

allenymt/PrivacySentry

Android隐私合规整改检测工具,注解+Asm修改字节码的检测方案

Kotlin

2,289

225 commits

updated Dec 30, 2025

See the code

README

PrivacySentry

License API

Android 隐私合规检测工具,可规避应用市场上架合规检测的大部分问题

✨ 核心特性

  • ✅ 编译期字节码插桩:基于 ASM + Booster 框架,零运行时性能损耗
  • 🎯 完整的拦截方案:支持 60+ 敏感 API 拦截,覆盖工信部合规要求
  • 📊 自动化检测报告:生成 Excel 格式的详细调用记录和统计分析
  • 🔄 智能缓存机制:内存/磁盘多级缓存,优化性能
  • 🌐 多进程支持:自动处理多进程场景,独立输出日志
  • 🔧 灵活可扩展:支持自定义拦截规则,黑名单配置

📚 文档导航

📱 社区支持

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

image

🚀 快速开始

版本要求

组件版本要求
AGP8.0+ (推荐 8.2.0)
Gradle8.0+
Kotlin1.8.10+
minSdk19+
compileSdk34+

注意:AGP 8.0 以下版本请使用 1.3.6 版本

Step 1: 添加插件依赖

在项目根目录的 build.gradle 中添加:

buildscript {
    repositories {
        maven { url 'https://jitpack.io' }
        mavenCentral()
        google()
    }

    dependencies {
        classpath 'com.github.allenymt.PrivacySentry:plugin-sentry:1.3.7_v820_beta4'
    }
}

allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
        mavenCentral()
        google()
    }
}

Step 2: 应用插件和依赖

在 app 模块的 build.gradle 中:

// 应用插件
apply plugin: 'privacy-sentry-plugin'

dependencies {
    def privacyVersion = "1.3.7_v820_beta4"

    // 核心库(必须)
    implementation "com.github.allenymt.PrivacySentry:hook-sentry:$privacyVersion"
    implementation "com.github.allenymt.PrivacySentry:privacy-annotation:$privacyVersion"

    // 预置拦截实现(强烈推荐)
    implementation "com.github.allenymt.PrivacySentry:privacy-proxy:$privacyVersion"

    // 类替换功能(已废弃,不推荐使用)
    // implementation "com.github.allenymt.PrivacySentry:privacy-replace:$privacyVersion"
}

Step 3: 插件配置

在 app 模块的 build.gradle 中添加 privacy 配置块:

privacy {
    // ========== 核心配置 ==========

    /**
     * 插件功能总开关
     * 类型:Boolean
     * 默认值:true
     * 说明:控制 PrivacySentry 插件是否生效
     *      - true: 启用插件,执行字节码转换
     *      - false: 禁用插件,相当于未集成
     */
    enablePrivacy = true

    /**
     * 黑名单配置
     * 类型:Set<String>
     * 默认值:null (空列表)
     * 说明:指定不进行字节码修改的包名列表
     *      - 适用场景:
     *        1. 使用了其他 ASM 字节码修改工具的三方库(如高德地图)
     *        2. 已知会导致冲突的 SDK
     *        3. 不需要监控的系统库或三方库
     *      - 注意:blackList 中的包不会被插件修改,也无法拦截其中的敏感 API
     */
    blackList = []

    // 常见黑名单配置示例:
    // blackList = [
    //     "com.loc",              // 高德地图(ASM 版本冲突)
    //     "com.amap.api",         // 高德地图 API
    //     "io.openinstall.sdk",   // OpenInstall SDK
    //     "com.google.android",   // Google 服务(可选)
    //     "androidx"              // AndroidX 库(可选)
    // ]

    /**
     * 静态扫描结果文件名
     * 类型:String
     * 默认值:"privacy_hook.json"
     * 说明:记录所有被代理的方法名和类名的文件名
     *      - 文件位置:项目根目录
     *      - 文件格式:JSON
     *      - 内容包含:
     *        1. hookServiceList: 被 hook 的 Service 列表
     *        2. replaceMethodMap: 被替换的方法映射表
     *      - 设置为 null 或空字符串则不生成文件
     */
    replaceFileName = "privacy_hook.json"

    // ========== 反射 Hook 配置(可选)==========

    /**
     * 反射方法 Hook 开关
     * 类型:Boolean
     * 默认值:false
     * 说明:是否拦截通过反射调用的敏感方法
     *      - true: 拦截反射调用(需配合 reflexMap 使用)
     *      - false: 不拦截反射调用
     *      - 适用场景:
     *        1. 三方 SDK 通过反射获取设备信息(如小米 OAID)
     *        2. 极光推送、个推、穿山甲等 SDK 的设备标识获取
     *      - 性能影响:轻微(仅影响 LDC 指令的匹配)
     */
    hookReflex = false

    /**
     * 反射拦截配置映射
     * 类型:Map<String, List<String>>
     * 默认值:null
     * 说明:配置需要拦截的反射调用
     *      - Key: 类的全限定名
     *      - Value: 该类中需要拦截的方法名列表
     *      - 只有 hookReflex = true 时才生效
     *      - 匹配原理:检测字节码中的 LDC 指令加载的字符串常量
     */
    reflexMap = [:]

    // 反射拦截配置示例:
    // reflexMap = [
    //     // 小米设备标识服务
    //     "com.android.id.impl.IdProviderImpl": [
    //         "getOAID",   // 开放匿名设备标识符
    //         "getAAID",   // 应用匿名设备标识符
    //         "getVAID"    // 开发者匿名设备标识符
    //     ],
    //     // 自定义类的反射方法
    //     "com.example.utils.DeviceUtils": [
    //         "getDeviceId",
    //         "getIMEI"
    //     ]
    // ]

    // ========== 已废弃功能(不推荐使用,仅供参考)==========

    /**
     * 字段 Hook 开关(已废弃)
     * 类型:Boolean
     * 默认值:false
     * 废弃原因:几乎没有业务场景,功能不稳定
     * 说明:是否 hook 字段访问(如 Build.SERIAL)
     *      - 不推荐使用,请使用方法 hook 代替
     */
    // hookField = false

    /**
     * 构造函数 Hook 开关(已废弃)
     * 类型:Boolean
     * 默认值:false
     * 废弃原因:实现复杂,稳定性差,已停止维护
     * 说明:是否 hook 构造函数(主要用于拦截 File 构造函数参数)
     *      - 相关模块:privacy-replace
     *      - 不推荐使用
     */
    // hookConstructor = false

    /**
     * Manifest 处理开关(已废弃)
     * 类型:Boolean
     * 默认值:false
     * 废弃原因:功能边界不清晰,已停止维护
     * 说明:是否处理 AndroidManifest.xml 文件
     *      - 主要用于处理 Service 的 Priority 和 Export
     *      - 不推荐使用
     */
    // enableProcessManifest = false

    /**
     * Service Priority 替换开关(已废弃)
     * 类型:Boolean
     * 默认值:false
     * 废弃原因:针对 MIUI 自启动问题的特殊方案,已停止维护
     * 说明:是否替换 Service 的 Priority 值
     *      - 部分 Service 设置 Priority = 1000 导致自启动
     *      - 开启后会将 Priority 替换为 replacePriority 的值
     *      - 不推荐使用
     */
    // enableReplacePriority = false

    /**
     * Service Priority 替换值(已废弃)
     * 类型:Int
     * 默认值:0
     * 说明:替换后的 Priority 值
     *      - 配合 enableReplacePriority 使用
     *      - 不推荐使用
     */
    // replacePriority = 0

    /**
     * Service Export 关闭开关(已废弃)
     * 类型:Boolean
     * 默认值:false
     * 废弃原因:可能影响厂商推送功能,已停止维护
     * 说明:是否关闭 Service 的 Export 功能
     *      - 注意:部分厂商推送(小米、VIVO、华为)的 PushService 不能关闭
     *      - 配合 serviceExportPkgWhiteList 使用
     *      - 不推荐使用
     */
    // enableCloseServiceExport = false

    /**
     * Service Export 白名单(已废弃)
     * 类型:Set<String>
     * 默认值:null
     * 说明:允许保持 Export 的 Service 包名列表
     *      - 配合 enableCloseServiceExport 使用
     *      - 不推荐使用
     */
    // serviceExportPkgWhiteList = []

    /**
     * Service StartCommand Hook 开关(已废弃)
     * 类型:Boolean
     * 默认值:false
     * 废弃原因:针对 MIUI 自启动问题的特殊方案,已停止维护
     * 说明:是否 hook Service 的 startCommand 方法
     *      - 不推荐使用
     */
    // enableHookServiceStartCommand = false
}

配置优先级说明:

  1. 必须配置:enablePrivacy(总开关)
  2. 强烈推荐:blackList(避免冲突)
  3. 按需配置:hookReflex + reflexMap(反射拦截)
  4. 可选配置:replaceFileName(静态扫描文件)
  5. 不推荐使用:所有标记为 @Deprecated 的配置项

Step 4: SDK 初始化

在 Application 中初始化 SDK:

Kotlin 示例:

class MyApplication : Application() {

    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)

        // ⚠️ 重要:尽可能在 attachBaseContext 中第一个调用
        // 这样可以确保捕获所有敏感 API 调用
        initPrivacySentry()
    }

    override fun onCreate() {
        super.onCreate()

        // 展示隐私协议弹窗
        showPrivacyDialog {
            // 用户同意后,必须调用此方法
            PrivacySentry.Privacy.updatePrivacyShow()
        }
    }

    private fun initPrivacySentry() {
        val builder = PrivacySentryBuilder()
            // 自定义输出文件名
            .configResultFileName("privacy_result")

            // 开启 debug 模式(可在 logcat 查看日志)
            .syncDebug(BuildConfig.DEBUG)

            // 开启文件输出(⚠️ 线上版本请关闭)
            .enableFileResult(BuildConfig.DEBUG)

            // 监控时长(30 分钟)
            .configWatchTime(30 * 60 * 1000)

            // 文件输出完成回调
            .configResultCallBack(object : PrivacyResultCallBack {
                override fun onResultCallBack(filePath: String) {
                    Log.i("PrivacySentry", "结果文件:$filePath")
                }
            })

        PrivacySentry.Privacy.init(this, builder)
    }
}

Java 示例:

public class MyApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        // ⚠️ 重要:尽可能在 attachBaseContext 中第一个调用
        initPrivacySentry();
    }

    @Override
    public void onCreate() {
        super.onCreate();

        // 展示隐私协议弹窗
        showPrivacyDialog(() -> {
            // 用户同意后,必须调用此方法
            PrivacySentry.Privacy.INSTANCE.updatePrivacyShow();
        });
    }

    private void initPrivacySentry() {
        PrivacySentryBuilder builder = new PrivacySentryBuilder()
                .configResultFileName("privacy_result")
                .syncDebug(BuildConfig.DEBUG)
                .enableFileResult(BuildConfig.DEBUG)
                .configWatchTime(30 * 60 * 1000)
                .configResultCallBack(new PrivacyResultCallBack() {
                    @Override
                    public void onResultCallBack(@NonNull String filePath) {
                        Log.i("PrivacySentry", "结果文件:" + filePath);
                    }
                });

        PrivacySentry.Privacy.INSTANCE.init(this, builder);
    }
}

Step 5: 查看检测结果

方式 1:Logcat 日志

# 实时查看日志
adb logcat | grep "PrivacyOfficer"

方式 2:Excel 文件

# 拉取结果文件
adb pull /storage/emulated/0/Android/data/{your.package.name}/files/privacy/

# 文件名格式
# 主进程:privacy_result.xls
# 子进程:{进程名}_privacy_result.xls

Excel 文件包含两个 Sheet:

  1. Sheet 1 - 隐私合规明细:按时间倒序记录所有敏感 API 调用

    • 调用时间
    • 方法别名
    • 函数名
    • 完整调用堆栈
  2. Sheet 2 - 调用次数统计:按堆栈聚合统计调用次数

    • 方法别名
    • 函数名
    • 调用堆栈
    • 调用次数

⚙️ 插件配置详解

核心配置项完整说明

配置项类型默认值说明
enablePrivacyBooleantrue插件功能总开关
- true: 启用插件,执行字节码转换
- false: 禁用插件
blackListSet<String>null黑名单配置
指定不进行字节码修改的包名列表
- 适用场景:ASM 冲突的三方库
- 注意:黑名单中的包无法拦截敏感 API
replaceFileNameString"privacy_hook.json"静态扫描结果文件名
记录所有被代理的方法和类
- 文件位置:项目根目录
- 文件格式:JSON
- 设置为 null 则不生成
hookReflexBooleanfalse反射方法 Hook 开关
是否拦截通过反射调用的敏感方法
- 需配合 reflexMap 使用
- 适用:三方 SDK 反射获取设备信息
reflexMapMap<String, List<String>>null反射拦截配置
- Key: 类的全限定名
- Value: 需要拦截的方法名列表
- 只有 hookReflex=true 时生效

已废弃配置项

配置项类型默认值废弃原因
hookFieldBooleanfalse几乎没有业务场景,功能不稳定
hookConstructorBooleanfalse实现复杂,稳定性差,已停止维护
enableProcessManifestBooleanfalse功能边界不清晰,已停止维护
enableReplacePriorityBooleanfalse针对 MIUI 特殊问题,已停止维护
replacePriorityInt0配合 enableReplacePriority 使用
enableCloseServiceExportBooleanfalse可能影响厂商推送,已停止维护
serviceExportPkgWhiteListSet<String>null配合 enableCloseServiceExport 使用
enableHookServiceStartCommandBooleanfalse针对 MIUI 特殊问题,已停止维护

⚠️ 重要提示:所有标记为"已废弃"的配置项均不推荐使用,可能在未来版本中移除。

黑名单配置说明

什么情况需要配置黑名单?

  1. ASM 版本冲突

    • 使用高德地图 SDK(ASM 9.1 vs 其他版本)
    • 使用其他字节码修改工具的三方库
  2. 已知冲突的 SDK

    • OpenInstall SDK
    • 部分混淆工具
  3. 不需要监控的库

    • 系统库(可选)
    • 不涉及隐私的三方库

黑名单工作原理

  • 插件在 Transform 阶段会跳过黑名单中的包
  • 黑名单采用前缀匹配规则
  • 例如:"com.loc" 会匹配 com.loc.* 下的所有类

配置示例

privacy {
    blackList = [
        // 高德地图(ASM 版本冲突)
        "com.loc",
        "com.amap.api",

        // OpenInstall SDK
        "io.openinstall.sdk",

        // Google 服务(可选)
        "com.google.android",

        // AndroidX 库(可选)
        "androidx",

        // 自定义不需要监控的包
        "com.example.thirdparty"
    ]
}

黑名单注意事项

  • ✅ 推荐:只添加确实冲突的包
  • ❌ 不推荐:盲目添加大量包到黑名单
  • ⚠️ 影响:黑名单中的包无法拦截敏感 API 调用

反射 Hook 配置详解

使用场景

反射 Hook 用于拦截通过反射方式调用的敏感方法,常见场景:

  1. 设备标识获取

    • 小米设备 OAID/AAID/VAID
    • 华为设备标识
    • OPPO/VIVO 设备标识
  2. 三方 SDK

    • 极光推送(JPush)
    • 个推(GeTui)
    • 穿山甲广告 SDK
    • 友盟统计
  3. 自定义反射调用

    • 项目中通过反射获取的敏感信息

工作原理

// 原始代码(反射调用)
Class.forName("com.android.id.impl.IdProviderImpl")
    .getMethod("getOAID")
    .invoke(obj)

// 字节码层面
LDC "com.android.id.impl.IdProviderImpl"  // ← hookReflex 检测这里
LDC "getOAID"                              // ← reflexMap 匹配方法名
INVOKEVIRTUAL Method.invoke()              // ← 替换为代理方法

配置示例

场景 1:小米设备标识

privacy {
    hookReflex = true

    reflexMap = [
        "com.android.id.impl.IdProviderImpl": [
            "getOAID",   // 开放匿名设备标识符
            "getAAID",   // 应用匿名设备标识符
            "getVAID"    // 开发者匿名设备标识符
        ]
    ]
}

场景 2:多个 SDK 配置

privacy {
    hookReflex = true

    reflexMap = [
        // 小米设备标识
        "com.android.id.impl.IdProviderImpl": [
            "getOAID", "getAAID", "getVAID"
        ],

        // 华为设备标识
        "com.huawei.hms.ads.identifier.AdvertisingIdClient": [
            "getAdvertisingIdInfo"
        ],

        // 自定义工具类
        "com.example.utils.DeviceUtils": [
            "getDeviceId",
            "getIMEI",
            "getAndroidId"
        ]
    ]
}

场景 3:极光推送/个推配置

privacy {
    hookReflex = true

    reflexMap = [
        // 极光推送反射获取设备信息
        "cn.jpush.android.api.JCoreInterface": [
            "getDeviceId",
            "getRegistrationID"
        ],

        // 个推反射获取设备信息
        "com.igexin.sdk.PushManager": [
            "getClientid"
        ]
    ]
}

反射 Hook 注意事项

  • ✅ 精确匹配:类名和方法名必须完全匹配
  • ✅ 性能影响:轻微(仅影响 LDC 指令匹配)
  • ⚠️ 必须启用:hookReflex = true 才生效
  • ⚠️ 无法拦截:动态生成的类名或方法名

静态扫描文件说明

replaceFileName 配置

privacy {
    // 生成静态扫描文件
    replaceFileName = "privacy_hook.json"

    // 不生成文件
    // replaceFileName = null
}

文件内容示例

文件位置:项目根目录 /privacy_hook.json

{
    "hookServiceList": [
        "com.example.TestService",
        "com.example.BackgroundService"
    ],

    "replaceMethodMap": {
        "android.app.ActivityManager.getRunningTasks": {
            "count": 5,
            "originMethodList": [
                {
                    "originClassName": "com.example.MainActivity",
                    "originMethodName": "checkRunningTasks"
                },
                {
                    "originClassName": "com.example.utils.AppUtils",
                    "originMethodName": "getRunningApps"
                }
            ]
        },
        "android.telephony.TelephonyManager.getDeviceId": {
            "count": 2,
            "originMethodList": [
                {
                    "originClassName": "com.example.DeviceManager",
                    "originMethodName": "getIMEI"
                }
            ]
        }
    }
}

文件用途

  1. 静态分析:离线分析敏感 API 调用情况
  2. 合规检查:提供给安全团队审查
  3. 调试参考:确认插件是否正确拦截了目标方法
  4. 版本对比:对比不同版本的 API 调用变化

配置优先级建议

优先级配置项建议值说明
⭐⭐⭐ 必须enablePrivacytrue插件总开关
⭐⭐⭐ 强烈推荐blackList根据实际情况避免 ASM 冲突
⭐⭐ 推荐replaceFileName"privacy_hook.json"生成静态扫描文件
⭐ 按需配置hookReflexfalse 或 true根据是否有反射调用
⭐ 按需配置reflexMap[:] 或配置配合 hookReflex 使用
❌ 不推荐所有废弃配置-已停止维护

🔧 SDK 初始化

PrivacySentryBuilder 配置项

方法参数类型默认值说明
configResultFileName(String)String自动生成自定义输出文件名
syncDebug(Boolean)Booleanfalse开启 debug 日志
enableFileResult(Boolean)Booleantrue是否输出到文件
configWatchTime(Long)Long (毫秒)180000 (3分钟)监控时长
configResultCallBack(PrivacyResultCallBack)PrivacyResultCallBacknull文件输出完成回调
enableReadClipBoard(Boolean)Booleantrue是否允许读取剪贴板

最佳实践

1. 初始化时机

class MyApplication : Application() {
    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)

        // ✅ 推荐:第一个调用
        PrivacySentry.Privacy.init(this, builder)

        // ❌ 不推荐:在其他初始化之后
        // MultiDex.install(this)
        // PrivacySentry.Privacy.init(this, builder)
    }
}

原因:attachBaseContext 之后才能通过反射获取 ActivityThread 的 application,如果初始化太晚,可能无法捕获早期的敏感 API 调用。

2. Debug vs Release 配置

private fun initPrivacySentry() {
    val builder = PrivacySentryBuilder()
        .syncDebug(BuildConfig.DEBUG)           // Debug 开启日志
        .enableFileResult(BuildConfig.DEBUG)    // Release 关闭文件输出
        .configWatchTime(
            if (BuildConfig.DEBUG) 30 * 60 * 1000  // Debug: 30分钟
            else 3 * 60 * 1000                      // Release: 3分钟(谨慎)
        )

    PrivacySentry.Privacy.init(this, builder)
}

3. 隐私协议状态管理

override fun onCreate() {
    super.onCreate()

    // 检查是否已同意隐私协议
    if (!hasAgreedPrivacyPolicy()) {
        showPrivacyDialog {
            // 用户同意后保存状态
            savePrivacyAgreement()

            // ⚠️ 必须调用,告知 SDK 用户已同意
            PrivacySentry.Privacy.updatePrivacyShow()
        }
    } else {
        // 已同意,直接告知 SDK
        PrivacySentry.Privacy.updatePrivacyShow()
    }
}

重要提示:

  • ✅ 用户同意隐私协议后,必须调用 updatePrivacyShow()
  • ✅ 调用前的敏感 API 会返回空数据并标记 check!!!
  • ✅ 调用后的敏感 API 返回真实数据并记录日志

4. 手动停止监控

// 手动停止监控和文件写入
PrivacySentry.Privacy.stop()

🎯 自定义拦截

创建自定义代理类

import androidx.annotation.Keep
import com.yl.lib.privacy_annotation.MethodInvokeOpcode
import com.yl.lib.privacy_annotation.PrivacyClassProxy
import com.yl.lib.privacy_annotation.PrivacyMethodProxy
import com.yl.lib.sentry.hook.PrivacySentry
import com.yl.lib.sentry.hook.util.PrivacyProxyUtil.Util.doFilePrinter

@Keep
@PrivacyClassProxy
object MyCustomProxy {

    /**
     * 示例 1:拦截实例方法
     */
    @PrivacyMethodProxy(
        originalClass = YourClass::class,
        originalMethod = "getSensitiveData",
        originalOpcode = MethodInvokeOpcode.INVOKEVIRTUAL
    )
    @JvmStatic
    fun getSensitiveData(
        instance: YourClass,  // 第一个参数是实例对象
        param1: String
    ): String {
        // 记录日志
        doFilePrinter("getSensitiveData", "获取敏感数据: $param1")

        // 检查隐私协议状态
        if (PrivacySentry.Privacy.inDangerousState()) {
            return ""  // 未同意返回空
        }

        // 调用原始方法
        return instance.getSensitiveData(param1)
    }

    /**
     * 示例 2:拦截静态方法
     */
    @PrivacyMethodProxy(
        originalClass = YourUtilClass::class,
        originalMethod = "getDeviceId",
        originalOpcode = MethodInvokeOpcode.INVOKESTATIC
    )
    @JvmStatic
    fun getDeviceId(): String {
        doFilePrinter("getDeviceId", "获取设备ID")

        if (PrivacySentry.Privacy.inDangerousState()) {
            return ""
        }

        return YourUtilClass.getDeviceId()
    }

    /**
     * 示例 3:拦截接口方法
     */
    @PrivacyMethodProxy(
        originalClass = YourInterface::class,
        originalMethod = "getData",
        originalOpcode = MethodInvokeOpcode.INVOKEINTERFACE
    )
    @JvmStatic
    fun getData(instance: YourInterface): String {
        doFilePrinter("getData", "接口方法调用")
        return instance.getData()
    }
}

Java 自定义代理示例

import androidx.annotation.Keep;
import com.yl.lib.privacy_annotation.MethodInvokeOpcode;
import com.yl.lib.privacy_annotation.PrivacyClassProxy;
import com.yl.lib.privacy_annotation.PrivacyMethodProxy;
import com.yl.lib.sentry.hook.PrivacySentry;
import static com.yl.lib.sentry.hook.util.PrivacyProxyUtil.Util.doFilePrinter;

@Keep
@PrivacyClassProxy
public class MyCustomProxyJava {

    @PrivacyMethodProxy(
        originalClass = YourClass.class,
        originalMethod = "getSensitiveData",
        originalOpcode = MethodInvokeOpcode.INVOKEVIRTUAL
    )
    public static String getSensitiveData(YourClass instance, String param) {
        doFilePrinter("getSensitiveData", "获取敏感数据: " + param, false);

        if (PrivacySentry.Privacy.INSTANCE.inDangerousState()) {
            return "";
        }

        return instance.getSensitiveData(param);
    }
}

方法签名规则

实例方法 (INVOKEVIRTUAL)

// 原始方法
class TargetClass {
    fun method(param1: String, param2: Int): String
}

// 代理方法:第一个参数是实例对象
@JvmStatic
fun method(
    instance: TargetClass,  // ⬅️ 额外的第一个参数
    param1: String,
    param2: Int
): String

静态方法 (INVOKESTATIC)

// 原始方法
object TargetClass {
    fun method(param1: String): String
}

// 代理方法:参数完全相同
@JvmStatic
fun method(param1: String): String

接口方法 (INVOKEINTERFACE)

// 原始接口
interface TargetInterface {
    fun method(param: String): String
}

// 代理方法:第一个参数是接口实例
@JvmStatic
fun method(
    instance: TargetInterface,  // ⬅️ 接口实例
    param: String
): String

📋 支持的敏感 API

设备标识

  • ✅ IMEI / DeviceId (TelephonyManager.getDeviceId())
  • ✅ IMSI (TelephonyManager.getSubscriberId())
  • ✅ MEID (TelephonyManager.getMeid())
  • ✅ Android ID (Settings.Secure.getAndroidId())
  • ✅ Serial (Build.getSerial(), Build.SERIAL)
  • ✅ MAC 地址 (WifiInfo.getMacAddress())
  • ✅ ICCID (TelephonyManager.getSimSerialNumber())

网络信息

  • ✅ WiFi 信息 (WifiManager.getConnectionInfo())
  • ✅ WiFi 扫描结果 (WifiManager.getScanResults())
  • ✅ IP 地址 (NetworkInterface, WifiInfo.getIpAddress())
  • ✅ DHCP 信息 (WifiManager.getDhcpInfo())

位置信息

  • ✅ GPS 定位 (LocationManager.getLastKnownLocation())
  • ✅ 基站信息 (TelephonyManager.getAllCellInfo())
  • ✅ 位置监听 (LocationManager.requestLocationUpdates())

应用信息

  • ✅ 已安装应用列表 (PackageManager.getInstalledPackages())
  • ✅ 运行中任务 (ActivityManager.getRunningTasks())
  • ✅ 运行中进程 (ActivityManager.getRunningAppProcesses())
  • ✅ 最近任务 (ActivityManager.getRecentTasks())

联系人和日历

  • ✅ 联系人查询 (ContentResolver.query())
  • ✅ 联系人插入 (ContentResolver.insert())
  • ✅ 日历事件 (Calendar Provider)

传感器

  • ✅ 传感器列表 (SensorManager.getSensorList())
  • ✅ 传感器注册 (SensorManager.registerListener())

其他

  • ✅ 剪贴板 (ClipboardManager.getPrimaryClip())
  • ✅ 蓝牙 (BluetoothAdapter.getAddress())
  • ✅ 权限请求 (requestPermissions())
  • ✅ SIM 卡信息 (TelephonyManager.getSimOperator())

完整列表请参考 privacy-proxy 模块

🔍 检测结果说明

日志格式

[PrivacyOfficer] getDeviceId-线程名: main | 读取IMEI | com.example.MainActivity.onCreate(MainActivity.kt:42)
                                                           ↑ 调用堆栈

危险状态标记

如果在日志中看到 check!!! 标记:

check!!! 还未展示隐私协议,Illegal print

说明:此时还未同意隐私协议,调用了敏感 API

解决方法:

  1. 检查是否在隐私协议同意后调用了 updatePrivacyShow()
  2. 优化代码,避免在隐私协议同意前调用敏感 API

🛠️ 常见问题

Q1: 为什么某些 API 没有被拦截?

可能原因:

  1. 该 API 未在 privacy-proxy 中实现
  2. 包名在黑名单中
  3. 使用动态加载的代码(热修复、插件化)

解决方法:

  1. 查看 privacy_hook.json 确认是否包含该 API
  2. 检查黑名单配置
  3. 自定义拦截规则

Q2: 编译失败或运行时崩溃

可能原因:

  1. ASM 版本冲突(特别是高德地图)
  2. AGP 版本不兼容

解决方法:

  1. 添加冲突库到黑名单
  2. 升级到 AGP 8.0+
  3. 检查 Gradle 和 Kotlin 版本

Q3: 如何在线上环境使用?

建议配置:

PrivacySentryBuilder()
    .syncDebug(false)              // 关闭 debug 日志
    .enableFileResult(false)       // 关闭文件输出
    .configWatchTime(3 * 60 * 1000) // 缩短监控时间

注意:

  • ❌ 线上版本不要开启 enableFileResult,避免隐私数据泄露
  • ✅ 可以通过 configResultCallBack 上报统计数据

Q4: 多进程如何处理?

SDK 自动支持多进程,会为不同进程生成独立的日志文件:

主进程:privacy_result.xls
子进程:com.example.service_privacy_result.xls

Q5: 性能影响如何?

  • 编译时间:增加 < 2 秒
  • APK 体积:增加 ~200KB
  • 运行时性能:零额外开销(字节码已修改)
  • 内存占用:缓存数据占用,可通过 watchTime 控制

📖 更新日志

1.3.7_v820_beta4 (2025-05-18)

  • ✅ 支持 AGP 8.0+
  • ❌ 不兼容 AGP 8.0 以下版本(请使用 1.3.6)

1.3.6 (2024-11-01)

  • 修复 T.(args..):T 函数 hook 失败的问题

1.3.5 (2024-05-08)

  • 修复读取小米系统 OAID 反射代理失败的问题
  • 修复 SHA-256 digest error 问题

1.3.4 (2023-09-18)

  • 修复内存缓存数据转换问题
  • 修复 getSimState 闪退问题

1.3.3 (2023-08-22)

  • 重构 plugin 部分,引入 Booster
  • 适配 AGP 和 Gradle 高版本
  • 支持 AGP 7.0+

完整更新日志请查看 CHANGELOG

🤝 贡献指南

欢迎提交 Issue 和 Pull Request!

  • Bug 报告:请详细描述问题和复现步骤
  • 功能建议:欢迎提出改进建议
  • Pull Request:请先创建 Issue 讨论

📄 许可证

本项目采用 MIT License

💰 打赏支持

如果这个项目对你有帮助,欢迎打赏支持!

image image

🌟 Star History

如果觉得有用,请给个 Star ⭭!

🔗 相关资源


注意事项:

  1. ⚠️ 线上版本请关闭 enableFileResult
  2. ⚠️ 尽可能在 attachBaseContext 中第一个调用初始化
  3. ⚠️ 用户同意隐私协议后必须调用 updatePrivacyShow()
  4. ⚠️ 使用高德地图等三方库需配置黑名单
  5. ⚠️ 动态加载的代码无法被拦截

如有问题,请提 Issue

兄弟们,走过路过请给个 Star ⭭~~~

Significant stargazers

薛翔

1,253 followers · starred Aug 2022

880634

26 followers · starred Dec 2021

JustFor

16 followers · starred Feb 2023

Chu Zheng

93 followers · starred Dec 2023