网络协议逆向中的状态推断,是指在不掌握协议规范的前提下,通过观测真实通信产生的报文序列,推导出对端或本端所遵循的状态机模型。 Ruby语言凭借其灵活的字符串处理、丰富的网络库以及简洁的脚本能力,非常适合用来快速搭建一个报文采集与状态聚类的实验环境。本文围绕如何用Ruby从原始报文序列中推断状态机展开,涵盖数据获取、特征抽象、状态合并与转移图生成四个层面。

报文序列的采集与预处理
要进行状态推断,第一步是拿到足够多的真实报文。 Ruby的Socket和PCAP相关库可以分别在应用层镜像和链路层抓包两个层面工作。如果目标协议跑在TCP上,最简单的方式是在客户端或服务端用TCPSocket做中间人转发,把双向数据都记录下来;若需要更底层的视角,可以使用ruby-pcap绑定libpcap,直接读取网络接口的原始帧。
拿到原始字节流后,不能把每一条报文都当作独立样本,因为状态机关注的是“顺序”。我们需要按连接五元组(源IP、目的IP、源端口、目的端口、协议)将报文分组,再在每个连接内按时间排序,形成一条报文序列。随后做基础清洗:去掉重传包、合并TCP分段、根据端口或 magic number 切分出应用层PDU。下面代码展示了一个最简的TCP中转记录器,它把客户端发来和服务端回应的内容分别追加到数组,最终返回一个有序序列。
require 'socket'
def capture_session(client_port, server_host, server_port)
client = TCPServer.new(client_port)
sessions = []
loop do
c = client.accept
s = TCPSocket.new(server_host, server_port)
buf_c = []
buf_s = []
threads = []
threads << Thread.new do
while data = c.recv(4096)
break if data.empty?
s.write(data)
buf_c << data.bytesize
end
end
threads << Thread.new do
while data = s.recv(4096)
break if data.empty?
c.write(data)
buf_s << data.bytesize
end
end
threads.each(&:join)
sessions << {c: buf_c, s: buf_s}
c.close
s.close
end
sessions
end
上面的脚本只记录了报文长度序列,没有存载荷内容,这在某些场景下已经足够做状态划分,因为很多协议的状态切换会伴随明显的包长变化。如果需要更细的区分,可以把前N字节的十六进制摘要也存下来。预处理阶段还要注意时钟同步问题,分布式抓包时要给每条报文打上统一时间源的时间戳,避免排序错乱导致状态跳转看起来不合逻辑。
从报文特征到状态节点的抽象
原始报文序列不能直接变成状态机,必须先映射成离散的“事件符号”。常见做法是把(方向,长度区间,首字节类型)组成一个元组作为符号。例如客户端发出一个64到128字节的报文且首字节为0x01,记作C_MID_AUTH。 Ruby中可以用哈希表来定义规则,把每条报文转成符号串。这样做既压缩了数据量,也让后续统计转移概率变得可行。
当我们有了几千条符号序列后,可以用频繁序列挖掘或简单的前缀树(Trie)来发现稳定出现的子结构。如果一个符号组合总是在固定位置出现,它极可能对应协议中的一个明确状态,比如“登录请求-登录响应”配对。我们还可以借助编辑距离合并相似符号,防止因为个别字节浮动而产生过多碎片状态。以下代码演示了如何把长度转成区间标签:
def size_label(n)
if n < 16
'S'
elsif n < 64
'M'
elsif n < 256
'L'
else
'X'
end
end
def to_symbol(dir, pkt)
first = pkt[0].unpack('C').first
"#{dir == :c ? 'C' : 'S'}_#{size_label(pkt.bytesize)}_#{first}"
end
seq = []
io_pairs.each do |dir, raw|
seq << to_symbol(dir, raw)
end
抽象后的符号序列仍然可能有噪声。此时可以引入隐马尔可夫模型(HMM)的思想,用Ruby的矩阵计算库估计发射概率和转移概率,把观测符号对应到少数几个隐藏状态。即便不实现完整HMM,仅用马尔可夫链统计“符号A之后出现符号B”的频率,也能画出带权有向图,权值低的分支往往是异常或重传,可以从主状态机里剥离。
状态机重建与可视化验证
推断出的状态节点和转移关系,最终要落成一张可读的图。我们可以用Ruby生成Graphviz的DOT语言文本,再调用系统命令渲染成图片,或者在终端用ASCII方式打印邻接表。每个节点标上代表性符号,边标上出现次数或概率。人工核对时重点看三件事:是否存在明显的“握手—业务—断开”三段式,是否有回到初始态的复位边,以及是否有双向心跳维持边。
验证阶段建议拿新抓取的会话去回放,用推断出的状态机做模式匹配。如果新会话中大部分转移都能在模型内找到对应边,说明推断稳定;若频繁落到未知边,就要回到特征抽象层调整符号规则。 Ruby的轻量特性让我们能快速迭代这几层逻辑,不必每次都重启复杂环境。下面例子把统计好的转移哈希输出为DOT:
trans = Hash.new(0)
seq.each_cons(2) do |a, b|
trans[[a, b]] += 1
end
puts 'digraph G {'
trans.each do |(a, b), v|
puts " "#{a}" -> "#{b}" [label="#{v}"];"
end
puts '}'
通过这种脚本化流程,即使是完全没有文档的私有协议,也能在数小时内梳理出可用状态草图,为后续写模拟客户端或做协议兼容层打下基础。相比纯手工用Wireshark逐条看包,Ruby批处理的方式在覆盖率和可复用性上优势明显,而且规则都以代码形式留存,方便团队交接与持续完善。
protocol_reverse_engineeringstate_machine_inferenceRuby修改时间:2026-08-17 23:32:37