无侵入-Android隐私API检测方案

7,313 阅读10分钟

大家好,我是八两,今年对游戏行业是特殊(shiye)的一年,防沉迷,隐私合规折磨着无数从业者。
作为一个老天还肯赏口饭吃的程序员,咱也得研究研究政策相关的东西。防沉迷各家渠道都推出了自己的SDK,实在不济,国家也给了API,没什么好说的。但上架渠道动不动因为合规问题被打回就很蛋疼了。更蛋疼的是,要是渠道没打回,上线被国家发现违规,就可以直接去财务部领“奖金”了。
本着不麻烦公司财务部小姐姐的想法,八两同学很早开始就在调研一种合规方案自检查机制。


方案调研

在抛出方案前,咱先带大家一起思考一下目前各家常见的方案,您要是自己思考过了,可以直接跳过看方案部分。
首先吧,众所周知,检测代码分动态检测静态检测
但是国家说了,敏感权限不是不给你调用,你只要跟用户说清楚你为啥调,用户同意了隐私协议后,你爱咋调咋调。八两研究了半天,静态检测找到敏感调用不难,难的是判断敏感信息调用链,再通过调用链去判断是否合规。而且万一CP混淆下代码,加个固之类的,咋咋都扫不了。分析下来静态检测像寒冰在泉水开大,不是不行,只是技术难度极高而且没必要。作为一个懒字当头的程序员,咱果断放弃,看看隔壁动态检测

动态检测方案也很多,先讲两种常见且典型的吧, Gradle transformxposed. Gradle transform 的核心奥义是在编译时替换代码,把敏感API的代码,替换成某些可以被检测到的具有标记的代码,标记代码可以是一个警告日志,埋点上传后端,喜欢怎么,怎么来。这个方案,优缺缺缺点很明显。作为游戏行业从业者,很多时候“编译时”和咱并没有什么关系,说服上下游其他部门接入一套插件的难度并不亚于手动检测一遍,更何况大多数时候,要说服的是其他公司。单纯的Gradle transform的方案并不适合我们苦逼的游戏发行公司。

当然,借鉴Gradle transform的思路,我们可以对最终包进行反编译,扫描替换敏感API重编译后再运行APP进行检测的方法,此变种方案可行。可惜检测时间会稍微拉长,且在我们解决反编译重编译可能遇到的所有问题后,还需要论证反编译的包有合规问题,原包是否一定有合规问题的问题。她很好,但一看就是富家小姐,我配不上。建议家里已经有成熟的反编译框架和代码扫描框架的同学可以尝试追追她。

要不我们来看看 xposed 方案?xposed 本身作为一个优秀的Android Hook框架,的确可以解决 Gradle transform 运行在编译时以及在检测的APK是否是需要被检测的APK的问题。她很美,可惜她早已退隐江湖,不再谈婚论嫁。从她家走出的Exposed和太极小姐还太年轻,追追可以,要么难以承受繁重枯燥的检测工作,要么桀骜不驯难以进行必要的自动化调教。


八两方案

所谓众里寻她别找百度,功夫不负有心人,我们找到了Frida.
Frida 是一款全平台的 Hook 框架,其原理是在目标机器上创建一个守护进程,需要被Hook的进程启动时,修改进程的调用内存,使其调用到我们自己定义的Hook函数上。众所周知,Android底层也是Linux系统,而Android系统的函数调用本质上也是对特定进程地址的访问,Frida就是在这个访问的过程中,通过守护进程欺骗了Android系统,进行替换。

不过不出所料,进行这种欺骗系统的事,肯定需要 root 权限,虽然Frida还提供了一种将so打入APK包,以应用权限进行Hook的方法。但本着反正root手机可以现买,重新打包费时费力的想法,我们采用直接在目标设备开启守护进程进行Hook的方式。

框架确定了,咱再明确下咱的目标,合规要做的工作很多,我们初期的设想是检测在APK弹出用户隐私政策且用户同意前,APP不调用任何敏感系统API。方案是通过Frida守护进程Hook所有敏感系统API,向敏感系统API注入我们的埋点代码,一旦调用我们就输出特别日志,这里埋点代码需要输出调用堆栈,再通过调用堆栈输出检测报告用来排查(shibi)问题。

思路明确了,咱开始动手吧:
Frida 分成服务端和客户端,服务端是常驻在手机设备的守护进程,客户端则是向服务端请求检测指定进程,并且向服务端注入被替换代码的设备。搞不清楚也没关系,不重要。只需要记住,使用Frida Hook前,要先把守护进程开起来就行。


安装Frida 客户端

Frida 客户端通过 JS 代码将Hook函数注入到Frida服务端中, 不过对于隐私检测系统,我们并不需要很复杂的Hook逻辑。只需要判断目标函数有没有被调用,再输出其调用堆栈即可。准备动手前,咱瞄到 Frida 本身在JS的基础上,提供了一层python的封装层与刚刚好符合我们需求的 frida-trace 工具库。 安装方法很简单,如果你有python环境的话:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple frida
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple frida-tools

安装完成后运行 frida --version 检查一下,能看到版本就说明成功了


配置Frida服务端

配置前,我们需要做两件事,准备一台 root 手机,下载 Frida 服务端。
Root手机请自备,frida服务端到这下载: github.com/frida/frida…
找到frida-server-xx.x.x-android-yy xx表示版本号,无脑用最新的就好,yy表示架构,下载手机架构配套的就好。
下载后解压缩:

unxz frida-server.xz

之后adb 连接你的root手机,运行如下命令开启守护进程

adb push frida-server /data/local/tmp/  # 将Frida server文件push到手机上
adb shell   # 进入shell,下述命令基于 adb shell 命令运行
su # 进入 root 权限
chmod 755 /data/local/tmp/frida-server
/data/local/tmp/frida-server &

其后,正常退出 adb shell 即可。 让我们快速冒烟测试一下吧,打开电脑终端运行 frida-ps -U, 正常情况的话,你可以在终端看到手机上的所有进程。

检测敏感API

首先是检测敏感的系统方法是否被调用,这个简单,frida-trace 天然支持,其就在我们安装的 frida-tools 中,连接好手机,以监控包名是 com.frida.test 应用是否调用获取Mac地址为例,Mac地址的其中一个获取方法是 android.net.wifi.WifiInfo.getMacAddress
咱直接运行命令:

frida-trace -U -f com.frida.test -j 'android.net.wifi.WifiInfo*!*getMacAddress'

(不要忘记方法前后的单引号哦)
你会发现手机上启动了 com.frida.test 应用,如果此时你手动操作触发了获取IMEI的操作,在终端能看到:

Instrumenting...                                                        
WifiInfo.getMacAddress: Auto-generated handler at "/Users/kaelluo/Documents/__handlers__/android.net.wifi.WifiInfo/getMacAddress.js"
Started tracing 1 function. Press Ctrl+C to stop.                       
           /* TID 0x4698 */
  9464 ms  WifiInfo.getMacAddress()
  9465 ms  <= "02:00:00:00:00:00"

最后两行给出了APP去获取Mac地址的时间,有相关输出便表示检测到相关获取。同时监控多个方法,也很简单

frida-trace -U -f com.frida.test -j 'android.net.wifi.WifiInfo*!*getMacAddress' -j 'another.class*!*method'

至此,一套隐私检测系统的核心逻辑已经完成,你可以检测任何你关心的敏感API调用了。


输出敏感API堆栈

等等,似乎我们还差点东西,虽然很简单的就知道APP是否调用了敏感API,但不知道调用堆栈,怎么排查信息呢。
需要输出堆栈信息,就需要我们对 Frida 客户端工作的原理有一点点了解了,或者说对 frida-trace 工具的封装进行一点点研究。
细心的同学,应该已经发现,在我们运行了 frida-trace 命令后,可以在你运行的目录找到一个 __handlers__ 的文件夹。打开看看,里面有一个 android.net.wifi.WifiInfo 的目录结构,其中有一个 getMacAddress.js 的文件。
对的,没有错,frida 客户端通过python api创建了一些 js 代码,再将 js 代码注入到 Frida 服务端,Frida 服务端解析并处理这些 js 代码,在检测到目标方法被调用的时候,便会额外执行其中的替换函数。

这样要达成我们的目的就很简单了,只需要找到 frida-trace 生成 js 代码的地方,再进行修改,怎么做?也不难:
首先是尝试找到 frida-trace 的源代码,咱随便创建个python项目, import frida-tools, 超链点进去,很容易就找到了一个tracer.py的文件, 对照着刚才的 js 代码,进行搜索,很快就找到了 _create_stub_native_handler_create_stub_java_handler 两个方法。
很明显_create_stub_java_handler就是我们要找的它。
在这两个函数中,直接使用字符串的形式作为函数返回值了,咱对字符串中的 onEnter 方法稍微进行下修改,对了,onEnter方法表示检测到时做的动作,原方法是直接打印时间和参数名:

onEnter(log, args, state) {
  log(`%(display_name)s(${args.map(JSON.stringify).join(', ')})`);
},

咱加上打印堆栈信息:

onEnter(log, args, state) {
  var Log = Java.use('android.util.Log');
  var Exception = Java.use('java.lang.Exception');
  var String = Java.use('java.lang.String')
  var stack = String.valueOf(Log.getStackTraceString(Exception.$new())).replaceAll("\\n", 'newLine');
  log(`%(display_name)s(${args.map(JSON.stringify).join(', ')})` + "policy_stacktrace: " + stack.replaceAll("/(?:\\r\\n|\\r|\\n)/g", 'newLine'));
},

至此,去运行我们修改过的 trace 方法,就可以在终端输出函数调用堆栈了。


优势与局限性

优势很简单,那就是配置好系统后,能够检测所有安卓应用,不管是否是咱家开发的,还是市面上任何地方你下载的,只要在手机上能够正常运行的,都可以检测,且无需对APK本身进行任何修改。能输出调用堆栈,为到底哪家公司哪个部门的谁来改这一扯皮大业提供了充足的子弹。
至于局限性,此方案对于合规,只检测了用户同意隐私政策前是否违规调用了敏感API。对于隐私政策文本是否漏声明了权限,漏声明了第三方SDK,并未做分析(要想做这事,这是另外一个故事了)。这是此方案对于合规本身的局限。
另外就是动态检测本身的局限,动态检测只能判定该次动态运行的检测时间内是否违规,这会导致漏放某些违规事件,比如弱网等特殊场景才触发违规,可能就检测不到。虽然我觉得吴签很大,但他能一直很大么,能保证其他情况下他不会小么,大概是这么个道理。
当然,我们可以通过模拟弱网等不同场景进行多次检测,或者延长检测时等方式来提高容错率,虽然永远无法达到100%。但这事,只要我们做的比国家严格,就不会有问题。国家合规这一波,咱游戏行业同胞包括APP行业同胞一起共勉吧。


结语

本文只大概说了下进行敏感API检测的大体思路和核心方法,实际实施过程中,我们需要去解决很多问题,比如同时检测很多方法时,检测进程启动会有200-400ms的延时,而APP启动可能更快,这样一些直接运行在Application#onCreate中的延时可能会被漏掉。
另外还需要处理何时停止检测,如何通过日志输出检测报告以及在手机上安装被检测应用,同时多款应用想要检测需要排队咋办等问题,所幸python简单而行之有效的语言特性以及众多的第三方库,能够很快的让你编写出运行起来不怎么快但是稳定的程序,从而解决检测上下游的一些繁杂工作。
如果大家有兴趣,等八两空了,也可以考虑整理以开源或者开放服务的方式公开目前正在使用的隐私检测系统。