协议逆向的核心难题之一,是面对大量无结构的二进制报文时缺少切入线索。关键词字典通过预先收集协议中常见的字段名、命令字、magic值和特征字符串,为报文比对、模糊测试和格式推断提供参照基准。Ruby语言语法简洁、字符串处理能力强,配合原生正则引擎,非常适合编写这类字典生成工具。本文从字典的定位讲起,逐步给出完整的Ruby实现方案。

协议关键词字典的构成与来源
所谓协议关键词字典,本质上是一个经过筛选和标注的字符串集合。它通常包含四类内容:第一类是明文字段名,例如HTTP协议中的Content-Type、User-Agent,这类词出现在文本型协议中;第二类是命令字与magic值,例如许多私有协议在报文头固定位置放置0xAA55或0x7E这样的同步字;第三类是错误信息与提示字符串,固件中的调试日志往往残留大量有价值的命令描述;第四类是业务特征词,比如物联网设备协议中常见的login、report、heartbeat等。
字典来源主要有三个方向。一是公开协议文档与RFC,可以直接提取字段定义;二是固件镜像,通过strings或binwalk解包后提取字符串;三是pcap抓包文件,从真实流量中筛选出高频且位置稳定的字节序列。三种来源各有侧重,文档来源准确但覆盖面窄,固件来源信息量大但噪音多,流量来源最贴近真实场景但需要清洗。实践中的做法是多源融合,再按可信度加权排序。
需要注意的是,字典并非越大越好。过多的通用英文单词会带来大量误匹配,反而干扰分析。因此构建过程必须包含频度统计、长度过滤和黑名单剔除三个环节,这也是后面Ruby脚本重点处理的部分。
用Ruby从二进制样本提取候选关键词
第一步是从原始数据中提取候选字符串。Ruby的String#scan配合正则可以快速切分出连续的可打印字符段,再按最小长度过滤掉噪音。下面是一个完整的提取脚本:
# extract.rb 从二进制样本中提取候选关键词
MIN_LEN = 4 # 最短字符串长度
MAX_LEN = 64 # 最长字符串长度
def extract_strings(data)
# 匹配连续可打印ASCII字符段
data.scan(/[\x20-\x7e]{#{MIN_LEN},#{MAX_LEN}}/)
end
def filter_candidates(words)
stop_words = %w[this that with from there here http html text name]
words.map do |w|
w.strip
end.reject do |w|
# 剔除纯数字、全元音、无重复字母价值的词
w =~ /\A\d+\z/ || stop_words.include?(w.downcase)
end.uniq
end
if __FILE__ == $PROGRAM_NAME
data = File.binread(ARGV[0])
words = filter_candidates(extract_strings(data))
words.each { |w| puts w }
end这段脚本的运行方式是ruby extract.rb firmware.bin > raw_words.txt。它的过滤逻辑分为三层:长度区间约束、停用词剔除和去重。停用词表可以根据目标协议类型自行扩充,例如分析视频协议时可以把frame、audio这类过于宽泛的词也加入黑名单,避免字典被高频通用词淹没。
对于Unicode编码的样本(部分设备固件使用UTF-16LE存储字符串),需要先做编码转换再提取,可以用data.force_encoding("UTF-16LE").encode("UTF-8")处理后重新扫描,或者直接在正则中加入对宽字符零字节的容忍模式。
频度统计与加权排序生成结构化字典
提取只是第一步,字典的核心价值在于排序和标注。对流量样本而言,一个词在不同会话中反复出现且位置稳定,说明它很可能是协议固定字段;而只出现一次的词大概率是业务数据。Ruby的Hash配合Enumerable#group_by可以优雅地完成统计工作:
# build_dict.rb 统计词频并生成加权字典
require "json"
def build_dictionary(samples)
stats = Hash.new { |h, k| h[k] = { count: 0, files: [] } }
samples.each do |path, words|
words.each do |w|
stats[w][:count] += 1
stats[w][:files] << path unless stats[w][:files].include?(path)
end
end
stats.map do |word, info|
coverage = info[:files].size.to_f / samples.size
{
word: word,
count: info[:count],
coverage: coverage.round(3),
score: (info[:count] * coverage).round(2),
type: classify(word)
}
end.sort_by { |e| -e[:score] }
end
def classify(word)
case word
when /\A(GET|POST|PUT|DELETE)\z/i then "method"
when /-/ then "header_field"
when /\A[A-Z]{2,}\z/ then "command"
else "string"
end
end
dict = build_dictionary(samples)
File.write("protocol_dict.json", JSON.pretty_generate(dict))这里的评分公式是词频 × 覆盖率。覆盖率指该词出现在多少个不同样本文件中,这个因子非常关键:一个词在单个抓包文件中出现一万次,可能只是传输了一个大文件;而在一百个文件中各出现一次,才是真正的协议特征。分类函数classify负责给每个词打上类型标签,方便后续工具按类型匹配。
输出的JSON字典可以直接被其他分析脚本加载,例如配合Ruby的PacketFu或pcapgem做实时报文匹配。如果需要导入Wireshark或商业分析平台,也可以额外输出CSV格式,只需将JSON生成部分替换为CSV库的写法即可。
字典的验证与持续维护
字典生成后必须回测验证。最简单的验证方法是拿一批已知协议的报文做匹配,统计命中率与误报率。用Ruby实现一个简易回测器只需要十几行代码,核心思路是把报文转成十六进制字符串后,逐个检查字典词条是否命中,并记录命中位置。如果某个词条在报文中的偏移量高度集中,还可以进一步推断它是定长头部字段。
维护层面建议将字典纳入版本管理,每次新增样本后重新跑一遍构建脚本,用diff观察词条增减。对于长期项目,可以按协议版本拆分多个字典文件,在匹配阶段动态加载,避免新旧特征混杂造成误判。同时保留每条词条的首次发现时间和来源文件哈希,出现问题时能够快速溯源。
最后要提醒的是,关键词字典是辅助手段而非替代品。它能帮助快速定位可疑区域,但协议格式的最终确认仍需结合字段边界分析、动态调试和模糊测试。把字典工具与这些方法配合使用,逆向效率才能最大化。