第84章 恶意软件分析(四):勒索软件分析、加密逻辑还原、解密工具开发
勒索软件,说白了就是“绑架你的数据,拿钱来赎”。我这些年跟勒索软件打过不少交道,从早期的简单文件重命名,到现在的军工级加密算法,花样越来越多。但万变不离其宗——只要你能还原它的加密逻辑,就能写出解密工具。
这一章,我们就拿一个真实的勒索样本开刀。我会带你一步步拆解它的加密流程,然后手写一个解密器。嗯,过程可能会有点烧脑,但相信我,搞通之后你会对加密、文件I/O、PE结构有全新的理解。
样本初探:它到底干了什么?
先说说这个样本的行为。我拿到的是一个Windows PE文件,运行后会做以下几件事:
- 遍历所有本地磁盘和可移动驱动器
- 筛选特定后缀名的文件(.docx、.xlsx、.pdf、.jpg等)
- 对每个文件进行AES-256加密
- 用RSA-2048加密AES密钥
- 在原文件同目录下生成.encrypted文件
- 删除原始文件
- 弹出勒索信(通常是README.txt或HELP_DECRYPT.html)
你看,套路其实很清晰。关键就在于它怎么生成AES密钥、怎么用RSA保护它。只要我们能拿到RSA私钥或者找到密钥生成的漏洞,解密就成功了一半。
核心要点:勒索软件通常采用“混合加密”方案——对称加密(AES)处理大文件,非对称加密(RSA)保护对称密钥。破解的关键往往不在算法本身,而在密钥管理。
静态分析:从入口点开始
我习惯先用IDA Pro打开样本,看看导入表。这个样本导入了CryptAcquireContext、CryptGenKey、CryptEncrypt等Windows CryptoAPI函数。嗯,它用的是系统自带的加密库,不是自己实现的。
再往下翻,我注意到一个可疑的字符串:"C:\\ProgramData\\Microsoft\\Crypto\\RSA\\MachineKeys\\"。这是Windows存储RSA密钥对的地方。样本很可能在这里生成了一个临时密钥对,然后用公钥加密AES密钥,私钥……它可能直接丢弃了,也可能藏在某个注册表项里。
为什么会这样?因为勒索软件作者自己也需要解密能力——他们得给交了赎金的受害者恢复数据。所以私钥要么上传到C2服务器,要么用硬编码的密钥加密后藏在样本里。
个人经验:我在分析一个变种时,发现它把RSA私钥用XOR加密后嵌在资源节里。XOR密钥就是勒索信里的一个固定字符串。这种“掩耳盗铃”式的保护,其实很容易破解。
加密逻辑还原:一步步拆解
动态调试一下。我在CryptGenKey下断点,观察它生成的AES密钥。发现每次调用都生成一个32字节的随机数——这就是AES-256的密钥。然后它调用CryptExportKey把这个密钥导出为SIMPLBLOB格式,再用RSA公钥加密。
加密流程大致如下:
- 生成随机AES密钥(32字节)
- 生成随机IV(16字节)
- 用AES-256-CBC模式加密文件内容
- 用RSA-2048公钥加密AES密钥+IV
- 将加密后的密钥块(256字节)写入文件头部
- 将加密后的文件内容追加在后面
文件结构变成了这样:
| 偏移 | 大小 | 内容 |
|---|---|---|
| 0x00 | 4字节 | 魔数(0xDEADBEEF) |
| 0x04 | 4字节 | 原始文件大小 |
| 0x08 | 256字节 | RSA加密后的AES密钥+IV |
| 0x108 | 剩余 | AES加密后的文件数据 |
这个结构很关键。我们写解密工具时,就要按照这个格式去解析。
注意:有些勒索软件会在加密前对文件进行分块处理,或者使用不同的IV。一定要通过动态调试确认每个细节,不要想当然。
解密工具开发:从理论到代码
好,现在假设我们已经拿到了RSA私钥(比如通过内存dump或者C2通信截获)。接下来就是写解密程序了。我个人喜欢用Python,因为cryptography库用起来很顺手。
核心逻辑如下:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
import struct, os
def decrypt_file(encrypted_path, private_key_path, output_dir):
# 1. 加载RSA私钥
with open(private_key_path, 'rb') as f:
private_key = serialization.load_pem_private_key(
f.read(),
password=None
)
# 2. 读取加密文件
with open(encrypted_path, 'rb') as f:
data = f.read()
# 3. 解析头部
magic = struct.unpack('<I', data[0:4])[0]
if magic != 0xDEADBEEF:
print(f"无效的魔数: {encrypted_path}")
return
orig_size = struct.unpack('<I', data[4:8])[0]
encrypted_key = data[8:264] # 256字节的RSA加密块
encrypted_content = data[264:]
# 4. 解密AES密钥和IV
aes_key_iv = private_key.decrypt(
encrypted_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
aes_key = aes_key_iv[:32]
aes_iv = aes_key_iv[32:48]
# 5. AES解密
cipher = Cipher(algorithms.AES(aes_key), modes.CBC(aes_iv))
decryptor = cipher.decryptor()
decrypted_padded = decryptor.update(encrypted_content) + decryptor.finalize()
# 6. 去除PKCS7填充,恢复原始大小
decrypted = decrypted_padded[:orig_size]
# 7. 写回文件
output_path = os.path.join(output_dir, os.path.basename(encrypted_path).replace('.encrypted', ''))
with open(output_path, 'wb') as f:
f.write(decrypted)
print(f"解密成功: {output_path}")
这段代码看起来不长,但每一步都有坑。我曾经在解析头部时搞错了字节序——样本用的是小端序,我默认用了大端序,结果解密出来的文件全是乱码。排查了整整两个小时才找到问题。
避坑指南:我曾经在解析头部时搞错了字节序——样本用的是小端序,我默认用了大端序,结果解密出来的文件全是乱码。排查了整整两个小时才找到问题。建议你在写解析代码时,先用010 Editor或者HxD看一眼原始二进制,确认字段布局。
自动化批量解密
单个文件解密没问题了,但受害者通常有成千上万个文件。我们需要批量处理。我写了一个简单的遍历脚本:
import os
import glob
def batch_decrypt(root_dir, private_key_path, output_dir):
pattern = os.path.join(root_dir, '**', '*.encrypted')
for encrypted_file in glob.glob(pattern, recursive=True):
try:
decrypt_file(encrypted_file, private_key_path, output_dir)
except Exception as e:
print(f"解密失败: {encrypted_file} - {str(e)}")
这里要注意异常处理。有些文件可能在加密过程中损坏了,或者被其他程序占用了。不要因为一个文件失败就中断整个流程。
知识体系总览
下面这张图概括了勒索软件分析的核心流程,从样本获取到解密工具开发,每一步都环环相扣:
你看,整个流程就像一条流水线。从样本获取开始,经过静态分析、动态调试,到加密逻辑还原和密钥提取,最后才是解密工具开发。每一步都依赖前一步的输出。我见过很多人一上来就写解密代码,结果连加密算法都没搞清楚,白白浪费时间。
写在最后
勒索软件分析,说白了就是一场“猫鼠游戏”。作者不断升级加密手段,我们不断寻找破解方法。但万变不离其宗——只要它还需要在受害者机器上执行,就一定会留下痕迹。密钥生成、内存中的明文密钥、文件操作模式……这些都是我们可以利用的突破口。
我个人觉得,做这行最重要的不是技术有多牛,而是耐心和细心。一个字节的偏移错误,一个填充模式的误解,都可能导致解密失败。嗯,慢慢来,比较快。
公众号:蓝海资料掘金营,微信deep3321