资讯详情

APP逆向分析实战:破解列表页加密签名与数据解密全链路

📅 2026/9/15 22:19:40 | 华诺云谱 👁 阅读
APP逆向分析实战:破解列表页加密签名与数据解密全链路
手里拿到的目标是某款分类信息类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 9Frida 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脚本拿到的输入字符串长这样category1001city北京page1pageSize20timestamp1699999999。注意参数顺序不是请求体里的顺序而是按照参数名的ASCII码升序排列的。这是签名算法里最常见的策略目的是保证服务端用同样序列计算签名的结果一致性。排序拼接之后末尾还跟了一个固定字符串这个在业内叫“盐”。我是怎么发现的呢先用无盐的字符串算MD5结果跟Hook输出对不上差得还很远。然后在末尾逐个尝试常见的盐值形式比如固定的AppSecret、包名、版本号等试到包名拼接时结果对上了。这个盐值在逆向分析中有个专业名词叫“硬编码密钥”它的存在意味着如果服务端验证签名用的是同一个固定盐值那整个签名体系的安全性就取决于这个值没有泄露。推导出签名规则后我总结了一个通用的签名生成流程表格方便后面写脚本时对照步骤操作示例1收集所有待签名参数不含sign本身page1, pageSize20, city北京2按参数名ASCII码升序排序category, city, page, pageSize, timestamp3用keyvalue形式拼接category1001city北京page1...4末尾拼接盐值...timestamp1699999999com.example.app5计算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 b0123456789abcdef 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, paramsparams) 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有没有用第三方加固很多疑难杂症的根源其实都在加固层。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。