0. 引言:那串令人绝望的sign
作为一名爬虫工程师或者协议分析爱好者,你一定经历过这样的绝望时刻:
你挂上 Charles,配置好 SSL Unpinning,成功抓到了包。你兴奋地打开请求体,准备复现 API。
然而,映入眼帘的不仅有常规的userId、timestamp,还有一个让你透心凉的参数:
"sign":"a1b2c3d4e5f6..."你尝试修改任何一个参数,服务器都会冷冷地回你一句:{"code": 403, "msg": "Invalid Signature"}。
这个sign是怎么来的?是 MD5?是 SHA256?还是加了盐?盐是在本地生成的,还是服务器下发的?
面对大厂 App 动辄几百 MB 的混淆代码(Obfuscated Code),静态分析就像在大海捞针。但是,动态 Hook技术的出现,让这场游戏变得公平了。
今天,我不谈枯燥的汇编,只讲实战。我将带你用Frida这把手术刀,直接切入 App 的内存血管,在加密函数执行的前一毫秒,截获它的“核心机密”。
⚠️ 免责声明:
本文技术仅用于移动安全研究与教学。请勿用于通过非法手段获取数据、攻击服务器或进行任何商业黑产行为。逆向工程应严格遵守《网络安全法》,白帽子需自律。
1. 战前准备:工欲善其事
在开始“手术”之前,我们需要准备好全套工具。
1.1 核心武器库
- 测试设备:一台已 Root 的 Android 手机(推荐 Pixel 或 Nexus,系统 Android 8-10 最佳)。
- 抓包工具:Charles 或 Fiddler(配合 Postern/Drony 解决代理检测)。
- 静态分析:Jadx-GUI(看 Java 层代码)、IDA Pro(看 So 层代码,本篇主要聚焦 Java 层)。
- 动态注入:Frida (Client + Server)。
- 开发环境:Python 3 + VS Code。
1.2 目标确认
为了避嫌,我们将目标 App 称为“TargetApp”。
目标:破解其核心搜索接口的sign生成逻辑,并实现 Python 自动化调用。
2. 第一阶段:侦察(抓包与定位)
2.1 抓包分析
打开 TargetApp,进行一次搜索操作。Charles 抓到的请求如下:
POST /api/v2/search HTTP/1.1 Content-Type: application/json { "keyword": "逆向手机", "page": 1, "timestamp": 1678888888, "nonce": "Ax9871hz", "sign": "3f8a0b9c7d6e5f4a3b2c1d0e9f8a7b6c" }分析:
timestamp和nonce(随机数)通常用于防重放。sign长度为 32 位,且由 0-9, a-f 组成。初步推测是 MD5。- 现在的任务是:找到生成这个 MD5 之前的原始内容(Plaintext)。
2.2 静态分析定位 (Jadx)
将 APK 拖入 Jadx,等待反编译完成。
面对成千上万个类,不要慌。我们要利用关键词搜索。
搜索策略:
- 搜索
"sign"(双引号包裹,精确匹配字符串)。 - 搜索 URL 路径
"/api/v2/search"。 - 搜索常见的加密类名,如
SecurityUtil,SignUtil,Encrypt。
经过一番苦搜,我发现了一个可疑的类com.targetapp.utils.SecurityUtils,里面有一个静态方法:
// 混淆后的代码片段publicstaticStringa(Contextcontext,Map<String,String>map){// ... 省略部分代码 ...StringsortedString=b(map);// 看起来像是在对参数排序Stringsalt=AppConfig.getSalt();// 获取盐值returnMD5.encode(sortedString+salt);// 疑似最终加密点}虽然代码被混淆成了a,b,c,但逻辑结构出卖了它:排序 -> 加盐 -> MD5。
3. 第二阶段:手术(Frida Hook)
静态分析只能通过猜测。万一AppConfig.getSalt()是在 Native 层(.so)生成的动态盐呢?万一排序规则很特殊呢?
这时候,Frida登场了。
3.1 Frida 原理图解
Frida 就像是一个植入 App 大脑的“寄生虫”。它将 V8 引擎注入到目标进程中,允许我们使用 JavaScript 代码动态地修改 Java 方法的行为。
3.2 编写 Hook 脚本
我们要做的就是 Hook 上面找到的com.targetapp.utils.SecurityUtils.a方法。我想看看,传入这个方法的map到底长什么样?返回的String又是什么?
创建一个hook_sign.js:
Java.perform(function(){console.log("[*] Frida script started...");// 1. 定位目标类varSecurityUtils=Java.use("com.targetapp.utils.SecurityUtils");// 2. Hook 目标方法 'a'// 注意:如果方法有重载,需要用 .overload(...) 指明参数类型SecurityUtils.a.overload('android.content.Context','java.util.Map').implementation=function(context,map){console.log("\n================= HOOK DETECTED =================");// 3. 打印入参// 将 Map 转换为 JSON 字符串方便查看varmapStr=Java.cast(map,Java.use("java.util.HashMap")).toString();console.log("[+] Input Map: "+mapStr);// 4. 执行原方法,获取结果varresult=this.a(context,map);// 5. 打印结果console.log("[+] Output Sign: "+result);// 6. 打印堆栈信息(可选,用于追踪是谁调用了它)// console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));console.log("=================================================\n");returnresult;};});3.3 注入并见证奇迹
在 PC 终端运行 Frida 命令:
frida -U -f com.targetapp.android -l hook_sign.js手机上 App 自动启动。我随意在搜索框输入“Python”,点击搜索。
那一刻,终端屏幕疯狂滚动,直到这一行日志出现,我悟了:
================= HOOK DETECTED ================= [+] Input Map: {keyword=Python, page=1, timestamp=1678888888, nonce=Ax9871hz} [+] Output Sign: 3f8a0b9c7d6e5f4a3b2c1d0e9f8a7b6c =================================================等等,这还不够。我还需要知道salt是什么!
我们可以进一步 Hook 那个 MD5 的入口,或者直接在刚才的代码里,Hook 那个b(map)方法(疑似排序拼接字符串的方法)。
修改脚本,HookSecurityUtils.b:
SecurityUtils.b.implementation=function(map){varresult=this.b(map);console.log("[*] Serialized String (Pre-Salt): "+result);returnresult;}再次运行,输出:
[*] Serialized String (Pre-Salt): keyword=Python&nonce=Ax9871hz&page=1×tamp=1678888888看来是标准的 ASCII 排序拼接。
那么盐呢?通常盐是直接拼在字符串后面的。如果我 Hook MD5 函数的入参,就能看到完整体了。
Hookcom.targetapp.utils.MD5.encode(String str):
varMD5=Java.use("com.targetapp.utils.MD5");MD5.encode.implementation=function(str){console.log("[!!!] MD5 Input (THE SECRET): "+str);returnthis.encode(str);}最终的真相:
[!!!] MD5 Input (THE SECRET): keyword=Python&nonce=Ax9871hz&page=1×tamp=1678888888&secret=Unicorn_2024_@!#我悟了!原来所谓的“高强度加密”,就是在参数屁股后面拼了一个固定的盐值&secret=Unicorn_2024_@!#,然后做了一次 MD5。
所谓的“大厂防线”,在内存可视化的那一刻,就像没穿衣服一样。
4. 第三阶段:武器化(Python RPC)
知道了原理,我们就可以用 Python 复现算法。但有些时候,算法可能极其复杂(例如魔改的 AES,或者逻辑在 .so 层很难还原)。
这时候,最好的办法不是还原,而是借用。我们要利用Frida-RPC,让 Python 脚本远程调用手机里的加密函数。
4.1 修改 JS 脚本为 RPC 模式
// rpc_sign.jsrpc.exports={// 导出这个函数给 Python 调用getSign:function(keyword,page,timestamp,nonce){varresult="";// 必须在 Java 线程中执行Java.perform(function(){// 获取当前 Context (通常用 ActivityThread 或者 Application)varcurrentApplication=Java.use('android.app.ActivityThread').currentApplication();varcontext=currentApplication.getApplicationContext();// 构造 HashMapvarHashMap=Java.use("java.util.HashMap");varmap=HashMap.$new();map.put("keyword",keyword);map.put("page",page.toString());// 注意类型转换map.put("timestamp",timestamp.toString());map.put("nonce",nonce);// 主动调用加密函数varSecurityUtils=Java.use("com.targetapp.utils.SecurityUtils");result=SecurityUtils.a(context,map);});returnresult;}};4.2 编写 Python 脚本
importfridaimporttime# 连接 USB 设备device=frida.get_usb_device()# 启动或附加到进程session=device.attach("com.targetapp.android")# 加载 JSwithopen("rpc_sign.js")asf:script=session.create_script(f.read())script.load()# 调用导出函数# 这里的 script.exports 对应 js 里的 rpc.exportsprint("[*] Calling App function via RPC...")keyword="CSDN"page=1ts=int(time.time())nonce="random123"# 就像调用本地函数一样调用手机里的函数!sign=script.exports.get_sign(keyword,page,ts,nonce)print(f"[-] Keyword:{keyword}")print(f"[-] Timestamp:{ts}")print(f"[-] Generated Sign:{sign}")# 接下来就可以拿着这个 sign 去发 requests 请求了效果:
你运行 Python 脚本,手机后台默默地计算出了sign并返回给了你。你不需要扣算法细节,不需要关心盐值变没变。只要 App 不更新,你的爬虫就永远有效。
这就是RPC (Remote Procedure Call)的降维打击。
5. 进阶思考:如果逻辑在 Native 层 (.so) 怎么办?
如果SecurityUtils.a是native方法:
publicstaticnativeStringa(Contextcontext,Map<String,String>map);Jadx 就看不到了。这时候我们需要:
- 解压 APK,提取
.so文件。 - 使用IDA Pro进行反汇编。
- 利用 Frida Hook
Interceptor.attach(Module.findExportByName("libnative.so", "Java_com_..."), ...)。 - 分析汇编指令(ARM64),查看寄存器(x0, x1…)中的值。
虽然难度升级,但核心思路不变:Input -> BlackBox -> Output。只要我们能在 BlackBox 执行前截获数据,我们就赢了。
6. 总结与反思
回顾整个过程,为什么我说“我悟了”?
- 加密不是魔法:无论 App 吹嘘多么安全的算法,在客户端层面,它必须包含“加密逻辑”和“密钥”。只要它在本地运行,就有被逆向的可能。
- Hook 的上帝视角:静态分析是读天书,动态分析是看电影。Frida 让我们跳过了复杂的逻辑推导,直接看到了结果。
- 攻防的不对称性:开发者为了保护一个参数,可能写了上千行混淆代码;而逆向者只需要几十行 Frida 脚本就能绕过。
给开发者的建议(防御):
- 不要把核心盐值硬编码在 Java 层字符串里。
- 关键逻辑放入 Native 层(C++),并进行虚假控制流混淆(OLLVM)。
- 增加 Frida 检测、Root 检测、模拟器检测。
- 最重要的:风控要放在服务端!不要轻信客户端传来的任何
sign,要结合 IP、行为轨迹、设备指纹做综合判定。
互动环节 (Hook)
👀你在逆向过程中遇到过最奇葩的加密算法是什么?
- 把参数转成 Base64 再倒序?
- 把时间戳藏在 User-Agent 里?
- 或者是把密钥写死在图片 Exif 信息里?
在评论区晒出你的“脱坑”经历,点赞最高的评论,我下期专门出一篇针对 Native 层 .so 逆向的保姆级教程!别忘了关注我,带你硬核玩转 Android 安全!👇