为什么加密算法识别是协议逆向的第一道关卡
拿到一份未知的网络协议流量后,如果载荷部分是密文,那么一切后续分析工作都会陷入僵局。此时最重要的任务是判断目标程序使用了哪种加密算法,因为不同算法对应的破解思路完全不同:AES和DES属于分组密码,需要关注分组长度、工作模式(ECB、CBC等)和填充方式;RC4属于流密码,密钥流直接与明文异或,密文长度就等于明文长度。用Ruby做这类分析有天然优势,它的字符串就是字节序列,配合String#unpack、String#bytes等API可以非常方便地做字节级统计和特征比对。

识别算法特征有三个主要途径。第一是密文长度规律,分组密码的密文长度几乎总是8或16的整数倍;第二是常量特征,AES的S盒、DES的置换表、RC4的状态数组初始化逻辑都有公开且固定的数值,一旦在二进制或内存转储中匹配到这些常量,基本可以锁定算法;第三是行为特征,通过控制明文输入观察密文输出,推断分组大小、是否使用IV、雪崩效应是否明显等。三种途径互相印证,可以显著降低误判率。
需要强调的是,算法识别不等于破解。识别只是确定分析方向,例如确认是AES-CBC后,下一步就是找密钥和IV,而不是暴力枚举密文。明确这一点有助于把精力放在正确的分析路径上。
基于密文长度和结构的初步判别
分组密码输出的密文长度有明显的数学规律。DES和3DES的分组是8字节,AES的分组是16字节,在PKCS7填充模式下,密文长度一定是分组大小的整数倍。因此拿到一段密文后,先计算长度并对8和16取模,是最快速的第一步筛查。
下面这段Ruby代码实现了基础的长度特征分析:
def analyze_length(data)
len = data.bytesize
puts "密文长度: #{len} 字节"
puts "是否为8的倍数(DES候选): #{len % 8 == 0}"
puts "是否为16的倍数(AES候选): #{len % 16 == 0}"
# 同时是8和16的倍数时无法区分,需要进一步分析
if len % 16 == 0
puts "可能是AES,也可能是DES-CBC加密后恰好对齐"
elsif len % 8 == 0
puts "更可能是DES/3DES"
else
puts "长度不对齐,可能是流密码(RC4)或未填充的CTR/OFB模式"
end
end
cipher_text = ["5f1a2b3c4d5e6f708192a3b4c5d6e7f8"].pack("H*")
analyze_length(cipher_text)需要注意两个陷阱。其一,AES-CTR、OFB、CFB这类流式模式不强制填充,密文长度可以任意,不能因为长度不对齐就排除AES;其二,长度恰好是16的倍数时也无法确定就是AES,因为DES-CBC加密32字节数据同样输出32字节。所以长度分析只能作为初筛,必须结合其他特征验证。
另一个实用技巧是观察多条消息的长度分布。如果采集到的几十条密文长度全部落在8或16的倍数上,分组密码的判断基本可以坐实;如果长度完全随机分布,则更倾向于RC4或分组密码的流式模式。还可以做差分测试:发送两个只差一个字节的明文,观察密文长度是否改变,分组密码加填充时差分可能引起整块长度跳变,流密码则密文长度恒等于明文长度。
通过常量匹配确认算法类型
分组密码的实现中必然包含公开的查找表和常量,这是最可靠的识别手段。AES的S盒以0x63开头、以0x52结尾,共256字节;DES的IP初始置换表、S盒以及密钥置换表PC1、PC2都是固定数组;RC4虽然没有大常量表,但其密钥调度算法KSA的结构特征明显,逆向时看到256次循环交换状态数组的代码即可判断。
下面的Ruby代码演示如何在一段提取出的二进制数据中搜索这些特征常量:
# AES S盒前16字节特征
AES_SBOX_HEAD = [0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5,
0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76].pack("C*")
# DES 置换选择表PC2开头部分
DES_PC2_HEAD = [14, 17, 11, 24, 1, 5, 3, 28].pack("C*")
def scan_constants(binary)
hits = []
hits << "AES S盒" if binary.include?(AES_SBOX_HEAD)
hits << "DES PC2表" if binary.include?(DES_PC2_HEAD)
# RC4特征:查找典型KSA循环结构中的256常量
# 状态数组S[i]=i的初始化在机器码中体现为0-255的顺序填充
hits << "RC4候选(需人工确认KSA循环)" if rc4_pattern?(binary)
hits
end
def rc4_pattern?(binary)
# 简化示例:检查是否存在连续的0x00到0x0F递增序列(状态数组初始化痕迹)
pattern = (0..15).to_a.pack("C*")
binary.include?(pattern)
end如果手头只有流量而没有目标二进制,可以改用黑盒方式验证。用Ruby构建已知明密文对,逐一尝试调用OpenSSL绑定库以不同算法加密对比:
require "openssl"
def identify_by_plaintext(key, iv, plain, target_cipher)
candidates = {
"AES-128-ECB" => nil, "AES-128-CBC" => iv,
"DES-ECB" => nil, "DES-CBC" => iv,
"RC4" => nil
}
candidates.each do |algo, iv_or_nil|
cipher = OpenSSL::Cipher.new(algo)
cipher.encrypt
cipher.key = key.byteslice(0, cipher.key_len)
cipher.iv = iv_or_nil if iv_or_nil
result = cipher.update(plain) + cipher.final
if result == target_cipher
return "匹配算法: #{algo}"
end
end
"未匹配,可能密钥或IV有误"
end这种方法的前提是能拿到密钥来源,比如密钥硬编码在客户端、由简单字符串派生或通过可解密的握手过程传输。当密钥正确时,只有真实算法能产生一致的输出,这本身就是最强的验证。
RC4与分组密码的行为差异分析
RC4的行为特征与分组密码差异很大,可以设计实验加以区分。RC4是同步流密码,相同密钥下不同位置的相同明文片段加密结果不同(密钥流位置不同),但如果两条消息都用相同密钥从头加密且明文前缀相同,则对应密文前缀也完全相同——这是RC4复用密钥流的致命特征,AES-CBC在相同IV和密钥下才会有类似表现,而ECB模式则是相同明文块对应相同密文块,呈现块级重复。
用Ruby统计密文中的重复块可以区分ECB模式与流密码:
def detect_block_repetition(data, block_size = 16)
blocks = data.bytesize / block_size
seen = Hash.new(0)
blocks.times do |i|
chunk = data.byteslice(i * block_size, block_size)
seen[chunk] += 1
end
dup = seen.values.select { |v| v > 1 }.sum { |v| v - 1 }
if dup > 0
"发现#{dup}个重复块,高度疑似ECB模式的分组密码"
else
"无重复块,可能为CBC/CTR模式或RC4流密码"
end
end此外,RC4密钥流的字节分布虽然理论上均匀,但存在著名的初始字节偏差:密钥流第二个字节为0的概率约为1/256偏上,累积数百条样本可以做统计检测。相比之下,AES和DES的输出在无明显结构信息时接近随机。用Ruby做卡方检验统计字节频率,若偏差显著超出随机范围且数据前缀一致性好,RC4的可能性就很大。
实战建议与常见误判排查
实际分析中建议建立一套固定的排查流程:先做长度取模初筛,再统计重复块判断工作模式,然后尝试常量匹配或黑盒比对确认算法,最后用已知明文验证。整个流程用Ruby脚本串联起来,可以在分钟级完成初步定型,避免在错误的方向上浪费时间。
常见误判来源包括:目标使用TEA、XTEA、SM4等非主流分组算法,常量匹配会失效,需要补充这些算法的特征表;目标对标准算法做了魔改,例如替换S盒或改轮数,此时黑盒比对失败但结构特征仍在,需要结合汇编分析;压缩后再加密的数据长度不规律,容易被误判为流密码,可以先检查数据是否带压缩头(如zlib的0x78前缀)。遇到这些情况,多收集样本、保持怀疑、交叉验证是唯一可靠的方法论。
总之,加密算法识别是经验与工具结合的工作。Ruby凭借简洁的字节操作API和强大的OpenSSL绑定,能够覆盖从特征扫描到黑盒验证的全流程,是协议逆向分析工具箱中非常值得掌握的一件利器。