ai取代大脑,thinking代替思考👍

(其中可视化部分可用代码还原,不过还原与否其实没什么关系

重点关注有代码示例的,其他的能识别特征就好,主要还是要练)

RC4

一、 RC4 的核心逻辑

RC4 的加解密过程是完全一样的(因为它是基于异或运算 XOR)。它主要分为两个部分:

  1. KSA (Key Scheduling Algorithm) - 密钥调度算法:初始化一个 256 字节的数组(S 盒)。
  2. 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,请盯住以下 关键特征:

  1. 数字 256 (0x100):这是 RC4 最显著的标志。你会看到循环次数是 256,或者数组大小是 256。
  2. 初始化循环:看到一个从 0 到 255 递增赋值的操作(s[i] = i)。
  3. 取模运算 % 256:在汇编中,由于 256 是 2 的幂,取模运算通常被优化为 and eax, 0FFh(与 0xFF 进行位与运算)。
  4. 两次交换 (Swap):在两个循环中都有 s[i]s[j] 的值互换逻辑。
  5. 异或运算 (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 时,可以按照这个思路验证:

  1. 观察常量:搜寻代码里是否有 0x100。
  2. 寻找 Key:RC4 需要一个 Key,看看函数参数里有没有一个被循环使用的字符串。
  3. 动态调试:
    • 在 KSA 循环结束处下断点,查看内存中的 256 字节数组是否变成了乱序。
    • 记录异或前的字节,手动异或回去,看是否能看到有意义的字符串。

总结:RC4 就是 “初始化 0-255” -> “根据 Key 打乱” -> “生成随机序列并 XOR”。

RC4变种

在逆向工程中,你很难遇到一个“教科书般”的 RC4,因为为了对抗静态分析或简单的自动化脚本,开发者经常会对 RC4 进行魔改(Variants)。

理解这些变种,能让你在遇到“看起来像 RC4 但解不出来”的情况时,迅速定位问题。


1. 参数级变种:简单的“数字游戏”

这是最常见的改动,原理完全不变,只是修改了某些常量。

  • S 盒大小变化:标准的 S 盒是 256 字节(0x100)。有些变种会改用 512、1024 甚至更小。
    • 逆向特征:如果你在代码里看到循环不是 0..255,而是 0..5110x400,但逻辑和 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 脚本解不开:

  1. 记录常量:把代码里所有出现的数字记录下来(比如 0x100, 0xFF, 0xAA 等)。
  2. 提取 Key:找到初始化 S 盒的那个变量。
  3. 打印中间态:这是最强大招。
    • 使用动态调试(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()}")

为什么逆向时要这么写脚本?

关键点:

  1. 直接传入 S 盒 (initial_sbox): 这是逆向小白进阶的必备思路。很多时候程序会在 KSA 阶段做极其复杂的位运算或者查表,你没必要去还原那个算法。直接在内存里看结果,把打乱后的 256 字节复制出来丢进脚本,瞬间通杀所有 KSA 阶段的魔改。

  2. drop 参数: 很多恶意软件(如早期 Cobalt Strike 的配置解密)会使用 RC4-drop768。如果没有这个功能,你解出来的永远是乱码。

  3. API 替代方案: 如果你在 Windows 环境下写脚本,也可以调用原生 API。在 Python 中可以使用 ctypes 调用 SystemFunction032


进阶:如何快速从内存获取 S 盒?

在 x64dbg 或 IDA 中:

  1. 找到 KSA 循环结束后的位置。
  2. 在内存窗口找到那个 256 字节的数组(通常以 00 01 02... 开头,打乱后变得杂乱无章)。
  3. 右键 -> 复制 -> 十六进制 (Hex)。
  4. 在 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。这是为了将寄存器清零,不是加密。
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)

小结

  1. XOR 是加解密的灵魂:看到循环里的 xor 就要注意,如果是 xor index, key 则是多字节循环异或。
  2. Base64 是数据的外壳:看到位移操作和 64 字节长的字符串,优先怀疑 Base64。
  3. 逆向第一步:先看字符串窗口有没有 自定义索引表。

Z3

一、 Z3的用处

在传统逆向中,如果遇到一段复杂的逻辑: input[0] * 2 + input[1] / 3 == 0x55input[1] ^ input[2] == 0x20...(假设有 50 行这种逻辑)

小白的硬刚做法:手动推导数学公式,尝试写反向逻辑。这非常痛苦且容易出错。 大佬的降维打击:把这些逻辑直接翻译成 Z3 能听懂的语言,Z3 会在几秒钟内算出结果。

*

二、 Z3 的核心工作流程(三步走)

Z3 的脚本通常用 Python 编写,逻辑非常简单:

  1. 定义变量:告诉 Z3 哪些是未知数(比如你的 Flag 或输入)。
  2. 添加约束(Constraints):把你在 IDA 里看到的那些 if 条件、数学运算全都原封不动写进去。
  3. 求解:调用 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 在逆向中的常见场景

  1. Keygen(注册机)编写:当你分析出注册码的校验逻辑,但懒得写反向算法时。
  2. VM(虚拟机保护)逆向:很多复杂的混淆会将逻辑变成成千上万条算式,手动分析是不可能的,只能用 Z3。
  3. 符号执行:像 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. 给小白的避坑指南

  1. 位宽扩展 (ZeroExt / SignExt): 当一个 8位 的数和一个 32位 的数相加时,Z3 会报错。你需要用 ZeroExt(24, val8) 把 8 位无符号扩展成 32 位(32-8=24)。
  2. 常量处理: 如果你要定义一个固定值的 Z3 变量,使用 BitVecVal(0x123, 32)
  3. 不要在 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 6789 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:

  1. 修改初始常量:把 0x67452301 改成别的数字。这样标准工具(如 Python 的 hashlib)算出来的结果就对不上了。
  2. 修改 T 表:改动那 64 个正弦值常量。
  3. 改变逻辑函数:MD5 内部有四个函数 F, G, H, I(例如 (B & C) | (~B & D)),开发者可能会微调这些位运算逻辑。
  4. 结果处理:算出 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。

  • 0xA3B1BAC6
  • 0x56AA3350
  • 0x677D9197
  • 0xB27022DC
2. 固定参数 CK (Constant Key)

SM4 总共 32 轮,每轮都有一个对应的固定参数(共 32 个)。它们是通过某种规则计算出来的。

  • 前几个通常是:0x00070E15, 0x1C232A31, 0x383F464D ...
  • 特征:这些数很有规律,通常是步长为 7 的倍数增长(在字节层面)。

三、 算法结构特征

SM4 的结构非常规整。在 IDA Pro 的伪代码中,你会看到以下逻辑:

  1. 32 轮循环:

    for (i = 0; i < 32; i++) {
       // 这里的逻辑就是轮函数
       // 包含:非线性变换 (S盒) 和 线性变换 (位移+异或)
    }
  2. S 盒变换 (S-box): SM4 也有一个 256 字节的 S 盒。它和 AES 的 S 盒长得不一样。

    • SM4 S 盒的第一个字节通常是:0xD6
    • 你可以通过 IDA 插件 FindCrypt 自动识别这个表。
  3. 线性变换 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 时:

  1. 搜常量:搜索 0xA3B1BAC6。如果搜到了,恭喜你,基本实锤。
  2. 看循环:找找看有没有 for 循环到 32 的地方。
  3. 看位移:是否有 2, 10, 18, 24 这四个数字作为循环左移的位数。
  4. 解密思路:
    • 如果是标准 SM4,直接找个 Python 库(如 gmssl)尝试解密。
    • 如果解不出来,检查开发者是否修改了 S 盒或修改了 FK 初始常量。

小技巧:在逆向中,如果看到一组 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 的矩阵。每一轮(除了最后一轮)都包含以下四个步骤:

  1. AddRoundKey(轮密钥加):矩阵与轮密钥异或。
  2. SubBytes(字节代替):通过 S 盒 进行查表替换。
  3. ShiftRows(行移位):每一行进行循环左移(第0行不动,第1行移1位...)。
  4. 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?

  1. 看长度:输入输出都是 16 字节的倍数。
  2. 搜常量:搜索 0x637C777B(S盒)或 0x01020408(轮常数)。
  3. 看模式:
    • ECB:每 16 字节独立加密(最不安全,容易看出特征)。
    • CBC:需要一个 IV,每一块加密依赖前一块。
  4. 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 的升级版,它们的区别主要在于移位和异或的顺序:

  1. TEA:最原始版本,如上文代码。
  2. XTEA:为了修复 TEA 的安全缺陷,修改了密钥调度的顺序。
    • 识别点:sum 在对 v0 运算前增加,在对 v1 运算后增加,且移位逻辑更复杂。
  3. 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:

  1. 魔术常量:搜索到 0x9E3779B9
  2. 简单循环:循环次数通常是 32(或者是 0x20)。
  3. 数据类型:操作的数据通常是 两个 32 位整数(uint32_t)。
  4. 运算特征:大量的 << 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. 逆向实战中的“小白技巧”

  1. 数据块提取:TEA 系列处理的都是 32 位整数(DWORD)。如果你拿到的密文是字节流,记得先用 struct.unpack('<II', data) 转换成两个整数。
  2. 符号位问题:在 C/C++ 逆向中,如果看到 v0signed int,那么在 Python 脚本里可能需要处理符号转换,但大多数情况下直接按 无符号(Unsigned) 处理 & 0xFFFFFFFF 即可。
  3. 寻找 Delta:如果代码里没搜到 0x9E3779B9,但逻辑长得像 TEA,去看看有没有 0x61C88647
    • 为什么? 因为 0 - 0x9E3779B9 = 0x61C88647。有些开发者会使用减法来混淆。
  4. 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

  1. 交叉引用 (Xref):在 0x405000 上按 x,找到哪个函数引用了这个 S 盒。这个函数通常就是 AES_InitAES_Encrypt
  2. 定位 Key:在加密函数附近找参数。通常 Key 是一个 16/24/32 字节的长字符串。
  3. 编写脚本:
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

  1. 定位 Key:TEA 的 Key 是 16 字节,通常在循环开始前被赋值给 4 个变量。
  2. 提取 Key:从内存或 mov 指令中提取这 4 个 32 位整数。
  3. 编写脚本:直接套用我们上一节学的 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:

  1. 动态生成常量:不直接写 0x9E3779B9,而是写 0x9E3779B8 + 1,或者通过几个数算出来。
    • 对策:盯住循环和异或逻辑,不要只看数字。
  2. 查表加密/混淆:把常量存进加密后的资源里,运行后再解密出来。
    • 对策:动态调试。在程序运行起来后,在内存里手动搜索这些魔术常量。
  3. 魔改常量:把 0x9E3779B9 改成 0x12345678
    • 对策:这就是你之前学 Z3 的用武之地。不要管常量是什么,把逻辑丢进 Z3 自动解。

解题

下面按照最常见的 AES 和 TEA 两种情况,掌握如何从 IDA 的界面,一步步写出解密脚本。


场景一:FindCrypt 识别出 AES,如何写脚本?

假设 FindCrypt 提示 AES_Sbox found at 0x408000

第一步:提取 Key 和 IV(最关键!)
  1. 双击地址:双击 FindCrypt 提示的 0x408000,跳转到 S 盒位置。
  2. 查找引用 (Xref):选中 S 盒的首地址,按键盘 x
  3. 定位初始化函数:通常会有两个引用,一个是 KeyExpansion(密钥扩展),一个是 Encrypt/Decrypt。双击进入那个看起来代码比较短的函数(通常是密钥扩展)。
  4. 找参数:查看这个函数的参数。
    • 在 32 位程序中,参数通常通过 push 入栈。
    • 在 64 位程序中,看 RDI, RSI, RDX 等寄存器。
    • 你会看到类似 mov dword ptr [esp], offset Key_String 的指令。

假设你在 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(四个整数)
  1. 定位循环:双击常量地址跳转,按 x 找到引用代码。你会看到类似 sum += 0x9E3779B9 的汇编。
  2. 找 Key 数组:在循环指令上方,看哪个寄存器或者内存地址被用来参与异或运算。
    • 汇编特征:mov eax, [ebp+var_key] 或者 lea rdx, [Key_Array]
  3. 提取数值:
    • 注意!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 里找到比较指令(cmpmemcmp),提取那串目标 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

总结:解题脚本的“万能模板”

不管什么算法,解题脚本的结构永远是三段式:

  1. Imports:
    • AES/DES/RC4 -> from Crypto.Cipher import ...
    • MD5/SHA -> import hashlib
    • TEA/自写逻辑 -> import struct, ctypes
  2. Data Preparation (数据清洗):
    • Key, IV, Ciphertext 全部从 IDA 复制出来。
    • 重点:分清你是要把它们当成 bytes (字节流) 还是 int (整数)。
      • AES/RC4 用 bytes
      • TEA 用 int 数组。
  3. Execution (执行解密):
    • 标准库直接调 .decrypt()
    • 非标准库粘贴解密函数代码。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐