Android逆向实战:QP棋牌App协议透析与数据流分析

📅 发布时间:2026/9/9 19:04:26
Android逆向实战:QP棋牌App协议透析与数据流分析 做逆向分析这些年我其实很少把同一类目标完整走两遍。但最近一个某QP棋牌类App的案例因为涉及到的协议体系比较典型我从脱壳到数据流透析又重新手撕了一遍整个过程踩了不少坑也沉淀出几条可复用的分析路径。这篇文章就围绕这个QP游戏逆向实战展开重点讲怎么把“透S”理解为协议透明化、数据流透析而不是外挂层面那些灰色操作。文章里的所有内容都基于合规的安全研究和漏洞测试适合正在学Android逆向、想做App协议分析、或者准备接逆向靶场的同学参考。逆向这行最忌讳的就是上来就扎进汇编和so文件里硬啃。拿到一个目标先搞清楚它是什么类型的应用、通信模型长什么样、哪些数据值得透析比马上开Frida到处hook重要得多。这篇实战分享我会尽量把分析链路写完整从环境搭建、抓包定位到加解密还原、字段映射再到常见坑的排查每一步都会给出原理层面的解释而不是单纯丢命令。1. 项目整体设计与思路拆解1.1 分析目标与合规边界先说清楚这次的目标形态。某QP棋牌类App属于典型的“游戏壳业务逻辑”分离结构外层是加固过的Android应用程序内层包含大量so文件承载核心逻辑网络通信既有HTTP请求又有长连接Socket用于实时对战和房间状态同步。这类App在逆向圈子里被当作练手靶场很大原因在于它的技术栈足够密集——加固、反调试、自定义加密、协议混淆全都集齐了。我给自己划定的边界是只分析登录、房间列表、对局状态同步这几类非敏感业务的数据流目标是把通信协议“透析”干净也就是搞清楚客户端发出了什么、服务端返回了什么、中间经过哪些变换最终还原出一张完整的协议字段映射表。不碰支付链路、不做任何绕过风控或模拟用户行为的自动化工具更不碰任何影响游戏公平性的功能。这个边界必须在一开始就明确否则很容易滑向灰色地带这对技术成长没有任何好处。1.2 为什么选QP类项目作为逆向练手如果你刚开始从“能看jadx代码”进阶到“能完整还原一个协议”QP类App其实是比普通电商类App更好的教材。原因有三点。第一协议类型足够丰富。登录用HTTPJSON房间列表可能是WebSocket推送对局操作走的是私有二进制协议一个项目里能同时练到好几种数据格式的解析。第二安全防护强度适中。这类App通常有加固、有反调试、有so层的自定义加密但强度又没到银行App那种级别适合练手又不至于让人劝退。第三业务逻辑清晰字段语义相对好猜。比如房间号、玩家ID、下注筹码、牌局状态这些字段即使不知道原始定义通过抓包比对也能反推个八九不离十这对我后面做字段映射特别有利。1.3 整体技术路线这次实战的技术路线可以概括成四段式静态摸底、动态验证、抓包建表、回放验证。整个链路是闭环的。静态摸底阶段先用jadx打开加固壳里dump出来的dex梳理App的包结构、网络层封装、加密工具类的位置。动态验证阶段用Frida hook关键函数确认哪些数据是在Java层处理的哪些是下沉到Native层的。抓包建表阶段通过Charles或Burp把登录、房间列表等请求响应全部记录下来再结合加密函数的逆向结果把密文字段逐一对回明文。回放验证阶段用修改过的请求重放看服务端是否正常响应以此验证我对协议的理解是否正确。这个方法论的优点在于每一步都有可验证的产出物而不是只看代码猜测。即使某个环节卡住了也可以从前一步的产出里重新找线索不会出现“分析半天不知道分析得对不对”的情况。2. 环境准备与工具选型2.1 设备、系统与基础网络配置做Android逆向设备选择首先就是一道坎。我实测下来真机加root比模拟器省心很多。原因很简单很多QP类App会检测模拟器特征检测到模拟器就断开长连接或者直接弹窗退出。你可以用真机Pixel系列或者一加、小米都行只要系统是Android 8到Android 12之间。太新的Android 13以上有些Frida老版本会不太稳定虽然现在兼容性已经好很多但没必要在这个环节给自己添堵。网络环境也需要提前布置好。手机和电脑连同一个局域网手机代理指向电脑的Charles监听端口这样HTTP和WebSocket流量就能被记录到。需要注意代理配置好之后先访问一次http://chls.pro/ssl下载并安装Charles的CA证书这一步是为了后面做HTTPS解密。如果App有证书校验后面还要配合Frida绕过这个我在第5章具体讲。2.2 静态分析工具链静态分析的部分我主要用三个工具各有分工。jadx用来做DEX层的反编译。它能把Java代码还原到非常接近源码的程度尤其适合快速浏览包结构、搜索关键字符串调用。比如我要搜“AES”“encrypt”“Base64”这些关键词jadx的全局搜索功能基本是秒级响应。GDA是一款国产的集成逆向工具它的优势在于能同时处理DEX和so当我要从一个native函数跳回Java层调用点时GDA的交叉引用比jadx直观。Ghidra则用来啃so文件特别是涉及自定义加密算法的时候Ghidra的反编译C伪代码虽然不能保证完全准确但用来定位关键函数和常量已经足够。这里多说一句很多新手喜欢用一个工具打天下我建议别这样。jadx适合看业务逻辑Ghidra适合看二进制逻辑两者配合才能覆盖“Java入口到Native实现”的完整调用链。2.3 动态调试与Hook工具动态这一层Frida是目前绕不开的标配。我的建议是直接使用Frida 16.x的稳定版配合对应的frida-tools。安装本身不复杂但要注意Python版本和系统架构匹配的问题。有时候pip install frida-tools装好了结果手机端frida-server的版本和电脑端不一致一连接就报“unable to communicate”这个问题我见得太多次了。所以装完第一步先做版本检查# 电脑端 frida --version # 手机端 ./frida-server --version两个版本号必须保持一致。如果不想手动管理frida-server的启动可以顺手用objection做辅助它自带一些常用命令比如“android sslpinning disable”用来快速绕过SSL Pinning能省下写Frida脚本的时间。不过要注意objection仅适合快速验证真正复杂的Hook逻辑还是要手写Frida脚本。2.4 抓包工具选型对比抓包这部分从浅到深我通常按这个组合来Charles处理HTTP/HTTPSBurp Suite处理需要改包的场景tcpdump加Wireshark处理TCP/UDP长连接的裸流量。三者之间是互补关系。Charles上手最快界面直观但遇到自定义协议就抓瞎。Burp的Repeater和Intruder在重放攻击和参数测试上更顺手。tcpdump则能抓到所有经过网卡的包特别是在App走了非标准端口的私有协议时只有它能兜底。表里简单列一下工具选型对比工具适用场景优点缺点CharlesHTTP/HTTPS/WebSocket界面直观证书管理方便对私有TCP/UDP协议无能为力Burp Suite请求重放、模糊测试Repeater/Intruder功能强大同样受限于HTTP类协议tcpdump WiresharkTCP/UDP长连接、私有协议全量抓包可看二进制载荷需要自己过滤和分析数据门槛高所以我的策略是先用Charles把HTTP层摸清楚发现长连接和私有协议后立刻上tcpdump抓全量包再用Wireshark做流追踪和二进制解析。3. 核心细节解析与实操要点3.1 脱壳先拿到能看的DEX打开jadx准备看代码的时候才发现整个App是加固的classes.dex里只有几百K实际业务代码被抽到了so里或者被加密存放在assets目录下。这种情况不需要慌用frida-dexdump就能解决。它通过扫描内存中的dex镜像把已经被加载并解密出来的完整dex dump下来。实际操作路径是启动App让进程加载完所有类和资源。运行frida-dexdump的批量dump命令指定进程名。把dump出来的dex文件拉回本地丢给jadx重新反编译。这里有个很关键的细节要在App启动完成、进入主界面之后再去dump而不是进程一启动就立刻执行。因为加固壳的解密过程是分阶段的太早dump可能只抓到壳自己的代码业务dex还没落地。我这次就是等到登录页完全渲染后再执行的dump出来的文件明显大了好几倍jadx打开后包结构一目了然。3.2 网络层定位从关键字到调用链脱壳完成后第一步不是急着看加密算法而是先找到网络请求的入口。用jadx打开工程全局搜索“okhttp”“Retrofit”“WebSocket”“Socket”这些关键字基本能看到所有网络相关的封装类。这次目标App的网络封装比较典型HTTP走的是OkHttp长连接走的是Netty。顺着OkHttp的拦截器链往下走很快就发现每个请求都会被一个名为“EncryptInterceptor”的拦截器处理。拦截器就是所有HTTP流量汇聚的地方只要盯住它所有出站请求的明文和密文都能在这里Hook到。我常在Frida里直接hook OkHttp的RequestBody因为那是请求体的最终形态。写一个快速脚本把所有请求体打出来Java.perform(function () { var RequestBody Java.use(okhttp3.RequestBody); RequestBody.writeTo.overload(okio.BufferedSink).implementation function (sink) { var buffer Java.use(okio.Buffer).$new(); this.writeTo(buffer); var content buffer.readUtf8(); console.log([] RequestBody - content); return this.writeTo(sink); }; });这个脚本的实用价值在于不依赖服务端返回就能直接看到客户端实际发送的字节流。如果再配合Charles的抓包记录就能快速判断哪些字段是明文、哪些字段已经走了加密。3.3 加密点还原从Java层到Native层继续沿着网络层往下挖发现EncryptInterceptor内部调用了一个字符串加密工具类。刚开始我在Java层跟踪“encrypt”方法确实能拿到加密结果但直觉告诉我核心算法很可能不只存在于Java层因为这么明显的类名很容易被风控和检测系统盯上。果不其然继续往底层跟发现Java层的encrypt方法只是一个壳它把数据传给一个native方法“nativeEncrypt”真正的AES密钥和IV都在so文件里。还原这个过程我分了三步走。第一步用Frida hook native方法观察输入输出长度判断是AES还是DES之类的块加密。第二步在Ghidra里定位nativeEncrypt的实现搜索AES的S盒常量或查表常量确认算法类型。第三步回到内存中提取密钥和IV具体做法是Frida hook内存分配函数或者直接在so文件的数据段搜索可能的密钥串。这次的目标App算是比较老实密钥直接静态存储在了so文件的数据段用Ghidra搜索“32 bytes”大小的连续数据就能找到。但我也处理过密钥由服务端下发的App那就需要在登录响应里找密钥字段再跟踪密钥如何被写入加密对象。3.4 协议字段透析把密文映射回业务语义拿到加解密算法之后剩下的大头就是协议字段透析。这一步本质上是“拿密文和明文做对照猜字段语义”。做法不复杂但工作量比较大。我用登录接口做例子。用抓包工具分别录制两次不同账号登录的请求先手动把两次的密文进行对比如果密文发生了变化就能确认里面有随机数或者时间戳参与。再把同一个账号的同一个请求重放一遍如果服务端拒绝或者返回token异常就说明请求里有防重放校验。这些信息都能帮我对字段做语义映射。更实用的做法是写一个Frida脚本自动记录每个字段的“明文到密文”的变换痕迹。例如hook加密方法的入参和返回值把时间戳、随机数、用户名、密码等字段顺序打印出来这样就能在协议文档里把它们对号入座。4. 实操过程与核心环节实现4.1 抓包拿到第一手协议数据具体操作时我先把手机代理指向电脑的Charles安装好CA证书后打开目标App同时保持Charles开启SSL Proxying确认能正常解密HTTPS流量。然后用一个测试账号执行登录动作这时候Charles里会出现一批请求。我关注的是两个点登录接口和房间列表接口。从登录接口的响应里能看到一个有点反常的字段一个很长的Base64字符串长度远超过常规token。直觉告诉我这可能是服务端下发的密钥或者加密的业务数据。把它复制出来Base64解码后发现是一段二进制数据前16字节看起来像随机数后面的部分有明显的块对齐特征这基本确定是AES-CBC加密的产物。此刻的收获是已经把协议入口抓到手里了请求路径、请求头、请求体格式、响应体格式都齐了。接下来要做的就是搞清楚这段二进制的明文是什么。4.2 用Frida定位加密函数并还原明文拿着从响应里解出的加密数据回到jadx里面搜索响应解析相关的代码。很快就找到响应解码的调用链里面有一个名为“parseUserInfo”的方法它在拿到服务端返回的加密串后先做Base64解码再调用一个“decryptData”方法最后用Gson解析成用户对象。我来回在这个方法上打log打印一下解密前后的数据写了一个较完整的Frida脚本Java.perform(function () { var CipherUtil Java.use(com.xxx.qp.crypto.CipherUtil); CipherUtil.decryptData.implementation function (encryptedData) { var plainText this.decryptData(encryptedData); console.log([decryptData] input - encryptedData); console.log([decryptData] output - plainText); return plainText; }; });执行之后日志里很清楚地打印出了解密后的明文JSON里面就包含了用户ID、昵称、金币余额等字段。这一步做完协议透析就成功了一大半。接下来只需要再对其他几个接口重复同样操作就能把所有关键接口的字段全部映射出来。4.3 长连接私有协议的解析思路除了HTTP房间列表和对局状态走的是长连接。这时候Charles已经不够用了我换成tcpdump在root过的手机上抓取目标进程的流量# 手机上执行抓取目标App所有网络包 tcpdump -i any -s 0 -w /sdcard/qp_capture.pcap host 192.168.1.100抓完以后把pcap文件拉回电脑用Wireshark打开。找到目标服务器IP和端口后按TCP流追踪能看到载荷是二进制格式分析这类私有协议时需要先区分帧头和业务内容。这里有个实操技巧先触发一次固定的业务操作比如进入房间然后对比这次操作前后抓到的包找出变化的那一段字节这通常就是业务字段所在的偏移位置。对私有协议字段的解析我的做法是先把二进制流按常见类型拆开尝试按4字节对齐读int32按8字节读int64看看哪些字段的值有业务含义。比如房间号通常是int32玩家ID可能是int64下注额可能会被乘以100或者10000再传输这些都是QP类App里常见的处理习惯。4.4 修改请求重放验证协议理解协议透析完成后还需要最后一道验证重放。我用Burp的Repeater手动修改登录请求里的某个字段并观察服务端返回是否正常。比如我把请求里的“clientVersion”从2.3.0改成2.3.1看返回是否有对应的版本校验提示。或者是把“roomId”改成另一个存在的房间号看服务端返回的数据结构是否与预期一致。这一步看起来简单但很有价值。它能验证我之前关于字段语义的猜测——如果字段理解错了服务端通常会返回报错码或者直接断开连接如果返回结果符合预期则证明字段映射基本准确。5. 常见问题与排查技巧实录5.1 抓不到包与证书校验不过抓包最常遇到的坑就是证书校验。很多QP类App在OkHttp里加了一层自定义的证书校验要么校验客户端证书指纹要么校验服务端证书是否在系统信任链中。Charles装了CA证书也没用因为App根本不信任它。解决方案是绕过SSL Pinning。最快捷的方式是用objection的“android sslpinning disable”命令它会自动hook OkHttp和TrustManager的相关方法让证书校验直接失效。如果objection对某些版本不兼容再手动写Frida脚本绕过TrustManagerJava.perform(function () { var TrustManager Java.registerClass({ name: com.xxx.TrustAll, implements: [Java.use(javax.net.ssl.X509TrustManager)], methods: { checkClientTrusted: function () {}, checkServerTrusted: function () {}, getAcceptedIssuers: function () { return []; } } }); var SSLContext Java.use(javax.net.ssl.SSLContext); SSLContext.init.overload([Ljavax.net.ssl.KeyManager;, [Ljavax.net.ssl.TrustManager;, java.security.SecureRandom).implementation function (km, tm, sr) { var tms [TrustManager.$new()]; this.init(km, tms, sr); }; });5.2 反调试检测与Frida被检测分析到一半Frida连接可能会闪退或者App直接进入死循环这通常是触发了反调试。排查思路是先看App有没有检测“/proc/self/maps”里的frida痕迹或者检查默认的frida-server端口。针对这种检测较轻量的办法是修改frida-server的文件名和端口号再启动比如# 修改frida-server默认端口 ./frida-server -l 127.0.0.1:8899 然后连接的时候指定端口frida -H 127.0.0.1:8899 -f com.xxx.qp如果只是这种级别的检测改端口就已经能绕过。但遇到更复杂的检测比如扫描内存特征、检测ptrace状态就建议用定制版frida或者配合unidbg去做静态模拟调用不在真机上动态调试。5.3 SO函数被混淆或指令抽取再一种常见情况是so文件里的函数名被混淆成“sub_123456”这种无意义名称或者函数体被加固抽取真正逻辑在运行时才回填。遇到这种情况我先用Ghidra看函数引用的字符串常量比如哪个函数引用了“AES/ECB/PKCS5Padding”这种字符串那它的身份基本就能确定。如果函数体被抽取到动态解码那么静态反编译出来的代码就是断的这种情况就只能在运行时从内存里把真正的代码段dump出来再用IDA重新分析。5.4 问题排查速查表我把这次实战中遇到的典型问题整理成了一张速查表后面再做类似项目时可以直接对照问题现象可能原因解决思路抓包只有CONNECT请求没有HTTPS内容未安装CA证书或SSL Pinning安装证书绕过SSL PinningFrida attach时报unable to communicatefrida-server版本不匹配把电脑端和手机端版本统一App检测到root后退出root检测用Magisk hide或备份原环境DEX dump出来之后代码仍然缺失dump时机太早等主界面加载后再dumpnative函数反编译代码为空指令抽取/动态解密运行时从内存dump代码段解密明文出现乱码算法类型或模式猜错检查块大小确认是AES/CBC还是ECB5.5 独家避坑技巧最后分享两个独家的习惯。第一个每次Frida hook之前先确认目标进程没有被附加过如果有之前卡死的frida会话进程会残留ptrace状态导致重复连接失败。我在实操中会先“ps -A | grep frida”看一下确认没有残留进程再操作。第二个对长连接的私有协议做解析时别一上来就硬啃整个二进制流。先在业务侧找一个可以手动控制的字段比如昵称、房间号然后修改这个字段的值再对比前后的二进制流变化。哪个字节段跟着变了那个字节段就是该字段的编码位置。这和动态调试里的“差分分析法”是一样的思路放在协议解析里同样适用。6. 从这次实战里沉淀下来的经验这次某QP游戏逆向实战给我的最大感受是逆向分析不是靠单个技巧而是靠一条完整的方法论串起来。从脱壳、网络层定位、加密还原到协议字段映射和重放验证每一个环节都有明确的产出物也都有可排查的问题清单。这种项目做多了之后最大的收获不是某个App的具体代码而是看到一个新的未知目标时能快速判断出它的技术栈、可能的薄弱点以及最佳分析路径。我个人的习惯是每完成一次实战都会把关键信息整理成一份映射表和一份脚本合集。映射表里记录的是接口路径、字段名、加解密方式脚本合集里是当时能用的Frida脚本和Ghidra分析笔记。下次再遇到同类型App时直接复用这套体系效率能提升一半以上。最后想提醒一句任何逆向分析都应该限定在合规范围内用于漏洞挖掘、安全评估和学习研究。这篇文章里描述的技术方法初衷是帮助安全从业者理解攻击面、加固防护体系而不是为黑灰产提供素材。合规使用技术这条路才能走得长远。