网络协议逆向分析中,加密参数的识别是关键难点。客户端通信常使用AES、DES等对称加密算法保护数据,但抓包工具只能获取到密文,无法直接看到密钥Key和初始化向量IV。要还原通信内容,需要从客户端程序入手,定位加密函数的调用位置、参数生成逻辑以及内存中的关键数据。Ruby语言虽然在底层调试方面不如C或Python生态丰富,但凭借灵活的字符串处理、二进制打包能力和外部进程调用,可以快速构建一套参数识别工具链。本文将从协议流量特征、内存扫描和动态插桩三个角度,说明如何利用Ruby提取IV与Key,并用OpenSSL进行验证。

一、先定位加密函数和参数来源
识别IV和Key的前提是知道客户端在哪里调用了加密函数。通常可以通过静态搜索特征字符串或导入表定位。例如Windows程序如果使用了系统CryptoAPI,会导入CryptEncrypt、CryptDeriveKey等函数;如果使用了OpenSSL,则二进制中可能出现EVP_EncryptInit_ex、AES_set_encrypt_key等符号。使用Ruby可以批量处理字符串提取结果,过滤出与加密相关的API名称或错误提示信息。
抓包数据也能提供线索。对称加密的分组长度会体现在密文长度上,AES加密后的数据长度通常是16字节的整数倍,DES则是8字节的整数倍。如果发现某协议请求或响应体的密文长度满足这个规律,就可以初步判断使用了分组加密。进一步观察同一明文的多次加密结果,如果密文前16字节每次不同但后续相同,说明IV可能使用了随机数并且附着在密文头部;如果全套密文每次都一样,则IV大概率是固定值。
参数来源包括硬编码、配置文件、服务端下发和本地派生。硬编码的Key和IV可以直接在二进制中搜索连续的可打印或不可打印字节;服务端下发的参数则需要逆向握手过程;本地派生一般调用密钥派生函数,例如PBKDF2、bcrypt或自定义哈希。明确来源后,才能选择适合的提取策略。
二、用Ruby做内存扫描与候选参数提取
在客户端运行并建立加密会话后,Key和IV通常会短暂存在于进程内存中。可以借助调试器或注入工具把进程内存dump成文件,再用Ruby脚本扫描。Ruby的String#unpack和Array#pack可以方便地在字节层面处理二进制数据。下面这段代码演示如何读取内存镜像文件,并按16字节窗口滑动查找高熵块,高熵往往是密钥或随机数的特征。
# 扫描内存镜像中的高熵16字节块
def entropy(bytes)
freq = Array.new(256, 0)
bytes.each_byte { |b| freq[b] += 1 }
len = bytes.bytesize.to_f
freq.reduce(0.0) do |sum, count|
next sum if count.zero?
p = count / len
sum - p * Math.log2(p)
end
end
dump = File.binread('client_memory.dump')
window = 16
offset = 0
while offset + window <= dump.bytesize
chunk = dump.byteslice(offset, window)
# 熵大于7.5,接近随机数据
if entropy(chunk) > 7.5
hex = chunk.unpack1('H*')
puts "offset=#{offset} hex=#{hex}"
end
offset += 1
end
脚本使用香农熵评估数据随机性,输出所有接近随机分布的16字节片段。这样可以得到一批Key和IV的候选值,之后再用密文进行验证。除了熵检测,还可以结合已知明文特征,例如协议头部通常包含固定魔数,解密后如果匹配则说明参数正确。
如果候选值过多,可以缩小扫描范围。对称加密库在调用加密函数时,Key和IV往往位于栈或堆的邻近区域。通过调试器下断点,在加密函数入口处抓取寄存器或栈指针附近的缓冲区,Ruby脚本再对这个小范围数据做精确提取,命中率会更高。Ruby可以通过Open3模块调用gdb或Frida的命令行工具,将断点输出保存为文本,再自动解析其中的十六进制数据。
三、结合动态插桩提取参数
动态插桩可以在不修改程序二进制的情况下,在加密函数被调用时拦截参数。Frida是常用的跨平台插桩框架,支持JavaScript编写hook逻辑,并可以通过消息将数据传给Ruby进程。Ruby利用frida-gum的JSON-RPC接口或直接通过子进程调用Frida CLI,能够实现自动化提取。下面的脚本展示了一个简单的思路:约定Frida输出JSON数组,Ruby负责解析并格式化候选参数。
require 'json'
require 'open3'
# 假设 frida 输出每一行 JSON:{"name":"AES_set_encrypt_key","key":"..."}
stdout, stderr, status = Open3.capture3('frida -q hook_script.js -o output.json')
if status.success?
lines = File.readlines('output.json')
lines.each do |line|
data = JSON.parse(line)
key_hex = data['key']
iv_hex = data['iv']
puts "Key: #{key_hex}"
puts "IV: #{iv_hex}"
end
else
warn stderr
end
在实际操作中,需要先确定加密函数在目标进程中的地址。对于OpenSSL动态库,可以在运行时遍历模块导出符号,使用Frida的Module.enumerateExports找到AES_set_encrypt_key和EVP_EncryptInit_ex。在这些函数入口处,读取参数寄存器或栈上的指针。以x64调用约定为例,第一个参数通常是上下文指针,后续参数包括密钥和IV指针。Frida脚本可以调用Memory.readByteArray读取指定长度的数据,并转为十六进制字符串发回Ruby端。
动态插桩的优点是能捕获经过变换后的最终Key和IV。很多程序在初始化时会对硬编码字符串做异或、反转或Base64解码,静态搜索原始字符串效率很低。在加密函数入口处拦截到的参数已经是真实可用的值,省去了逆向变换逻辑的步骤。不过需要注意的是,某些程序会自定义加密函数或使用混淆,此时需要先通过特征码或交叉引用找到实际调用点。
四、用OpenSSL验证并还原明文
拿到候选Key和IV后,可以使用Ruby的OpenSSL库快速验证。假设目标协议使用AES-128-CBC加密,密文和预期明文头部已知。下面的脚本遍历所有候选组合,尝试解密,并检查解密后是否包含协议魔数。
require 'openssl'
ciphertext = File.binread('captured_cipher.bin')
known_plain_prefix = "x00x01x02x03" # 协议固定头部示例
candidate_keys = [
['0123456789abcdef'].pack('H*'),
['fedcba9876543210'].pack('H*')
]
candidate_ivs = [
['000102030405060708090a0b0c0d0e0f'].pack('H*'),
['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'].pack('H*')
]
candidate_keys.each do |key|
candidate_ivs.each do |iv|
decipher = OpenSSL::Cipher.new('aes-128-cbc')
decipher.decrypt
decipher.key = key
decipher.iv = iv
begin
plain = decipher.update(ciphertext) + decipher.final
if plain.start_with?(known_plain_prefix)
puts "Found key=#{key.unpack1('H*')} iv=#{iv.unpack1('H*')}"
puts "Plaintext: #{plain[0, 64].inspect}"
end
rescue OpenSSL::Cipher::CipherError
# padding 错误,忽略
end
end
end
验证阶段的目标是快速过滤无效参数。对称解密算法对错误Key或IV非常敏感,CBC模式下还会校验填充,解密结果若不符合PKCS#7规则会抛出CipherError。利用这一机制,即使不知道完整明文,也能通过异常与否判断Key和IV是否正确。如果协议使用GCM等带认证模式,还可以在解密后用认证标签进一步确认。
如果解密后得到的数据仍有压缩或自定义编码,需要继续分析上一层协议。但此时加密层已经被突破,后续还原会更加容易。对于RC4这类流式算法,虽然没有IV概念,但同样可以通过定位密钥和初始状态来还原。
五、常见误区和排查思路
一个常见误区是认为Key和IV一定以连续的字节序列存储在内存中。实际代码可能将Key拆分为多个变量,在调用加密函数前才通过memcpy或循环拼接。此时内存扫描无法直接找到完整Key,需要从加密函数的参数读取还原。解决思路是动态断点配合寄存器值,而不是盲目搜索整个内存空间。
另一个陷阱是Key和IV可能经过编码存储。例如某些客户端把密钥以Base64字符串形式写入配置文件或资源段,运行时解码后传入加密接口。如果只对进程内存做字节扫描,看到的是解码后的二进制Key,无法反推文件中的原始字符串。此时需要回到静态分析,从配置读取逻辑或资源文件入手,找到解码前的存储形式。
还有一类情况是每次会话都会重新协商Key,抓包只能看到一个随机数或握手包。这种设计常见于TLS协议以及自定义的密钥交换流程。仅靠单次内存扫描可能只能拿到当前会话的临时Key,无法破解其他会话。分析时应先梳理协议握手流程,明确Key是从固定种子派生的还是独立生成的。只有在确定参数生成规则后,才能实现稳定还原。