标题: 安卓 Frida 入门 分类: 逆向 创建: 2022-11-14 23:17 修改: 2022-11-21 22:12 链接: http://0x2531.tech/reverse/202211142317.txt -------------------------------------------------------------------------------- 目录: 1. 简介 2. 架构 3. 安装 4. frida-tools a) frida b) frida-ps c) frida-trace d) frida-discover e) frida-ls-devices f) frida-kill 5. Hook 6. Frida + Python 7. 应用 a) App 隐私合规检测 b) http(s) 抓包 c) 逆向和破解 * Bypass SSL Pinning * 三板斧 1. 简介 "Dynamic instrumentation toolkit for developers, reverse-engineers, and security researchers." --- Frida 官网(https://frida.re/) 简单来说,Frida 是一个实现了动态二进制插桩技术的工具平台。动态意味着更加灵活、准确和高效,二进 制意味着黑盒,适用于分析闭源软件。 动态插桩是在二进制程序执行时修改其指令、基本块和函数的过程。 Frida 内置了 QuickJS 和 V8 两种 JavaScript 引擎,因此通过将 JS 代码注入应用程序进程,可以 实现 Hook 函数、监视加密 API 或跟踪调试私有应用程序代码等目标。 2. 架构 架构图:https://frida.re/docs/hacking/ 架构分工具侧和目标应用侧。 在工具侧,frida-core 会将 frida-gum 和 gumjs 打包进 frida-agent 共享库(.so),并将 frida-agent 库和 js 脚本注入到目标应用进程空间。同时,通过 PIPE 实现和 frida-agent 的双向 通信。 在目标应用侧,被注入的高性能 frida-gum 库提供动态二进制插桩功能,gumjs 是其 JavaScript 绑定。在 js 脚本中,则通过调用 gumjs 暴露出的 JavaScript API 动态插桩,实现 hook 函数、枚 举已加载的库及其导入导出函数、读写内存和以特定模式搜索内存等目的。 3. 安装 Frida 平台是典型的 C/S 架构,在安卓平台上部署,需分别在安卓设备上安装 Frida Server 和在电脑 上安装 Frida 工具包。 在电脑上: $ pip install frida-tools 检查是否安装成功。如一切顺利,则将在本地安装 frida、frida-ps 等可执行程序。 $ frida --version 15.1.24 如需在 Python、Node.js 中使用 Frida,通过包管理工具安装对应绑定即可。 $ pip install frida # Python 绑定,在 Python 中使用 Frida $ npm install frida # Node.js 绑定 在安卓设备上: 按设备是否需 root,有需 root 的 frida-server 和无需 root 的 frida-gadget 两种部署方 式。 首先,介绍 frida-server 方式。 a) 下载 frida-server https://github.com/frida/frida/releases 页面下载 frida-server,需下载与设备 CPU 架构 和 Frida CLI 版本一致的安装包。 $ adb shell getprop ro.product.cpu.abi armeabi-v7a 这里下载 frida-server-15.1.24-android-arm.xz。 b) 解压并 push 到设备内 $ adb push frida-server /data/local/tmp/frida-server 因为 shell 账号对 /data/local/tmp/ 目录具有读写权限,所以将 frida-server 拷贝到该目录 下。 c) 赋予 frida-server 执行权限,并运行 $ adb shell $ su # cd /data/local/tmp/ # chmod 777 frida-server # nohup ./frida-server > /dev/null 2>&1 & # netstat -tunlp | grep frida tcp 0 0 127.0.0.1:27042 0.0.0.0:* LISTEN 24781/frida-server tcp6 0 0 :::37677 :::* LISTEN 24781/frida-server 接着,介绍 frida-gadget 方式。以 Transformers.apk 为例: a) 反编译 apk $ apktool d Transformers.apk -o Transformers b) 下载和 frida 版本一致的 frida-gadget,并拷贝进 apk 库目录 https://github.com/frida/frida/releases 页面下载 frida-gadget 库,这里是 frida-gadget-15.1.24-android-arm.so.xz。解压后拷贝进 apk 库目录: $ cp frida-gadget-15.1.24-android-arm.so Transformers/lib/armeabi-v7a/ libfrida-gadget.so c) 修改 smail 代码,加载 frida-gadget 库 找到 Activity 入口,在初始化构造方法返回语句前插入如下 smail 代码: const-string v0, "frida-gadget" invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V 最终效果如下: ========== .method public constructor ()V .locals 0 .line 18 invoke-direct {p0}, Landroid/support/v7/app/AppCompiatActivity;->()V const-string v0, "frida-gadget" invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V return-void .end method ========== d) 添加网络权限 如果配置清单没有配置网络权限,需增加如下设置开启网络权限,使得 Gadget 能够打开一个 socket e) 重打包 apk,并签名 $ apktool b -o 1.apk Transformers/ $ jarsigner -verbose -keystore ~/Sec/app-reverse/abc.keystore -signedjar 1-sign. apk ./1.apk abc.keystore f) 安装 apk $ adb install 1-sign.apk g) 使用 frida 此时打开应用会看到空白屏幕,加载的 frida-gadget 库已经打开了一个 tcp socket 等待 frida 的连接。 使用 adb logcat 查看日志,可以看到类似下面的日志: Frida : Listening on 127.0.0.1 TCP port 27042 使用 frida-ps 工具查看进程。frida-ps 和 frida 工具下文有介绍,可以先跳过这段等了解了工具的 使用再回过头来看。 $ frida-ps -U PID Name ----- ------ 21487 Gadget 可以看到正常的包名被 Gadget 代替了 这时可以通过 frida 附着进程进行动态调试了 $ frida -U Gadget [Redmi 6::Gadget ]-> Process.id 21487 [Redmi 6::Gadget ]-> 此时应用正常打开了。 4. frida-tools 在电脑上安装 Frida 工具包后,有如下的命令行工具可用。 frida --- Frida 交互式 CLI frida-ps --- Frida 版 ps 工具 frida-trace --- 动态跟踪函数调用 frida-discover --- 发现程序内部函数,和 frida-trace配合使用 frida-ls-devices --- 显示设备清单 frida-kill --- 杀进程 a) frida Frida 交互式命令行工具,用来辅助快速开发和调试。 $ frida -U -f com.android.browser --no-pause [Redmi 6::com.android.browser]-> Script.runtime "QJS" -U 选项表明通过 USB 连接设备。 通过 -l 选项加载 JS 脚本,进而分析 App 程序是非常常见的做法。如以下脚本将列出安卓浏览器启动后 加载的类名包含 okhttp 的类: ========== function enumAllClasses() { var allClasses = []; var classes = Java.enumerateLoadedClassesSync(); classes.forEach(function(aClass) { try { var className = aClass.match(/[L](.*);/)[1].replace(/\//g, "."); } catch(err) {} // avoid TypeError: cannot read property 1 of null allClasses.push(className); }); return allClasses; } setTimeout(function() { Java.perform(function() { var a = enumAllClasses(); a.forEach(function(s) { if (typeof s !== "undefined" && s.indexOf("okhttp") !== -1) { console.log(s); } }); }); }, 0); ========== 所有 Java 代码都需放在 Java.perform(fn) 回调中执行。同时,Java.perform(fn) 确保在执行回 调函数时,应用程序的类加载器已就位。 使用命令行方式加载 JS 脚本时,需带上 --no-pause 选项才能执行回调函数中的代码。 $ frida -U -l demo.js -f com.android.browser --no-pause [Redmi 6::com.android.browser]-> com.android.okhttp.Protocol com.android.okhttp.TlsVersion com.android.okhttp.CipherSuite Frida 插桩有两种方式,一种是 spwan 模式,由 Frida 调度 zygote 启动进程并挂起,启动的同时注 入 frida 代码,适用于在进程启动前就需执行 hook 的场景,如 hook registerNatives函数,注入 完成后调用 resume 指令恢复进程执行主线程;另一种是 attach 模式,附着到一个已存在的进程,工作 原理是使用系统调用 ptrace 修改进程内存,由于进程最多同时只能被一个进程调试,如果此时进程处于调 试状态(TracerPid 不为0),则 attach 将失败。具体到命令行,使用 -f 选项指定包名即使用 spwan 模式,不使用 -f 选项则使用 attach 模式。 $ frida -U -l demo.js -n 浏览器 --no-pause Attaching... com.android.okhttp.Protocol com.android.okhttp.TlsVersion com.android.okhttp.CipherSuite okhttp3.ConnectionSpec okhttp3.internal.http2.ErrorCode okhttp3.Protocol okhttp3.internal.http2.Huffman$Node okhttp3.TlsVersion okhttp3.CipherSuite okhttp3.internal.http2.Http2Stream okhttp3.internal.http2.Header [Redmi 6::浏览器]-> b) frida-ps 显示设备上正在运行的应用 $ frida-ps -Ua PID Name Identifier ----- ------- ----------------------- 1369 USIM卡应用 com.android.stk 1779 安全中心 com.miui.securitycenter 10177 浏览器 com.android.browser 8379 用户反馈 com.miui.bugreport 9268 相机 com.android.camera c) frida-trace 跟踪浏览器中所有的 JNI 函数 $ frida-trace -U -i "Java_*" -f com.android.browser d) frida-discover 发现程序内部函数,之后使用 frida-trace 跟踪 e) frida-ls-devices 显示设备清单 $ frida-ls-devices Id Type Name ------------ ------ ------------ local local Local System 4d0b25497d2a usb Redmi 6 socket remote Local Socket 当有多台 Frida 设备时,可以通过该工具获取要连接的设备 ID。 $ frida-ps -D 4d0b25497d2a -a PID Name Identifier ----- ------- ------------------------ 1369 USIM卡应用 com.android.stk 11448 个性主题 com.android.thememanager 1779 安全中心 com.miui.securitycenter 11478 小爱同学 com.miui.voiceassist 11678 应用商店 com.xiaomi.market 14352 浏览器 com.android.browser 12492 相机 com.android.camera 11612 短信 com.android.mms 12257 设置 com.android.settings f) frida-kill kill 指定设备上运行的进程。通过 frida-ls-devices 和 frida-ps 分别获取设备 ID 和进程 ID。 $ frida-kill -D 4d0b25497d2a 14352 $ frida-ps -D 4d0b25497d2a -a PID Name Identifier ----- ------- ------------------------ 1369 USIM卡应用 com.android.stk 11448 个性主题 com.android.thememanager 1779 安全中心 com.miui.securitycenter 11478 小爱同学 com.miui.voiceassist 11678 应用商店 com.xiaomi.market 12492 相机 com.android.camera 11612 短信 com.android.mms 12257 设置 com.android.settings 5. Hook Hook 是一种通过拦截函数调用,重写函数已达到特定目的的技术。 下面通过伪代码进一步解释 Hook 的工作机制 原始执行路径: ========== Main() { var Call Func1(a, b) } Func1(a, b) { c = a + b return c } Main() { var = c } ========== Hook 后执行路径: ========== Main() { var Call Hook_Func1(a, b) <--- 拦截函数 Func1 调用,重写函数 } Hook_Func1(a, b) { d = a - b return d } Main() { var = d } ========== 如何 Hook App 里调用的某个 Java 方法呢?Frida 提供了接口: a) 使用 Java.use(className) 获取 JavaScript wrapper b) 重写 JavaScript wrapper 对象的方法 [JavaScript wrapper对象].[要Hook的方法名]. implementation=function(){...} 由于 Java 支持同名方法重载,当要 Hook 的方法有多个重载时,须使用 overload 方法,参数数量和 类型必须完全匹配。 Hook 方法中的参数,可以不声明然后通过 arguments 数组访问,也可以在 implementation 函数中 显式声明对应的形参。 ========== Java.perform(function () { var MainActivity = Java.use("com.github.fridademo.MainActivity"); MainActivity.test.overload("java.lang.String").implementation = function () { console.log("test3(String): " + arguments[0]); this.private_func(arguments[0]); }; MainActivity.test.overload("java.lang.String", "boolean").implementation = function (s, b) { console.log("test4(String,boolean): " + s + ", " + b); this.private_func(s, b); }; }); ========== 以下代码通过 Hook 系统方法监控获取设备安卓 ID 的情况 ========== var SettingsSecure = Java.use("android.provider.Settings$Secure"); SettingsSecure.getString.implementation = function (p1, p2) { var temp = this.getString(p1, p2); if (p2.indexOf("android_id") == -1) { // 找不到 android_id,直接返回。不影响 getString 方法正常使用。 return temp; } send("获取Android ID", "参数为:" + p2 + ",获取到的ID为:" + temp); return temp; }; ========== 6. Frida + Python 通过 Frida CLI 加载 JS 的方式,可以快速进行调试。但在对自动化程度有更高要求的场景下,就需要通 过 Python 来调度使用 Frida 了。 大致流程如下: a) 通过 USB 获取设备对象 b) 使用 spawn 方法启动 App,并获取进程 ID c) 附着进程,生成会话 d) 读取 JS 代码,在会话中创建脚本对象 e) 将 JS 脚本对象注入 App 进程 ========== try: if hostport is None: device = frida.get_usb_device(1) else: device = frida.get_device_manager().add_remote_device(hostport) pid = device.spawn([app_name]) except Exception as e: print("[*] hook error") print(e) exit() time.sleep(1) session = device.attach(pid) time.sleep(1) with open("./script.js", encoding="utf-8") as f: script_read = f.read() if wait_time: script_read += "setTimeout(main, {0}000);".format(str(wait_time)) else: script_read += "setImmediate(main);" script = session.create_script(script_read) script.on("message", my_message_handler) script.load() ========== 最后,再介绍下 Python 和 JS 相互通信机制。Python 运行在电脑侧,JS 运行在设备侧,Python 需 要给 JS 发送指令,JS 需要将结果返回给 Python,那两者是如何通信的呢? a) JS -> Python JS 侧: 调用 send 函数发送信息,如:send({"type": "alert", "message": "balabala", "time": "2022-11-14 21:03"})。 Python 中将接收到 JSON 消息 {"type": "send", "payload": {"type": "alert", "message": "balabala", "time": "2022-11-14 21:03"}}。 Python 侧: 脚本对象调用 on 函数注册 message 回调函数,接收 JS 发送来的 JSON 消息。 ========== import codecs import frida def on_message(message, data): if message['type'] == 'send': print(message['payload']) elif message['type'] == 'error': print(message['stack']) session = frida.attach('iTunes') with codecs.open('./agent.js', 'r', 'utf-8') as f: source = f.read() script = session.create_script(source) script.on('message', on_message) script.load() print(script.exports.add(2, 3)) print(script.exports.sub(5, 3)) session.detach() ========== b) Python -> JS Python 侧: 脚本对象调用 post 函数向 JS 发送消息 script.post({"my_data": data}) JS 侧: 调用 recv 函数接收消息 recv(function(json_object) { data = json_object.my_data }).wait() 此外,还可以通过接口将 JS 函数导出,以便在 Python 里直接调用。具体方法如下: JS 侧: 导出指定函数 rpc.exports = { foo: Foo, bar: Bar } 导出符号名称不能包含大写字母和下划线 Python 侧: 调用导出函数 script.exports.foo() script.exports.bar() Python 调度使用 Frida 被称为 RPC 模式因此而得名。 7. 应用 a) App 隐私合规检测 隐私主要是指用户设备标识信息(如:Android ID、IMEI等)、设备上网信息(如:IP、SSID等)、设备 状态信息(如:安装 App、进程信息等)和位置信息(如:定位信息等)等,安卓系统在 Java 接口层提供 了获取这些信息的接口,在有权限的情况下,App 调用这些接口就可获取用户隐私。 因此,只要关注这些系统接口的调用情况,就可实现隐私合规检测。大致流程如下: * 枚举出尽可能多的隐私信息。这部分可以从监管机构发布的法律法规、管理办法和国标等处获取 * 找出获取这些隐私信息的系统接口 * 编写 frida 脚本,实现对接口的 hook * Python 中调度 frida,实现自动化的检测 具体实现上,上文通过 hook 系统接口关注安卓 ID 的获取情况便是一例,其它的类似这里不再赘述。 b) http(s) 抓包 App 里发送和接收数据包由网络通信库来完成,安卓平台上,主流的网络通信库有自带的 HttpURLConnection 和第三方的 okhttp3。 网络通信库包含处理 http 请求和响应各组成部分的类和方法,以 HttpURLConnection 举例来说: * URL 类的构造方法参数为 url 地址 * setRequestMethod() 和 setRequestProperty() 方法设置请求方法和请求头 * getInputStream() 方法获取响应 通过 hook 这些方法,我们就能获取到对应的 http 组成部分,达到抓包的目的。 ========== setImmediate(function() { Java.perform(function() { var url = Java.use("java.net.URL"); url.$init.overload('java.lang.String').implementation = function (var0) { console.log("[*] Created new URL with value: " + var0 +"\n"); return this.$init(var0); }; var URLConnection = Java.use("java.net.URLConnection"); URLConnection.getInputStream.implementation = function () { console.log("[*] Get input stream called.\n"); return this.getInputStream; }; var httpURLConnection = Java.use("com.android.okhttp.internal.huc. HttpURLConnectionImpl"); httpURLConnection.setRequestMethod.overload('java.lang.String'). implementation = function (var0) { console.log("[*] Set request method called: " + var0 + "\n"); this.setRequestMethod(var0); }; httpURLConnection.setRequestProperty.overload('java.lang.String', 'java. lang.String').implementation = function (var0, var1) { console.log("[*] URLConnection.setRequestProperty called with key: " + var0 + " and value: " + var1 + ".\n"); this.setRequestProperty(var0, var1); }; }); }); ========== 其中,URLConnection 类为 HttpURLConnection 类的父类,HttpURLConnectionImpl 类为 HttpURLConnection 类的实现类。 在安卓浏览器进程中注入脚本后,将看到一些打印信息。 $ frida -U -f com.android.browser -l .\1.js --no-pause [*] Created new URL with value: https://api.ad.xiaomi.com/browser_feed/ uploadLogAdSDK [*] URLConnection.setRequestProperty called with key: gzip and value: 0. [*] Set request method called: POST ... 使用 okhttp3 开发"自吐"脚本也是类似的方法,都需要站在开发者角度先了解如何使用这些网络通信库。 以上"自吐"脚本存在一个缺陷,就是对请求和响应方法分开去 hook,导致的后果就是发送和接收的数据包 是割裂的,无法将请求和响应关联起来。解决方法是利用 okhttp 的拦截器机制,编写拦截器类,编译为 dex 后通过 frida 注入到应用内存空间,然后通过 hook build 方法将其添加到原有的拦截器链中。 c) 逆向和破解 * Bypass SSL Pinning 为了避免被中间人抓包分析,App 会实现 SSL Pinning 机制。具体实现就是通过 okhttp3 和 TrustManager 等网络框架进行 SSL 证书绑定,读取本地的 SSL 证书确定当前请求的域名是否合法,达 到防中间人抓包的目的。 知道了 SSL Pinning 的工作机制后,Bypass 的方案就清晰了,就是 Hook 证书绑定函数使其失效。 ========== setImmediate(function() { Java.perform(function() { var trustManagerExtensions = Java.use("android.net.http. X509TrustManagerExtensions"); trustManagerExtensions.checkServerTrusted.overload('java.lang.Object', 'java.lang.String', 'java.lang.String').implementation = function (var0, var1, var2) { console.log("[*] X509TrustManagerExtensions.checkServerTrusted called for host: " + var2 +"\n"); return this.checkServerTrusted(var0, var1, var2); }; }); }); ========== 工作量主要在收集各类网络框架实现的证书绑定函数上,幸运的是,像 Objection 和 DroidUnpinning 等项目完成了这个工作。但如果 App 做了混淆,证书绑定函数名被改写,以上的 Hook 都将失效。 * 三板斧 三板斧即"hook、invoke、rpc",是逆向过程中的一种常用思路。 在 hook 前,还需先定位 hook 的对象,即 Java 方法,可以通过 Objection 进行快速定位。 Objection 可以快速的罗列已加载类,提供了快速 hook 某个类下的全部方法以及 hook 某个方法的功 能。 定位 hook 对象后,就可以写脚本去 hook 关键方法,修改参数或逻辑了。 除了 hook 方法调用,在 JS 脚本里也可以主动调用 Java 方法或 JNI 函数。 最后,如果需要规模化的调用关键方法,就需要使用 Python 进行 RPC 调用了。