re算法入门(ai指北)
水水水水水
ai取代大脑,thinking代替思考👍
(其中可视化部分可用代码还原,不过还原与否其实没什么关系
重点关注有代码示例的,其他的能识别特征就好,主要还是要练)
RC4
一、 RC4 的核心逻辑
RC4 的加解密过程是完全一样的(因为它是基于异或运算 XOR)。它主要分为两个部分:
- KSA (Key Scheduling Algorithm) - 密钥调度算法:初始化一个 256 字节的数组(S 盒)。
- PRGA (Pseudo-Random Generation Algorithm) - 伪随机子密码生成算法:利用 S 盒生成随机流,并与明文异或。
二、 算法详解(原理 + 代码)
1. KSA 阶段(打乱 S 盒)
首先,我们会有一个长度为 256 的数组(通常称为 S),初始值是 0, 1, 2, ..., 255。 然后,根据你输入的密钥(Key),把这个数组打乱。
C 语言实现:
void rc4_init(unsigned char *s, unsigned char *key, int key_len) {
int i, j = 0;
unsigned char temp;
// 1. 填充 0-255
for (i = 0; i < 256; i++)
s[i] = i;
// 2. 根据密钥打乱 S 盒
for (i = 0; i < 256; i++) {
j = (j + s[i] + key[i % key_len]) % 256;
// 交换 s[i] 和 s[j]
temp = s[i];
s[i] = s[j];
s[j] = temp;
}
}
2. PRGA 阶段(生成密钥流并异或)
打乱之后,我们不再直接使用密钥,而是通过 S 盒不断生成“伪随机数”。将这些生成的数与明文进行 XOR(异或),就得到了密文。
C 语言实现:
void rc4_crypt(unsigned char *s, unsigned char *data, int data_len) {
int i = 0, j = 0, t, k;
unsigned char temp;
for (k = 0; k < data_len; k++) {
i = (i + 1) % 256;
j = (j + s[i]) % 256;
// 再次交换
temp = s[i];
s[i] = s[j];
s[j] = temp;
// 得到生成的密钥流字节
t = (s[i] + s[j]) % 256;
// 异或操作:明文 ^ 密钥流 = 密文
data[k] ^= s[t];
}
}
三、 逆向中的“特征识别”(重点!)
在逆向工具(如 IDA Pro, Ghidra)中,你看到的往往是汇编或伪代码。要一眼认出 RC4,请盯住以下 关键特征:
- 数字 256 (0x100):这是 RC4 最显著的标志。你会看到循环次数是 256,或者数组大小是 256。
- 初始化循环:看到一个从 0 到 255 递增赋值的操作(
s[i] = i)。 - 取模运算
% 256:在汇编中,由于 256 是 2 的幂,取模运算通常被优化为and eax, 0FFh(与 0xFF 进行位与运算)。 - 两次交换 (Swap):在两个循环中都有
s[i]和s[j]的值互换逻辑。 - 异或运算 (XOR):最后一定有一个明文与生成的随机值异或的指令。
四、 流程可视化
让我们通过图表来看看 S 盒是如何变化的:
{
"title": { "text": "RC4 状态变化示意图" },
"tooltip": {},
"legend": { "data": ["S盒索引值", "打乱后的可能值"] },
"xAxis": { "data": ["0", "1", "2", "...", "255"] },
"yAxis": {},
"series": [
{
"name": "初始 S 盒 (s[i]=i)",
"type": "line",
"data": [0, 1, 2, 128, 255]
},
{
"name": "KSA 后的 S 盒 (乱序)",
"type": "bar",
"data": [120, 45, 201, 33, 189]
}
]
}
五、 逆向实战建议
当你怀疑某个函数是 RC4 时,可以按照这个思路验证:
- 观察常量:搜寻代码里是否有 0x100。
- 寻找 Key:RC4 需要一个 Key,看看函数参数里有没有一个被循环使用的字符串。
- 动态调试:
- 在 KSA 循环结束处下断点,查看内存中的 256 字节数组是否变成了乱序。
- 记录异或前的字节,手动异或回去,看是否能看到有意义的字符串。
总结:RC4 就是 “初始化 0-255” -> “根据 Key 打乱” -> “生成随机序列并 XOR”。
RC4变种
在逆向工程中,你很难遇到一个“教科书般”的 RC4,因为为了对抗静态分析或简单的自动化脚本,开发者经常会对 RC4 进行魔改(Variants)。
理解这些变种,能让你在遇到“看起来像 RC4 但解不出来”的情况时,迅速定位问题。
1. 参数级变种:简单的“数字游戏”
这是最常见的改动,原理完全不变,只是修改了某些常量。
- S 盒大小变化:标准的 S 盒是 256 字节(
0x100)。有些变种会改用 512、1024 甚至更小。- 逆向特征:如果你在代码里看到循环不是
0..255,而是0..511或0x400,但逻辑和 RC4 一模一样,那就是改了盒子的尺寸。
- 逆向特征:如果你在代码里看到循环不是
- 初始化赋值变化:标准 RC4 是
S[i] = i。- 魔改方案:
S[i] = 255 - i或者S[i] = (i + key[0]) % 256。 - 逆向特征:盯住 KSA 阶段的第一个
for循环,看看它赋值给数组的值到底是什么。
- 魔改方案:
2. 逻辑级变种:增加复杂性
这些变种在标准算法的基础上增加了一些“小动作”。
RC4-dropN (丢弃前 N 字节)
由于标准 RC4 的前几个字节存在明显的统计学偏差,很多程序(如早期的 SSL)会生成前 N 个字节并直接扔掉,从第 N+1 个字节开始真正加密。
- 常见的 N:256, 768, 1024, 3072。
- 逆向特征:在调用加密函数之前或函数开头,有一个循环在空跑 PRGA,但不进行任何 XOR 操作。
RC4+ (RC4-plus)
这是对 RC4 的增强版,增加了更多的交换逻辑和移位操作。
- 关键差异:在 PRGA 阶段,计算
t = (S[i] + S[j]) % 256之后,还会进行复杂的位运算(如<<或>>)来重新计算输出字节。 - 逆向特征:看到
S[i],S[j]交换后,输出字节的生成逻辑里出现了大量<< 5,>> 3这种移位,那极有可能是 RC4+。
VMPC (Variably Modified Permutation Composition)
这是 RC4 的一个亲戚,修改了 j 的更新方式。
- 核心逻辑:标准 RC4 是
j = (j + S[i] + key) % 256;VMPC 可能是j = S[(j + S[i]) % 256]。 - 逆向特征:
j的索引不是直接加法得到的,而是通过 S 盒嵌套查找得到的(即S[S[...]])。
3. 系统 API 变种:隐藏的 RC4
有些 Windows 程序不会自己写 RC4 函数,而是调用系统未公开的 API。这是小白最容易漏掉的地方。
SystemFunction032/SystemFunction033: 这两个函数隐藏在Advapi32.dll中,它们本质上就是 标准的 RC4。- 特征:你在代码里找不到任何 XOR 或循环,但看到了对这两个函数的调用。
- 参数:通常是一个包含待加密数据和密钥的结构体。
4. 变种识别“套路图”
当你怀疑是 RC4 变种时,请按此顺序检查:
<svg width="600" height="250" viewBox="0 0 600 250" xmlns="http://www.w3.org/2000/svg">
<!-- 背景 -->
<rect width="600" height="250" fill="#f9f9f9" rx="10" />
<!-- 逻辑节点 -->
<rect x="50" y="30" width="100" height="40" fill="#4a90e2" rx="5" />
<text x="100" y="55" text-anchor="middle" fill="white" font-size="14">循环次数</text>
<rect x="200" y="30" width="100" height="40" fill="#4a90e2" rx="5" />
<text x="250" y="55" text-anchor="middle" fill="white" font-size="14">S盒初值</text>
<rect x="350" y="30" width="100" height="40" fill="#4a90e2" rx="5" />
<text x="400" y="55" text-anchor="middle" fill="white" font-size="14">PRGA跳过</text>
<rect x="500" y="30" width="80" height="40" fill="#e67e22" rx="5" />
<text x="540" y="55" text-anchor="middle" fill="white" font-size="14">系统API</text>
<!-- 对应说明 -->
<text x="100" y="100" text-anchor="middle" font-size="12" fill="#666">不是256? -> 变长RC4</text>
<text x="250" y="100" text-anchor="middle" font-size="12" fill="#666">不是 S[i]=i? -> 预处理魔改</text>
<text x="400" y="100" text-anchor="middle" font-size="12" fill="#666">空跑前N位? -> RC4-dropN</text>
<text x="540" y="100" text-anchor="middle" font-size="12" fill="#666">SystemFunction032</text>
<!-- 线条 -->
<path d="M100 70 V85" stroke="#ccc" fill="none" />
<path d="M250 70 V85" stroke="#ccc" fill="none" />
<path d="M400 70 V85" stroke="#ccc" fill="none" />
<path d="M540 70 V85" stroke="#ccc" fill="none" />
</svg>
五、 实战演练:小白如何应对?
如果你在 IDA 里看到一段很像 RC4 的代码,但用标准的 RC4 脚本解不开:
- 记录常量:把代码里所有出现的数字记录下来(比如
0x100,0xFF,0xAA等)。 - 提取 Key:找到初始化 S 盒的那个变量。
- 打印中间态:这是最强大招。
- 使用动态调试(x64dbg/OD),在 KSA 循环结束处(即打乱 S 盒后)记录下内存中这 256 个字节的值。
- 直接将这段乱序后的内存作为你解密脚本的“初始 S 盒”,跳过 KSA 阶段,看看能否解密。
总结:变种 RC4 虽然多,但关键就在于 “基于 S 盒交换” 和 “异或输出” 这两个核心。
RC4脚本
在逆向工程中,拥有一个灵活的脚本至关重要,因为你经常需要根据调试出的中间结果(比如导出的 S 盒)来直接解密。
下面我为你准备了一个 “逆向专用 RC4 工具脚本”。它不仅支持标准加解密,还针对逆向中常见的 “魔改”(如跳过前 N 字节、直接加载内存 S 盒)做了优化。
逆向专用 RC4 Python 脚本
class RC4:
def __init__(self, key: bytes = None, initial_sbox: list = None):
"""
:param key: 密钥 (bytes)
:param initial_sbox: 可选,如果你从内存中 dump 出了打乱后的 S 盒,可以直接传入
"""
self.s = list(range(256))
if initial_sbox:
self.s = initial_sbox
elif key:
self._ksa(key)
def _ksa(self, key: bytes):
"""密钥调度算法 (Key Scheduling Algorithm)"""
j = 0
for i in range(256):
j = (j + self.s[i] + key[i % len(key)]) % 256
self.s[i], self.s[j] = self.s[j], self.s[i]
def crypt(self, data: bytes, drop: int = 0) -> bytes:
"""
加解密核心逻辑
:param data: 待处理数据
:param drop: 变种特有 - 丢弃前 N 字节的密钥流 (RC4-dropN)
"""
out = []
i = 0
j = 0
s = self.s[:] # 复制一份 S 盒,防止多次调用污染状态
# 变种处理:跳过前 N 字节
for _ in range(drop):
i = (i + 1) % 256
j = (j + s[i]) % 256
s[i], s[j] = s[j], s[i]
# 正式加解密
for byte in data:
i = (i + 1) % 256
j = (j + s[i]) % 256
s[i], s[j] = s[j], s[i]
t = (s[i] + s[j]) % 256
k = s[t]
out.append(byte ^ k)
return bytes(out)
# --- 实战用法举例 ---
# 1. 标准模式
key = b"secret_key"
data = b"\x12\x34\x56\x78" # 假设这是密文
cipher = RC4(key=key)
print(f"标准解密: {cipher.crypt(data).hex()}")
# 2. 应对 RC4-drop768 变种 (常见于配置解密)
cipher_drop = RC4(key=key)
print(f"跳过768字节解密: {cipher_drop.crypt(data, drop=768).hex()}")
# 3. 终极大招:直接使用 Dump 出来的 S 盒
# 如果你在动态调试时,发现 KSA 逻辑被魔改得面目全非
# 那么直接在 KSA 结束处把内存里的 256 字节 S 盒拖出来
dumped_sbox = [0x55, 0x12, ...] # 长度为 256 的数组
cipher_custom = RC4(initial_sbox=dumped_sbox)
print(f"内存 S 盒解密: {cipher_custom.crypt(data).hex()}")
为什么逆向时要这么写脚本?
关键点:
-
直接传入 S 盒 (
initial_sbox): 这是逆向小白进阶的必备思路。很多时候程序会在 KSA 阶段做极其复杂的位运算或者查表,你没必要去还原那个算法。直接在内存里看结果,把打乱后的 256 字节复制出来丢进脚本,瞬间通杀所有 KSA 阶段的魔改。 -
drop参数: 很多恶意软件(如早期 Cobalt Strike 的配置解密)会使用 RC4-drop768。如果没有这个功能,你解出来的永远是乱码。 -
API 替代方案: 如果你在 Windows 环境下写脚本,也可以调用原生 API。在 Python 中可以使用
ctypes调用SystemFunction032。
进阶:如何快速从内存获取 S 盒?
在 x64dbg 或 IDA 中:
- 找到 KSA 循环结束后的位置。
- 在内存窗口找到那个 256 字节的数组(通常以
00 01 02...开头,打乱后变得杂乱无章)。 - 右键 -> 复制 -> 十六进制 (Hex)。
- 在 Python 中用
bytes.fromhex("...")配合list()即可转化为脚本需要的initial_sbox。
base64与xor
在逆向工程中,XOR 和 Base64 是出镜率最高的一对“黄金搭档”。它们通常不被称为复杂的算法,而是被视为数据处理的基石。
如果说 RC4 是你在森林里遇到的小径,那么 XOR 和 Base64 就是随处可见的草地。
一、 XOR(异或):加密的本质
在汇编中,XOR 指令极其常见。但在逆向中,我们要区分它是用于清零寄存器还是用于加解密。
1. 核心特性
- 自反性:
A ^ B = C,那么C ^ B = A。这意味着加解密逻辑完全相同。 - 特征识别:
- 单字节 XOR:
data[i] ^ 0x55。特征是循环中有一个固定的常数。 - 循环多字节 XOR:
data[i] ^ key[i % len]。特征是有一个 Key 数组和取模运算。 - 寄存器自异或:
xor eax, eax。这是为了将寄存器清零,不是加密。
- 单字节 XOR:
2. 逆向中的 XOR 识别
当你看到如下汇编代码时,就要警惕了:
lodsb ; 加载一个字节到 al
xor al, 0x42 ; 核心操作:与固定 Key 异或
stosb ; 写回内存
loop ... ; 循环
二、 Base64:编码的变色龙
Base64 严格来说不是加密,而是编码。它将 3 个字节的二进制数据转化为 4 个可打印字符。
1. 标准 Base64 的特征
- 索引表(Alphabet):
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ - 填充符:末尾可能出现的
=或==。 - 位运算逻辑:
- 大量的移位操作:
>> 2、<< 4、>> 4、<< 2。 - 与运算限制范围:
& 0x3F(因为 64 的索引范围是 0-63)。
- 大量的移位操作:
2. 3变4原理可视化
Base64 的核心是将 8 位一组的字节拆成 6 位一组:
{
"title": { "text": "Base64 编码原理 (3字节变4字符)" },
"tooltip": { "trigger": "item" },
"series": [
{
"type": "sankey",
"data": [
{ "name": "Byte 1 (8bit)" }, { "name": "Byte 2 (8bit)" }, { "name": "Byte 3 (8bit)" },
{ "name": "Group 1 (6bit)" }, { "name": "Group 2 (6bit)" }, { "name": "Group 3 (6bit)" }, { "name": "Group 4 (6bit)" }
],
"links": [
{ "source": "Byte 1 (8bit)", "target": "Group 1 (6bit)", "value": 6 },
{ "source": "Byte 1 (8bit)", "target": "Group 2 (6bit)", "value": 2 },
{ "source": "Byte 2 (8bit)", "target": "Group 2 (6bit)", "value": 4 },
{ "source": "Byte 2 (8bit)", "target": "Group 3 (6bit)", "value": 4 },
{ "source": "Byte 3 (8bit)", "target": "Group 3 (6bit)", "value": 2 },
{ "source": "Byte 3 (8bit)", "target": "Group 4 (6bit)", "value": 6 }
]
}
]
}
三、 重点:Base64 的魔改变种
在逆向中,直接调用标准 Base64 是没意义的,开发者最喜欢改两个地方:
1. 自定义索引表(Custom Alphabet)
这是最常见的魔改。开发者会把标准表里的字符打乱顺序,或者替换掉 + 和 /。
- 逆向对策:在 IDA 的
Strings窗口中搜索ABCDEF...。如果你发现一段长度为 64 的字符串,看起来很像索引表但顺序不对,那就是它了。 - 解密方法:直接用这个自定义表替换掉标准库里的表即可。
2. 索引偏移 / 异或
在查表之前,对计算出来的索引值(0-63)进行加减法或异或。
- 特征:在代码里看到
alphabet[index ^ 5]这种逻辑。
四、 实战:XOR + Base64 组合拳
很多恶意软件的配置信息是这样处理的:原始数据 -> XOR加密 -> Base64编码。
为什么? 因为 XOR 后的数据包含不可见字符,Base64 能把它们变成纯文本,方便在网络上传输或存在资源里。
逆向识别套路图:
<svg width="500" height="180" viewBox="0 0 500 180" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="10" width="480" height="160" fill="#f4f4f4" stroke="#ccc" rx="10"/>
<!-- Step 1 -->
<rect x="30" y="50" width="80" height="40" fill="#3498db" rx="5"/>
<text x="70" y="75" text-anchor="middle" fill="white" font-weight="bold">原始配置</text>
<path d="M110 70 L150 70" stroke="#666" stroke-width="2" marker-end="url(#arrow)"/>
<!-- Step 2 -->
<rect x="150" y="50" width="100" height="40" fill="#e74c3c" rx="5"/>
<text x="200" y="75" text-anchor="middle" fill="white" font-weight="bold">XOR 循环</text>
<text x="200" y="110" text-anchor="middle" font-size="12" fill="#666">核心特征: XOR AL, CL</text>
<path d="M250 70 L290 70" stroke="#666" stroke-width="2" marker-end="url(#arrow)"/>
<!-- Step 3 -->
<rect x="290" y="50" width="120" height="40" fill="#2ecc71" rx="5"/>
<text x="350" y="75" text-anchor="middle" fill="white" font-weight="bold">魔改 Base64</text>
<text x="350" y="110" text-anchor="middle" font-size="12" fill="#666">核心特征: 自定义码表</text>
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="0" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#666" />
</marker>
</defs>
</svg>
五、 快速处理脚本 (Python)
当你遇到自定义表的 Base64 时,不要手写解密逻辑,用 Python 的 str.maketrans 极速搞定:
import base64
def custom_base64_decode(data, custom_table):
# 标准 Base64 表
standard_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
# 创建映射表:将自定义表映射回标准表
trans = str.maketrans(custom_table, standard_table)
# 转换数据并解密
return base64.b64decode(data.translate(trans))
# 示例:假设逆向发现的索引表是反过来的
my_table = "zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA9876543210+/"
cipher_text = "..." # 目标密文
# 步骤 1: Base64 还原
temp_data = custom_base64_decode(cipher_text, my_table)
# 步骤 2: XOR 还原 (假设 Key 是 0x42)
final_data = bytes([b ^ 0x42 for b in temp_data])
print(final_data)
小结
- XOR 是加解密的灵魂:看到循环里的
xor就要注意,如果是xor index, key则是多字节循环异或。 - Base64 是数据的外壳:看到位移操作和 64 字节长的字符串,优先怀疑 Base64。
- 逆向第一步:先看字符串窗口有没有 自定义索引表。
Z3
一、 Z3的用处
在传统逆向中,如果遇到一段复杂的逻辑: input[0] * 2 + input[1] / 3 == 0x55input[1] ^ input[2] == 0x20...(假设有 50 行这种逻辑)
小白的硬刚做法:手动推导数学公式,尝试写反向逻辑。这非常痛苦且容易出错。 大佬的降维打击:把这些逻辑直接翻译成 Z3 能听懂的语言,Z3 会在几秒钟内算出结果。
*
二、 Z3 的核心工作流程(三步走)
Z3 的脚本通常用 Python 编写,逻辑非常简单:
- 定义变量:告诉 Z3 哪些是未知数(比如你的 Flag 或输入)。
- 添加约束(Constraints):把你在 IDA 里看到的那些
if条件、数学运算全都原封不动写进去。 - 求解:调用
check()检查是否有解,调用model()输出结果。
*
三、 实战演练:用 Z3 解一个简单的 Crackme
假设你在 IDA 中看到以下伪代码:
// 输入是一个 3 字节的数组 input
if (input[0] + input[1] == 100) {
if (input[1] * input[2] == 500) {
if ((input[0] ^ input[2]) == 10) {
printf("Success!");
}
}
}
使用 Z3 编写的 Python 脚本:
from z3 import *
# 1. 定义未知数 (8位无符号整数,对应 unsigned char)
input0 = BitVec('i0', 8)
input1 = BitVec('i1', 8)
input2 = BitVec('i2', 8)
# 2. 创建一个求解器容器
solver = Solver()
# 3. 添加约束条件 (直接照抄 IDA 里的逻辑)
solver.add(input0 + input1 == 100)
solver.add(input1 * input2 == 500)
solver.add(input0 ^ input2 == 10)
# 4. 求解
if solver.check() == sat: # sat 表示 "Satisfiable" (有解)
ans = solver.model()
print(ans)
else:
print("无解")
*
四、 Z3 在逆向中的常见场景
- Keygen(注册机)编写:当你分析出注册码的校验逻辑,但懒得写反向算法时。
- VM(虚拟机保护)逆向:很多复杂的混淆会将逻辑变成成千上万条算式,手动分析是不可能的,只能用 Z3。
- 符号执行:像 Angr 这种顶级逆向框架,其底层核心就是 Z3。
*
五、 Z3 的优势与局限性
优势:
- 不动脑子:只要能看懂汇编逻辑,不需要数学功底。
- 处理位运算极强:它原生支持 XOR、移位、溢出 等计算机特性(使用
BitVec)。
局限性:
- 路径爆炸:如果代码里有成千上万个分支,Z3 会跑得很慢。
- 不支持非线性运算:比如复杂的哈希算法(MD5, SHA256),Z3 是解不出来的(否则世界上的密码学就崩塌了)。
Z3:从c到python
将 IDA 里的 C 语言逻辑翻译成 Z3 脚本,本质上是一个“语义平移”的过程。你不是在编写一段“运行”的代码,而是在编写一段“描述”逻辑的代码。
为了让你听懂,我把这个过程拆解为 “数据类型转换”、“操作符对应” 和 “逻辑转换技巧” 三部分。
1. 数据类型的“精准对标”
在 C 语言中,变量有明确的位数(8位、32位等),这涉及到了溢出。Z3 的 BitVec(位向量)完美模拟了这一点。
| C 语言类型 | Z3 定义方式 | 说明 |
|---|---|---|
unsigned char / uint8_t |
BitVec('v', 8) |
8位,值域 0-255 |
unsigned int / uint32_t |
BitVec('v', 32) |
32位,值域 0-0xFFFFFFFF |
int (有符号) |
BitVec('v', 32) |
注意:定义一样,但在运算时要用特殊函数 |
char flag[10] |
[BitVec(f'v_{i}', 8) for i in range(10)] |
用列表生成式定义数组 |
2. 操作符的“无缝衔接”
大部分位运算在 Z3 中是直接支持的,但涉及到“有符号/无符号”的除法和右移时需要小心。
| 逻辑类型 | C 语言 | Z3 (Python) | 备注 |
|---|---|---|---|
| 算术 | +, -, * |
+, -, * |
自动处理溢出(如 255+1=0) |
| 位运算 | &, `\ |
, ^, ~` |
&, `\ |
| 移位 | <<, >> |
<<, >> |
>> 在 Z3 中默认为算术右移 |
| 无符号右移 | (unsigned)x >> n |
LShR(x, n) |
极其重要! 逆向中常见 |
| 无符号除法 | (unsigned)a / b |
UDiv(a, b) |
防止符号位干扰 |
| 无符号取模 | (unsigned)a % b |
URem(a, b) |
3. 逻辑转换核心技巧:从“运行”到“约束”
这是小白最容易卡住的地方。
(1) 处理循环
如果 IDA 里的逻辑是一个 for 循环,你在 Python 里也写一个 for 循环,但循环体内部不是在计算结果,而是在不断堆叠 solver.add()。
IDA 代码:
for (int i = 0; i < 5; i++) {
input[i] ^= 0x42;
res += input[i];
}
if (res == 0x1337) ...
Z3 翻译:
res = BitVecVal(0, 32) # 初始值必须是 Z3 的常量对象
for i in range(5):
temp = input[i] ^ 0x42 # 模拟中间计算
res = res + ZeroExt(24, temp) # 8位扩充到32位再加,防止溢出丢失
solver.add(res == 0x1337)
(2) 处理中间变量
在 C 语言中,变量的值会不断改变。在 Z3 中,如果你需要多次修改同一个变量,建议使用中间变量,或者不断覆盖 Python 的变量名。
IDA 代码:
a = a + 10;
a = a ^ 0x55;
if (a == 100) ...
Z3 翻译:
a_init = BitVec('a', 32)
a1 = a_init + 10
a2 = a1 ^ 0x55
solver.add(a2 == 100)
4. 一个完整的实战案例
假设 IDA 里的加密函数长这样(一个简单的自定义加密):
// 假设 input 长度为 4
void encrypt(unsigned char *input) {
for (int i = 0; i < 4; i++) {
input[i] = (input[i] << 3) | (input[i] >> 5); // 循环左移 3 位
input[i] ^= 0x22;
}
if (input[0] == 0xAA && input[1] == 0xBB && input[2] == 0xCC && input[3] == 0xDD)
printf("Correct!");
}
对应的 Z3 脚本翻译过程:
from z3 import *
# 1. 定义变量:我们要找的是 4 个字节的输入
input_bytes = [BitVec(f'i_{i}', 8) for i in range(4)]
s = Solver()
# 2. 翻译逻辑
for i in range(4):
# C语言:(input[i] << 3) | (input[i] >> 5)
# 注意:Z3中的 >> 是算术右移,无符号右移要用 LShR
rotated = (input_bytes[i] << 3) | LShR(input_bytes[i], 5)
# C语言:input[i] ^= 0x22
final_val = rotated ^ 0x22
# 3. 翻译判定条件 (if input[i] == ...)
target = [0xAA, 0xBB, 0xCC, 0xDD]
s.add(final_val == target[i])
# 4. 求解
if s.check() == sat:
m = s.model()
# 按照索引排序打印
result = [m[input_bytes[i]].as_long() for i in range(4)]
print("Found input:", [hex(x) for x in result])
else:
print("Unsolvable")
5. 给小白的避坑指南
- 位宽扩展 (
ZeroExt/SignExt): 当一个8位的数和一个32位的数相加时,Z3 会报错。你需要用ZeroExt(24, val8)把 8 位无符号扩展成 32 位(32-8=24)。 - 常量处理: 如果你要定义一个固定值的 Z3 变量,使用
BitVecVal(0x123, 32)。 - 不要在
solver.add()里写if: 如果你遇到if (x > 10) a = 5; else a = 2;这种逻辑,不能直接在 Z3 里写 Python 的if。 要用If(x > 10, a == 5, a == 2)。
总结:翻译的过程就是:盯着 IDA 的变量位宽 -> 选对 BitVec 大小 -> 照抄数学算式 -> 用 solver.add() 替代 if 判定。
MD5
学习 MD5 是逆向工程中非常重要的一环。在之前的 RC4、Base64 学习中,我们处理的都是“加解密”或“编码”,而 MD5 属于“哈希算法”(Hash Algorithm)。
在逆向中看到 MD5,你的第一反应不应该是“怎么解密它”,而是“它在校验什么”。
一、 核心概念:它是“指纹”,不是“锁”
MD5(Message-Digest Algorithm 5) 的本质是将任意长度的数据映射成一个 128 位(16 字节) 的固定长度散列值。
- 不可逆性:理论上你不能通过 MD5 值反推原始明文(除非暴力破解或查彩虹表)。
- 抗碰撞性:不同的输入很难产生相同的 MD5 值。
- 特征长度:在逆向中,如果你看到一个 32 位的十六进制字符串(如
5d41402abc4b2a76b9719d911017c592),那极大概率就是 MD5。
二、 逆向中的“指纹识别”:盯住魔术常量
在 IDA 或 x64dbg 中识别 MD5,不需要看懂复杂的数学位移,只需要找 初始化常量。MD5 内部有 4 个非常著名的 32 位魔术常量。
1. 核心 A、B、C、D 寄存器初值
当算法开始时,会给四个变量赋初始值。如果看到这四个数字,百分之百是 MD5:
0x67452301(A)0xEFCDAB89(B)0x98BADCFE(C)0x10325476(D)
小白提示:有时在内存里看,因为是小端序,它们显示为
01 23 45 67、89 AB CD EF。
2. T 表常量(Sine 表)
MD5 还有一个包含 64 个常量 的表(T[1] 到 T[64]),这些值是基于正弦函数计算出来的。如果你在反汇编代码里看到一串密密麻麻的十六进制数(如 0xd76aa478, 0xe8c7b756 等),这通常就是 MD5 的核心计算环节。
三、 算法流程可视化
MD5 将数据分成 512 位(64 字节) 的块进行处理。每一块都要经过 4 轮(Round) 运算,每轮 16 步,总共 64 步。
{
"title": { "text": "MD5 单步运算结构" },
"tooltip": {},
"series": [{
"type": "tree",
"data": [{
"name": "MD5 Step",
"children": [
{ "name": "A", "children": [{"name": "B + ((A + F(B,C,D) + M[i] + K[i]) <<< s)"}] },
{ "name": "B", "children": [{"name": "原来的 B"}] },
{ "name": "C", "children": [{"name": "原来的 B"}] },
{ "name": "D", "children": [{"name": "原来的 C"}] }
]
}],
"top": "18%",
"bottom": "14%",
"layout": "radial",
"symbolSize": 7,
"initialTreeDepth": 3
}]
}
四、 逆向中 MD5 的变种(魔改)
为了防止被直接识别,开发者经常会改动 MD5:
- 修改初始常量:把
0x67452301改成别的数字。这样标准工具(如 Python 的hashlib)算出来的结果就对不上了。 - 修改 T 表:改动那 64 个正弦值常量。
- 改变逻辑函数:MD5 内部有四个函数
F, G, H, I(例如(B & C) | (~B & D)),开发者可能会微调这些位运算逻辑。 - 结果处理:算出 16 字节后,再进行一次 XOR 或者位翻转。
五、 实战:遇到 MD5 怎么搞?
1. 快速识别工具
- IDA 插件:FindCrypt。它会自动扫描二进制文件中的特征常量(如上面提到的魔术数字),直接在函数名上标注
MD5_Init。 - PEID 的 KANAL 插件:同样能识别各种算法特征。
2. 动态调试思路
- Hook 关键点:在 MD5 计算函数的入口下断点,看它输入的是什么(是明文密码?还是机器码?)。
- 导出结果:在函数返回处看内存,MD5 的结果通常存放在一个 16 字节的缓冲区里。
3. Z3 能解 MD5 吗?
答案是:几乎不能。 虽然你学了 Z3,但哈希算法(MD5/SHA)的设计初衷就是为了产生极其复杂的非线性位运算。Z3 面对 64 步高强度的位移和逻辑嵌套,计算量会呈指数级爆炸。逆向中解 MD5 依然靠撞库(Cmd5.com)或爆破。
六、 总结与对比
| 特性 | RC4 | Base64 | MD5 |
|---|---|---|---|
| 类型 | 流加密(可逆) | 编码(可逆) | 哈希散列(不可逆) |
| 主要用途 | 数据通信加密 | 文本传输二进制 | 完整性校验、密码存储 |
| 关键特征 | 256字节S盒、0x100 | 64字节字符索引表 | 4个32位魔术常量 |
| 结果长度 | 与输入相同 | 比输入长约 1/3 | 固定 128 位 (16字节) |
小白进阶建议: 在逆向中,MD5 经常被用来验证注册码。比如:MD5(用户名 + 机器码 + 某个盐值) 是否等于你输入的 注册码。
SM4
SM4 是中国国家密码管理局发布的分组对称加密算法(原名 SMS4)。在国产软件、政务系统以及越来越多的金融设备中,它是标配。在逆向工程中,它被称为“国产版 AES”。
一、 SM4 的核心规格
在逆向中,先看它的“身板”:
- 分组长度:128 位(16 字节)。也就是说,它每次加密 16 字节的数据。
- 密钥长度:128 位(16 字节)。
- 迭代轮数:32 轮。这是识别它的重要特征,代码里通常有一个循环 32 次的操作。
二、 逆向中的“指纹”:魔术常量(最重要!)
识别 SM4 的最快方法就是看它的系统参数。SM4 算法定义了两组非常著名的常量,它们就像身份证号一样:
1. 系统参数 FK (Family Key)
这是在密钥扩展阶段使用的四个初始常量。你在内存或代码段中搜索这些 16 进制数,如果成对出现,基本就是 SM4。
0xA3B1BAC60x56AA33500x677D91970xB27022DC
2. 固定参数 CK (Constant Key)
SM4 总共 32 轮,每轮都有一个对应的固定参数(共 32 个)。它们是通过某种规则计算出来的。
- 前几个通常是:
0x00070E15,0x1C232A31,0x383F464D... - 特征:这些数很有规律,通常是步长为 7 的倍数增长(在字节层面)。
三、 算法结构特征
SM4 的结构非常规整。在 IDA Pro 的伪代码中,你会看到以下逻辑:
-
32 轮循环:
for (i = 0; i < 32; i++) { // 这里的逻辑就是轮函数 // 包含:非线性变换 (S盒) 和 线性变换 (位移+异或) } -
S 盒变换 (S-box): SM4 也有一个 256 字节的 S 盒。它和 AES 的 S 盒长得不一样。
- SM4 S 盒的第一个字节通常是:
0xD6。 - 你可以通过 IDA 插件
FindCrypt自动识别这个表。
- SM4 S 盒的第一个字节通常是:
-
线性变换 L: SM4 的线性变换公式非常固定:L(B) = B \oplus (B \lll 2) \oplus (B \lll 10) \oplus (B \lll 18) \oplus (B \lll 24)。 特征:在汇编或伪代码中,你会看到大量的循环左移 (
<<配合>>) 和 异或 (^) 操作,且位移量分别是 2, 10, 18, 24。
四、 SM4 流程可视化
{
"title": { "text": "SM4 加密流程简化图" },
"tooltip": {},
"series": [{
"type": "graph",
"layout": "none",
"symbolSize": 50,
"roam": true,
"edgeSymbol": ["circle", "arrow"],
"data": [
{ "name": "明文 (16字节)", "x": 0, "y": 0 },
{ "name": "32轮迭代", "x": 200, "y": 0 },
{ "name": "反序变换", "x": 400, "y": 0 },
{ "name": "密文 (16字节)", "x": 600, "y": 0 }
],
"links": [
{ "source": "明文 (16字节)", "target": "32轮迭代" },
{ "source": "32轮迭代", "target": "反序变换" },
{ "source": "反序变换", "target": "密文 (16字节)" }
],
"lineStyle": { "opacity": 0.9, "width": 2, "curveness": 0 }
}]
}
五、 SM4 与 AES 的逆向对比
由于两者很像,小白容易搞混。请记住以下区别:
| 特性 | AES | SM4 |
|---|---|---|
| 轮数 | 10 / 12 / 14 轮(常见) | 固定 32 轮 |
| S 盒第一个值 | 0x63 |
0xD6 |
| 核心变换 | 列混合 (MixColumns) 等多种操作 | 主要是循环左移 + 异或 |
| 结构 | SPN 结构 | 类似 Feistel 的迭代结构 |
六、 逆向实战建议
当你怀疑一个函数是 SM4 时:
- 搜常量:搜索
0xA3B1BAC6。如果搜到了,恭喜你,基本实锤。 - 看循环:找找看有没有
for循环到 32 的地方。 - 看位移:是否有
2, 10, 18, 24这四个数字作为循环左移的位数。 - 解密思路:
- 如果是标准 SM4,直接找个 Python 库(如
gmssl)尝试解密。 - 如果解不出来,检查开发者是否修改了 S 盒或修改了 FK 初始常量。
- 如果是标准 SM4,直接找个 Python 库(如
小技巧:在逆向中,如果看到一组 128 位的数据被分成了 4 个 32 位整数(unsigned int),并且在 32 轮内不断互相异或,这几乎就是 SM4 或其亲戚算法的通病。
AES
AES(Advanced Encryption Standard) 是逆向工程中最常遇到的对称加密算法。如果你在逆向一个程序时看到了加密逻辑,且它不是 RC4 或 SM4,那么 90% 的概率它是 AES。
AES 的识别度极高,因为它拥有非常庞大且固定的 “指纹”。
一、 逆向中的 AES 关键特征(指纹识别)
识别 AES,不需要看代码逻辑,只需要看 常量(Constants)。
1. S 盒 (Substitution Box)
这是 AES 最核心的非线性变换表。几乎所有的 AES 实现都会包含这个 256 字节的数组。
- 特征值:前几个字节通常是
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B...。 - 逆向技巧:在 IDA 中按
Shift+F12搜索十六进制字节流。如果你看到63 7C 77 7B,那它就是 AES。
2. 逆 S 盒 (Inverse S-Box)
用于解密。
- 特征值:前几个字节通常是
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36...。
3. 轮常数 (Rcon)
用于密钥扩展逻辑。
- 特征值:
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36。
4. 迭代轮数
根据密钥长度不同,循环次数也不同:
- AES-128: 10 轮
- AES-192: 12 轮
- AES-256: 14 轮
二、 AES 的四步操作 (每一轮都在干什么)
AES 处理的是 4x4 的矩阵。每一轮(除了最后一轮)都包含以下四个步骤:
- AddRoundKey(轮密钥加):矩阵与轮密钥异或。
- SubBytes(字节代替):通过 S 盒 进行查表替换。
- ShiftRows(行移位):每一行进行循环左移(第0行不动,第1行移1位...)。
- MixColumns(列混合):最复杂的数学运算,通过矩阵乘法打乱列。
{
"title": { "text": "AES 内部处理矩阵 (State)" },
"tooltip": {},
"xAxis": { "type": "category", "data": ["Col 0", "Col 1", "Col 2", "Col 3"] },
"yAxis": { "type": "category", "data": ["Row 3", "Row 2", "Row 1", "Row 0"] },
"series": [{
"type": "heatmap",
"data": [
[0,0, "S0,0"], [1,0, "S0,1"], [2,0, "S0,2"], [3,0, "S0,3"],
[0,1, "S1,0"], [1,1, "S1,1"], [2,1, "S1,2"], [3,1, "S1,3"],
[0,2, "S2,0"], [1,2, "S2,1"], [2,2, "S2,2"], [3,2, "S2,3"],
[0,3, "S3,0"], [1,3, "S3,1"], [2,3, "S3,2"], [3,3, "S3,3"]
],
"label": { "show": true }
}]
}
三、 代码示例
在逆向分析中,我们通常使用 Python 来编写解密脚本。目前最主流的库是 pycryptodome。
1. Python 解密脚本(标准 AES)
假设你在逆向中找到了 Key 和 IV(初始化向量),以及加密模式是 CBC。
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
# 1. 准备数据 (通常从逆向的内存或资源文件中提取)
# 这里的 key 必须是 16, 24 或 32 字节
key = b'this_is_a_key123'
iv = b'1234567890123456'
cipher_text = bytes.fromhex('a1b2c3d4...') # 假设的密文
def aes_decrypt(ciphertext, key, iv):
# 2. 创建加密器对象
# AES.MODE_CBC 是最常见的模式,还有 MODE_ECB, MODE_GCM 等
cipher = AES.new(key, AES.MODE_CBC, iv)
# 3. 解密并去填充 (AES 是分组加密,末尾通常会有 Padding)
try:
decrypted = cipher.decrypt(ciphertext)
return unpad(decrypted, AES.block_size)
except Exception as e:
return f"解密失败: {e}"
# print(aes_decrypt(cipher_text, key, iv))
2. “小白”理解 AES 的伪逻辑(识别算法用)
如果你在 IDA 中看到类似下面的逻辑,它就是 AES:
// 伪代码特征
void aes_encrypt_round(byte* state, byte* roundKey) {
sub_bytes(state); // 查 S 盒表
shift_rows(state); // 数组索引搬移
mix_columns(state); // 复杂的位移和异或组合
add_round_key(state, roundKey); // 异或
}
四、 逆向中的坑:T-Tables (查表优化)
这是小白最容易看懵的地方。为了运行速度,很多高效的 C++ 实现(如 OpenSSL)会将 SubBytes + ShiftRows + MixColumns 合并成 4 个巨大的表,称为 T-Tables。
- 特征:如果你在代码里没看到 S 盒,而是看到 4 个每个大小为 1024 字节(256 个 int) 的表,且运算全是
(Table[byte] ^ ...),这就是 AES 的 T-Table 实现。
五、 总结:遇到加密如何判断是 AES?
- 看长度:输入输出都是 16 字节的倍数。
- 搜常量:搜索
0x637C777B(S盒)或0x01020408(轮常数)。 - 看模式:
- ECB:每 16 字节独立加密(最不安全,容易看出特征)。
- CBC:需要一个 IV,每一块加密依赖前一块。
- Z3 能解吗?:绝对不能。AES 的设计就是为了防御数学求解,只能通过找 Key 来解密。
TEA
TEA (Tiny Encryption Algorithm) 是逆向工程中名副其实的“常客”。正如它的名字一样,它最大的特点就是极简——代码量极小,通常只有十几行,且不需要像 AES 那样庞大的 S 盒。
在恶意软件、游戏协议(如早期的 QQ 协议)和简单的 Keygen 中,TEA 的出镜率极高。
一、 逆向中的“指纹”:魔术常量
识别 TEA 算法,你只需要死死盯住一个数字:0x9E3779B9。
1. 为什么是这个数?
这个常量被称为 Delta。它是基于黄金分割比计算出来的(\frac{\sqrt{5}-1}{2} \times 2^{31})。
- 特征:在加密循环中,你会看到一个变量(通常叫
sum)不断累加或累减这个值。 - 变种:有时开发者会改变这个值,但
0x9E3779B9是最标准、最常见的。
2. 结构特征
- 分组长度:64 位(8 字节)。通常被拆成两个 32 位整数(
v0,v1)处理。 - 密钥长度:128 位(16 字节)。通常被拆成四个 32 位整数(
k0, k1, k2, k3)。 - 运算类型:只包含 加法 (
+)、异或 (^) 和 逻辑移位 (<<,>>)。
二、 TEA 的代码实现
1. 标准 C 语言实现
你在 IDA 伪代码中看到的 TEA 几乎长这样:
void encrypt (uint32_t* v, uint32_t* k) {
uint32_t v0 = v[0], v1 = v[1], sum = 0, i; /* 初始状态 */
uint32_t delta = 0x9e3779b9; /* 魔术常量 */
uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3]; /* 密钥 */
for (i = 0; i < 32; i++) { /* 通常迭代 32 轮 */
sum += delta;
v0 += ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
v1 += ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
}
v[0] = v0; v[1] = v1;
}
2. TEA 加密逻辑可视化
{
"title": { "text": "TEA 算法单轮变换示意" },
"tooltip": {},
"series": [
{
"type": "graph",
"layout": "none",
"symbolSize": 40,
"data": [
{ "name": "V0", "x": 100, "y": 100 },
{ "name": "V1", "x": 300, "y": 100 },
{ "name": "Sum", "x": 200, "y": 200 },
{ "name": "Key", "x": 200, "y": 50 }
],
"links": [
{ "source": "V1", "target": "V0", "label": { "show": true, "formatter": "Shift + XOR" } },
{ "source": "Sum", "target": "V0" },
{ "source": "Key", "target": "V0" },
{ "source": "V0", "target": "V1", "lineStyle": { "curveness": 0.2 } }
]
}
]
}
三、 TEA 家族的三个兄弟
逆向时,你可能会遇到 TEA 的升级版,它们的区别主要在于移位和异或的顺序:
- TEA:最原始版本,如上文代码。
- XTEA:为了修复 TEA 的安全缺陷,修改了密钥调度的顺序。
- 识别点:
sum在对v0运算前增加,在对v1运算后增加,且移位逻辑更复杂。
- 识别点:
- XXTEA:目前最安全的版本,支持变长数据块(不限于 8 字节)。
- 识别点:代码里通常有处理
n个字(words)的循环,且有一个复杂的MX宏定义。
- 识别点:代码里通常有处理
四、 逆向实战:如何写解密脚本?
因为 TEA 只使用基础运算且对称,它的解密过程就是完全反向:
- 加密是
sum += delta,解密就是sum = delta * 32然后sum -= delta。 - 加密是
v1 += ...,解密就是v1 -= ...。
Python 解密脚本示例:
import ctypes
def decrypt(v, k):
v0, v1 = ctypes.c_uint32(v[0]), ctypes.c_uint32(v[1])
k0, k1, k2, k3 = [ctypes.c_uint32(x) for x in k]
delta = 0x9E3779B9
# 32 轮加密,所以初始 sum 是 delta * 32
total_sum = ctypes.c_uint32(delta * 32)
for i in range(32):
# 先减 v1,再减 v0 (与加密顺序相反)
v1.value -= ((v0.value << 4) + k2.value) ^ (v0.value + total_sum.value) ^ ((v0.value >> 5) + k3.value)
v0.value -= ((v1.value << 4) + k0.value) ^ (v1.value + total_sum.value) ^ ((v1.value >> 5) + k1.value)
total_sum.value -= delta
return v0.value, v1.value
# 测试数据
encrypted_data = [0x12345678, 0x9abcdef0]
key = [1, 2, 3, 4]
print([hex(x) for x in decrypt(encrypted_data, key)])
五、 总结:小白如何快速识别 TEA?
当你在 IDA 中扫描函数时,如果看到以下画面,直接判定为 TEA:
- 魔术常量:搜索到
0x9E3779B9。 - 简单循环:循环次数通常是 32(或者是
0x20)。 - 数据类型:操作的数据通常是 两个 32 位整数(
uint32_t)。 - 运算特征:大量的
<< 4、>> 5和^堆叠在一起。
TEA 算法是逆向小白最容易通过“硬啃汇编”来复现的算法,因为它没有复杂的表格。
TEA家族
TEA 家族(TEA, XTEA, XXTEA)在逆向工程中非常常见。它们的共同点是:代码极简、使用位运算、有一个共同的魔术常量 0x9E3779B9。
在 Python 中实现这些算法时,最麻烦的是 Python 的整数是无限精度的,而 C 语言是 32 位溢出的。因此,我们需要使用 & 0xFFFFFFFF 来模拟 32 位无符号整数 的溢出行为。
1. TEA (Tiny Encryption Algorithm)
这是最原始的版本。它的特征是:加密过程分两次更新 v0 和 v1,每次更新都用到完整的 Key 数组。
import struct
def tea_encrypt(v, k):
v0, v1 = v[0], v[1]
s = 0
delta = 0x9e3779b9
for i in range(32):
s = (s + delta) & 0xFFFFFFFF
v0 = (v0 + (((v1 << 4) + k[0]) ^ (v1 + s) ^ ((v1 >> 5) + k[1]))) & 0xFFFFFFFF
v1 = (v1 + (((v0 << 4) + k[2]) ^ (v0 + s) ^ ((v0 >> 5) + k[3]))) & 0xFFFFFFFF
return [v0, v1]
def tea_decrypt(v, k):
v0, v1 = v[0], v[1]
delta = 0x9e3779b9
s = (delta * 32) & 0xFFFFFFFF # 32 轮累加后的结果
for i in range(32):
v1 = (v1 - (((v0 << 4) + k[2]) ^ (v0 + s) ^ ((v0 >> 5) + k[3]))) & 0xFFFFFFFF
v0 = (v0 - (((v1 << 4) + k[0]) ^ (v1 + s) ^ ((v1 >> 5) + k[1]))) & 0xFFFFFFFF
s = (s - delta) & 0xFFFFFFFF
return [v0, v1]
2. XTEA (eXtended TEA)
XTEA 是 TEA 的改进版,主要修改了 密钥表的设计。
- 逆向特征:你会发现
sum(即s) 的使用位置变了。在更新 v0 时,它用s & 3作为索引去查 Key 数组;而在更新 v1 前,s会先改变。
def xtea_encrypt(v, k, rounds=32):
v0, v1 = v[0], v[1]
s = 0
delta = 0x9e3779b9
for i in range(rounds):
# 更新 v0 时使用 sum
v0 = (v0 + ((((v1 << 4) ^ (v1 >> 5)) + v1) ^ (s + k[s & 3]))) & 0xFFFFFFFF
s = (s + delta) & 0xFFFFFFFF
# 更新 v1 时使用更新后的 sum
v1 = (v1 + ((((v0 << 4) ^ (v0 >> 5)) + v0) ^ (s + k[(s >> 11) & 3]))) & 0xFFFFFFFF
return [v0, v1]
def xtea_decrypt(v, k, rounds=32):
v0, v1 = v[0], v[1]
delta = 0x9e3779b9
s = (delta * rounds) & 0xFFFFFFFF
for i in range(rounds):
v1 = (v1 - ((((v0 << 4) ^ (v0 >> 5)) + v0) ^ (s + k[(s >> 11) & 3]))) & 0xFFFFFFFF
s = (s - delta) & 0xFFFFFFFF
v0 = (v0 - ((((v1 << 4) ^ (v1 >> 5)) + v1) ^ (s + k[s & 3]))) & 0xFFFFFFFF
return [v0, v1]
3. XXTEA (Corrected Block TEA)
这是目前最安全的 TEA 版本。
- 逆向特征:它不再局限于 8 字节(2 个 int),而是可以加密 任意长度 的数组。
- 代码表现:它通常有一个复杂的循环嵌套,并且包含一个名为
MX的宏(在 IDA 里表现为一长串复杂的位运算)。
def xxtea_encrypt(v, k):
n = len(v)
delta = 0x9e3779b9
rounds = 6 + 52 // n
s = 0
z = v[n-1]
for _ in range(rounds):
s = (s + delta) & 0xFFFFFFFF
e = (s >> 2) & 3
for p in range(n):
y = v[(p + 1) % n]
# MX 宏的逻辑实现
term = (((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((s ^ y) + (k[(p ^ e) & 3] ^ z)))
v[p] = (v[p] + term) & 0xFFFFFFFF
z = v[p]
return v
def xxtea_decrypt(v, k):
n = len(v)
delta = 0x9e3779b9
rounds = 6 + 52 // n
s = (rounds * delta) & 0xFFFFFFFF
y = v[0]
for _ in range(rounds):
e = (s >> 2) & 3
for p in range(n - 1, -1, -1):
z = v[(p - 1) % n]
term = (((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((s ^ y) + (k[(p ^ e) & 3] ^ z)))
v[p] = (v[p] - term) & 0xFFFFFFFF
y = v[p]
s = (s - delta) & 0xFFFFFFFF
return v
4. 逆向对比与结构图
为了让你更直观地分辨它们,我们可以看它们的结构差异:
<svg width="600" height="200" viewBox="0 0 600 200" xmlns="http://www.w3.org/2000/svg">
<!-- TEA -->
<rect x="20" y="20" width="150" height="120" fill="#e1f5fe" stroke="#01579b" rx="5"/>
<text x="95" y="45" text-anchor="middle" font-weight="bold">TEA</text>
<text x="35" y="75" font-size="12">固定 32 轮</text>
<text x="35" y="95" font-size="12">v0, v1 顺序更新</text>
<text x="35" y="115" font-size="12">Key 使用固定</text>
<!-- XTEA -->
<rect x="210" y="20" width="150" height="120" fill="#fff3e0" stroke="#e65100" rx="5"/>
<text x="285" y="45" text-anchor="middle" font-weight="bold">XTEA</text>
<text x="225" y="75" font-size="12">Key 选择依赖 sum</text>
<text x="225" y="95" font-size="12">交替更新 sum</text>
<text x="225" y="115" font-size="12">安全性更高</text>
<!-- XXTEA -->
<rect x="400" y="20" width="150" height="120" fill="#e8f5e9" stroke="#1b5e20" rx="5"/>
<text x="475" y="45" text-anchor="middle" font-weight="bold">XXTEA</text>
<text x="415" y="75" font-size="12">支持 n 个数据块</text>
<text x="415" y="95" font-size="12">引入 MX 复杂逻辑</text>
<text x="415" y="115" font-size="12">轮数随长度变化</text>
</svg>
5. 逆向实战中的“小白技巧”
- 数据块提取:TEA 系列处理的都是 32 位整数(DWORD)。如果你拿到的密文是字节流,记得先用
struct.unpack('<II', data)转换成两个整数。 - 符号位问题:在 C/C++ 逆向中,如果看到
v0是signed int,那么在 Python 脚本里可能需要处理符号转换,但大多数情况下直接按 无符号(Unsigned) 处理& 0xFFFFFFFF即可。 - 寻找 Delta:如果代码里没搜到
0x9E3779B9,但逻辑长得像 TEA,去看看有没有0x61C88647。- 为什么? 因为 0 - 0x9E3779B9 = 0x61C88647。有些开发者会使用减法来混淆。
- Z3 求解 TEA: 由于 TEA 只有简单的算术运算(没有 MD5 那种复杂的非线性),Z3 是可以秒杀 TEA 的。如果你觉得解密脚本写起来麻烦,直接把加密逻辑丢进 Z3 即可。
FindCrypt
在逆向工程中,FindCrypt(及其衍生版本如 FindCrypt2, FindCrypt-yara)是 IDA Pro 的“外挂级”神器。它能帮你跳过枯燥的逻辑分析,直接告诉你:“这一段就是 AES!”或“看,这是 TEA 的魔术常量!”
下面我教你如何安装、使用识别以及将识别结果转化为可用的脚本。
第一阶段:使用 FindCrypt 识别算法
1. 安装与运行
- 安装:将
findcrypt.py或插件文件放入 IDA 的plugins文件夹下。 - 运行:打开 IDA 加载好程序后,按下快捷键
Ctrl + Alt + F(或者在菜单栏Edit -> Plugins -> FindCrypt)。
2. 看懂扫描结果
运行后,IDA 会弹出一个窗口(Output Window 或单独的 Signatures Window),显示发现的常量:
| FindCrypt 提示名称 | 对应算法 | 逆向关注点 |
|---|---|---|
AES_Sbox |
AES | 关注 Key 和 IV 的位置 |
TEA_Magic / 0x9E3779B9 |
TEA 家族 | 关注 128 位密钥 (Key) |
MD5_Constants |
MD5 | 关注输入数据的来源 |
Base64_Table |
Base64 | 检查码表是否被修改 |
SM4_FK / SM4_CK |
SM4 | 国产对称加密特征 |
第二阶段:从识别到脚本的“三步跨越”
当你识别出算法后,最关键的任务是:找到 Key(密钥)。算法是公开的,但密钥是私有的。
案例一:识别到 AES
FindCrypt 提示: AES_Sbox at 0x405000
- 交叉引用 (Xref):在
0x405000上按x,找到哪个函数引用了这个 S 盒。这个函数通常就是AES_Init或AES_Encrypt。 - 定位 Key:在加密函数附近找参数。通常 Key 是一个 16/24/32 字节的长字符串。
- 编写脚本:
from Crypto.Cipher import AES
# 1. 填入你从 IDA 内存中导出的 Key 和 IV
key = b"1234567812345678"
iv = b"0000000000000000"
# 2. 识别模式:根据函数调用判断 (CBC 还是 ECB)
# 如果看到两个参数异或,通常是 CBC;如果没有,通常是 ECB
cipher = AES.new(key, AES.MODE_CBC, iv)
# 3. 填入密文并解密
ciphertext = bytes.fromhex("...")
print(cipher.decrypt(ciphertext))
案例二:识别到 TEA
FindCrypt 提示: 0x9E3779B9
- 定位 Key:TEA 的 Key 是 16 字节,通常在循环开始前被赋值给 4 个变量。
- 提取 Key:从内存或
mov指令中提取这 4 个 32 位整数。 - 编写脚本:直接套用我们上一节学的 TEA Python 脚本。
第三阶段:识别特征可视化(备忘录)
你可以参考这个图表来快速匹配 FindCrypt 的输出:
{
"title": { "text": "FindCrypt 常量匹配字典" },
"tooltip": { "trigger": "axis" },
"xAxis": { "type": "category", "data": ["AES", "MD5", "TEA", "SHA1", "SM4"] },
"yAxis": { "type": "value", "show": false },
"series": [{
"type": "bar",
"data": [
{ "value": 1, "name": "0x637C777B" },
{ "value": 1, "name": "0x67452301" },
{ "value": 1, "name": "0x9E3779B9" },
{ "value": 1, "name": "0x67452301" },
{ "value": 1, "name": "0xA3B1BAC6" }
],
"label": {
"show": true,
"position": "top",
"formatter": "{b}"
}
}]
}
第四阶段:小白必看——如果 FindCrypt 没搜到怎么办?
开发者也不是笨蛋,他们会通过以下手段绕过 FindCrypt:
- 动态生成常量:不直接写
0x9E3779B9,而是写0x9E3779B8 + 1,或者通过几个数算出来。- 对策:盯住循环和异或逻辑,不要只看数字。
- 查表加密/混淆:把常量存进加密后的资源里,运行后再解密出来。
- 对策:动态调试。在程序运行起来后,在内存里手动搜索这些魔术常量。
- 魔改常量:把
0x9E3779B9改成0x12345678。- 对策:这就是你之前学 Z3 的用武之地。不要管常量是什么,把逻辑丢进 Z3 自动解。
解题
下面按照最常见的 AES 和 TEA 两种情况,掌握如何从 IDA 的界面,一步步写出解密脚本。
场景一:FindCrypt 识别出 AES,如何写脚本?
假设 FindCrypt 提示 AES_Sbox found at 0x408000。
第一步:提取 Key 和 IV(最关键!)
- 双击地址:双击 FindCrypt 提示的
0x408000,跳转到 S 盒位置。 - 查找引用 (Xref):选中 S 盒的首地址,按键盘
x。 - 定位初始化函数:通常会有两个引用,一个是
KeyExpansion(密钥扩展),一个是Encrypt/Decrypt。双击进入那个看起来代码比较短的函数(通常是密钥扩展)。 - 找参数:查看这个函数的参数。
- 在 32 位程序中,参数通常通过
push入栈。 - 在 64 位程序中,看
RDI,RSI,RDX等寄存器。 - 你会看到类似
mov dword ptr [esp], offset Key_String的指令。
- 在 32 位程序中,参数通常通过
假设你在 IDA 里看到了这样的数据:
- Key: 在内存
0x602040处,看到字符串"SecretKey123456"(16字节)。 - IV: 在内存
0x602060处,看到十六进制数据00 01 02 ... 0F。 - 密文: 从文件中提取出来的一串 Hex。
第二步:编写 Python 脚本
对于 AES,千万不要自己写算法,直接用 pycryptodome 库。
# pip install pycryptodome
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
# 1. 填入从 IDA 提取的参数
# 注意:如果 Key 是字符串,转成 bytes;如果是 Hex,用 bytes.fromhex()
key = b"SecretKey123456" # 16字节 -> AES-128
iv = bytes.fromhex("000102030405060708090A0B0C0D0E0F") # 16字节 IV
# 密文 (从 Hex 窗口复制出来的)
ciphertext_hex = "4A8F... (很长一串)"
ciphertext = bytes.fromhex(ciphertext_hex)
# 2. 编写解密逻辑
try:
# 模式通常是 CBC (有IV) 或 ECB (无IV)
# 看到有 IV 变量,90% 是 CBC
cipher = AES.new(key, AES.MODE_CBC, iv)
# 3. 解密并去填充 (Unpad)
# AES 是块加密,最后一定有 Padding,不去掉可能会乱码
decrypted_data = unpad(cipher.decrypt(ciphertext), AES.block_size)
print("解密成功:", decrypted_data.decode('utf-8', errors='ignore'))
except Exception as e:
print("解密失败:", e)
# 如果报错 "Padding is incorrect",尝试换一下 Key,或者换模式 (ECB)
场景二:FindCrypt 识别出 TEA/XTEA,如何写脚本?
假设 FindCrypt 提示 0x9E3779B9。
第一步:提取 Key(四个整数)
- 定位循环:双击常量地址跳转,按
x找到引用代码。你会看到类似sum += 0x9E3779B9的汇编。 - 找 Key 数组:在循环指令上方,看哪个寄存器或者内存地址被用来参与异或运算。
- 汇编特征:
mov eax, [ebp+var_key]或者lea rdx, [Key_Array]。
- 汇编特征:
- 提取数值:
- 注意!TEA 的 Key 是 4 个 32 位整数。
- 如果 IDA 里显示的是字符串
"1234abcd...",你需要把它切分成0x34333231(小端序) 这样的整数。
假设你在 IDA 看到 Key 是内存里的 0x11223344, 0x55667788, 0x99AABBCC, 0xDDEEFF00。
第二步:编写 Python 脚本
TEA 没有标准库,必须复制粘贴解密函数(就是我们之前学的那一套),然后传入参数。
import struct
import ctypes
# --- 粘贴之前学的解密函数 (以标准 TEA 为例) ---
def decrypt_tea(v, k):
v0, v1 = ctypes.c_uint32(v[0]), ctypes.c_uint32(v[1])
k0, k1, k2, k3 = [ctypes.c_uint32(x) for x in k]
delta = 0x9E3779B9
total_sum = ctypes.c_uint32(delta * 32)
for i in range(32):
v1.value -= ((v0.value << 4) + k2.value) ^ (v0.value + total_sum.value) ^ ((v0.value >> 5) + k3.value)
v0.value -= ((v1.value << 4) + k0.value) ^ (v1.value + total_sum.value) ^ ((v1.value >> 5) + k1.value)
total_sum.value -= delta
return v0.value, v1.value
# --- 你的解题逻辑开始 ---
# 1. 填入从 IDA 提取的 Key (4个整数)
# 如果是字节流,用 struct.unpack('<4I', key_bytes) 转成整数
key = [0x11223344, 0x55667788, 0x99AABBCC, 0xDDEEFF00]
# 2. 填入密文
# 假设密文是 8 字节的 Hex String: "AABBCCDDEEFF0011"
cipher_hex = "AABBCCDDEEFF0011"
cipher_bytes = bytes.fromhex(cipher_hex)
# TEA 是 8 字节一组,处理时要拆分
v_list = struct.unpack('<2I', cipher_bytes) # 转成 [v0, v1]
# 3. 调用解密
decrypted_v = decrypt_tea(v_list, key)
# 4. 把结果转回字符串/字节
# '<2I' 表示两个 unsigned int,小端序
result_bytes = struct.pack('<2I', *decrypted_v)
print("解密结果(Hex):", result_bytes.hex())
print("解密结果(Str):", result_bytes)
场景三:FindCrypt 识别出 MD5,如何写脚本?
MD5 是哈希,不可逆。所以脚本通常不是用来解密,而是用来爆破或验证。
题目类型通常是: 输入 flag,程序计算 MD5(flag),然后跟一个硬编码的 Hash 比较。
第一步:提取目标 Hash
在 IDA 里找到比较指令(cmp 或 memcmp),提取那串目标 Hash 值。
第二步:编写 Python 爆破脚本
import hashlib
import itertools
import string
# 1. 从 IDA 拿到的目标 Hash
target_hash = "e10adc3949ba59abbe56e057f20f883e" # 例子 (对应 "123456")
# 2. 定义爆破字符集 (根据题目提示调整)
chars = string.digits + string.ascii_letters # 0-9, a-z, A-Z
# 3. 爆破逻辑 (假设 flag 长度是 4-6 位)
found = False
for length in range(4, 7):
print(f"正在尝试长度: {length}")
for guess in itertools.product(chars, repeat=length):
guess_str = "".join(guess)
# 计算 MD5
m = hashlib.md5()
m.update(guess_str.encode())
if m.hexdigest() == target_hash:
print(f"找到 Flag: {guess_str}")
found = True
break
if found: break
总结:解题脚本的“万能模板”
不管什么算法,解题脚本的结构永远是三段式:
- Imports:
- AES/DES/RC4 ->
from Crypto.Cipher import ... - MD5/SHA ->
import hashlib - TEA/自写逻辑 ->
import struct,ctypes
- AES/DES/RC4 ->
- Data Preparation (数据清洗):
- Key, IV, Ciphertext 全部从 IDA 复制出来。
- 重点:分清你是要把它们当成
bytes(字节流) 还是int(整数)。- AES/RC4 用
bytes。 - TEA 用
int数组。
- AES/RC4 用
- Execution (执行解密):
- 标准库直接调
.decrypt()。 - 非标准库粘贴解密函数代码。
- 标准库直接调
更多推荐


所有评论(0)