手里拿到的目标是某款分类信息类APP的最新版本,需求很明确:弄清楚它列表页的数据是怎么加载的,以及那些加密参数到底怎么生成。这类逆向分析任务我做过不少,但每次都会碰到新东西,这个案例之所以能拿到95分,是因为整个链路从静态分析、动态调试到协议还原,每一步都走得比较完整,没有靠猜,最后也跑通了完整的验证闭环。
这篇文章就把整个分析过程原原本本梳理一遍,包括工具选型、踩坑记录、参数推导思路,以及最后是怎么把结论固化成可复用脚本的。如果你正准备入门APP逆向分析,或者在做类似数据协议解析时卡在某个环节,这篇应该能帮你把思路理顺。
1. 拿到目标先别急着上工具:逆向分析的前置梳理与目标界定
很多人拿到APK第一件事就是拖进Jadx开始看代码,我建议先冷静一下。逆向分析最怕的不是技术难点,而是目标不清,最后分析到一半发现方向错了,前面全是白干。所以第一步,我会先想清楚三个问题:这个APP的通信模型是什么样、我要拿到的数据链路在哪里、分析到什么程度算“完成”。
这个案例的目标明确指向列表页数据,那核心链路就是“客户端请求服务端接口→服务端返回加密数据→客户端解密渲染”。我要找的,实际上就是这条链路上的三个关键节点:请求参数怎么拼的、签名怎么生成的、响应数据怎么解密的。这三个点全部打通,整个分析就算完成了,其他的代码可以完全不看。
1.1 环境与工具选型:这套组合兼顾效率与可复现性
这一步直接决定后面顺不顺利。这次分析我用的工具组合是:Jadx负责静态反编译、Frida做动态Hook、Objection辅助快速定位、Charles配合抓包验证。这套组合是目前做Android逆向最主流的方案,没有之一,原因有三点。
第一,Jadx的反编译输出可读性在同类工具里属于第一梯队,Gradle插件还能直接导出Gradle工程,方便全局搜索关键字符串。第二,Frida的脚本注入模式让动态调试不需要重启APP,基于JS/Python的API设计在快速验证想法时非常高效。第三,Charles的SSL代理配置成熟,处理HTTPS解密很稳定,配合Frida的SSL Pinning绕过,能形成一个完整的数据观察闭环。
还有个细节容易被忽略:分析机的Android版本不要太高,我习惯用Android 8~10的模拟器或者真机。太高的系统版本对Frida的兼容性要求更严,SSL Pinning绕过的方案也可能因为系统证书信任机制的变化而失效。这次用的是Pixel模拟器,Android 9,Frida 15.x,一切都是最顺的状态。
1.2 分析边界确认:哪些内容属于“可逆向”的范围
接到任务先确认边界,这既是技术问题也是职业操守问题。像这次分析的APP,我用的是公开可下载的版本,分析目的是了解数据协议结构和加密流程,用于个人的学习研究和接口对接验证,不涉及任何数据篡改、批量抓取用户隐私、绕过付费或授权机制。这个边界一旦越了,性质就完全不同了,所以整个分析过程中我都会刻意避开跟账号体系、支付逻辑、用户隐私数据相关的代码路径。
实际操作中我会做一个简单的流程梳理表,列清楚分析范围和禁区:
| 分析范围 | 涉及模块 | 处理方式 |
|---|---|---|
| 列表页数据协议 | 网络层、JSON解析、列表渲染 | 重点分析 |
| 签名参数生成 | 加密工具类、公共方法 | 重点分析 |
| 响应数据解密 | 解密工具类、密钥管理 | 重点分析 |
| 账号登录体系 | 登录、Token存储 | 只观察不介入 |
| 支付与订单 | 支付SDK、订单创建 | 完全不碰 |
这样表一列,后面的工作就清晰了,该看代码的时候集中火力,不该看的地方自动绕开,效率反而更高。
2. 静态层摸底:从APK外壳到核心代码定位的完整链路
环境准备到位后,正式进入分析的第一阶段——静态分析。这个阶段的目标不是把所有代码都看懂,而是快速建立整个APP的“代码地图”,搞清楚关键逻辑住在哪个类、哪个方法里。很多人在这一步会被海量的反编译代码吓到,其实掌握几个搜索和定位技巧后,效率会高很多。
2.1 APK基础信息采集:包名、版本、加固状态一次摸清
先把APK的基础信息采集完整。用Jadx打开目标APK后,第一件事是查看AndroidManifest.xml里的包名、版本号和Application类。包名决定了代码的主路径,Application类则是APP启动时最早执行的入口,很多全局初始化和解密逻辑都会放在这里。
然后是判断加固状态。这一步特别重要,如果APP做了加固,直接看反编译代码是看不到真实逻辑的,看到的只是一个壳。判断方法很简单,看Application节点和入口Activity的类名是否为加固厂商的特征包名,比如某些在线加固平台的名字特征非常明显。如果确定有加固,就得先脱壳,这会额外引入一套操作流程。
这个案例比较走运,是裸包,没有加固,Jadx打开就能看到完整源码。不过这年头裸包越来越少了,如果遇到加固APP,我一般会先用Frida加内存脱壳或者配合模拟器脱壳方案处理,但那是另一个复杂度等级了,等以后再单独开一篇讲。
2.2 全局搜索定位“签名”与“加密”关键词的实战手法
裸包情况下,定位核心代码最直接的办法就是全局搜索。我这次主要搜了三批关键词:第一批是“sign”“signature”“token”这类跟签名相关的;第二批是“AES”“DES”“RSA”“encrypt”“decrypt”这类跟加密算法相关的;第三批是“MD5”“SHA”这类跟摘要算法相关的。
搜索结果通常会很多,这时候要靠代码的上下文去筛选。一个很实用的技巧是:关注那些被多个网络请求类共同引用的工具类,这种类大概率就是所有请求参数的统一加工入口。比如这个案例里,我搜“sign”找到了一个名为RequestSigner的类,它在多个API调用中反复出现,顺着引用关系往上追,很快就能找到列表页的网络请求入口。
另一个技巧是看网络层的封装方式。主流方案有OkHttp拦截器、Retrofit注解、RxJava链式调用等,找到OkHttp的Interceptor实现类,基本就等于找到了所有请求的统一出口,签名逻辑九成就在拦截器里对Request.Builder做加工。
2.3 列表页数据链路还原:从UI组件追溯到网络请求入口
定位到网络入口后,需要把“列表页刷新→构造请求→发送→解析”这条链路完整串起来。我的做法是先用关键词“RecyclerView”“Adapter”定位列表页的Fragment或Activity,然后找到这个页面里加载数据的方法,通常叫loadData()、refreshData()或者getList()之类的名字。
顺着这个方法往下看,会看到一个数据仓库类或网络仓库类,它负责组装请求参数并调用API接口。这个位置就是一个关键路口:在这里会看到请求参数的名称、值的来源、以及哪些是固定值哪些是动态生成的。把这个路口的信息记录下来,后面的动态分析就用得上了。
这次案例里,我定位到的列表接口路径是 /api/v1/list,请求参数里有 page、pageSize、city、category 和一个重要的 sign 参数。page和pageSize好理解,city和category是业务筛选条件,sign则是32位十六进制字符串,一眼就能看出是MD5或类似摘要算法的产物。至此,静态分析告一段落,接下来需要动态手段来验证这个sign到底怎么生成的。
3. 动态层交锋:抓包、Hook与反调试绕过的实战细节
静态分析只能告诉我们“在哪里”,真正要搞清楚“怎么算出来”,必须做动态分析。动态分析的两大核心工具就是抓包和Hook,一个负责看数据流转,一个负责改运行逻辑。这个阶段也是整个逆向过程里最容易出问题的地方。
3.1 抓包方案对比:Charles与Frida联动的SSL Pinning绕过记录
抓包的目的很简单——直接看APP发到服务端的完整请求长什么样。但现实总是比理想残酷,很多APP会在客户端做SSL Pinning,也就是只在客户端内置信任特定证书,Charles的证书根本不被接受,导致HTTPS流量抓不到。
这个案例就是这样,直接代理抓包只能看到TCP层乱流,HTTPS完全解密不出来。绕过的方案就是用Frida Hook掉SSL证书校验相关的代码,让APP信任任意证书。最有名的是objection的android sslpinning disable命令,一行就能搞定。但有时候这个方法也会失效,因为APP可能会校验更底层的东西。
我这次遇到的情况就属于失效场景,objection直接执行后请求直接报错。后来我改成自定义Frida脚本,主动Hook javax.net.ssl.SSLContext 里的 checkServerTrusted 方法,让它直接返回空实现,这样就绕过了证书链校验。这里有个经验:先试objection,不行再上自定义脚本,不要一上来就写复杂的Hook逻辑,浪费时间的可能性很高。
绕过SSL Pinning之后,Charles里终于能看到请求了。打开Charles的SSL Proxying设置,把目标域名加进去,刷新列表页,请求的全部参数、Headers、Body原原本本地显示在面板里。这一步成功,代表动态分析的前置条件全部满足。
3.2 Frida Hook关键类:运行时验证静态分析结论
抓包看到的是“结果”,但sign参数的值还是30秒变一次的动态值。想看它怎么生成的,就必须动态Hook RequestSigner类,把它的入参和返回值打出来。Frida的Java.perform配合use方法,就能精准拦截目标方法。
先写一个最简单的Hook脚本,目标就是RequestSigner.sign方法,打印它的入参和返回值。脚本跑起来后,在APP里上拉刷新触发一次列表请求,控制台立刻打印出调用栈和参数信息。脚本核心逻辑类似这样:
Java.perform(function() { var RequestSigner = Java.use("com.example.app.sign.RequestSigner"); RequestSigner.sign.implementation = function(str) { var result = this.sign(str); console.log("[*] sign input: " + str); console.log("[*] sign output: " + result); return result; }; });打印结果显示,sign方法接收一个排序后的参数字符串,输出一个32位小写字符串。到这里,静态分析的判断基本得到验证,但还差一步:得确认这个32位字符串用的具体是什么算法。最简单的办法是先用测试数据自己算一遍MD5,如果结果和Hook输出的对得上,算法就实锤了。
3.3 反调试与模拟器检测:分析中会遇到的干扰项及应对思路
动态分析进行到这里,可能会碰到一些APP设下的干扰项。常见的有两种:一种是检测调试器是否附加,检测到就直接退出或静默崩溃;另一种是检测运行环境是否为模拟器,让模拟器上的请求直接返回异常数据。
这个案例里遇到的干扰不重,只是在某些接口请求时会检查Build.FINGERPRINT是否包含模拟器特征,如果匹配会返回伪造的空白列表。应对方法很简单,用Frida Hook掉那个检测方法,强制返回false,让APP认为自己是运行在真机上。
关于反调试,更常见的做法是APP检测Debug.isDebuggerConnected或者检测/proc/self/status里的TracerPid字段,非零就说明有调试器附着。这类检测的绕过逻辑本质上是“骗过检查点”,但具体写法和检测点位置需要针对每个APP单独适配,很难有一套通用的万能脚本。我的建议是:把主流检测方式在脑子里建个索引,碰到哪个Hook哪个。
4. 从加密参数到服务端协议:sign参数生成逻辑的推导与验证
动态Hook抓到了sign方法的输入输出,接下来就是一个数据人最熟悉的活了——逆向分析一个参数生成规则。整个过程可以拆成五步:拿原始参数、排序拼接、加盐、摘要计算、验证。
4.1 参数拼接规则拆解:排序、键值对与固定盐值的推导过程
从Hook脚本拿到的输入字符串长这样:category=1001&city=北京&page=1&pageSize=20×tamp=1699999999。注意,参数顺序不是请求体里的顺序,而是按照参数名的ASCII码升序排列的。这是签名算法里最常见的策略,目的是保证服务端用同样序列计算签名的结果一致性。
排序拼接之后,末尾还跟了一个固定字符串,这个在业内叫“盐”。我是怎么发现的呢?先用无盐的字符串算MD5,结果跟Hook输出对不上,差得还很远。然后在末尾逐个尝试常见的盐值形式,比如固定的AppSecret、包名、版本号等,试到包名拼接时,结果对上了。这个盐值在逆向分析中有个专业名词叫“硬编码密钥”,它的存在意味着如果服务端验证签名用的是同一个固定盐值,那整个签名体系的安全性就取决于这个值没有泄露。
推导出签名规则后,我总结了一个通用的签名生成流程表格,方便后面写脚本时对照:
| 步骤 | 操作 | 示例 |
|---|---|---|
| 1 | 收集所有待签名参数(不含sign本身) | page=1, pageSize=20, city=北京 |
| 2 | 按参数名ASCII码升序排序 | category, city, page, pageSize, timestamp |
| 3 | 用key=value&形式拼接 | category=1001&city=北京&page=1... |
| 4 | 末尾拼接盐值 | ...×tamp=1699999999com.example.app |
| 5 | 计算MD5摘要并转小写 | 32位十六进制字符串 |
4.2 响应数据解密:AES-CBC模式下的密钥与IV获取记录
sign参数的问题解决之后,还有一个关键环节没打通——服务端返回的响应体也是加密的。在Charles里可以看到返回的JSON体里只有一个data字段,里面的内容是一串Base64编码的密文,根本读不出列表数据。
这就需要找到解密逻辑。回到静态分析阶段,我注意到网络仓库类里有一个ResponseDecryptor,当时只是记了个位置,现在派上用场了。Frida Hook这个类的decrypt方法,打印入参和返回值,能很清楚地看到解密前后的数据差异。
从解密的方法实现里可以看到用的是AES/CBC/PKCS5Padding,密钥是一个16字节的硬编码字符串,IV是前16个字节的Base64解码后的内容。这里值得提一个细节:IV取的是密文的一部分,这种方案在防御某些密文分析攻击时有它的考虑,但也意味着只要拿到密钥,整个加密体系就形同虚设。从逆向工程师的角度看,这个设计对分析者还更友好,因为IV不用额外寻找。
拿到这些信息后,响应体的解密就已经从“黑盒”变成了“白盒”。我可以在Python里用PyCryptodome库直接复现解密过程,验证一下自己从Hook拿到的解密结果是不是跟APP渲染出来的一致。
4.3 版本差异与时间戳防重放:这类协议里容易被忽略的两个坑
分析过程中我踩到了两个很容易被忽略的坑,值得单独说一下。
第一个坑是版本差异。我一开始对照旧的请求格式写脚本,发现签名老是对不上,排查了很久才发现是APP升级后参数列表里多了一个platform字段。这提醒我:签名参数一旦变更,旧的推导结论就全部作废,分析过程中必须锁定APP版本号,并在记录里标注清楚。
第二个坑是时间戳防重放。请求参数里有个timestamp字段,这个值直接影响sign的计算结果,服务端验签时会检查时间戳跟服务器当前时间的偏差,超过一定阈值(比如300秒)就会拒绝。所以写脚本模拟请求时,timestamp必须实时生成,不能用录制时的旧值,否则sign会一直验不过。
5. 把分析结果固化:脚本化、文档化与自动化回归
协议逆向分析做到这里,理论上的结论已经完整了:请求参数怎么拼、sign怎么算、响应怎么解密,凡是逆向要的东西全齐了。但要是故事只讲到这里,这篇分析顶多算及格,离95分还差关键一步——把分析结论从“手工验证”变成“可复现的工程产物”。
5.1 用Python重写协议实现:从Hook验证到独立脚本的转换思路
验证结论最直接的方式,就是把Hook里看到的逻辑用Python完整重写一遍,然后独立请求一次接口。我的做法是先用requests库模拟完整请求流程:构造参数、排序拼接、加盐、算MD5、发请求、解密响应、解析JSON。这个过程里,任何一步结论有误,最终结果就会跟APP不一致,等于白纸黑字的验证。
这里有几个写脚本时很关键的小细节:一是参数顺序要按照字典序排序,不要用Python字典的默认顺序;二是时间戳必须实时生成,保持秒级精度;三是解密时要把Base64密文正确解码再切IV,索引别算错。
实现的大致逻辑是这样的:
import hashlib import time import requests from Crypto.Cipher import AES from Crypto.Util.Padding import unpad APP_SALT = "com.example.app" API_URL = "https://api.example.com/api/v1/list" KEY = b"0123456789abcdef" IV_PREFIX_LEN = 16 def build_sign(params): ordered = sorted(params.items()) raw = "&".join(f"{k}={v}" for k, v in ordered) + APP_SALT return hashlib.md5(raw.encode("utf-8")).hexdigest() def decrypt_response(data_b64): raw = __import__("base64").b64decode(data_b64) iv = raw[:IV_PREFIX_LEN] cipher = AES.new(KEY, AES.MODE_CBC, iv) plain = unpad(cipher.decrypt(raw[IV_PREFIX_LEN:]), AES.block_size) return plain.decode("utf-8") params = { "page": 1, "pageSize": 20, "city": "北京", "category": 1001, "timestamp": int(time.time()), } params["sign"] = build_sign(params) resp = requests.get(API_URL, params=params) data = decrypt_response(resp.json()["data"]) print(data[:500])跑一遍这个脚本,返回的JSON里能正常解析出列表标题、发布时间、封面图URL,和APP端完全一致。到这一步,整条分析链路就全部验证闭环了。
5.2 标注通配参数与固定字段:脚本的复用性和边界说明
脚本能跑通只是第一步,还差一步把它变得可复用。逆向分析里最常见的现象是:今天分析的是列表页,明天想分析详情页,后天想看搜索页,如果每个页面都重新来一遍分析,效率太低。
所以我在写脚本时会把所有“可变的业务参数”和“固定的协议字段”分开记录。像city、category、page、pageSize,属于业务参数,在不同页面和不同请求里会变化;而盐值、密钥、签名算法、参数排序规则这类协议字段,在整个APP内部大概率是全局统一的一套。
用表格记录比大段文字清晰得多:
| 参数/字段 | 类型 | 说明 |
|---|---|---|
| page/pageSize | 业务参数 | 分页控制,按需修改 |
| city/category | 业务参数 | 业务筛选条件 |
| timestamp | 动态参数 | 每秒变化,必须实时生成 |
| sign | 协议字段 | MD5摘要,算法全局统一 |
| APP_SALT | 协议字段 | 写死在代码里的固定盐值 |
| AES密钥 | 协议字段 | 写死在代码里的硬编码密钥 |
这样记录之后,以后换页面、换接口,只需要改动业务参数这一行,协议层面的东西完全不用碰。这个习惯让我的脚本复用性一下子提升了很多。
5.3 自动化回归:被动分析如何变成持续可用的监控工具
当脚本重写完成并验证通过后,这套东西就不再是一次性的分析了,而是一个可以持续使用的自动化工具。我的习惯是把它做成一个简单的定时任务,每天跑一次,检查接口返回数据结构是否变化、sign算法是否仍然有效、响应解密是否正常。如果有一天任务突然报错了,大概率说明APP发新版改了协议,那时候就可以快速定位差异。
当然这里要明确一点:自动化回归的用途是监控协议稳定性和做个人数据研究,不是用来大规模请求或者打扰服务端的。保持低频访问,对目标服务端和对自己都是一种保护。
另外,我会把所有脚本和记录放进一个带版本管理的目录里,每次APP更新后在新版本上重新复跑一遍脚本,把差异点记录下来。这样长期下来,你会慢慢积累出一套对目标APP协议演进的理解,这在做防御方安全研究或者需求分析时都很有价值。
6. 逆向过程里那些容易被忽略的“软性坑”:排错思路与合规边界
技术链路讲完了,再花点篇幅聊聊那些不会写在教科书里、但对整个分析质量影响很大的软性坑。这些坑每个都踩过,每次踩完都要浪费不少时间。
6.1 Frida版本与Android版本兼容性:最容易浪费一下午的坑
Frida的版本兼容性问题,几乎每个做Android逆向的人都遇到过。有时候脚本明明写得很对,但运行时报错让你完全摸不着头脑,最后发现是frida-server的版本跟手机Android版本不匹配,或者跟Frida客户端版本不一致。
我现在的固定操作是:先查目标Android版本的CPU架构(arm64还是x86),再去Frida官方仓库下载对应版本的frida-server,保证电脑端frida-tools和手机端frida-server的版本号完全一致。这一步确认了,后面才不会再被莫名其妙的问题打断。
还有一个相关的坑是模拟器的选择。Android 9以下的模拟器镜像通常是x86架构,有些Hook库对x86支持不完善,容易出问题。如果条件允许,我更推荐用真机,一台二手Pixel手机是逆向分析的最佳伴侣,便宜、解锁方便、系统能降级。
6.2 分析过程中的证据留痕:日志记录与版本标注习惯
一份合格的分析报告不仅要告诉你结论,还要能让你三个月后回头看时,还能完整还原当时每一步的判断依据。所以我每次分析都会保留三类记录:Hook脚本的完整代码、关键方法的调用栈输出、每个阶段的截图或日志文件。
更重要的是,每次分析前先记录目标APP的版本号。很多协议分析结论都跟版本强相关,版本一变,结论可能马上失效。没有版本标注的分析记录,在复盘时根本没有参考价值。
这种留痕习惯还可以帮你做横向对比:当你分析过三五个不同APP后,会发现很多签名算法都遵循“排序+拼接+加盐+摘要”的套路,这时你就能建立自己的参数识别模板库,新目标入手快的多。
6.3 安全研究的分寸感:哪些事能做,哪些事不该碰
这个部分必须说清楚,也是我在每次分析时都会给自己划的线。逆向分析本身是安全研究的重要手段,但它的应用方向决定了行为的性质。以学习、教学、安全评估为目的,针对自己有权分析的目标做技术研究,是正当的。但把它用去抓取他人隐私、绕过付费机制、攻击在线服务,那就是另一回事了。
我的原则很简单:分析过程中获取的任何数据,只用在自己的研究验证环境里;任何批量请求、压力测试直接不做;任何涉及账号、支付、用户数据的代码一律跳过。守住这条线,这门技术才能走得更远。
再补一个判断小技巧:如果在分析过程中发现某段代码明显涉及用户敏感信息的传输,先停下来确认一下这个信息的使用目的和边界。技术上能解,不代表应该解。这个分寸感,是一个逆向工程师从“能干活”到“靠得住”的必经门槛。
写在最后
这个案例从拿到APK到全链路跑通,一共花了一个下午加一个晚上,大部分时间其实耗在SSL Pinning绕过和版本差异排查上,真正定位算法只用了不到四十分钟。这个比例很能说明问题:APP逆向分析的核心不是你会不会用某个工具,而是面对一个陌生目标时,能不能有条理地拆解、验证并固化结论。
那5分的扣分点在哪儿?我觉得在时间效率上。如果我能在第一次做静态搜索时就更精准地定位工具类,而不是顺带浏览了一些无关代码,整个分析可以再快个把小时。但反过来想,这5分的差距,正好是我下次做得更好的动力。哦对了,如果你也卡在SSL Pinning绕过那一步,建议先确认一下目标APP有没有用第三方加固,很多疑难杂症的根源其实都在加固层。