ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

抓包全是图片?私有长连接协议逆向与容灾破局指南

抓包全是图片?私有长连接协议逆向与容灾破局指南 你有没有遇到过这种情况手机连着抓包代理CA 证书也按教程装好了代理配置完全正常但打开一个头部 App 随手刷了几屏抓包面板里满满当当全是图片请求偶尔冒出几个 JSON 还是启动配置、埋点上报之类无关紧要的东西最想看的业务数据一个都抓不到。很多人这个时候会陷入自我怀疑证书没装好App 检测到代理了要不要去网上找绕过证书校验的方案我早年也在这个圈子里绕了很久后来才意识到问题的方向从一开始就偏了——你盯着 HTTP 面板找数据可这个 App 的核心业务流量根本没走过 HTTP。它走的是另一条完全独立的通道私有长连接。这篇文章就把这件事背后的原理、协议特征、逆向路径以及我实战中真正好用的“逆向容灾”破局方法一次讲清楚给正在做安全研究和客户端开发的读者一个可落地的参考。1. 抓包面板只剩图片先搞清楚流量真正去了哪1.1 不是没有数据是数据走了你看不懂的通道先说一个很多人忽略的事实Charles、Fiddler 这类工具本质上是 HTTP/HTTPS 代理它们只关心带有 HTTP 语义的流量——方法、路径、头、状态码。App 里的图片、JS、CSS 这些静态资源请求确实走 HTTP所以你能在代理面板里看到一堆图片。但一个成熟的头部 App核心业务链路早就不是“每次请求都走一遍 HTTP”了。动态 Feed、搜索结果、评论、私信、IM 消息、系统通知这些数据走的是一套自研的 TCP 长连接协议通常是二进制格式跟 HTTP 没有任何关系。代理工具连解析入口都找不到自然不会在面板里展示给你。如果你这时候把抓包工具换成 Wireshark从网卡层面看流量你会发现数据一直在跑。一条持续不断的长连接占据着某个固定端口流量规律地进进出出只是一旦尝试解码应用层内容得到的全是乱码。数据一直都在只是它用了你“看不懂”的语言。1.2 长连接承载了几乎所有实时业务要理解这个现象得先从 App 的整体流量架构说起。一套典型的大型应用网络架构流量大致分两类。一类是“低频、大包、可缓存”的静态资源比如图片、视频、安装包、样式文件这些资源天然适合 HTTP CDN客户端发一次请求CDN 边缘节点直接返回响应快、不占源站带宽。你在抓包工具里看到的大量图片就是这类流量。另一类是“高频、小包、要实时”的业务数据比如列表页的下拉刷新、IM 消息、点赞评论通知、App 内的统一推送。这类流量有几个共同特点对延迟敏感、需要服务端主动推送、请求频率高但单包很小。如果每一条都走 HTTP每次都要重新建 TCP 连接、走 TLS 握手性能和功耗都是灾难。所以头部应用的做法是把一个应用里所有需要实时交互的能力全部收敛到一条长连接上。客户端启动后建立一个 TCP 长连登录之后保持着后续的所有业务请求和响应都在这条连接上跑。一个服务端网关统一接入、统一调度、统一推送。你的所有“核心数据”都在这条你看不见的连接里。1.3 “全是图片”这个现象本身就是线索我后来复盘时有个很重要的体会“抓包面板全是图片”这个现象不能只当作文下一次性的结论它本身就是一条高质量的线索。它至少告诉你三件事。第一这个 App 的业务流量通道和静态资源通道是分离的HTTP 层面的代理对你没有意义不用再纠结证书和绕过方案。第二存在一条值得被分析的私有长连接流量是二进制的大概率有固定协议头。第三如果你的 Wireshark 里出现了某个 TCP 流被启发式识别成媒体流的“异常”现象那往往不是真的图片而是协议解析器对未知二进制流的误判——这种现象本身就能帮你快速定位到那条关键流量。所以我的习惯是遇到“抓不到数据”的情况第一反应不是换抓包工具而是打开 Wireshark 看一眼完整的 TCP 连接列表先把“哪条连接在干活”摸清楚。2. 私有长连接协议的基础认知2.1 头部 App 为什么要自建长连接不止是省流量很多人以为大厂自研长连接协议是为了防抓包、防逆向这个理解其实本末倒置了。长连接的核心动机是工程上的性能诉求防抓包只是附带红利。首先是省电和资源开销。TCP 建连需要三次握手TLS 还需要 1-2 个 RTT 的密钥协商。一个高频使用的 App如果每次刷新都走一遍完整握手电量和网络资源的消耗会明显上升。保持一条长连接后续请求只需要在已有连接上发数据开销小得多。然后是实时性。HTTP 是“客户端主动拉”的模型服务端没法主动往客户端推数据。IM 消息、订单状态变更、风控通知这类场景要求服务端能随时触达客户端长连接天然支持双向通信。这也是为什么聊天类、直播类应用格外依赖长连接。还有一点是弱网适应性和调度能力。移动网络下 IP 会频繁切换长连接配合会话保持、连接迁移机制能比短连接更平滑地应对网络变化。服务端网关还能利用一条连接做批量请求合并、按优先级调度这些都是 HTTP 短连接模型做不到的。2.2 一个典型私有协议包长什么样既然要分析就得知道目标长什么样。虽然各家协议细节不同但二进制私有协议的设计思路上高度一致。我拆过几种不同应用的协议包结构基本都长这样字段长度作用魔数 Magic4 字节固定标识用于快速识别协议常见如 0xAA 0xBB 0xCC 0xDD版本号1 字节协议版本客户端升级后用来兼容新旧格式命令字2 字节表示消息类型比如 0x0001 是登录、0x0002 是拉取列表序列号4 字节请求和响应对应的凭证长连接上靠它区分是谁的响应包体长度4 字节用于粘包拆包接收方根据这个值读取完整包加密标志1 字节标识包体是否加密、用的是哪套算法包体N 字节真正数据可能是 protobuf、msgpack也可能是加密后的密文校验值4 字节CRC32 或哈希用于完整性校验这个结构里魔数和序列号对逆向分析尤其重要。魔数是你的“入场券”——只要在字节流里反复出现固定值基本就能确定协议边界序列号则帮你把请求和响应配对哪怕整条连接上消息交织在一起你也能理出逻辑。2.3 抓包工具集体失效的根本原因搞清楚了协议结构再回头看“为什么抓包工具在这里集体失效”原因就非常清晰了。第一是协议不可识别。HTTP 有明文的方法名、路径、请求头代理工具靠这些字段建立“请求-响应”模型。私有长连接没有这些语义只有二进制头代理工具不认自然不显示。第二是加密非标准化。TLS 还能靠代理重签证书来做中间人解密但自研协议往往用的是自定义密钥协商甚至直接在每次会话开始动态生成会话密钥。密钥不在握手阶段以标准方式交换中间人就算拿到了流量也没有解密入口。第三是状态机割裂。HTTP 请求天然是一问一答代理工具建立一个“请求→响应”配对很简单。长连接上同时跑着几十个未完成的请求靠序列号在应用层做关联抓包工具不知道序列号规则只能看到一堆无头无尾的字节流。三个原因叠加就是你“抓不到数据”的全部真相。3. 逆向分析的第一层摸清协议与定位加密点3.1 静态分析先定位协议框架和加密库面对一条私有长连接我建议的第一步不是急着 hook而是先做静态分析把协议栈的大致构成摸清楚。对 Android 应用来说拿到 APK 后直接丢进 jadx 打开搜索几个高频关键词Socket、OutputStream、Protocol、Encoder、Decoder、Cipher、AES、RSA、Message、Packet、Buffer。这些名字能帮你快速圈定协议处理代码的位置。然后是看 so 库。现在的头部 App 几乎不会把核心协议逻辑放在 Java 层太容易被 hook 了。你会在 APK 的lib/armeabi-v7a、lib/arm64-v8a目录下看到一堆.so文件重点关注名字里带core、nw、protocol、crypto的那几个。用 IDA 或 Ghidra 打开先看导出的符号表找send、recv、encrypt、decrypt这类函数名。还要留意它引用了哪些第三方序列化库。如果项目里用到了 protobuf你会看到很多parseFrom、writeTo的调用痕迹如果是 msgpack会有pack、unpackflatbuffers 则是一堆GetRootXX开头的函数。这一步决定了你后面还原业务数据时要写哪种解析器。3.2 动态插桩从系统调用层开始挂静态分析解决的是“代码在哪”的问题接下来要解决“数据长什么样”的问题这一步必须靠动态插桩。我最常用的工具是 Frida。起步姿势很固定先 hooksend和recv看看协议包原始字节流到底什么样。// frida -U -f com.example.app -l hook_send.js Java.perform(function () { var send Module.findExportByName(null, send); if (send) { Interceptor.attach(send, { onEnter: function (args) { var len args[2].toInt32(); if (len 0) { var data Memory.readByteArray(args[1], len); console.log([send] fd args[0].toInt32() len len); console.log(hexdump(data, { offset: 0, length: len, header: true, ansi: true })); } } }); } });脚本跑起来后操作一遍 App 的关键功能观察输出。这里有个经验日志一定会很爆炸所以要学会按条件过滤。先看 fd找到那条长连接的 fd 是几然后只打印这个 fd 的数据同时按长度过滤小于几十字节的心跳包直接跳过重点关注中等长度以上的业务包。如果 send 出来的字节流里带着明显的魔数开头比如aa bb cc dd说明协议是明文组包、后面再做加密或整体下发。如果满脸乱码、没有任何可读特征说明可能在更上游就已经加密了。这时候就要顺着调用栈往上找看数据在进入 send 之前经过了哪个函数——这个函数就是加密函数。3.3 从 TCP 流特征反推协议格式动态插桩能让你看到“活的”协议但有些场景下没法长时间挂 Frida那就回到流量侧用 tcpdump 或 Wireshark 抓到原始字节流再做离线分析。我常用的一套流程是这样的先在手机上用 tcpdump 抓一段流量导出到 PC 上用 Python 的 scapy 读取原始 TCP payload然后扫描整个流里重复出现的头部字节模式。from scapy.all import rdpcap packets rdpcap(capture.pcap) payload_chunks [] for pkt in packets: if pkt.haslayer(TCP) and pkt[2].payload: # 去掉 IP/TCP 头取原始负载 raw bytes(pkt[2].payload) if len(raw) 16: payload_chunks.append(raw) # 寻找出现次数最多的头部模式通常就是魔数 from collections import Counter prefix_counts Counter() for chunk in payload_chunks: prefix_counts[chunk[:4]] 1 print(prefix_counts.most_common(10))跑完之后你会看到一个出现频率极高的前 4 字节那基本就是魔数。有了魔数再去做字段切割比较多个包的长度与内容长度字段、命令字段、序列号字段会逐渐浮出水面。这一步的要点是“多取对比样本”。同一个接口多触发几次对比两次请求之间哪些字段在变、哪些不变。变的字段里序列号是递增的时间戳是接近当前时间的其他不定字段大概率是加密参数或随机量。4. 逆向容灾用工程冗余思维降维破局4.1 为什么单一路径经常走不通聊完常规路径我想讲一个比具体招式更重要的方法论也是标题里“逆向容灾”想表达的东西。实际对抗中你会发现单一分析路径特别容易断。你用代理抓包它有证书校验你去 hooksend它有反 Frida 检测你刚想静态分析 so发现代码早就被混淆加 VMP 了你好不容易定位到加密函数发现密钥是每次会话动态协商的。每一步都对应着一道防御单一路径走到底几乎必死。“容灾”这个词来自后端架构意思是系统出故障时能自动切换到备用节点保证服务不中断。我把这套思路搬到了逆向分析里不押注在单一方案上而是设计一套多路径并行的分析管线任何一条被对抗堵死立刻切换到另一条保证整个分析过程不中断。这就是“逆向容灾”的降维打击——你不是在和对方某一个防御点死磕而是用体系对抗单点。4.2 一套可切换的分析管线我实际用的分析管线分四层每层内部都有主备两条路径第一层是入口判断。主路径是 HTTP 代理抓包用来判断“业务流到底走不走 HTTP”备用路径是 tcpdump 底层抓包用来确认长连接的位置、端口、频率。这一层的产出不是数据而是“方向”。第二层是数据获取。主路径是 hooksend/recv拿到原始字节流备用路径是透明网关注入把流量重定向到分析机上做镜像。如果 App 检测到代理特征导致连接被重置就走备用路径。第三层是解密定位。主路径是 hook 已知系统加密库AES_encrypt、RSA_private_decrypt这类导出符号备用路径是 hook 应用自定义加解密方法直接在上层拿解密后的数据。第四层是算法还原。主路径是用unidbg这类 JNI 模拟框架把核心算法摘出来跑不依赖真机环境备用路径是直接内存 dump把密钥和中间结果一次性捞出来。这四层每一层都是独立的上一层的输出传给下一层但任意一层的“主”挂了都能用“备”顶上。整个管线像一个容灾系统从数据获取到算法复现始终保持链条畅通。4.3 容灾切换决策表与优先级规则为了让这套思路落地我习惯把不同场景下的卡点和切换策略整理成一张表分析卡壳时直接对着表做判断阶段卡点典型信号切换策略代理抓包无业务流量面板只有图片和静态资源放弃 HTTP 层转 tcpdump 找长连接连接建立后立刻断开疑似检测到代理特征换透明网关方案不在客户端本地挂代理hook send/recv 输出乱码字节流无魔数、无可读结构说明上游已加密向上查找加解密函数SSL 握手失败或证书错误连接被重置放弃重签证书直接 hook 加解密函数在内存中取明文加解密函数被混淆静态分析看半天没头绪转 unidbg 模拟执行不分析算法只观察输入输出模拟执行环境被检测进程运行崩溃或超时转内存 dump直接捞密钥和明文缓冲优先级规则也很简单能先走流量侧解决的不急着挂钩子能挂钩子解决的不急着抠算法能抠算法的不急着碰加固。每一步都在上一个方案失败后才升级避免在一棵树上吊死。5. 实战复盘从“全是图片”到协议复原5.1 案例信息收集与初步假设理论讲完我用一个虚拟案例把整套流程串一遍。假设你面对的是一个社交类 App目标是想搞清楚列表页数据是怎么下发的、协议长什么样。第一步按容灾管线的入口判断层操作。Charles 挂代理打开 App刷列表面板里除了图片就是埋点 JSON。业务数据一条都没有。到这里基本上可以判定它走的是私有长连接接下来切换到 tcpdump 从底层抓包。抓了几分钟拿到一份 pcap。过滤出 TCP 流后发现客户端和某个 IP 的某个固定端口之间有一条长连接维持了全程流量密集。导出 payload 后跑一遍前文说的前缀扫描很快就看到一个出现频率极高的四字节魔数0x12 0x34 0xAB 0xCD。方向确认。5.2 关键突破Hook 到加密入口拿到明文有了魔数下一步要确定包体是明文还是密文。直接在 Frida 里 hooksend看到你发出去的每一个包都是整齐的协议头但包体部分全是高熵乱码没有可读字段基本判定包体是加密的。顺着调用栈往上追。这种场景下frida 的Thread.backtrace能打出调用栈把栈顶附近的函数一个个看过去最后定位到一个 Native 函数名字大概叫xxx_encrypt_packet。这个函数接收一个明文 buffer、输出一个密文 buffer。在这个函数的返回处挂一个读内存的操作明文就直接出现了。跑通一次之后再把recv对应链路也 hook 上。响应侧同样有一层解密逻辑输入是密文、输出是明文。把两边的明文都打出来整个协议的业务层就完全暴露在你面前了。5.3 协议还原与验证的完整链路拿到明文的下一步是写一个离线解析脚本把消息体解出来。假设包体是 protobuf直接用protoc的--decode_raw看一眼原始字段编号再配合已知的明文内容很快能还原出业务字段。import protobuf_pb2 as pb data_bytes bytes.fromhex( 0a 11 1a 0f ... # 解密后的 protobuf 报文 ) msg pb.FeedResponse() msg.ParseFromString(data_bytes) print(msg)还原出消息结构之后还有一个必须做的步骤验证。把解析脚本跑一遍拿解析出的条数与 App 页面上实际展示的条数对照再对比一条数据的 ID 是否和页面显示一致。如果都对上了恭喜你这个协议就算被完整破译了。验证完之后还要做回归测试——升级版本后重测一遍看看协议头里有没有出现新字段、加密逻辑有没有变化。我遇到很多情况是算法没变、只是密钥协商方式变了这种情况下把密钥协商的部分单独拎出来分析即可不用从零重新来一遍。6. 几个我在实战中踩过的坑和最后的边界提醒最后聊几个实际的体会都是常规文章里不太会讲但很影响效率的细节。第一别只盯 Java 层。很多人在 jadx 里看到一堆眼花缭乱的 Java 代码以为协议逻辑都在这一层结果折腾半天发现真正的加解密核心在 so 的 Native 函数里Java 层只是做了层薄封装。我现在的习惯是一开始就做好双线排查Java 层和 Native 层同时铺开谁先有结果就顺着谁走。第二注意半包和粘包。长连接上数据是流式的你 hookrecv拿到的一次数据不一定正好是一个完整协议包可能是半个包也可能包含两个包。遇到这种问题别慌把字节先拼起来再依照长度字段拆包。这个坑在数据量大的时候尤其容易出现Frida 日志里如果全是长度对不上的乱包先想想是不是没做粘包处理。第三版本升级后协议一定会变。私有协议不是一次性定死的客户端发版通常伴随协议调整。所以逆向分析的结果要写成脚本、留下文档而不是记在脑子里。下次协议变更时对比新旧差异会省很多事。第四也是最重要的这套方法只应该用在你拥有或者已经获得授权的应用上。安全研究、漏洞挖掘、防御方案的制定都是正当用途。想通过逆向去爬取用户数据、绕过付费验证、破解商业产品拿去做灰黑产这既不合法也违背了技术研究的初衷。我在分析过程中还会刻意控制数据采集范围只抓必要的协议样本分析完就清理不做无意义的留存。回到最初的问题——抓包面板全是图片没有数据从来不是一个“抓不到”的问题而是一个“方向没找对”的问题。那些你看不见的数据永远都在那条你看不懂的长连接里流动着。理解了私有长连接的设计逻辑再配合一套“逆向容灾”的多路径分析思路主动权就能回到你手里。下次再碰上一个抓包抓了个寂寞的 App不妨先放下代理打开 Wireshark问自己一句它跟哪个 IP 的哪条连接一直在说话。
返回列表