移动App逆向实战:Frida动态Hook与协议分析全流程解析
1. 项目概述与核心目标
最近在分析一些移动应用的数据交互逻辑时,我选择了一个典型的阅读类App——“某点小说”作为实战对象。这个项目的目的很明确:完整地走一遍从网络数据抓取到应用层代码Hook的逆向分析流程。这不仅仅是破解某个功能,更重要的是理解一个商业化App是如何构建其通信、加密和业务逻辑的,这对于安全研究、竞品分析或是理解移动端架构都大有裨益。整个过程涉及静态分析、动态调试、协议逆向等多个环节,我会把每一步踩过的坑和总结的技巧都分享出来。
选择“某点小说”是因为它用户基数大,其客户端与服务器之间的通信机制、内容加密方式、用户认证流程都具有一定的代表性。通过这次实战,你不仅能学会如何使用Fiddler、Charles、BurpSuite等工具进行抓包,还能掌握如何利用Frida这个强大的动态插桩工具,去Hook关键的函数,解密数据,甚至修改应用行为。无论你是移动安全的新手,还是想深化逆向技能的开发者,这篇记录都能提供一个清晰的、可复现的路径。
2. 逆向分析环境与工具链搭建
工欲善其事,必先利其器。一个稳定、高效的逆向分析环境是成功的一半。这个环境需要兼顾网络抓包、动态调试和代码分析。
2.1 核心工具选型与配置
网络抓包工具是逆向的“眼睛”。我通常会准备两套方案:Fiddler/Charles和BurpSuite。Fiddler和Charles在HTTPS解密和移动端代理设置上非常直观友好,适合快速查看和修改HTTP/HTTPS请求。而BurpSuite更偏向安全测试,其Repeater、Intruder等功能在参数爆破和漏洞探测时无可替代。对于“某点小说”这类App,我建议先用Fiddler或Charles进行初步的协议分析。
Android测试环境的选择上,真机和模拟器各有优劣。真机(尤其是Root过的)能提供最真实的环境,但操作不便。我更多使用雷电模拟器,它兼容性好,自带Root权限(需要手动在设置中开启),方便安装Xposed、Frida等框架。在模拟器中安装目标App后,别忘了配置Wi-Fi代理,指向你运行抓包工具的主机IP和端口(如192.168.1.100:8888),并在主机上安装抓包工具的CA证书到模拟器中,以解密HTTPS流量。
Frida是整个动态分析的核心。它分为两部分:在PC上运行的客户端(frida-tools)和在移动设备(或模拟器)上运行的服务端(frida-server)。你需要根据模拟器或手机的架构(通常是x86或arm)下载对应版本的frida-server,通过adb push推送到设备,并赋予可执行权限后运行。PC端通过pip install frida-tools安装。一个常见的坑是版本不匹配,务必保持frida和frida-server版本一致。
2.2 辅助工具与脚本准备
除了核心工具,一些辅助脚本能极大提升效率。对于Frida,可以提前准备一些通用脚本,比如枚举所有类和方法、跟踪某个类的所有方法调用、以及拦截加解密函数(如javax.crypto.Cipher的doFinal方法)的脚本。网络上有很多开源的一体化Frida脚本,它们集成了常用功能,但理解其原理并自己修改以适应特定App更重要。
静态分析工具如JADX-GUI或APKTool也是必备的。在开始动态Hook之前,先用它们反编译APK,浏览一下Java代码,搜索关键词如“encrypt”、“decode”、“sign”、“token”,可以快速定位到可能的关键函数,为后续的Frida Hook提供精准目标。有时候,关键的逻辑可能写在Native层(.so文件),这就需要用到IDA Pro或Ghidra进行更底层的分析,本次实战以Java层为主。
注意:在模拟器或真机上运行Frida-server时,可能会遇到App检测Frida的情况。一些加固后的App会检测进程名、端口或内存中的特征。应对方法包括重命名Frida-server二进制文件、使用定制编译的Frida、或者先挂起App进程再注入。在本次对“某点小说”的分析中,其社区版未遇到强检测,但这是实际工作中必须考虑的环节。
3. 网络协议抓包与初步分析
一切就绪后,第一步就是观察App是如何与服务器“对话”的。这是理解其业务逻辑和数据流的起点。
3.1 HTTPS流量捕获与解密
启动雷电模拟器,设置好代理并安装CA证书。然后打开Fiddler,确保“Capture HTTPS CONNECTs”和“Decrypt HTTPS traffic”选项已勾选。接着在模拟器中启动“某点小说”App。
很快,Fiddler的会话列表中就会刷出大量请求。我们需要关注的是那些与核心业务相关的接口,例如获取书籍目录、章节内容、用户信息、书架同步等。通过观察URL路径(如包含/chapter/、/book/、/user/)和响应内容类型(application/json),可以快速筛选出目标接口。
一个关键步骤是解密HTTPS。如果配置正确,你应该能看到请求和响应的明文JSON数据。如果遇到“Tunnel to...443”或者响应乱码,说明证书安装有问题或App使用了证书绑定(SSL Pinning)。对于后者,就需要动用Frida来绕过。幸运的是,在初步测试中,“某点小说”的基础内容接口没有启用强证书绑定,我们可以直接看到类似以下的请求和响应:
请求示例:
GET /api/v3/book/123456/chapters?page=1 HTTP/1.1 Host: api.xxx.com User-Agent: .../Android Authorization: Bearer eyJhbGciOiJIUzI1NiIs... X-Sign: a1b2c3d4e5f6...响应示例(明文):
{ "code": 0, "message": "success", "data": { "chapterList": [ {"id": 1, "title": "第一章", "content": "5pWw5o2u5LqG...", "isVip": false}, {"id": 2, "title": "第二章", "content": "5LiN5Y+v5LqG...", "isVip": true} ] } }3.2 关键接口与参数识别
通过观察多个请求,我们可以总结出该App接口的一些共性:
- 认证方式:通常在请求头中使用
Authorization: Bearer <token>,这个token在登录后获得,是维持会话的关键。 - 签名机制:几乎每个请求都带有一个
X-Sign或类似名称的头部。这是客户端生成的、用于防止请求被篡改的签名。签名算法通常是服务端和客户端约定的,将请求参数(可能包括时间戳、设备信息等)按特定规则拼接后,再进行某种哈希(如MD5、SHA256)或HMAC计算。逆向签名算法是本次分析的核心难点之一。 - 数据加密:注意响应中
content字段的值,它看起来像Base64编码的字符串(字符集符合Base64特征)。解码后可能仍然是乱码,这说明服务器返回的章节内容很可能在Base64编码之上,还进行了额外的加密(可能是对称加密如AES)。VIP章节的content字段甚至可能直接返回null或一个提示,需要购买后才能解密。 - 分页与参数:列表类接口通常包含
page、size等分页参数。
此时,我们的目标变得清晰:一是找到生成X-Sign签名的方法;二是找到解密content内容的方法。这需要我们从动态运行的App中“钩取”这些关键函数。
4. 静态代码分析与关键函数定位
在盲目使用Frida进行大面积Hook之前,先通过静态分析缩小目标范围是高效的做法。使用JADX-GUI打开“某点小说”的APK文件。
4.1 关键词搜索与代码浏览
首先进行全局搜索。搜索关键词包括:
- “sign”: 用于寻找签名相关的类和方法。
- “encrypt”/“decrypt”/“cipher”: 用于寻找加解密相关代码。
- “AES”/“DES”/“RSA”/“MD5”/“SHA”: 常见的算法名。
- “Base64”: 用于定位编码解码处。
- 关键接口的路径片段: 如“
/api/v3/book/”,有时代码中会硬编码或拼接URL。
搜索“sign”后,我们可能会发现多个类,例如SignUtil、SecurityHelper、ApiSign等。点进去查看,重点关注那些包含Map<String, String>参数、返回一个String类型签名、并且方法内部有MessageDigest(用于MD5/SHA)或Mac(用于HMAC)初始化代码的函数。
例如,我们可能找到一个疑似的方法:
public class SecurityUtils { public static String generateSign(Map<String, String> params, String secretKey) { // 1. 对参数按key排序并拼接成字符串 // 2. 拼接上secretKey // 3. 进行MD5哈希 // 4. 返回哈希值的十六进制字符串(可能转为大写) } }同样,搜索“decrypt”或查看content字段的处理逻辑,可能会定位到一个DecryptUtil类,其中包含使用Cipher类进行AES解密的方法。
4.2 确认与记录目标
通过静态分析,我们假设了两个关键目标:
- 签名函数:
com.xxx.reader.utils.SecurityHelper.generateApiSign(Map, String) - 解密函数:
com.xxx.reader.utils.CryptoUtil.aesDecrypt(String, String)
记下这些类的完整路径和方法签名。静态分析的结果是推测性的,最终需要通过动态Hook来验证这些函数是否确实被调用,以及它们的输入输出是否符合我们的观察。
实操心得:大型App的代码往往经过混淆。类名和方法名可能变成
a、b、c。这时,关键词搜索可能失效。我们需要转变思路,通过搜索字符串常量来定位。例如,在抓包中看到的接口路径/api/v3/book/,或者错误信息“sign error”,在代码中搜索这些字符串,就能找到引用它们的方法,从而顺藤摸瓜找到关键逻辑。JADX的“查找用例”功能非常好用。
5. Frida动态Hook实战
这是整个流程中最精彩的部分。我们将编写Frida JavaScript脚本,将“钩子”注入到目标App进程中,拦截并监视关键函数的执行。
5.1 Frida脚本基础与函数Hook
首先,我们编写一个基础的脚本框架,用于Hook上面定位到的疑似签名函数。
// hook_sign.js Java.perform(function() { console.log("[*] Starting Frida Hook Script..."); // 定位目标类 var SecurityHelper = Java.use("com.xxx.reader.utils.SecurityHelper"); // Hook 目标方法 SecurityHelper.generateApiSign.implementation = function(paramsMap, secretKey) { console.log("[+] generateApiSign called!"); // 打印传入的参数 console.log(" |- paramsMap: " + JSON.stringify(paramsMap)); console.log(" |- secretKey: " + secretKey); // 调用原方法,获取真实的返回值 var originalResult = this.generateApiSign(paramsMap, secretKey); console.log(" |- originalResult: " + originalResult); // 返回原结果,不影响App正常运行 return originalResult; }; console.log("[*] generateApiSign Hook installed."); });保存脚本后,在命令行中运行:
frida -U -l hook_sign.js -f com.xxx.reader --no-pause-U: 连接到USB设备(模拟器)。-l: 加载脚本。-f: 启动目标App包名。--no-pause: 立即启动。
如果Hook成功,当App发起网络请求时,我们将在Frida控制台看到该函数被调用的日志,并清晰地看到传入的参数和计算出的签名值。通过对比多次请求的输入输出,我们可以逆向推导出签名算法:例如,是否所有参数都参与签名?参数拼接顺序是什么?使用的哈希算法是MD5还是SHA256?secretKey是固定的还是动态的?
5.2 复杂参数处理与算法验证
在实际Hook时,参数可能不是简单的Map<String, String>,可能是JSONObject或自定义对象。我们需要熟悉Frida的API来正确处理这些类型。
// 如果参数是JSONObject var JsonObject = Java.use("org.json.JSONObject"); SecurityHelper.generateApiSign.implementation = function(jsonParams, secretKey) { console.log("[+] generateApiSign called!"); // 将JSONObject转为字符串查看 var paramsString = jsonParams.toString(); console.log(" |- jsonParams: " + paramsString); // ... 后续操作 }; // 遍历Map的另一种方式 if (paramsMap) { var iterator = paramsMap.keySet().iterator(); while (iterator.hasNext()) { var key = iterator.next(); var value = paramsMap.get(key); console.log(` |- ${key} : ${value}`); } }为了验证我们对算法的猜测,可以在Hook函数中模拟计算。例如,我们怀疑它是MD5(排序后的参数字符串 + secretKey)。那么可以在脚本中引入JavaScript的Crypto库(或使用Frida的Crypto模块)进行实时计算,并将计算结果与原结果对比。如果一致,则算法破解成功。
5.3 Hook加解密函数与内容解密
接下来Hook疑似的内容解密函数。方法类似,但这里我们更关心解密后的明文。
// hook_decrypt.js Java.perform(function() { var CryptoUtil = Java.use("com.xxx.reader.utils.CryptoUtil"); CryptoUtil.aesDecrypt.implementation = function(encryptedBase64, key) { console.log("[+] aesDecrypt called!"); console.log(" |- encryptedBase64: " + encryptedBase64); console.log(" |- key: " + key); var originalResult = this.aesDecrypt(encryptedBase64, key); console.log(" |- decryptedResult: " + originalResult); // 这里应该是解密后的章节明文! return originalResult; }; });运行此脚本,触发一个章节内容的请求。如果Hook点正确,你将在日志中看到,传入的encryptedBase64就是响应中content字段的值,而originalResult就是解密后的、可读的小说文本。key的来历也需要关注,它可能是固定的字符串,也可能是从服务器另一个接口动态获取的。
有时,解密函数可能不是直接暴露的静态方法,而是在某个回调或网络库的拦截器中被调用。这就需要我们扩大Hook范围,或者去Hook更底层的Cipher.getInstance()和Cipher.doFinal()方法。
5.4 主动调用与算法复现
当我们确认了签名和加解密的算法细节后,最终目标是在脱离App环境的情况下,用Python或其他语言复现整个流程。Frida的另一个强大功能是主动调用(RPC)。
我们可以修改脚本,将关键函数暴露给外部调用:
// hook_rpc.js Java.perform(function() { var SecurityHelper = Java.use("com.xxx.reader.utils.SecurityHelper"); rpc.exports = { generatesign: function(paramsJson, secretKey) { var result = ""; Java.perform(function() { // 将JSON字符串转为Java的HashMap var HashMap = Java.use('java.util.HashMap'); var map = HashMap.$new(); var params = JSON.parse(paramsJson); for (var key in params) { map.put(key, params[key]); } // 主动调用 result = SecurityHelper.generateApiSign(map, secretKey); }); return result; }, decryptcontent: function(cipherText, key) { var result = ""; Java.perform(function() { var CryptoUtil = Java.use("com.xxx.reader.utils.CryptoUtil"); result = CryptoUtil.aesDecrypt(cipherText, key); }); return result; } }; });在Python端,我们可以这样调用:
import frida import json with open("hook_rpc.js", "r") as f: js_code = f.read() session = frida.get_usb_device().attach("某点小说") script = session.create_script(js_code) script.load() # 调用远程导出的函数 params = {"bookId": "123456", "page": "1"} secret = "固定的或动态获取的Secret" sign = script.exports.generatesign(json.dumps(params), secret) print("计算出的签名:", sign)这样,我们就拥有了一个可以独立运行的签名/解密服务,可以用于编写爬虫或进行其他自动化测试。
6. 问题排查与进阶对抗
在实际操作中,几乎不会一帆风顺。下面记录一些常见问题及其解决思路。
6.1 常见问题速查表
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| Frida连接被拒绝或进程崩溃 | App检测到Frida注入 | 1. 使用-f参数在App启动前注入。2. 使用 frida-server的隐藏版本(如frida-server-xx.x.x-android-xxx-hidden)。3. 修改 frida-server文件名和进程名。4. 使用 objection等工具的anti-frida绕过脚本。 |
| Hook函数后无任何日志输出 | 1. 类名/方法名错误(混淆导致)。 2. 方法签名不匹配(参数类型、数量)。 3. 该方法从未被调用。 | 1. 使用Java.choose()或Java.enumerateMethods()动态枚举已加载的类和方法。2. 检查方法重载,使用 overload()指定正确的参数类型。3. 扩大Hook范围,Hook其父类或接口方法。 |
| 抓包工具看不到HTTPS明文 | 1. CA证书未正确安装到系统信任区。 2. App使用了SSL Pinning(证书绑定)。 | 1. 确认证书已安装并开启“信任用户证书”。 2. 使用Frida Hook SSL验证相关方法(如 TrustManager、OkHttp的CertificatePinner),使其总是返回成功。 |
| 签名算法看似随机,每次不同 | 签名包含时间戳(timestamp)或随机数(nonce)。 | 在Hook中仔细检查传入参数,寻找ts、timestamp、nonce等字段。算法通常是MD5/SHA(排序参数 + secretKey),时间戳也作为参数之一参与计算。 |
| 解密函数Hook到,但key为空或错误 | key可能从本地存储(如SharedPreferences)或另一个网络接口获取。 | 1. HookSharedPreferences的getString方法,查看相关key。2. 搜索与“key”、“token”、“secret”相关的网络请求,并分析其响应。 |
6.2 应对代码混淆与加固
对于混淆严重的App,静态分析几乎失效。此时必须依赖动态分析。
- 堆栈跟踪法: Hook一个你确定会被调用的基础方法,例如
okhttp3.Request.Builder.build(),在Hook函数中打印当前的调用堆栈(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()))。从堆栈信息中,可以找到App自定义的类和方法名,即使它们是a.a.a.b这样的形式。 - 模糊Hook: 如果知道目标方法的大致特征(如参数数量、返回值类型),可以尝试枚举所有类的方法进行模糊匹配和Hook。这虽然效率低,但在没有头绪时是最后的手段。
- 关注系统API: 无论业务逻辑如何混淆,最终调用系统API(如
MessageDigest.getInstance()、Cipher.init())的代码是无法混淆的。直接Hook这些系统API,然后反向追溯调用栈,是定位关键逻辑的利器。
6.3 协议复现与自动化
当成功逆向出签名和加解密算法后,就可以用Python的requests库等工具,完全模拟客户端的行为。
- 构建请求: 按照规则组装请求参数,包括设备信息、时间戳等。
- 生成签名: 用Python复现签名算法,计算
X-Sign。 - 发送请求: 携带签名和Token(如果需要)发送请求。
- 处理响应: 对返回的加密内容,用Python复现的解密算法进行解密。
整个过程可以封装成一个爬虫或API客户端。但务必注意法律和道德边界,仅将技术用于学习、安全研究或经授权的测试,不要用于侵犯版权、干扰服务等非法用途。
逆向分析是一个需要耐心、细心和发散思维的过程。从抓包看到现象,到静态分析猜测可能,再到动态Hook验证猜想,最后复现算法完成自动化,每一步都环环相扣。这次对“某点小说”的实战,涵盖了从入门到进阶的典型路径。最大的收获不是破解了某个App,而是建立起一套应对未知App的分析方法论。工具在变,对抗技术在升级,但“观察-假设-验证”的核心逻辑不会变。
