尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

安卓逆向实战:从抓包绕过到So层算法还原全流程

发布时间:2026/9/24 18:24:31

资讯中心
01
ARTICLE

安卓逆向实战:从抓包绕过到So层算法还原全流程

安卓逆向实战:从抓包绕过到So层算法还原全流程
做这类App的协议分析我劝你先做好心理准备它不是你随便开个Fiddler就能看到明文的项目也不是下载一个抓包工具点两下就能出结果的小白教程。绝大多数人的卡点不是不会用工具而是被一层又一层的东西挡住——HTTPS加密只是最外面那层真正难的是当你剥开传输层、看到一堆疑似protobuf的二进制时下一步该往哪儿走再往下你会发现关键计算几乎全在So层IDEA里反编译出来的Java代码只是个壳真正干活的函数在native里用晦涩的汇编和混淆后的逻辑藏着。这篇文章不是科普“什么是抓包”而是把我自己梳理的一条完整路径摊开讲从抓包工具选型、SSL Pinning绕过到协议格式判断再到So层静态逆向、动态调试、算法还原最后到代码级复现。整个过程中我会穿插大量实操命令、踩坑记录和判断逻辑适合正在做安卓逆向、App协议分析、或者单纯想搞清楚“TK这类App的流量到底怎么加密”的读者。看完你至少能明白拿到一个类似的APP你的分析路线应该怎么画每一步该用什么工具遇到问题是往哪个方向排查。1. 抓包前的准备与整体分析路线1.1 先搞清楚目标App的技术栈TK这类应用和普通小App不是一个量级的。普通App可能就一个OkHttp JSON你挂个Charles代理就能看到明文的请求和响应。但这类大厂应用几乎不会让你过得这么舒服。从实际逆向经验来看它的网络层通常是这个组合传输层HTTPS HTTP/2部分版本还会启用QUICUDP协议的HTTP/3这就导致你在Charles里可能压根看不到这部分流量请求体不是普通JSON而是自定义二进制序列化格式或者就是Protobuf的变体看起来就是一段乱码签名与加密关键字段如签名、设备信息、时间戳通过Java层调用native方法在So层完成拼接、哈希、加密So层加固会对关键So做ollvm混淆、反调试检测、字符串加密甚至用VMP虚拟化保护把关键函数藏起来。所以在动手之前你要先评估手上App的版本不要盲目选最新的有些新版本加了更狠的壳分析难度陡增。我的习惯是先挑一个自己熟悉的旧版本练手把整个链路跑通再考虑升级到更高版本去比对差异。记住逆向不是“越新越牛”能高效拿到结果的版本才是好版本。1.2 工具清单与选型逻辑工具不是越多越好而是每类工具只留一个最顺手的。我的推荐清单分四组干四件事抓包工具PC端我用Charles为主Wireshark为辅。Charles的优势是能看HTTP/HTTPS的请求头、响应头和Body适合应用层协议分析Wireshark则用来抓TCP/UDP底层报文比如QUIC流量它在Charles里看不到但Wireshark能看到。真机上我用HttpCanary或者直接在终端用tcpdump原因后面细说。动态插桩工具这一步必须用Frida理由很简单它支持批量hook、实时修改函数返回值、注入脚本而且大部分绕过SSL Pinning、反调试的思路都有现成脚本可以抄。配合Objection也行它本质上就是Frida的命令行封装用来快速绕过Pinning很方便。静态逆向工具Java层用Jadx它可以一键反编译APK的dex并搜索字符串、定位调用链So层用IDA Pro你如果不想花钱可以用Ghidra但F5反编译的体验还是IDA更好。辅助工具还有readelf、objdump用来快速查看So的导出符号和架构信息。辅助工具包括frida-dexdump内存中脱壳、unicorn模拟执行So中的函数、frida-trace追踪函数调用、python做协议复现和脚本编写。1.3 我的分析路线四阶段递进法我把整个分析链路拆成四步每一步都依赖上一步的输出第一步抓外层拿到目标App的请求数据判断走了什么协议、什么加密、什么序列化方式 第二步找入口在Java层定位到关键的native方法搞清楚哪些参数是So层生成的 第三步破So层把So文件拖进IDA找到对应的导出函数逆出核心算法 第四步复现验证用Python或者C把算法重现出来或者直接用Frida动态调用So里的函数来验证结果。这条路看着简单实际每一步都有不少“暗坑”。下面我从第一步开始把每一步的关键细节和遇到的问题都讲清楚。2. 抓包实操从外到内剥洋葱2.1 开抓之前的两个关键动作直接拿手机挂着代理就抓包是新手最常犯的错误。原因在于这类型App常做代理检测一旦检测到系统代理设置它要么直接不走代理要么给你返回一个假数据甚至直接断网。所以我在抓包前会做两件事第一在PC端和手机端都装好CA证书并且把手机的系统证书区域也装进去很多App会做证书固定只信任系统CA第二不用传统代理方式而是用“透明代理”方案简单说就是让流量经过抓包工具但不被系统感知。具体做法可以是使用iptables重定向流量到Charles的代理端口或者使用一些支持透明代理的工具。这一步搞不定后面全是白搭。另外如果目标App有Root检测或调试检测你还在Root过的设备上挂着Magisk建议先用Magisk Hide或Shamiko把Root状态隐藏起来否则App可能直接闪退。2.2 抓包工具实操Charles tcpdump 双路夹击Charles的设置不复杂但有几个细节值得注意。装好证书后记得在Proxy-SSL Proxying Settings里勾选Enable SSL Proxying并添加目标域名或者直接用通配符“*”。不开启这个选项你看到的HTTPS流量全是乱码或“Tunnel to”字样。但Charles这种代理模式最大的问题是它对QUIC/HTTP3无效。TK这类App在新版本里很可能已经启用了QUIC协议这时候Charles里会什么都看不到或者只看到TCP的UDP数据流。碰到这种情况我的做法是真机/模拟器上用tcpdump直接抓底层包# 在root过的设备上抓包保存为pcap文件 tcpdump -i any -s 0 -w /sdcard/capture.pcap然后把pcap文件拉到电脑上用Wireshark打开。Wireshark支持QUIC解密如果你能从App里提取到TLS keyFrida脚本可以做到就能在Wireshark里解开QUIC流量看到里面的HTTP3请求头和body。这个过程有些繁琐但一旦拿到明文对后续协议分析帮助极大。2.3 SSL Pinning绕过两条路都走通如果在Charles里看到的是“Connection closed”或者一堆乱码多半是App做了SSL Pinning。它会在客户端代码里固定服务器的证书或公钥非白名单证书一律拒绝连接。最简单的绕过方式是使用Objection# 启动objection并hook目标app objection -g com.xxx.app explore # 直接运行绕过脚本 android sslpinning disable如果Objection不生效就得用Frida写脚本。常见的思路是hook SSL_CTX_set_custom_verify、X509_VERIFY_PARAM_set_hostflags等函数直接让证书校验永远返回成功。这类脚本在GitHub上非常多搜“frida ssl pinning bypass”就能找到这里不贴完整代码了因为版本不同脚本会有差异重点是你得理解原理它拦截了系统库里的证书验证函数并把验证结果强制改成通过。有个坑要提醒一下有些App不仅做了Java层的证书校验还会在So层自实现一个TLS堆栈比如用BoringSSL的定制版这时候普通的SSL Pinning绕过脚本会失效你需要hook的是So层里的验证函数比如SSL_CTX_set_verify、SSL_set_verify这类。定位方法很简单用Frida的Module.findExportByName去找这些符号然后逐个hook看哪个被调用。2.4 抓到包之后的第一步判断Body格式拿到明文HTTPS流量后先别急着看内容先看Content-Type和Body的前几个字节。如果是JSON好办直接看字段。如果是一堆二进制多半是Protobuf或自定义序列化。怎么区分Protobuf的二进制通常以08、0A、12之类的字段tag开头比如0a 0b 22 33 44 ... 12 05 68 65 6c 6c 6f这种都是Protobuf比较明显的特征。自定义格式则一般会有一个魔数magic number比如固定开头几个字节为0xAB 0xCD之类的。拿到疑似Protobuf的数据我的做法是直接用protoc反解字段定义或者下载一个protobuf-inspector工具自动识字段tag和类型。这步能省很多时间如果不懂protobuf结构后面就算逆出了算法拼请求包的时候也会一头雾水。3. So层逆向从Java入口到核心算法3.1 先用Jadx摸清Java层的调用关系抓包只是第一步真正的戏肉在So层逆向。但进入So层之前你得先在Java层找到“通往So层的门”。用Jadx打开脱壳后的APK如果App有壳先用frida-dexdump脱壳然后分三步定位第一步搜索网络请求相关的字符串比如/aweme/、/passport/这类接口路径找到发起请求的类 第二步顺着调用链往上追找到构造请求头或签名的地方通常会看到一个类似X-Gorgon、X-Signature这样的自定义Header 第三步看看这个Header的值是从哪里来的一般会有一个native方法比如public static native String getSign(String url, String params, String cookie);看到native关键字恭喜你这就是要去So层逆向的入口。3.2 拿到So文件并做基础体检用Apktool或者直接从APK里解压找到对应的So文件通常在lib/arm64-v8a/目录下。建议优先分析arm64-v8a的So因为现在的机型基本都是64位而且IDA对arm64的分析支持已经很成熟。先做几个基础命令# 查看文件架构 file libxx.so # 查看导出符号 readelf -Ws libxx.so | grep Java_ # 查看是否加壳或混淆 strings libxx.so | grep -i ollvm\|vmp\|mprotect如果导出符号里有Java_com_xxx_app_xxx_getSign这种格式的函数名说明它没有做符号隐藏逆向难度相对低如果导出符号很少甚至只有JNI_OnLoad那说明它很可能用了导出函数动态注册需要先看JNI_OnLoad里的注册逻辑才能找到Java层的native方法对应到哪个C函数。这一步的关键经验是每一个native方法名和So里的导出函数名遵循Java_package_Class_method的下划线命名规则这是JNI的默认映射。理解这个规则后你就能快速把Java层的调用点映射到So文件里的具体函数。3.3 静态分析IDA定位核心函数把So拖进IDA选择ARM64的处理器类型等待自动分析完成。这时你会看到两类函数一类是直接导出的Java_xxx函数这类可以直接用F5Hex-Rays反编译器转成伪代码去读 另一类是在JNI_OnLoad里通过RegisterNatives注册的函数它们的函数名是内部的可以结合String交叉引用去定位。比较常见的情况是导出的Java_xxx函数只是一个壳它做了一堆参数校验、反调试检查之后把真正的逻辑交给内部函数去执行。这种时候F5出来会看到大片大片的“花指令”或无意义的赋值代码。比如我遇到过一个典型结构__int64 __fastcall Java_com_example_getSign(JNIEnv *env, jobject thiz, jstring url) { // 反调试检查 if (check_ptrace() 0) return 0; // 解密一个全局字符串获取内部函数名 char *func_name decrypt_string(0xAB12CD); // 查表并调用真正的签名函数 return invoke_func(func_name, ...); }遇到这种结构别急着在Java_xxx里死磕先把decrypt_string、check_ptrace这些外部函数的实现看清楚找到它真正调用的内部函数再跳过去F5。这个跳转过程中IDA的交叉引用Xref功能非常好用直接右键函数名看它被谁引用、引用了谁基本能理清调用链。3.4 反调试与混淆怎么绕过去TK这类So里反调试是标配常见的招式有ptrace(PTRACE_TRACEME)防止被调试器附加检测方法是被附加后ptrace返回错误读取/proc/self/status里的TracerPid字段发现被调试就退出或写脏数据时间检测计算关键函数执行耗时如果发现异常慢就认为处于调试状态环境变量检测检测某些调试工具的痕迹比如检测LD_PRELOAD。常规绕法是动态patch用Frida在函数入口处hook把反调试函数的返回值直接改成正常值。// 例如hook ptrace让所有调用直接返回0 var ptrace Module.findExportByName(libc.so, ptrace); Interceptor.replace(ptrace, new NativeCallback(function(request, pid, addr, data) { return 0; }, long, [int, int, pointer, pointer]));如果So里用了ollvm混淆特征是一大堆br指令、b指令乱跳、流程平整化F5出来的代码会非常难看。这个时候我推荐两个方案第一用D810这个IDA插件尝试自动去混淆它能把ollvm的switch-case结构还原成正常的if-else效果时好时坏但值得一试第二放弃完全静态还原改用“trace 动态调试验证”的思路——不追求看懂每一行汇编而是用Frida hook关键函数观察输入输出直接猜出算法逻辑。后面我会说动态调试的具体操作。4. 协议还原从算法到可运行代码4.1 核心算法识别与常量表提取假设你已经定位到了生成签名的那段代码接下来要做的就是两件事识别算法模式、提取关键常量。先说算法模式。在逆向大量的So之后你会发现签名算法99%跑不出这几个套子MD5拼接sign md5(url params secret)HMAC系列sign hmac_sha256(key, data)AES加密后再做Base64sign base64(aes_encrypt(key, iv, data))自定义哈希或查表置换这种一般是套了个S盒之类的逻辑复杂但也不是无迹可循。识别的方法很简单看它调用了什么库函数或者看它引用了哪些常量。比如看到0x67452301、0xEFCDAB89这就是MD5初始常量看到0x428A2F98之类的那就是SHA256看到大量轮常量则可能涉及AES或国密SM4。用IDA的Binary search或者直接搜hex序列就能快速定位。再比如说如果它在代码里构造了类似于0x63, 0x7c, 0x77...的一段256字节的初始序列那你基本可以判断使用了自定义S盒大概率是把数据做了一次置换混淆。这时候最好的办法不是硬逆算法逻辑而是把So里的表直接抠出来在Python里同步复现。把to表抠出来的简单方式写个Frida脚本读内存var base Module.findBaseAddress(libxx.so); var tableAddr base.add(0x12345); console.log(hexdump(tableAddr, {length: 256, header: true, ansi: true}));然后把这256字节复制到Python脚本里后续按同样的索引规则查表就可以了。4.2 动手复现Python/C按图索骥拿到算法流程和常量表后就进入“翻译”阶段。我的经验是优先用Python快速验证再根据性能要求用C/C或Go重写。以一个典型的签名逻辑为例假设逆向结果大致是将URL、请求参数、设备ID拼接成一个字符串在字符串尾部追加一个固定的盐值对字符串做一次MD5取MD5结果的前8位再拼上一些固定的头部字段最后做一次自创的字节置换输出Base64。那么Python复现起来非常快import hashlib import base64 def generate_sign(url, params, device_id): raw f{url}|{params}|{device_id} salt your_extracted_salt md5_val hashlib.md5((raw salt).encode()).hexdigest() prefix md5_val[:8] # 假设逆向出来的字节置换逻辑 final_bytes custom_permutation(prefix.encode()) return base64.b64encode(final_bytes).decode()写完之后拿之前从Charles抓包结果里的一组真实输入跑一遍对比一下App实际发出的签名值。如果一致说明你逆对了如果不一致要回到第4.1步重点检查常量表和置换逻辑是否正确。这个“输入输出对比验证”是整个协议还原流程里最重要的闭环。我一直和做逆向的朋友强调不要相信你“理解”出来的逻辑只相信你“复现”之后能对得上的结果。4.3 我在复现中踩过的两个坑第一个坑是字节序问题。So里跑的是小端序little-endian而你在Python里处理字符串时默认用的是大端顺序尤其在处理AES的Key和IV时极易搞反。解决方法是检查数据落地的十六进制排列凡是涉及多字节整数的都要特别注意高低位反转。第二个坑是字符集问题。JNI拿到的Java字符串默认是UTF-8但So内部经常会做一层转码——有的转成UTF-16LE有的转成GBK有的干脆按字节硬取。字符串编码不对签名结果永远是错的但这种错又特别难发现。我的排查方法是先用Frida把native层的入参原样打印出来看它和Java层传入的String是不是同一个字节序列不一致就得在复现时补一个编码转换步骤。5. 工具链与版本坑那些“不试不知道”的细节5.1 常见问题速查表这里整理了一张表都是我实际踩过、也经常在群友那里看到的问题直接对着排查效率更高。现象可能原因排查思路Charles抓包看到Tunnel但无内容SSL Pinning生效用Objection或Frida绕过后重新抓包Frida连不上App设备版本不匹配、App有反Frida检测检查Frida与设备架构版本换用gum-js-loop或延迟attach方式IDA F5结果全是乱跳代码ollvm混淆用D810插件尝试还原或改用动态trace看执行流So加载后崩溃架构不匹配、缺少依赖符号检查是否是arm64用readelf -d查看依赖库用mmapdlopen测试Java层脱壳后找不到类加固壳有反frida-dexdump逻辑手动dump内存中的dex或用Frida主动调用ClassLoader加载复现签名与App不一致字节序/字符集/常量表复制错用Frida打印native层入参和返回值做中间对比tcpdump抓包没有数据权限不足或流量走了QUIC用root权限执行用Wireshark看UDP报文某个native函数不显示导出符号动态注册JNI在JNI_OnLoad里打断点或用Frida hook RegisterNatives5.2 版本升级带来的连锁变化版本差异是另一个大坑。TK这类App基本上每隔几周就更新一次每次更新可能变化的是网络层Header名称改了比如X-Gorgon换成X-Argus签名算法内部改了盐值或者拼接顺序这类变化最隐蔽你光看代码可能看不出差别只有抓一次新版本请求去对比才能发现Protobuf的字段tag重新编号导致已有的解析脚本失效So文件从无壳升级到有壳导致原有的IDA静态分析入口被压缩。所以我的建议是做分析前先锁定一个版本下载APK后禁止自动更新分析过程中不要轻易升级。如果确实要看新旧差异至少要把新旧两个版本的APK都保留下来用Beyond Compare去对比AndroidManifest.xml或反编译后的代码能看出大概改动点。5.3 动态调试的独门技巧用日志函数当“肉搏战”有些So函数就算你静态看懂了也无法确定关键中间变量的值是不是你猜的那样。这时候我常用一个土办法在So里找一个被频繁调用的日志输出函数比如__android_log_print用Frida把它替换成自己的实现。原理其实很简单__android_log_print是个导出函数我hook它每次调用都多打印一段“当前调用的返回地址”然后根据返回地址反推当前执行到哪了。这个思路可以定位到关键分支有没有走对。另一种做法是把某个已知的导出函数改成跳转到目标函数在目标函数头尾动态读取参数和返回值。一句话总结动态调试本事再大也不如“入参-出参-执行流”三个维度都拿捏住当你脑中对执行流有清晰画面时逆向已经成功一半。6. 最后的经验心得整个过程做下来我最深的感受是逆向这件事七分靠方法三分靠耐心。方法不对的人卡在抓包环节就觉得是工具问题换一个抓包工具再试还是不行方法对的人知道抓包失败要先查代理检测、查SSL Pinning、查QUIC一步一个排查很快就能看到明文。进入So层后也是如此很多人一上来就想着把每行汇编读懂结果被ollvm的乱流劝退而我习惯先定位边界函数、再观察输入输出、最后才动脚本复现很多“看不懂”的地方其实根本不需要懂验证能过就行。还有一句想啰嗦的逆向的最终目的不一定是要写出完美的攻击脚本更多的时候是理解一个系统的设计思路。TK这类App在安全防护上投入的成本极高完整体验一遍它们的攻防手段对任何做移动安全或者协议开发的人来说都是一次很好的训练。你可以不会对抗每一层防线但你应该知道防线长什么样子、为什么长这样、如何用合理的成本去评估和突破它。我把这套流程完整走下来之后现在拿到任何一个新出现的热门App第一反应不再是“它用了什么加密”或者“能不能抓到包”而是直接判断它在哪个环节做了投入、在哪个环节留了稀疏。这才是逆向分析最有意思的地方。下次你有机会拿到同类目标不妨按我这条路径试试至少能少走一半的弯路。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。