做TK系App的抓包和协议分析,是我近期一个比较折腾的逆向项目。这个App的流量分析麻烦不在于TLS证书校验本身,而在于它的网络栈几乎绕过了系统代理,同时请求签名全部下沉到了So层。如果你也遇到过Charles里一片请求显示Unknown、Fiddler里全是CONNECT隧道、或者辛辛苦苦抓到包后发现X-Gorgon、X-Bogus这类签名参数完全不知道从哪生成,那这篇文章应该能帮到你。
这个项目本质上做的是三件事:把App的通信用抓包工具可见化,把可见后的协议字段梳理清楚,再顺着协议字段反推到Native层找到生成逻辑。整个过程涉及环境搭建、SSL Pinning绕过、So静态定位、Frida动态Hook、算法复现与验证,是一条比较完整的移动端协议逆向链路。适合对安卓逆向有一定基础、想系统练习抓包和Native分析的人,也适合正在做某类App接口研究但卡在“抓不到包”阶段的朋友。
1. 方案选型与前置分析:为什么先碰So层
1.1 核心需求拆解:从抓不到包到协议还原
先说结论:TK系App的抓包难度,90%以上集中在流量链路和参数签名两件事上。
流量链路的问题在于,它没有老老实实走系统默认的Socket和代理配置。常规App抓包,你只要把Charles或Fiddler的代理设置好、装好证书,HTTP/HTTPS流量基本就能看个七七八八。但这个App的网络通信是由libsscronet.so发起的,这层基于Cronet的网络栈自带了完整的HTTP/2和QUIC协议实现,默认根本不读Java层的ProxySelector配置。结果就是代理工具只能看到一条条CONNECT隧道,甚至什么都看不到。
参数签名的问题更直接。请求头里那些X-Bogus、X-Gorgon、X-Khronos字段,不是Java/Kotlin层拼出来的,而是Native层用一段私有算法算出来的。Java层只是调用一个native函数,把URL和参数传进去,然后拿返回值填到请求头里。这意味着就算你抓到了明文请求,拿到了一堆签名参数,也没法自己构造一个合法请求——除非你把这几个So里的算法逻辑逆向出来。
所以整个项目的技术路线就非常清楚了:先解决抓包的可见性问题,再通过抓到的请求定位敏感参数,顺着Java层调用链进入So层,最终在Native层还原算法。这就是“抓包 -> 协议分析 -> So层逆向”这条主线的由来。很多人一上来就拿着IDA打开So文件开始分析,结果找半天不知道哪个函数关键,本质上是跳过了前面两步,缺少入口线索。
1.2 前置侦察:App壳、签名校验与Native库清单
在动手之前,我建议你先花半小时做个静态侦察。这一步能省掉后面大量试错时间。
先解包APK,看一下lib/arm64-v8a目录下有哪些So文件。以目标App为例,通常会出现这些关键库:
- libuserinfo.so:用户信息相关的native逻辑,设备指纹、用户态签名大概率在这。
- libsscronet.so:Cronet网络栈主体,负责HTTP/2、QUIC通信,抓包困难的主要来源。
- libbytehook.so:字节跳动自家的Hook框架,说明App内部有大量的native层动态监控能力。
- libcrypto.so / libssl.so:OpenSSL相关,证书校验的入口在很多场景下会出现在这里。
- libttboringssl.so:BoringSSL的定制版,SSL Pinning绕过如果走OpenSSL的hook方案会失效,得从BoringSSL的接口入手。
这个清单决定了你后续Hook和逆向的切入点。另外还要确认App是否加固。TK系App早期版本壳相对简单,但新版普遍有企业级加固和反调试策略。建议先用脱壳工具处理Java层,避免Jadx打开全是壳的入口,连个正常类都找不到。不过这不影响So层分析,因为native库一般不会被打进壳里,只是加载时机和反调试需要额外处理。
前置侦察阶段还有一件事要做:确认目标版本。同一个App不同版本,So文件路径、导出函数名、混淆程度都会变。我的习惯是锁定一个特定版本进行分析,并把APK、So文件、版本号一起存档,方便复现时对照。不要今天分析2.8版本,明天又切到2.9,否则之前Hook的地址全部失效,工作量翻倍。
2. 抓包环境搭建与协议可见性恢复
2.1 绕过SSL Pinning与强制走代理
先说环境配置。我用的抓包工具组合是Charles + mitmproxy,手机是Pixel模拟器,Android版本选择9或11,这两个版本对Frida的兼容性比较稳。Charles负责看可视化请求树,mitmproxy负责跑脚本做自动化篡改和记录。
常规HTTPS抓包步骤大家都熟:手机装Charles根证书,代理指向电脑IP和8888端口。但在这个App上走完这套流程后,你会发现问题依旧——因为App内置了SSL Pinning,它只信任自己打包进去的证书公钥,不认系统证书。所以绕过Pinning是第一步。
绕过方案我用过两条路。第一条是用Frida跑脚本,Hook掉证书校验相关函数。这里有个关键坑:因为App用的是BoringSSL,光Hook OpenSSL的SSL_CTX_set_verify没用,要同时Hook BoringSSL的SSL_CTX_set_custom_verify。我当时的做法是先用objection的android ssl pinning disable命令,如果失效再手动写Frida脚本,定位so里的verify回调函数,直接改成接受所有证书。
第二条路是对网络栈下手。libsscronet.so不吃系统代理,但它是支持外部配置代理的,只是暴露给Java层的开关被隐藏了。我通过Frida Hook Java层的NetworkSecurityPolicy和ProxySelector相关方法,强制让Cronet走Charles的代理端口。核心思路就是让所有连接都经过一个我们可控的中间人节点。
绕过之后,Charles里终于能看到一条条可见的HTTP/2明文请求了。这个过程中最费时间的其实是调试代理配置,Frida脚本反复加载,Charles反复刷新,差不多花了半天才稳定下来。建议先在同机型的普通App上验证抓包链路通不通,再切换到目标App,否则容易把环境问题和App问题混在一起。
2.2 识别关键请求链路与签名参数
抓包可见后,不要急着看So,先把协议字段梳理清楚。打开某一个典型的API请求,比如首页推荐流的请求,你会发现请求头里有几个字段特别显眼:
| 字段名 | 典型格式 | 作用推测 |
|---|---|---|
| X-Khronos | 10位时间戳 | 请求发起时间,防重放 |
| X-Gorgon | 40位十六进制字符串 | 请求头签名,校验头字段完整性 |
| X-Bogus | 64位左右的字符串 | URL签名,校验Query参数完整性 |
| X-Ladon | 16位字符串 | 设备信息加密串 |
| X-SS-STUB | 字符串 | 部分写操作请求体的Hash签名 |
| User-Agent | 带版本号和设备型号 | 设备标识和版本管理 |
这些字段里,X-Khronos最直观,就是Unix时间戳,这是从秒级到毫秒级都能直接算出来的,不需要逆向。X-Gorgon和X-Bogus是核心,每次都变,而且和具体URL、请求头强相关。X-SS-STUB则是POST请求体摘要,用来防篡改。
我的建议是把抓到的请求按功能分类:冷启动请求、首页Feed流、用户主页、评论列表。每类请求抓20到30条样本,用脚本把请求头、URL、请求体、签名参数全部归档。这样后面验证算法时,有足够的真实样本做输入输出比对。如果这一步做得粗糙,后面算法复现时你会发现没有对照数据,根本没法判断逆向结果对不对。
另外注意,QUIC流量的抓取是另一个大坑。Cronet在部分网络条件下会走UDP 443端口的QUIC协议,这个协议Charles默认是解析不了的。我的解决方法是全局禁用QUIC,强制让App走HTTP/2。怎么禁?比较简单的是在Frida里Hook Cronet的QUIC开关方法,把enable_quic直接置为false。这样所有请求都回到TCP,抓包工具才能正常工作。
3. So层定位:从入口到加密函数的静态逆向
3.1 JNI动态注册入口定位
协议字段梳理清楚了,下一步就是找到生成这些签名的Native函数入口。
先看Java层。用Jadx打开脱壳后的APK,全局搜索X-Gorgon、X-Bogus关键字。通常能找到类似这样的代码:
public static native String getGorgon(String url, String params, String header, long timestamp);这里就是Java层声明的native方法。但问题在于TK系App的native方法大多不是静态注册的,而是通过JNI_OnLoad动态注册。这意味着Jadx里看到的包名和函数名,并不直接等于So文件里的导出符号。你得先找到JNI_OnLoad,看它调用了哪个RegisterNatives函数,才能把Java方法和So里的函数地址对应起来。
我用IDA Pro加载libuserinfo.so,先看导出表,确认有没有JNI_OnLoad。然后跟进去,发现它调用了某个注册函数,这个函数内部会把函数名和函数指针放到一个JNINativeMethod数组里。在IDA里面搜索字符串“getGorgon”或者“X-Gorgon”,可以直接定位到该字符串所在的数据段,交叉引用过去就能看到具体的注册函数指针。
这种方法说起来简单,实际操作中最大的障碍是混淆。新版So文件对函数名做了很多字符串加密,你搜索X-Gorgon可能搜不到明文,而是搜到一段加密数据。这时候需要用动态办法辅助:用Frida先枚举Java层的native方法绑定地址,然后再回IDA里面去查找。
var targetClass = Java.use("com.xxx.core.network.sign.SignManager"); targetClass.getGorgon.implementation = function() { var result = this.getGorgon.apply(this, arguments); // 打印调用栈和native方法地址 console.log(Thread.currentThread().toString()); console.log("getGorgon called, result = " + result); return result; };Hook到Java层调用之后,再去So文件里找对应函数地址,效率比纯静态定位高很多。你可以理解为:静态分析告诉你“哪里可能有黄金”,动态Hook告诉你“黄金具体在第几层抽屉”。
3.2 还原算法结构:以X-Bogus为例
定位到关键函数之后,就到了最需要耐心的部分——还原算法。
以X-Bogus为例。这个参数本质上是针对URL Query部分的签名。函数输入通常是完整URL或参数列表,输出是一个固定长度的字符串。在IDA里F5反编译后,你会看到一大堆位运算、数组查表、移位、异或操作。这类算法本身就刻意写得混乱,没有清晰的数学结构,更像是一种定制的混淆编码。
我的还原策略是四步走:
第一步,确定输入。通过Frida Hook,打印出每次调用时的入参。确认X-Bogus的输入到底是URL全串,还是去掉了协议头和域名后的Path+Query部分。这一步必须用真实样本反复确认,因为很多人就是在这里搞错了输入范围,导致后面所有算法还原都对不上。
第二步,确定输出格式。X-Bogus的输出看起来是一串大小写混合的字母数字。这个字符串不是简单的Hex,它可能是Base64变种、自定义字母表、或者经过可逆变换的密文。可以先统计字符集,排除标准Base64,再看它是不是某个固定置换表映射出来的。
第三步,逐行翻译反编译代码。这里不要指望完全还原成和C源码一样的程度,重点是画出数据流:输入进了哪个函数、经过了哪几轮异或、最后查了哪张表。我习惯用注释把每段伪代码的功能标签打上,比如“第一步:对输入做URL Decode”“第二步:拼接固定盐值”“第三步:查S盒替代”。
第四步,用动态验证静态。每还原完一个关键步骤,就在Frida里主动调用一次原函数,取得标准输出,同时用自己的还原脚本计算一次,比对中间值。通过二分法逐步缩小出错位置,直到完全对上。
整套流程下来,X-Bogus的还原花了大约两天时间。说实话,这个效率不算快,但胜在稳妥,因为每一步都是验证过的。
3.3 注意:反调试与反Frida检测
在实际动态调试中,大概率会遇到反调试。TK系App对Frida的检测很敏感,常见表现是:Frida附加成功但脚本不执行,或者App一检测到调试就闪退。我当时遇到的情况是,Frida服务一启动,App直接弹窗提示异常并退出。
几个有效的应对策略:
- 使用Frida的gadget模式,把frida-gadget注入到APK里,比传统的attach模式隐蔽。
- 修改Frida默认端口号,避开常见的27042侦察。
- 先用自定义的dlopen方式手动加载目标So,绕过App的启动时检测,再加Hook。
- 用LSPosed配合Xposed模块做系统级Hook,部分场景下比Frida稳定。
另外还要说一点,如果你只是为了看协议和参数生成逻辑,不一定要正面突破反调试。可以换个思路,把So文件抠出来,放到一个临时写的demo App里,自己调用它的导出函数。这样完全没有调试检测的压力,输入输出一样可以正常采集。这个技巧在目标App有强反调试时非常实用。
4. 动态调试与协议还原的实现细节
4.1 Frida Hook Native层函数取参
静态分析确定了函数大致位置以后,动态调试的核心任务就是两件事:拿到函数的真实出入参,确认静态分析结果正确。
我先用Frida的Module.findBaseAddress拿到目标So的加载基址,再加上函数相对偏移,就能得到函数的绝对内存地址。然后通过Interceptor.attach在该函数入口和出口分别打印参数和返回值。这就是最基础的Native层Hook。
var base = Module.findBaseAddress("libuserinfo.so"); var func = base.add(0x12345); // 偏移量从IDA里读取 Interceptor.attach(func, { onEnter: function(args) { console.log("X-Bogus function called"); console.log("arg0=" + args[0].readCString()); console.log("arg1=" + args[1].readCString()); }, onLeave: function(retval) { console.log("return=" + retval.readCString()); } });这里有个很关键的时序问题:So文件不是App启动时就加载的,它可能在用到签名功能时才被Load。直接在脚本开头写Module.findBaseAddress,大概率返回null。正确姿势是等So加载完成后再attach,或者用Interceptor.attach的方式对模块加载事件做监听。
我当时的做法是写一个线程循环,每200毫秒检查一次模块是否已加载,加载成功后立即挂上Hook。代码不算优雅,但很管用。另一个坑是So会被系统多次加载或卸载,Hook回调有可能失效,需要在代码里加上防御性的重挂逻辑。
Hook到之后,把调用时的入参和返回值全部记录下来。我记录了两百多组数据,覆盖了不同的URL、不同的参数组合。这些真实样本是后面验证算法的“标尺”。
4.2 算法复现与抓包数据核验
算法还原完成后,还不能说大功告成。关键的最后一步是用独立脚本复现,并和真实抓包数据做一次严格的核验。
我把还原出来的X-Bogus和X-Gorgon算法分别用Python重写成了两个函数。然后用之前抓取的样本作为输入,调用自己的函数计算签名,拿计算结果和App真实生成的签名做比对。
比对结果并不完美。X-Bogus第一次全量比对时大概有30%是不一致的。这时候不要怀疑整个算法框架错了,而是大概率遗漏了某个隐蔽的输入。常见的遗漏点有:
- 时间戳参与计算,但之前就设了固定值,没有跟着样本变化。
- User-Agent里某个子串参与了签名计算,比如设备型号字段,不同设备上这个值会变。
- 请求头里的某些字段在Java层被自动排序后再传给Native层,顺序不同会影响到最终结果。
逐个排查这些变量后,算法终于能做到100%一致。我就用抓包工具篡改了某个请求的Query参数,同时重新计算签名,把请求重新发给服务端,服务端正常返回了数据。这样就证明签名算法还原是正确的,可以自己构造出合法请求。
这里我想多说一句:很多人做逆向止步于“反编译看到了关键函数”,但没有做完整的输出比对验证。结果算法逻辑改动一点点,后面的所有工作都白做。验证这一步虽然枯燥,但它是整个逆向项目里性价比最高的一步。
4.3 常用工具链组合推荐
工具选择直接影响效率。我这次项目的常用组合是这样的:
| 用途 | 工具 | 备注 |
|---|---|---|
| Java层反编译 | Jadx | 看Java层方法和字符串引用,速度快 |
| So静态反汇编 | IDA Pro 8.x | F5反编译是核心功能 |
| So深度分析 | Ghidra | 免费,处理混淆有时比IDA更灵活 |
| 动态Hook | Frida + Objection | 额外用Frida-Gadget模式绕过检测 |
| 抓包中间人 | Charles / mitmproxy | Charles看界面,mitmproxy做脚本自动化 |
| 脱壳 | frida-dexdump / BlackDex | 优先尝试frida-dexdump |
| 辅助分析 | CyberChef | Base64变种、XOR爆破等编码转换 |
这套组合覆盖了从脱壳、静态分析、动态Hook到数据验证的完整链路。如果你预算有限,Ghidra完全可以替代IDA用于大多数场景,只是在F5伪代码的可读性上稍弱一些。Frida是绝对不能省的,它是整个动态分析流程的基石。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
整个项目做下来,遇到的问题五花八门。我整理了一张速查表,把最典型的场景和对应解法列出来:
| 现象 | 可能原因 | 排查方案 |
|---|---|---|
| Charles完全看不到目标App流量 | Cronet不走代理 | Frida强制指定代理,或全局禁用QUIC |
| 能看到CONNECT但内部请求是Unknown | TLS握手被Pinning拦截 | Hook BoringSSL的verify回调,绕过证书校验 |
| Frida脚本附加成功但无输出 | So还没加载到内存 | 轮询等待模块加载后再Hook |
| Hook后App闪退 | 触发反调试或反Frida检测 | 换Gadget注入模式,修改服务器端口,延时附加 |
| 反编译代码大量乱码 | 有反混淆或字符串加密 | 用动态Hook先定位关键函数,再回IDA交叉分析 |
| 算法复现匹配率偏低 | 输入范围或参与计算的变量没对齐 | 重新收集真实样本,逐字段对照入参 |
| 请求重放后被风控拦截 | 校验了设备信息或时间窗口 | 确认X-Khronos实时同步,带上完整设备指纹字段 |
| Ghidra加载So后函数列表为空 | So文件被压缩或加壳 | 先确认文件MD5和APK内一致,再检查ELF头完整性 |
这张表里的问题,除了最后一行是文件问题,其他基本都是分析流程中必然会遇到的。尤其是“Frida无输出”和“Hook后闪退”这两个,几乎每个做Native逆向的人都会碰上,解决方案也不复杂,核心就是耐心调整注入方式和加载时机。
5.2 独家避坑心得:三个最容易走弯路的地方
第一个坑是浪费大量时间在“完美还原算法”上。很多初学逆向的人拿到一个函数,非得把每一行汇编都翻译成等价的C语言,追求百分之百理解。实际上没必要。协议分析最看重的是输入输出关系,以及算法里哪些参数是需要我们动态生成的。至于内部用的是查表还是位运算、用了多少轮混淆,其实只要能用另一种方式复现相同结果就够了。把时间花在验证输出一致性上,而不是跟反编译代码死磕。
第二个坑是低估了请求头字段之间的关联。X-Gorgon不是单独算出来的,它会用到前面的X-Khronos时间戳,也会和其他header内容拼接。如果你只盯着一个字段逆向,忽略其他头字段的关联,就会发现单独算出来的签名放回请求里总是校验失败。建议第一次做的时候把所有签名字段都列成一张变量依赖表,搞清楚谁依赖谁。
第三个坑是环境干净度。逆向分析过程中,模拟器里会装Frida、Magisk、抓包证书等一系列东西,这些痕迹很容易被服务端风控识别。一旦被标记,请求响应就开始异常——有时能抓到包但返回空数据,有时请求直接被断掉。我的做法是一台专用模拟器做抓包分析,另一台保持相对干净的设备做重放验证。两台设备分开,排查问题时少很多干扰。
5.3 从抓包到协议复用的完整流程建议
最后把这个项目整体梳理成一个可复用的操作流程。无论你后续分析的是哪个App,这套流程都能直接套用:
- 确定目标版本,解包APK,记录So文件清单和入口Activity。
- 搭建抓包环境,先解决代理可见性问题,再解决Pinning问题。
- 抓取大量真实请求,整理出关键请求头和签名参数。
- 用Jadx定位Java层的native方法声明,再通过Frida获取对应的native函数地址。
- 用IDA/Ghidra反编译So文件,按输入输出逆推出签名算法核心逻辑。
- 用Frida动态Hook验证出入参,修正静态分析误差。
- 用Python等独立语言复现算法,和真实样本比对。
- 用复现的算法重放请求,验证服务端接受。
每一步之间都有验证点,不做完验证不入下一环节。这样整个项目看起来很长,但每一步都是有迹可循、可回滚的,不容易在中途迷失方向。
做完这个项目,我最深的体会是:So层逆向不是一条道走到黑的线性过程,而是“抓包 -> Hook -> 静态 -> 动态 -> 复现”反复迭代的循环。刚开始进So的时候觉得函数铺天盖地无从下手,但只要你带着“这个函数吃了什么、吐出了什么、为什么这样算”三个问题去看代码,路就会一点点清晰起来。尤其是Cronet网络栈的拦截体验,真的是做这个项目前期最痛苦、后期最解气的一段。最后再分享一个小技巧:如果时间有限,优先Hook系统底层的TLS读写函数,而不是直接跟Native签名逻辑死磕,因为很多签名参数在TLS明文层就能看到直接来源,能帮你省下至少一半的定位时间。