简介面向Android开发者、安全研究人员与合规审计团队的隐私合规检测辅助工具源码包基于Frida动态插桩框架以Python驱动自动化检测流程可用于追踪App运行时的敏感信息流向核查是否存在非法收集、传输或存储用户数据的行为。包体共含12个文件其中以Python自动化脚本、Frida插桩脚本、Markdown说明、依赖清单、gitignore配置及7张效果示意图为主压缩包仅1.16MB结构精简便于直接阅读和二次开发。目前已有894人学习浏览。源码完整呈现camille-master项目的主体逻辑包括API调用监听、HTTP/HTTPS通信拦截、SharedPreferences与SQLite本地存储分析、数据流追踪等模块配合截图与说明文档开发者可快速理解动态插桩的接入方式并基于自身检测需求定制监控规则适用于App隐私合规自检、漏洞排查及大规模合规审计前的工具验证。1. 为什么用Frida做Android隐私合规检测动态插桩的选型理由做App合规自查的时候最头疼的是静态扫描只能看权限声明和代码特征但App运行时有没有真的把手机号、IMEI、定位信息传出去静态工具根本看不见。我试过抓包和主动遍历但遇到加密流量、高版本SSL Pinning就抓瞎更不用说那些在代码里动态拼接的API路径。后来换成Frida这套动态插桩思路才算是把“运行时到底调了什么、传了什么、存了什么”完整落到了日志里。这份camille-master源码解决的就是这个场景用Python驱动Frida向目标Android进程注入JavaScript脚本在系统API、网络库、存储组件的外层挂上Hook把涉及隐私数据的调用参数、调用栈、回传结果实时导出。它不需要重打包App也不依赖非公开API主要覆盖电话、位置、通讯录、剪贴板、网络请求、SharedPreferences和SQLite这几类典型隐私面。对于移动开发者、安全测试人员和合规审计人员来说这份源码的价值在于给出一套可以直接跑起来的检测骨架你只需要改包名和运行场景就能输出一份隐私行为日志再去和隐私政策逐条比对。比较反直觉的一点是这个工具的检测重点不是“拦截数据”而是“记录行为”。它的逻辑是只要隐私数据出现在不该出现的地方——比如写入日志、拼进HTTP请求、落在明文数据库里——就足以判定为可疑收集。理解了这个设计思路后面的Python会话管理、JS Hook点和结果解析就有了完整的判断依据。2. camille.py与Frida会话管理Python端如何驱动动态插桩2.1 解压后的项目结构与职责划分拿到camille-master压缩包后解压出来的核心文件其实很精简camille.py是Python主入口负责连接Frida、加载脚本、接收回调script.js是注入到目标进程的Hook逻辑所有隐私API拦截点都定义在里面requirements.txt解决了依赖版本README.md里通常会有运行参数说明剩下的是几张效果示意图和.gitignore。这个结构本身就是一种实践约定Python管“会话与数据处理”JavaScript管“进程内插桩”两类职责不混在一个文件里方便单独维护。我一般会先看requirements.txt里锁定的依赖。按Frida常见的配套这里至少需要frida和frida-tools两个主包。frida是Python绑定库负责启动和连接设备上的插桩进程frida-tools提供frida-ps、frida-spawn这类命令行辅助工具开发阶段用来确认设备连接和进程状态很有用。安装时注意Python版本建议用3.8以上的虚拟环境避免和系统Python环境里的包冲突。2.2 会话启动的两种方式spawn与attachcamille.py里最关键的决策是先spawn还是先attach。对于隐私合规检测我推荐优先用spawn方式也就是由Frida直接冷启动App进程这样能在App最早的代码执行之前注入Hook不会漏掉Application初始化阶段的操作。attach方式适合App已经在运行、不方便重启的场景但可能丢失启动阶段的调用记录。import frida import sys def on_message(message, data): # 接收JS脚本通过send()发来的日志消息 if message[type] send: print([Camille], message[payload]) elif message[type] error: print([HookError], message.get(stack, )) def main(pkg_name): # 连接真机或模拟器上的frida-server默认端口27042 device frida.get_usb_device() # 优先spawn冷启动pid返回后立即attach pid device.spawn([pkg_name]) session device.attach(pid) # 加载同级目录下的script.js with open(script.js, r, encodingutf-8) as f: js_code f.read() script session.create_script(js_code) script.on(message, on_message) script.load() # 恢复运行让App继续执行 device.resume(pid) sys.stdin.read() if __name__ __main__: main(com.example.target)这段代码的逻辑很直白先拿到USB设备引用spawn冷启动目标包名立刻attach到新进程加载JS脚本然后resume让进程继续跑。注意on_message里没有处理data参数如果脚本里需要回调二进制数据需要再扩展。create_script会返回本地脚本控制对象在Python进程退出前保持引用所以用sys.stdin.read()挂住主线程否则脚本可能被GC回收导致Hook失效。这里有几个隐含参数可以调device.spawn可以附加启动参数比如--args传入启动Activity或deep linksession.enable_child_gating()能控制子进程继承Hook适合多进程App。如果App在spawn后崩溃先在script.load()前用device.attach重试一次或者改用attach方式排除Frida在时序上的兼容问题。2.3 回调数据在两种语言之间的流转JavaScript脚本里通过send(payload)把日志送回Python端。这个传输通道是异步的所以Python端频繁收到许多小消息时不要在on_message里做重活比如写SQLite、发请求。我通常把原始日志先放进内存队列或者直接按行刷到本地文件检测结束后再统一解析。script.on(message, on_message)只能注册一个回调如果JS端又用send又用post注意区分消息的type字段。为了更好地管理多类日志可以在JS端约定一个统一的JSON结构{ type: network, timestamp: 2025-01-20 15:04:02, api: okhttp3.Request$Builder.url, pid: 12345, stack: com.example.MainActivity.onCreate, content: https://api.example.com/v1/user?phone138xxxx }这样Python端拿到payload后按type字段放进离散的分组后面解析时也能直接通过键名取值。眼睛盯着调试是一回事跑长时间的批量采集则是另一回事没有结构化日志后面根本无从下手。3. script.js的Hook矩阵定位敏感API、网络通信与数据存储3.1 按隐私面分类的API锚点script.js里罗列的Hook点本质是对Android隐私API的一张对照表。这张表覆盖的数据面直接决定了检测的广度和召回率。我自己整理时通常按四个小组展开设备标识IMEI、Android ID、MAC地址、用户身份手机号、通讯录、SIM序列号、位置轨迹经纬度、基站信息、行为习惯剪贴板、应用列表、Settings缓存。下面的表格展示了这几个分组里最高频的Hook锚点以及对应的Java层方法。注意这些都是Android公开SDK不涉及任何非公开API所以从合规检测角度看Hook到它们不会触发系统完整性校验。隐私面目标类关键方法检测要点设备标识TelephonyManagergetImei / getMeid / getDeviceId返回值是否被传出进程设备标识Settings$SecuregetStringkey为android_id时重点记录位置LocationManagergetLastKnownLocation / getCurrentLocation经纬度精度与频率通讯录ContactsContractquery / resolve_activity调用方包名URI剪贴板ClipboardManagergetPrimaryClip读取人及内容类型网络外传okhttp3.Request$Builderurl / header / methodURL与Body里的隐私字段hook这些点时有个原则不要Hook过分底层的系统服务比如Binder或者SystemProperties.get否则日志会被非隐私数据淹没。抓API方法名比抓字段更稳因为方法名天然有调用栈方便溯源到具体业务代码。3.2 拦截HTTP/HTTPS外传流量App的网络通信是隐私泄露的重灾区尤其是第三方统计SDK偷偷上传设备信息。抓包工具看的是明文流量但App内部可能走OkHttp、HttpURLConnection、WebSocket甚至自定义Socket统一抓手只有使用Java.use拦截请求构造阶段。下面这段是常用的OkHttp拦截逻辑它不关心Hook执行前后是否被加密只记录Request构建时传入的URL和Headerif (Java.available) { Java.perform(function () { var ReqBuilder Java.use(okhttp3.Request$Builder); // 常见SDK路径okhttp3.Request$Builder.url(String) ReqBuilder.url.overload(java.lang.String).implementation function (url) { // url是String类型可以直接用正则判断是否含隐私参数 var urlStr url.toString(); if (urlStr.length 0) { send({ type: network, api: okhttp3.Request$Builder.url, content: urlStr, stack: Java.use(android.util.Log).getStackTraceString( Java.use(java.lang.Thread).currentThread().getStackTrace() ) }); } return this.url(url); }; }); }注意这里用的是overload(java.lang.String)因为url存在多个重载分别接收String、HttpUrl和URL。不加overload的话Frida会报歧义错误。stack字段通过Thread.currentThread().getStackTrace()拿到调用栈并转成字符串这一步对后续定位到是哪个Activity或哪个SDK泄露的至关重要。我一般还会再加一个url(HttpUrl)的hook因为很多加密流量SDK会把URL先封装成HttpUrl对象再传给Builder。对于HTTPS流量如果只想看明文body可以通过Java.use(okhttp3.RequestBody)的writeTo方法抓写入前的Buffer内容。不过注意OkHttp在压缩和加密前后body的内容差异很大最稳妥的仍是拦截应用层API的参数而不是协议层数据。3.3 SharedPreferences与SQLite的读取写入路径隐私数据除了外传还面临本地明文存储的合规风险。Hook本地存储时不能只盯调用点要同时记录“谁在读写”和“读写了什么”。以SharedPreferences为例edit().putString()的次数远没有getString()那么有威胁性——读取意味着App把数据拉回到内存再配合前面的网络Hook就能拼出完整的数据流。// 监听SP的getString读取操作 Java.perform(function () { var SPImpl Java.use(android.app.SharedPreferencesImpl); SPImpl.getString.implementation function (key, defValue) { var value this.getString(key, defValue); send({ type: sp_get, api: SharedPreferencesImpl.getString, key: key.toString(), value: value, stack: Java.use(android.util.Log).getStackTraceString( Java.use(java.lang.Thread).currentThread().getStackTrace() ) }); return value; }; });这段代码直接从SharedPreferencesImpl入手绕开了SharedPreferences接口能在应用调用getString时捕获实际key和默认值。value: value是防止null时send序列化崩溃。sqlite部分可以用类似方式区分query、insert和update重点记录表名与selection参数。但注意系统组件也可能频繁读SP所以在Python端做聚合时要把系统UID或系统进程的日志过滤掉否则日志一小时能膨胀到上百兆。3.4 用调用栈关联数据走向单独的API调用记录只是点要连成线就得依赖调用栈。Android的数据流通常是这样SDK内部 - 系统API - App业务层 - 网络发送。利用我在前面两个code block里写到的stack字段Python端可以直接按方法签名分组画出“哪些业务代码读取了IMEI之后又在同一栈里触发了HTTP请求”。实际项目中我遇到过奇妙的案例一个App在启动阶段通过ContentResolver.query读了一次通讯录很隐蔽地把数据写进Application私有目录的本地文件等到用户下次联网时才上传。如果用单纯API日志抓不到这个过程但用stack分组后能看到同一个MainActivity.onCreate的栈里依次出现了通讯录query和FileOutputStream操作这就能推断出存在“先存储后上传”的违规模式从合规角度可以直接判为过度收集。4. 从检测日志到合规结论结果收集、解析与误报排查4.1 落盘日志与结构化转换会话结束后的第一件事是把on_message收到的所有瞬间打印内容转成可检索的文件。我常用的做法是在Python端按时间戳和来源PID分文件写出每一行是一条JSON。这样无论是用jq、Python脚本打开还是导入ElasticSearch都不会有歧义。下面这段是Python端把消息内容按JSON行追加写入文件的典型写法import json import datetime def parse_line(raw_payload): try: if isinstance(raw_payload, str): return json.loads(raw_payload) return raw_payload except json.JSONDecodeError: # 非JSON结构作为普通文本行保留 return {raw: raw_payload} def on_message(message, data): if message[type] send: payload parse_line(message[payload]) # 只保留包含关键字段的日志 required_fields {type, api, stack} if req_fields.issubset(payload.keys()): payload[time] datetime.datetime.now().isoformat() with open(detect_%s.ndjson % payload[type], a, encodingutf-8) as f: f.write(json.dumps(payload, ensure_asciiFalse) \n)这里有几个工程细节用按type分文件写日志类型之间不互相干扰要求type、api和stack三个字段必须存在否则丢弃避免噪音数据污染后续分析。ensure_asciiFalse是为了中文内容在文件里可读不然一批包名全变成了\u转义。这段代码放在on_message里会对每个消息做一次磁盘IO高频时CPU占用会升高更优做法是攒到一定条数或按时间批量flush不过这个阶段为了日志可靠性直接写入也够用。4.2 按合规条款归类与告警阈值拿到结构化日志后第二步就是进行分类和打分。我把常规边界设成四个等级无风险系统自身行为、低风险本地读取但未外传、中风险读取隐私字段越界传给了第三方SDK、高风险明文外传PII。然后让Python后台脚本去把所有日志标记为不同等级。这里一个简单但不失严谨的分级处理逻辑是过滤pid属于系统进程的日志通常pid 1000或进程名包含android:的直接剔除。系统组件在读设备标识时不该计入App行为。过滤同一个Hook点重复触发的日志。比如getDeviceId在冷启动时被连续调用10次在总日志里保留条数记录分析时降权为1次有效调用。将网络日志中URL和body里出现的手机号、IMEI、经纬度字符串做正则匹配命中一条记一次“高危外传”。所有命中的项目输出成表格与App隐私政策声明一一对照。下面这个表格能帮快速定性一份日志中的异常项日志特征判定倾向处理动作高频读取IMEI且出现网络外传大概率违规列入高优先级复核读取通讯录但未出网可能违规查看用途结合权限弹窗时机剪贴板在前后台切换时读取疑似违规采集前后台状态再定位仅存储到SharedPreferences一般违规检查是否明文及持久化范围这个阶段不需要机器学习正则和计数就能发挥很大作用。因为大多数App的泄露行为都有重复性调用次数异常高本身就意味着没有按最小必要原则做去重。4.3 两类高频误报的来源与排除误报主要有两类。第一类是第三方SDK引发的“代理调用”比如App嵌入了推送SDK、地图SDK共同使用同一个AIDL系统服务时栈里面能同时看到App自己的Activity和SDK的内部方法。这并非App主动行为但检测工具会先把调用归属到宿主App进而误报。处理时需要在stack里识别SDK的包名特征例如com.tencent.*、com.bun.miitmdid.*把它们单独建白名单不进最终告警。第二类是静态Hook位置选错导致的“误伤”。典型的是Settings$Secure.getString函数很多系统组件也会调它查设备参数由于Frida把App进程和系统进程的调用混在一起不加getApplicationContext判断的话会把系统调用算进App头上。我在JS端通常先通过Java.use(android.app.ActivityThread).currentApplication()拿到当前Application再比对getCallingPid()和Process.myPid()是否一致不等的话直接跳过记录。4.4 日志信号与App隐私政策的字段对齐输出合规结论时需要把技术字段翻译成业务语言。拿一份隐私政策文档做比对列表比如政策里写“仅用于安全风控会收集设备型号和IMEI”但日志里却出现了通讯录写库行为这直接构成“未声明收集违规使用”。实际交付报告时我习惯把技术字段和条款字段拼接成一条记录[高风险] 读取IMEI (android: 5d1300f2) - 由 com.example.sdk 调用 - 外传至 https://trace.example.com/log 对应声明: 未在隐私政策中包含“设备标识收集用于数据分析”的描述这一条从“谁读的、传给谁、对应的声明在哪”三个维度都落地了能在评审会上直接当证据用而不是丢一摞日志让人自己看。5. 扩展Hook规则与批量审计技巧5.1 用注册表替代硬编码Hook点script.js里的Hook逻辑如果全写成一个长文件后续每新增一个API就要改中间代码维护成本很高。常见做法是把Hook声明拆成一个hooks数组每条包含类名、方法名、参数列表、日志类型等元数据再通过一个通用方法去动态注册。这样新增检测点时只需要往数组里增加一条配置不用再复制粘剪几十行模板代码。var hooks [ { target: android.telephony.TelephonyManager, method: getLine1Number, overload: [], type: phone }, { target: android.location.LocationManager, method: getLastKnownLocation, overload: [java.lang.String], type: location } ]; hooks.forEach(function (hook) { var targetClass Java.use(hook.target); var overloads hook.overload.length ? hook.overload : hook.method; targetClass[hook.method].overloads.forEach(function (ov) { if (hook.overload.length hook.overload.indexOf(ov.argumentTypes.join(,)) -1) return; ov.implementation function () { var args Array.prototype.slice.call(arguments).map(String); send({ type: hook.type, api: hook.target . hook.method, args: args, stack: ... }); return ov.apply(this, arguments); }; }); });上面这段会让Hook漏掉部分细节比如overload的格式没有做归一化但作为骨架足够清晰。推荐把hooks数组拆到独立的hook_registry.js文件中script.js只做加载和启动执行这种分离在后续配合Python热加载时有明显优势。5.2 批量审计多App和多设备绕过单次检测的临时性批量审计是真正的提效点。我实现过一套方案让camille.py从一个targets.txt读取包名列表遍历所有已连接的真机/模拟器设备每台设备按顺序执行同一个场景脚本。为了减少APP间切换等待可以在device.spawn时用--pre-session注入一个初始等待逻辑让App停在启动页再由Python定时驱动自动化工具模拟操作最后统一拉取日志目录。# 每台设备单独运行一个检测实例 for device_id in $(frida-ls-devices | awk {print $1} | grep -v Local); do python3 camille.py --device $device_id --pkg-list targets.txt \ --duration 120 --output ./report_$device_id.ndjson done wait简单粗暴地并行跑多个进程缺点是设备资源占用偏高但胜在互不干扰。如果是云真机集群还可以把frida-server的启动命令做成模板镜像批量创建实例后再调用这套脚本逻辑一致。5.3 处理App崩溃与反调试的稳定性踩坑对于加壳或反调试的App最稳妥的改善措施是调整Frida启动方式。从spawn改成attach能显著降低启动时的检测特征但会丢失冷启动阶段日志。另一种常见的做法是修改script.js内Hook的时机把敏感API的拦截放在Java.performNow之外用定时器延迟执行让App的主逻辑先走过反调试检测阶段。无论哪种方案都要保留一份原始崩溃日志再对照着在JS里加try/catch。try { targetClass[hook.method].overloads.forEach(...) } catch (e) { send({ type: hook_fail, target: hook.target . hook.method, error: String(e) }); }任何hook注册过程中发生的异常都必须捕获并上报到Python端否则会因为一个方法签名不匹配导致整个脚本中断后面的所有隐私检测全部失效。我把这条规则写成“强制收尾项”脚本加载完成后在Python端先等待5秒再开始操作App确认日志里有hook init done才执行自动遍历没有就退出重跑。5.4 验证Hook是否真正命中目标最终判断这批Hook规则是否有效不能只看有没有日志。正确验证方法是在检测过程中手动触发一条明确的隐私读取动作——比如拨号盘里输入一个号码、点击一次拍照权限弹窗——然后去日志里查对应事件是否有唯一的栈记录。如果连这种终极人为信号都抓不到说明Hook逻辑有问题而不是App没有问题。这样做的优势是可以在无代码改动的情况下完成全套合规验收但缺陷是无法覆盖所有异常分支比如不同机型上的厂商定制ROM会替换某些系统服务实现后续需要为不同系统版本维护对应的Hook指纹表。本文还有配套的精品资源点击获取