网络协议逆向:从抓包到自定义协议解析
说实话,网络协议逆向这块,是二进制安全里最「实在」的技能之一。你想想看,无论是分析恶意软件、做漏洞挖掘,还是搞工控安全,最终都要落到「这个数据包到底在说什么」这个问题上。我个人习惯把协议逆向分成三个层次:先会用工具看,再能理解标准协议,最后能搞定那些奇奇怪怪的自定义协议。
Wireshark 抓包:不只是点个开始按钮
很多人觉得 Wireshark 就是点一下「开始捕获」就完事了。嗯,我在项目里见过太多人抓了一堆包,结果全是噪音,根本找不到关键数据。这里我分享几个实战技巧。
- 先确定要抓哪个接口——无线网卡还是有线?本地回环还是远程?
- 设置捕获过滤器,比如
host 192.168.1.100或port 443 - 如果流量太大,用
tcpdump -w capture.pcap先存文件,回头再分析
我曾经遇到过一个案例:某工控设备每隔 5 秒发一个心跳包,但偶尔会夹带一个异常数据包。如果不设过滤器,抓 10 分钟就是几千个包,眼睛都看花了。后来我直接用 tcp.port == 502 过滤 Modbus 协议,问题一下子就清晰了。
Wireshark 的显示过滤器才是真正的利器。记住几个常用的:
# 只看特定 IP 之间的通信
ip.addr == 10.0.0.1 && ip.addr == 10.0.0.2
# 只看 TCP 三次握手
tcp.flags.syn == 1 && tcp.flags.ack == 0
# 只看重传包
tcp.analysis.retransmission
# 只看特定 payload 长度
frame.len > 1000
TCP/UDP 协议分析:理解传输层的「潜规则」
TCP 和 UDP 的区别,教科书上讲得很清楚。但实际逆向中,你需要关注的是那些「教科书不会告诉你」的细节。
TCP 的「指纹」特征
每个操作系统、甚至每个版本的 TCP/IP 协议栈,在发送 SYN 包时都会有一些细微差异。这就是所谓的 TCP 指纹。我在分析恶意软件时,经常通过 TCP 指纹来判断 C2 服务器跑的是什么系统。
| 特征字段 | Windows 10 | Linux 5.x | macOS |
|---|---|---|---|
| 初始 TTL | 128 | 64 | 64 |
| MSS | 1460 | 1460 | 1460 |
| 窗口缩放 | 8 | 7 | 3 |
| SACK 许可 | 是 | 是 | 是 |
你看,光是一个窗口缩放因子,就能帮你缩小范围。我当年逆向一个 IoT 僵尸网络时,就是靠这个特征发现它的 C2 服务器其实跑在 Windows 上,而不是一开始以为的 Linux。
UDP 的「无状态」陷阱
UDP 没有连接状态,但这不代表它没有规律。很多自定义协议选择 UDP 是因为延迟低,但代价是应用层必须自己处理丢包和重传。我在分析一个游戏外挂时,发现它用 UDP 发送坐标数据,但每隔 5 个包会夹带一个序列号校验包——这就是应用层自己实现的「伪 TCP」机制。
自定义协议识别与解析:从零到一
这才是真正的硬核部分。标准协议有 RFC 文档可以参考,但自定义协议?你只能靠逆向。我总结了一套方法论,叫做「三步走」。
第一步:找边界
拿到一堆二进制数据,第一件事不是看内容,而是找「边界」。怎么找?看长度字段。几乎所有自定义协议都会在头部某个位置标明 payload 长度。找到这个字段,你就知道一个完整的数据包从哪里开始、到哪里结束。
// 假设我们抓到这样一段数据
// 0x02 0x00 0x1A 0x00 0x01 0x03 ...
// 如果 0x02 0x00 是小端序的 2,那可能是协议版本
// 如果 0x1A 0x00 是小端序的 26,那很可能是 payload 长度
// 验证方法:从头部开始数 26 个字节,看看是不是一个完整的数据单元
我曾经逆向过一个工业机器人协议,它的长度字段藏在第 4-5 字节,但用的是大端序。我一开始按小端序解析,结果怎么都对不上,折腾了两天才发现这个坑。
第二步:找特征
每个协议都有「指纹」。可能是固定的魔数(Magic Number),可能是特定的命令码,也可能是某种编码方式。把多个数据包放在一起对比,找出那些不变的部分。
xxd 或 hexdump 查看,肉眼对比不同数据包的差异。我习惯用 Beyond Compare 做二进制对比,效率很高。
第三步:写解析器
当你摸清了协议结构,就可以写解析器了。不一定非要用 C 或 Python,我有时候直接用 Wireshark 的 Lua 插件来解析,这样可以直接在 Wireshark 里看到解析结果。
-- 一个简单的 Wireshark Lua 解析器示例
local my_proto = Proto("myproto", "My Custom Protocol")
local f_magic = ProtoField.uint16("myproto.magic", "Magic", base.HEX)
local f_cmd = ProtoField.uint8("myproto.cmd", "Command", base.DEC)
local f_len = ProtoField.uint16("myproto.len", "Length", base.DEC)
local f_payload = ProtoField.bytes("myproto.payload", "Payload")
my_proto.fields = { f_magic, f_cmd, f_len, f_payload }
function my_proto.dissector(buffer, pinfo, tree)
if buffer:len() < 5 then return false end
local magic = buffer(0, 2):uint()
if magic ~= 0xABCD then return false end
local subtree = tree:add(my_proto, buffer(), "My Protocol Data")
subtree:add(f_magic, buffer(0, 2))
subtree:add(f_cmd, buffer(2, 1))
subtree:add(f_len, buffer(3, 2))
local len = buffer(3, 2):uint()
if buffer:len() >= 5 + len then
subtree:add(f_payload, buffer(5, len))
end
return true
end
-- 注册到 TCP 端口 8888
local tcp_table = DissectorTable.get("tcp.port")
tcp_table:add(8888, my_proto)
这个脚本虽然简单,但已经能处理大部分自定义协议了。我在实际项目中,经常在这个基础上加一些高级功能,比如自动识别变长字段、处理加密数据等。
SVG:协议逆向的核心流程
实战中的几个坑
最后,分享几个我在实际项目中踩过的坑,希望能帮你少走弯路。
我曾经逆向一个金融交易协议,它的长度字段在第 2-3 字节,我按小端序解析,结果长度总是对不上。后来发现这个协议是 Motorola 68000 架构的遗留系统,用的是大端序。从那以后,我拿到任何协议数据,第一件事就是确认端序。
有些协议的长度字段包含头部自身的长度,有些只包含 payload 的长度。如果不搞清楚这一点,解析出来的数据会整体偏移。我的习惯是:先假设长度字段只包含 payload,如果解析结果不对,再尝试包含头部。
协议逆向说到底就是「模式识别」——在看似随机的二进制流中找到规律。这个能力需要大量练习,但一旦掌握了,你会发现很多看似复杂的系统,底层逻辑其实很简单。嗯,今天就先聊到这里,下次有机会再深入讲讲加密协议的逆向思路。
公众号:蓝海资料掘金营,微信deep3321