解析2025强网拟态决赛MimicWorld
文章详细分析了一道CTF逆向题,涉及PE文件解析、虚拟机保护、流加密和分组加密。通过提取VM字节码、使用LCG算法解密、反汇编虚拟机指令及破解TEA变种加密,最终获得flag{Sh4d0w_0f_Th3_M1m1c_K1ng!!}。 2025-12-15 05:56:51 Author: www.freebuf.com(查看原文) 阅读量:3 收藏

MimicWorld - 虚拟机保护与TEA加密的逆向分析

前言

本文详细分析一道综合性的CTF逆向题目,涉及PE文件解析、虚拟机保护、流加密、分组加密等多个技术点。题目难度适中,非常适合学习虚拟机逆向和加密算法分析。

文章将从零开始,逐步分析程序结构,解密字节码,理解虚拟机执行流程,最终破解加密算法获得flag。所有步骤都会详细说明技术原理,确保初学者也能跟随并理解。

一、初步侦察

1.1 文件基本信息

拿到题目后,首先查看文件类型:

file MimicWorld.exe

输出结果:

MimicWorld.exe: PE32+ executable (GUI) x86-64, for MS Windows, 6 sections

关键信息:

  • 文件格式:PE32+(64位Windows可执行文件)

  • 架构:x86-64

  • 节数量:6个

这说明我们面对的是一个标准的Windows 64位程序。

1.2 为什么不能直接运行?

在Linux环境下,我们无法直接运行这个Windows程序。虽然可以使用Wine等工具,但对于逆向分析来说,静态分析往往更加高效和安全。因此我们选择:

  1. 静态分析PE文件结构

  2. 提取关键数据

  3. 编写Python脚本模拟执行

这种方法的优点是:

  • 不需要Windows环境

  • 可以完全控制分析流程

  • 避免潜在的恶意行为

  • 便于自动化处理

1.3 字符串分析

使用strings命令查看程序中的可见字符串:

strings MimicWorld.exe | head -50

经过检查,程序中没有明显的flag或关键提示信息。这说明关键逻辑被隐藏或加密了,需要深入分析。

二、PE文件结构深度解析

2.1 PE文件格式概述

PE(Portable Executable)是Windows系统的可执行文件格式。理解PE结构对于逆向分析至关重要。

PE文件的基本结构:

+------------------+
| DOS Header       |  <- 兼容性头部
+------------------+
| DOS Stub         |  <- DOS存根程序
+------------------+
| PE Signature     |  <- "PE\0\0"标识
+------------------+
| File Header      |  <- COFF文件头
+------------------+
| Optional Header  |  <- 可选头(实际必需)
+------------------+
| Section Table    |  <- 节表
+------------------+
| Section 1 (.text)|  <- 代码段
+------------------+
| Section 2 (.data)|  <- 数据段
+------------------+
| Section 3 (.rdata)| <- 只读数据段
+------------------+
| ...              |
+------------------+

2.2 为什么需要手动解析PE文件?

在逆向分析中,我们经常需要提取程序中的特定数据。对于本题:

  1. 程序包含加密的虚拟机字节码

  2. 字节码存储在某个数据段中

  3. 我们需要找到并提取这些数据

通过IDA Pro等工具可以定位到数据的虚拟地址(Virtual Address),但要从文件中提取,需要将虚拟地址转换为文件偏移(File Offset)。

2.3 虚拟地址到文件偏移的转换

关键概念:

  • 虚拟地址(VA):程序加载到内存后的地址,例如 0x140042A80

  • 相对虚拟地址(RVA):相对于基地址的偏移,例如 0x42A80(减去基地址0x140000000)

  • 文件偏移(File Offset):数据在文件中的实际位置

转换公式:

File Offset = Section Raw Offset + (RVA - Section Virtual Address)

2.4 节表结构解析

节表是一个数组,每个条目40字节,描述一个节的信息:

struct SectionHeader {
    char Name[8];              // 节名称
    DWORD VirtualSize;         // 内存中的大小
    DWORD VirtualAddress;      // 内存中的RVA
    DWORD SizeOfRawData;       // 文件中的大小
    DWORD PointerToRawData;    // 文件中的偏移
    // ... 其他字段
};

2.5 编写数据提取脚本

现在我们可以编写Python脚本来提取VM字节码。通过静态分析工具,我们已知:

  • 目标数据的RVA:0x42A80

  • 数据大小:0x5407字节(21511字节)

完整的提取脚本:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
PE文件数据提取脚本
功能:从MimicWorld.exe中提取VM字节码
"""

def extract_vm_bytecode():
    # 1. 读取整个PE文件
    with open('MimicWorld.exe', 'rb') as f:
        pe_data = f.read()

    # 2. 读取PE头偏移
    # DOS头的0x3C偏移处存储PE头的位置
    pe_offset = int.from_bytes(pe_data[0x3C:0x3C+4], 'little')
    print(f"[+] PE头偏移: 0x{pe_offset:X}")

    # 3. 读取节表数量
    # PE头偏移+6处是节数量(2字节)
    number_of_sections = int.from_bytes(pe_data[pe_offset+6:pe_offset+8], 'little')
    print(f"[+] 节表数量: {number_of_sections}")

    # 4. 读取可选头大小
    # PE头偏移+20处是可选头大小(2字节)
    size_of_optional_header = int.from_bytes(pe_data[pe_offset+20:pe_offset+22], 'little')
    print(f"[+] 可选头大小: 0x{size_of_optional_header:X}")

    # 5. 计算节表开始位置
    # 节表在:PE签名(4) + 文件头(20) + 可选头之后
    section_table_offset = pe_offset + 24 + size_of_optional_header

    # 6. 目标数据信息
    target_rva = 0x42A80    # 目标RVA
    data_size = 0x5407       # 数据大小

    print(f"\n[+] 目标 RVA: 0x{target_rva:X}, 大小: 0x{data_size:X}")
    print("\n[+] 节表信息:")

    # 7. 遍历节表,查找包含目标RVA的节
    for i in range(number_of_sections):
        # 每个节表条目40字节
        section_offset = section_table_offset + i * 40

        # 读取节信息
        section_name = pe_data[section_offset:section_offset+8].rstrip(b'\x00').decode('ascii', errors='ignore')
        virtual_size = int.from_bytes(pe_data[section_offset+8:section_offset+12], 'little')
        virtual_address = int.from_bytes(pe_data[section_offset+12:section_offset+16], 'little')
        size_of_raw_data = int.from_bytes(pe_data[section_offset+16:section_offset+20], 'little')
        pointer_to_raw_data = int.from_bytes(pe_data[section_offset+20:section_offset+24], 'little')

        print(f"  [{i}] {section_name:8s} VirtAddr: 0x{virtual_address:08X} VirtSize: 0x{virtual_size:08X} "
              f"RawAddr: 0x{pointer_to_raw_data:08X} RawSize: 0x{size_of_raw_data:08X}")

        # 8. 检查目标RVA是否在这个节的范围内
        if virtual_address <= target_rva < virtual_address + virtual_size:
            # 9. 计算文件偏移
            file_offset = pointer_to_raw_data + (target_rva - virtual_address)

            print(f"\n[+] 找到目标节: {section_name}")
            print(f"[+] 计算过程:")
            print(f"    RVA: 0x{target_rva:X}")
            print(f"    节虚拟地址: 0x{virtual_address:X}")
            print(f"    节内偏移: 0x{target_rva - virtual_address:X}")
            print(f"    节文件偏移: 0x{pointer_to_raw_data:X}")
            print(f"    最终文件偏移: 0x{file_offset:X}")

            # 10. 提取数据
            vm_bytecode = pe_data[file_offset:file_offset+data_size]
            print(f"\n[+] 成功提取 {len(vm_bytecode)} 字节数据")

            # 11. 保存为十六进制字符串
            with open('vm_bytecode.hex', 'w') as f:
                f.write(vm_bytecode.hex())

            print(f"[+] 数据已保存到 vm_bytecode.hex")
            return vm_bytecode.hex()

    return None

if __name__ == '__main__':
    extract_vm_bytecode()

运行脚本:

python3 extract_data.py

输出结果:

[+] PE头偏移: 0x78
[+] 节表数量: 6
[+] 可选头大小: 0xF0

[+] 目标 RVA: 0x42A80, 大小: 0x5407

[+] 节表信息:
  [0] .text    VirtAddr: 0x00001000 VirtSize: 0x0003F956 RawAddr: 0x00000400 RawSize: 0x0003FA00
  [1] .rdata   VirtAddr: 0x00041000 VirtSize: 0x000161F0 RawAddr: 0x0003FE00 RawSize: 0x00016200

[+] 找到目标节: .rdata
[+] 文件偏移: 0x41880
[+] 成功提取 21511 字节数据

我们成功从.rdata(只读数据)节中提取了21511字节的数据。这段数据就是加密的VM字节码。

三、流加密解密 - LCG算法

3.1 加密的必要性

为什么程序要加密字节码?这是一种常见的代码保护技术:

  1. 防止静态分析:加密后的字节码无法直接理解

  2. 增加逆向难度:需要先理解加密算法才能分析VM

  3. 对抗反编译:即使提取数据也无法直接使用

3.2 LCG算法原理

通过静态分析程序的解密函数,我们识别出使用了LCG(Linear Congruential Generator)算法生成密钥流。

LCG是什么?

LCG是一种伪随机数生成器,广泛用于快速生成伪随机序列。其递推公式为:

X(n+1) = (a * X(n) + c) mod m

参数说明:

  • X(n):当前状态(种子)

  • a:乘数(multiplier)

  • c:增量(increment)

  • m:模数(modulus)

  • X(0):初始种子

本题使用的LCG参数:

通过逆向分析得到:

初始种子: 0xDEADBEEF
a = 1664525
c = 1013904223
m = 2^32(通过 & 0xFFFFFFFF 实现)

这些参数实际上是著名的Numerical Recipes中推荐的LCG参数,质量较好。

3.3 流加密原理

程序使用LCG生成的伪随机数作为密钥流,与原始数据进行XOR运算:

密文[i] = 明文[i] XOR LOBYTE(Seed[i])

其中:

  • LOBYTE(Seed[i]):取种子的最低字节(0-255)

  • XOR:异或运算

为什么用XOR?

XOR运算有一个重要特性:

A XOR B XOR B = A

这意味着加密和解密使用完全相同的算法,只需要用相同的密钥流再次XOR即可。

3.4 解密算法实现

完整的解密脚本:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
VM字节码解密脚本
使用LCG算法生成密钥流并解密
"""

import struct

def decrypt_vm_bytecode(hex_data):
    """
    解密VM字节码

    参数:
        hex_data: 十六进制字符串格式的加密数据

    返回:
        解密后的字节列表
    """
    print("[+] 开始解密VM字节码...")

    # 1. 将十六进制字符串转换为字节数组
    raw_bytes = bytes.fromhex(hex_data.replace(' ', '').replace('\n', ''))
    print(f"[+] 原始数据大小: {len(raw_bytes)} 字节")

    # 2. 初始化
    decrypted_code = []
    seed = 0xDEADBEEF  # 初始种子

    # 3. 逐字节解密
    for i, encrypted_byte in enumerate(raw_bytes):
        # 3.1 生成当前字节的密钥(取种子的最低字节)
        key_byte = seed & 0xFF

        # 3.2 解密:密文 XOR 密钥 = 明文
        decrypted_byte = (encrypted_byte ^ key_byte) & 0xFF
        decrypted_code.append(decrypted_byte)

        # 3.3 更新种子(LCG算法)
        seed = (1664525 * seed + 1013904223) & 0xFFFFFFFF

        # 3.4 每1000字节显示一次进度
        if (i + 1) % 1000 == 0:
            print(f"    已处理: {i+1}/{len(raw_bytes)} 字节")

    print(f"[+] 解密完成,得到 {len(decrypted_code)} 字节明文")

    return decrypted_code

算法步骤详解:

  1. 读取加密数据:将十六进制字符串转为字节数组

  2. 初始化种子:设置seed = 0xDEADBEEF

  3. 对每个字节:

    • 取种子的最低字节作为密钥

    • 使用XOR解密

    • 更新种子(LCG公式)

  4. 返回解密结果

四、虚拟机架构分析

4.1 什么是虚拟机保护?

虚拟机(VM)保护是一种代码混淆技术:

  1. 定义自定义指令集:不使用x86等真实指令

  2. 编写虚拟机解释器:在运行时解释执行虚拟指令

  3. 保护关键逻辑:将关键算法编译为虚拟指令

优点:

  • 大幅增加逆向难度

  • 隐藏真实算法逻辑

  • 可以实现代码变形

缺点:

  • 执行效率低

  • 增加程序体积

4.2 VM指令格式分析

通过分析解密后的字节码,我们发现每条指令固定为7字节:

+--------+--------+--------+--------------------+
| Opcode |   P1   |   P2   |    Immediate       |
| (1字节)| (1字节)| (1字节)|    (4字节)         |
+--------+--------+--------+--------------------+

字段说明:

  • Opcode:操作码,决定指令类型(0-11)

  • P1:参数1,通常是目标寄存器编号

  • P2:参数2,通常是源寄存器编号

  • Immediate:32位立即数,小端序存储

4.3 VM指令集定义

通过逆向分析VM解释器函数,识别出12种指令:

操作码助记符格式功能说明
0EXIT-退出虚拟机
1MOVRx, INPUT[Ry]从输入数组读取数据到寄存器
2MOVRx, imm加载立即数到寄存器
3MOVRx, Ry寄存器间复制
4XORRx, Ry寄存器间异或:Rx ^= Ry
5ADDRx, Ry寄存器间加法:Rx += Ry
6SHLRx, imm左移:Rx <<= imm
7SHRRx, imm右移:Rx >>= imm
8XORRx, imm立即数异或:Rx ^= imm
9ADDRx, imm立即数加法:Rx += imm
10CMPRx, imm比较(校验点):检查Rx是否等于imm
11INCRx自增:Rx++

4.4 反汇编器实现

编写VM反汇编器:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
VM指令反汇编器
将字节码转换为可读的汇编代码
"""

import struct

def disassemble_vm_code(decrypted_code, output_file):
    """
    反汇编VM指令

    参数:
        decrypted_code: 解密后的字节码列表
        output_file: 输出文件路径
    """
    print(f"[+] 开始反汇编,结果将保存到: {output_file}")

    with open(output_file, "w", encoding="utf-8") as f:
        # 写入文件头
        f.write(";; MimicWorld VM Disassembly\n")
        f.write(";; " + "=" * 58 + "\n")
        f.write(";; 指令格式: [地址] 操作码 操作数\n")
        f.write(";; " + "=" * 58 + "\n\n")

        pc = 0  # 程序计数器(Program Counter)
        instruction_count = 0

        while pc < len(decrypted_code):
            # 检查剩余长度是否足够一条指令
            if pc + 7 > len(decrypted_code):
                f.write(f"\n;; 警告: 剩余 {len(decrypted_code) - pc} 字节,不足7字节\n")
                break

            # 解析指令各字段
            opcode = decrypted_code[pc]
            p1 = decrypted_code[pc + 1]
            p2 = decrypted_code[pc + 2]

            # 解析4字节立即数(小端序)
            imm_bytes = bytes(decrypted_code[pc+3:pc+7])
            imm = struct.unpack('<I', imm_bytes)[0]

            # 构建反汇编代码
            addr_str = f"[0x{pc:04X}] "

            # 根据操作码生成对应的汇编指令
            if opcode == 0:
                asm_line = "EXIT       ; 程序正常退出"
                f.write(addr_str + asm_line + "\n")
                break  # 遇到EXIT指令停止

            elif opcode == 1:
                asm_line = f"MOV  R{p1}, INPUT[R{p2}]    ; 从输入读取"

            elif opcode == 2:
                asm_line = f"MOV  R{p1}, 0x{imm:X}"
                # 如果立即数看起来像常量,添加注释
                if imm in [0x1337CAFE, 0xDEADBEEF, 0x8BADF00D, 0xFEEDC0DE, 0x7FC3951D]:
                    asm_line += "    ; 魔数常量"

            elif opcode == 3:
                asm_line = f"MOV  R{p1}, R{p2}"

            elif opcode == 4:
                asm_line = f"XOR  R{p1}, R{p2}           ; R{p1} ^= R{p2}"

            elif opcode == 5:
                asm_line = f"ADD  R{p1}, R{p2}           ; R{p1} += R{p2}"

            elif opcode == 6:
                asm_line = f"SHL  R{p1}, 0x{imm:X}        ; R{p1} <<= {imm}"

            elif opcode == 7:
                asm_line = f"SHR  R{p1}, 0x{imm:X}        ; R{p1} >>= {imm}"

            elif opcode == 8:
                asm_line = f"XOR  R{p1}, 0x{imm:X}    ; R{p1} ^= 0x{imm:X}"

            elif opcode == 9:
                asm_line = f"ADD  R{p1}, 0x{imm:X}    ; R{p1} += 0x{imm:X}"

            elif opcode == 10:
                # CMP指令是关键的校验点
                asm_line = f"CMP  R{p1}, 0x{imm:08X}    ; *** 校验点 ***"

            elif opcode == 11:
                asm_line = f"INC  R{p1}               ; R{p1}++"

            else:
                asm_line = f"UNKNOWN_OP_{opcode}     ; 未知操作码"

            # 写入反汇编结果
            f.write(addr_str + asm_line + "\n")

            # PC前进到下一条指令
            pc += 7
            instruction_count += 1

        # 写入统计信息
        f.write(f"\n;; " + "=" * 58 + "\n")
        f.write(f";; 总指令数: {instruction_count}\n")
        f.write(f";; 字节码大小: {len(decrypted_code)} 字节\n")

    print(f"[+] 反汇编完成")
    print(f"[+] 总指令数: {instruction_count}")

运行反汇编器后,得到3072条VM指令。

4.5 VM执行流程分析

查看反汇编代码的开头部分:

[0x0000] MOV  R7, 0x0
[0x0007] MOV  R0, INPUT[R7]    ; 读取INPUT[0]
[0x000E] MOV  R7, 0x1
[0x0015] MOV  R7, INPUT[R7]    ; 读取INPUT[1]
[0x001C] SHL  R7, 0x8          ; 左移8位
[0x0023] ADD  R0, R7           ; 组合成16位
[0x002A] MOV  R7, 0x2
[0x0031] MOV  R7, INPUT[R7]    ; 读取INPUT[2]
[0x0038] SHL  R7, 0x10         ; 左移16位
[0x003F] ADD  R0, R7           ; 继续组合
[0x0046] MOV  R7, 0x3
[0x004D] MOV  R7, INPUT[R7]    ; 读取INPUT[3]
[0x0054] SHL  R7, 0x18         ; 左移24位
[0x005B] ADD  R0, R7           ; 完成32位整数组合

这段代码的功能:

将输入的前4个字节(INPUT[0]到INPUT[3])组合成一个32位整数存入R0,使用小端序:

R0 = INPUT[0] | (INPUT[1] << 8) | (INPUT[2] << 16) | (INPUT[3] << 24)

类似的代码将INPUT[4]到INPUT[7]组合成R1。

小端序(Little Endian)解释:

在小端序中,低位字节存储在低地址:

地址:   0    1    2    3
内容:  0x66 0x6C 0x61 0x67
整数:  0x67616C66 (即"flag"的ASCII码倒序)

五、TEA加密算法分析

5.1 识别加密模式

继续查看反汇编代码,发现大量重复的指令模式:

[0x00CB] MOV  R2, 0x0
[0x00D2] MOV  R3, 0x7FC3951D    ; DELTA常量
[0x00D9] ADD  R2, R3            ; R2 += DELTA

;; 第一组运算(处理R1,更新R0)
[0x00E0] MOV  R4, R1
[0x00E7] SHL  R4, 0x5           ; R4 = R1 << 5
[0x00EE] ADD  R4, 0x1337CAFE    ; R4 += 0x1337CAFE
[0x00F5] MOV  R5, R1
[0x00FC] ADD  R5, R2            ; R5 = R1 + R2
[0x0103] MOV  R6, R1
[0x010A] SHR  R6, 0x3           ; R6 = R1 >> 3
[0x0111] ADD  R6, 0xDEADBEEF    ; R6 += 0xDEADBEEF
[0x0118] XOR  R4, R5            ; R4 ^= R5
[0x011F] XOR  R4, R6            ; R4 ^= R6
[0x0126] ADD  R0, R4            ; R0 += R4

;; 第二组运算(处理R0,更新R1)
[0x012D] MOV  R4, R0
[0x0134] SHL  R4, 0x5           ; R4 = R0 << 5
[0x013B] ADD  R4, 0x8BADF00D    ; R4 += 0x8BADF00D
[0x0142] MOV  R5, R0
[0x0149] ADD  R5, R2            ; R5 = R0 + R2
[0x0150] MOV  R6, R0
[0x0157] SHR  R6, 0x3           ; R6 = R0 >> 3
[0x015E] ADD  R6, 0xFEEDC0DE    ; R6 += 0xFEEDC0DE
[0x0165] XOR  R4, R5            ; R4 ^= R5
[0x016C] XOR  R4, R6            ; R4 ^= R6
[0x0173] ADD  R1, R4            ; R1 += R4

;; 继续下一轮
[0x017A] ADD  R2, R3            ; R2 += DELTA
...

这种模式重复32次后,出现CMP指令。

5.2 TEA算法原理

通过分析这些指令的模式,识别出这是TEA(Tiny Encryption Algorithm)的变种。

标准TEA算法:

TEA是一种分组密码,特点是:

  • 分组大小:64位(8字节)

  • 密钥大小:128位(16字节)

  • 迭代轮数:通常32轮

  • 结构:Feistel网络

标准TEA加密伪代码:

void tea_encrypt(uint32_t v[2], uint32_t k[4]) {
    uint32_t v0 = v[0], v1 = v[1];
    uint32_t sum = 0;
    uint32_t delta = 0x9E3779B9;

    for (int i = 0; i < 32; i++) {
        sum += delta;
        v0 += ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]);
        v1 += ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]);
    }

    v[0] = v0;
    v[1] = v1;
}

5.3 本题的TEA变种

本题的实现有所不同:

  1. DELTA值不同:使用0x7FC3951D而非标准的0x9E3779B9

  2. 密钥嵌入代码:密钥以常量形式硬编码

  3. 移位量不同:使用<<5和>>3,而非<<4和>>5

加密函数F(处理R1,更新R0):

def F(v, k):
    """
    TEA变种的F函数
    v: 输入值(R1)
    k: 密钥(R2)
    """
    r4 = ((v << 5) + 0x1337CAFE) & 0xFFFFFFFF
    r5 = (v + k) & 0xFFFFFFFF
    r6 = ((v >> 3) + 0xDEADBEEF) & 0xFFFFFFFF
    return (r4 ^ r5 ^ r6) & 0xFFFFFFFF

加密函数G(处理R0,更新R1):

def G(v, k):
    """
    TEA变种的G函数
    v: 输入值(R0)
    k: 密钥(R2)
    """
    r4 = ((v << 5) + 0x8BADF00D) & 0xFFFFFFFF
    r5 = (v + k) & 0xFFFFFFFF
    r6 = ((v >> 3) + 0xFEEDC0DE) & 0xFFFFFFFF
    return (r4 ^ r5 ^ r6) & 0xFFFFFFFF

加密流程:

# 初始化
R0 = INPUT[0:3]组成的32位整数
R1 = INPUT[4:7]组成的32位整数
R2 = 0
DELTA = 0x7FC3951D

# 32轮加密
for round in range(1, 33):
    R2 = (R2 + DELTA) & 0xFFFFFFFF
    R0 = (R0 + F(R1, R2)) & 0xFFFFFFFF
    R1 = (R1 + G(R0, R2)) & 0xFFFFFFFF

5.4 提取校验值

搜索CMP指令,找到校验点:

grep "CMP" vm_disassembly.txt

结果:

[0x14F2] CMP  R0, 0xC1636AF0    ; *** 校验点 ***
[0x14F9] CMP  R1, 0x66011374    ; *** 校验点 ***
[0x29F2] CMP  R0, 0x7C1868BD    ; *** 校验点 ***
[0x29F9] CMP  R1, 0xBE23BA8D    ; *** 校验点 ***
[0x3EF2] CMP  R0, 0x9E9765E2    ; *** 校验点 ***
[0x3EF9] CMP  R1, 0x16C7A9B0    ; *** 校验点 ***
[0x53F2] CMP  R0, 0x70DB4A7E    ; *** 校验点 ***
[0x53F9] CMP  R1, 0xBEBE3372    ; *** 校验点 ***

分析:

  • 共有8个CMP指令,分为4组

  • 每组检查R0和R1的值

  • 这说明输入被分为4组,每组8字节

  • 总输入长度:4 × 8 = 32字节

4组密文:

组1: R0=0xC1636AF0, R1=0x66011374
组2: R0=0x7C1868BD, R1=0xBE23BA8D
组3: R0=0x9E9765E2, R1=0x16C7A9B0
组4: R0=0x70DB4A7E, R1=0xBEBE3372

六、解密算法推导

6.1 加密与解密的关系

对于TEA类算法,加密和解密是相反的过程。

加密过程:

for round = 1 to 32:
    k = round * DELTA
    R0 += F(R1, k)
    R1 += G(R0, k)  // 注意:这里用的是更新后的R0

解密推导:

要逆向这个过程,需要从最后一轮倒推回第一轮。

设加密后的值为(R0_enc, R1_enc),我们要求(R0_plain, R1_plain)。

关键观察:

在第i轮加密中:

  1. 先更新R0:R0_new = R0_old + F(R1_old, k)

  2. 再更新R1:R1_new = R1_old + G(R0_new, k)

解密时必须反向操作:

  1. 先撤销R1的更新:R1_old = R1_new - G(R0_new, k)

  2. 再撤销R0的更新:R0_old = R0_new - F(R1_old, k)

注意顺序和使用的值!

6.2 解密算法实现

完整的解密脚本:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
TEA变种解密算法
从密文恢复明文
"""

def F(v, k):
    """
    TEA变种的F函数
    所有运算都是32位无符号整数
    """
    r4 = ((v << 5) + 0x1337CAFE) & 0xFFFFFFFF
    r5 = (v + k) & 0xFFFFFFFF
    r6 = ((v >> 3) + 0xDEADBEEF) & 0xFFFFFFFF
    return (r4 ^ r5 ^ r6) & 0xFFFFFFFF


def G(v, k):
    """
    TEA变种的G函数
    所有运算都是32位无符号整数
    """
    r4 = ((v << 5) + 0x8BADF00D) & 0xFFFFFFFF
    r5 = (v + k) & 0xFFFFFFFF
    r6 = ((v >> 3) + 0xFEEDC0DE) & 0xFFFFFFFF
    return (r4 ^ r5 ^ r6) & 0xFFFFFFFF


def decrypt_block(r0_encrypted, r1_encrypted):
    """
    解密一个8字节数据块

    参数:
        r0_encrypted: 加密后的R0值
        r1_encrypted: 加密后的R1值

    返回:
        (r0_plain, r1_plain): 明文的R0和R1值
    """
    DELTA = 0x7FC3951D

    r0 = r0_encrypted
    r1 = r1_encrypted

    print(f"  加密值: R0=0x{r0:08X}, R1=0x{r1:08X}")

    # 从第32轮倒推回第1轮
    for round_num in range(32, 0, -1):
        # 计算当前轮的密钥
        k = (round_num * DELTA) & 0xFFFFFFFF

        # 步骤1: 撤销R1的更新
        # 加密时: R1_new = R1_old + G(R0_new, k)
        # 解密时: R1_old = R1_new - G(R0_new, k)
        r1 = (r1 - G(r0, k)) & 0xFFFFFFFF

        # 步骤2: 撤销R0的更新
        # 加密时: R0_new = R0_old + F(R1_old, k)
        # 解密时: R0_old = R0_new - F(R1_old, k)
        # 注意:这里使用的是步骤1中恢复的R1_old
        r0 = (r0 - F(r1, k)) & 0xFFFFFFFF

    print(f"  解密值: R0=0x{r0:08X}, R1=0x{r1:08X}")

    return r0, r1


def main():
    """
    解密所有数据块并拼接成flag
    """
    print("=" * 60)
    print("MimicWorld Flag 解密工具")
    print("=" * 60)
    print()

    # 从VM反汇编中提取的4组密文
    ciphertexts = [
        (0xC1636AF0, 0x66011374),
        (0x7C1868BD, 0xBE23BA8D),
        (0x9E9765E2, 0x16C7A9B0),
        (0x70DB4A7E, 0xBEBE3372),
    ]

    flag_parts = []

    print("[+] 开始解密4组数据块...\n")

    for i, (c0, c1) in enumerate(ciphertexts):
        print(f"[数据块 {i+1}/4]")

        # 解密当前块
        p0, p1 = decrypt_block(c0, c1)

        # 将32位整数转换为4字节(小端序)
        bytes0 = p0.to_bytes(4, 'little')
        bytes1 = p1.to_bytes(4, 'little')

        # 拼接成8字节
        plaintext_chunk = bytes0 + bytes1

        # 尝试解码为ASCII字符串
        try:
            plaintext_str = plaintext_chunk.decode('utf-8')
            print(f"  明文字符串: '{plaintext_str}'")
        except UnicodeDecodeError:
            print(f"  明文字节: {plaintext_chunk.hex()}")
            plaintext_str = plaintext_chunk.decode('latin-1')

        flag_parts.append(plaintext_str)
        print()

    # 拼接所有明文片段
    final_flag = "".join(flag_parts)

    print("=" * 60)
    print("[+] 解密完成!")
    print("=" * 60)
    print(f"\nFlag: {final_flag}\n")

    return final_flag


if __name__ == '__main__':
    main()

6.3 运行解密脚本

执行脚本:

python3 decrypt_flag.py

输出结果:

============================================================
MimicWorld Flag 解密工具
============================================================

[+] 开始解密4组数据块...

[数据块 1/4]
  加密值: R0=0xC1636AF0, R1=0x66011374
  解密值: R0=0x67616C66, R1=0x3468537B
  明文字符串: 'flag{Sh4'

[数据块 2/4]
  加密值: R0=0x7C1868BD, R1=0xBE23BA8D
  解密值: R0=0x5F773064, R1=0x545F6630
  明文字符串: 'd0w_0f_T'

[数据块 3/4]
  加密值: R0=0x9E9765E2, R1=0x16C7A9B0
  解密值: R0=0x4D5F3368, R1=0x63316D31
  明文字符串: 'h3_M1m1c'

[数据块 4/4]
  加密值: R0=0x70DB4A7E, R1=0xBEBE3372
  解密值: R0=0x6E314B5F, R1=0x7D212167
  明文字符串: '_K1ng!!}'

============================================================
[+] 解密完成!
============================================================

Flag: flag{Sh4d0w_0f_Th3_M1m1c_K1ng!!}

七、解密正确性验证

7.1 为什么需要验证?

解密得到的flag可能不正确,原因包括:

  • 解密算法有误

  • 密文提取错误

  • 参数设置不对

最可靠的验证方法是:将明文重新加密,检查是否得到相同的密文。

7.2 编写验证脚本

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Flag验证脚本
通过重新加密来验证解密结果的正确性
"""

def F(v, k):
    """TEA变种的F函数"""
    r4 = ((v << 5) + 0x1337CAFE) & 0xFFFFFFFF
    r5 = (v + k) & 0xFFFFFFFF
    r6 = ((v >> 3) + 0xDEADBEEF) & 0xFFFFFFFF
    return (r4 ^ r5 ^ r6) & 0xFFFFFFFF


def G(v, k):
    """TEA变种的G函数"""
    r4 = ((v << 5) + 0x8BADF00D) & 0xFFFFFFFF
    r5 = (v + k) & 0xFFFFFFFF
    r6 = ((v >> 3) + 0xFEEDC0DE) & 0xFFFFFFFF
    return (r4 ^ r5 ^ r6) & 0xFFFFFFFF


def encrypt_block(r0_plain, r1_plain):
    """
    加密一个8字节数据块

    参数:
        r0_plain: 明文的R0值
        r1_plain: 明文的R1值

    返回:
        (r0_encrypted, r1_encrypted): 加密后的R0和R1值
    """
    DELTA = 0x7FC3951D

    r0 = r0_plain
    r1 = r1_plain
    r2 = 0

    # 32轮加密
    for round_num in range(1, 33):
        r2 = (r2 + DELTA) & 0xFFFFFFFF
        r0 = (r0 + F(r1, r2)) & 0xFFFFFFFF
        r1 = (r1 + G(r0, r2)) & 0xFFFFFFFF

    return r0, r1


def main():
    """验证flag的正确性"""
    print("=" * 60)
    print("Flag正确性验证")
    print("=" * 60)
    print()

    # 解密得到的flag
    flag = "flag{Sh4d0w_0f_Th3_M1m1c_K1ng!!}"

    # 题目中的密文(期望值)
    expected_ciphertexts = [
        (0xC1636AF0, 0x66011374),
        (0x7C1868BD, 0xBE23BA8D),
        (0x9E9765E2, 0x16C7A9B0),
        (0x70DB4A7E, 0xBEBE3372),
    ]

    print(f"测试Flag: {flag}")
    print(f"Flag长度: {len(flag)} 字节\n")

    # 转换为字节
    flag_bytes = flag.encode('utf-8')

    if len(flag_bytes) != 32:
        print(f"[!] 错误: Flag长度应为32字节,实际{len(flag_bytes)}字节")
        return False

    print("[+] 开始验证...\n")

    all_match = True

    # 分4组验证
    for i in range(4):
        offset = i * 8
        chunk = flag_bytes[offset:offset+8]

        # 转换为两个32位整数(小端序)
        r0 = int.from_bytes(chunk[0:4], 'little')
        r1 = int.from_bytes(chunk[4:8], 'little')

        print(f"[组 {i+1}/4]")
        print(f"  明文: '{chunk.decode('utf-8')}'")
        print(f"  明文值: R0=0x{r0:08X}, R1=0x{r1:08X}")

        # 加密
        c0, c1 = encrypt_block(r0, r1)

        print(f"  加密结果: R0=0x{c0:08X}, R1=0x{c1:08X}")
        print(f"  期望密文: R0=0x{expected_ciphertexts[i][0]:08X}, R1=0x{expected_ciphertexts[i][1]:08X}")

        # 比较
        if c0 == expected_ciphertexts[i][0] and c1 == expected_ciphertexts[i][1]:
            print(f"  [验证] 匹配成功!\n")
        else:
            print(f"  [验证] 匹配失败!\n")
            all_match = False

    print("=" * 60)
    if all_match:
        print("[验证结果] 所有测试通过,Flag完全正确!")
        print(f"\n正确的Flag: {flag}")
    else:
        print("[验证结果] 验证失败,Flag不正确!")
    print("=" * 60)

    return all_match


if __name__ == '__main__':
    main()

7.3 验证结果

运行验证脚本:

python3 verify_flag.py

输出:

============================================================
Flag正确性验证
============================================================

测试Flag: flag{Sh4d0w_0f_Th3_M1m1c_K1ng!!}
Flag长度: 32 字节

[+] 开始验证...

[组 1/4]
  明文: 'flag{Sh4'
  明文值: R0=0x67616C66, R1=0x3468537B
  加密结果: R0=0xC1636AF0, R1=0x66011374
  期望密文: R0=0xC1636AF0, R1=0x66011374
  [验证] 匹配成功!

[组 2/4]
  明文: 'd0w_0f_T'
  明文值: R0=0x5F773064, R1=0x545F6630
  加密结果: R0=0x7C1868BD, R1=0xBE23BA8D
  期望密文: R0=0x7C1868BD, R1=0xBE23BA8D
  [验证] 匹配成功!

[组 3/4]
  明文: 'h3_M1m1c'
  明文值: R0=0x4D5F3368, R1=0x63316D31
  加密结果: R0=0x9E9765E2, R1=0x16C7A9B0
  期望密文: R0=0x9E9765E2, R1=0x16C7A9B0
  [验证] 匹配成功!

[组 4/4]
  明文: '_K1ng!!}'
  明文值: R0=0x6E314B5F, R1=0x7D212167
  加密结果: R0=0x70DB4A7E, R1=0xBEBE3372
  期望密文: R0=0x70DB4A7E, R1=0xBEBE3372
  [验证] 匹配成功!

============================================================
[验证结果] 所有测试通过,Flag完全正确!

正确的Flag: flag{Sh4d0w_0f_Th3_M1m1c_K1ng!!}
============================================================

验证通过!这证明我们的解密算法完全正确。

八、完整解题流程总结

8.1 技术路线图

MimicWorld.exe
      |
      v
[1] PE文件分析
      |
      +---> 定位.rdata节
      +---> 提取VM字节码(21511字节)
      |
      v
[2] LCG解密
      |
      +---> 识别LCG算法
      +---> 确定参数(seed=0xDEADBEEF)
      +---> 生成密钥流并XOR解密
      |
      v
[3] VM反汇编
      |
      +---> 识别指令格式(7字节)
      +---> 定义指令集(12种操作)
      +---> 反汇编3072条指令
      |
      v
[4] 加密算法分析
      |
      +---> 识别TEA变种
      +---> 提取常量和参数
      +---> 理解加密流程
      |
      v
[5] 提取密文
      |
      +---> 定位CMP指令
      +---> 提取4组校验值
      |
      v
[6] 编写解密算法
      |
      +---> 推导逆向算法
      +---> 实现解密函数
      +---> 解密4组数据
      |
      v
[7] 验证结果
      |
      +---> 重新加密明文
      +---> 对比密文
      +---> 确认正确性
      |
      v
  flag{Sh4d0w_0f_Th3_M1m1c_K1ng!!}

8.2 关键技术点回顾

1. PE文件结构

  • DOS头、PE头、节表的作用

  • RVA到文件偏移的转换公式

  • 如何手动解析二进制文件

2. LCG算法

  • 线性同余生成器的数学原理

  • 为什么常用于流加密

  • XOR加密的对称性

3. 虚拟机保护

  • VM保护的原理和目的

  • 自定义指令集的设计

  • 如何反汇编虚拟机代码

4. TEA加密算法

  • Feistel网络结构

  • 迭代加密的思想

  • 如何逆向Feistel结构

5. 逆向工程思维

  • 从整体到局部的分析方法

  • 识别算法特征的技巧

  • 验证结果的重要性

8.3 工具和脚本

解题过程中编写的脚本:

  1. extract_data.py- PE文件数据提取

  2. decrypt_and_disasm.py- LCG解密和VM反汇编

  3. decrypt_flag.py- TEA解密获取flag

  4. verify_flag.py- 加密验证flag正确性

所有脚本都是纯Python实现,无需额外依赖。

8.4 最终答案

flag{Sh4d0w_0f_Th3_M1m1c_K1ng!!}

九、学习建议与扩展

9.1 前置知识

要完成这类题目,需要掌握:

  1. 二进制文件格式

    • PE、ELF等可执行文件格式

    • 文件头、节表等结构

    • 虚拟内存映射

  2. 密码学基础

    • 对称加密(分组密码、流密码)

    • 常见算法(DES、AES、TEA等)

    • 加密模式(ECB、CBC等)

  3. 编程能力

    • Python字节操作

    • 位运算(AND、OR、XOR、移位)

    • 大小端序处理

  4. 逆向工程

    • IDA Pro、Ghidra等工具使用

    • 汇编语言基础

    • 控制流分析

9.2 练习建议

  1. 从简单开始

    • 先做基础的XOR加密题

    • 逐步过渡到复杂算法

  2. 理解原理

    • 不要只记脚本模板

    • 深入理解算法原理

  3. 动手实践

    • 自己编写所有脚本

    • 尝试优化和改进

  4. 建立知识库

    • 整理常见算法特征

    • 记录解题思路

9.3 相关题型

类似的CTF题目类型:

  1. VM保护变种

    • 不同指令集设计

    • 多层VM嵌套

    • 动态生成指令

  2. 加密算法变种

    • RC4、AES的修改版

    • 自定义Feistel结构

    • 混合多种算法

  3. 反调试技术

    • 时间检测

    • 调试器检测

    • 代码混淆

9.4 深入研究

想要深入学习可以:

  1. 阅读源码

    • VMProtect、Themida等商业保护

    • LLVM Obfuscator等开源项目

  2. 研究论文

    • 虚拟化保护相关论文

    • 代码混淆技术研究

  3. 实践项目

    • 实现简单的VM保护

    • 编写自动化分析工具

十、总结

本题综合考查了PE文件分析、流加密解密、虚拟机逆向、分组密码分析等多个知识点,是一道优秀的综合性逆向题目。

通过完整的分析过程,我们:

  1. 掌握了PE文件的手动解析方法

  2. 理解了LCG算法在流加密中的应用

  3. 学会了虚拟机保护的分析思路

  4. 熟悉了TEA加密算法及其变种

  5. 掌握了逆向工程的验证方法

最重要的是,整个过程展示了逆向分析的完整思路:从文件格式分析开始,逐层剥离保护,最终破解核心算法。这种系统性的分析方法,是逆向工程的核心能力。

希望本文能帮助读者理解虚拟机保护和加密算法的逆向分析方法,在CTF比赛和实际工作中有所收获。


文章来源: https://www.freebuf.com/articles/others-articles/462028.html
如有侵权请联系:admin#unsafe.sh