本文详细分析一道综合性的CTF逆向题目,涉及PE文件解析、虚拟机保护、流加密、分组加密等多个技术点。题目难度适中,非常适合学习虚拟机逆向和加密算法分析。
文章将从零开始,逐步分析程序结构,解密字节码,理解虚拟机执行流程,最终破解加密算法获得flag。所有步骤都会详细说明技术原理,确保初学者也能跟随并理解。
拿到题目后,首先查看文件类型:
file MimicWorld.exe
输出结果:
MimicWorld.exe: PE32+ executable (GUI) x86-64, for MS Windows, 6 sections
关键信息:
文件格式:PE32+(64位Windows可执行文件)
架构:x86-64
节数量:6个
这说明我们面对的是一个标准的Windows 64位程序。
在Linux环境下,我们无法直接运行这个Windows程序。虽然可以使用Wine等工具,但对于逆向分析来说,静态分析往往更加高效和安全。因此我们选择:
静态分析PE文件结构
提取关键数据
编写Python脚本模拟执行
这种方法的优点是:
不需要Windows环境
可以完全控制分析流程
避免潜在的恶意行为
便于自动化处理
使用strings命令查看程序中的可见字符串:
strings MimicWorld.exe | head -50
经过检查,程序中没有明显的flag或关键提示信息。这说明关键逻辑被隐藏或加密了,需要深入分析。
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)| <- 只读数据段
+------------------+
| ... |
+------------------+
在逆向分析中,我们经常需要提取程序中的特定数据。对于本题:
程序包含加密的虚拟机字节码
字节码存储在某个数据段中
我们需要找到并提取这些数据
通过IDA Pro等工具可以定位到数据的虚拟地址(Virtual Address),但要从文件中提取,需要将虚拟地址转换为文件偏移(File Offset)。
关键概念:
虚拟地址(VA):程序加载到内存后的地址,例如 0x140042A80
相对虚拟地址(RVA):相对于基地址的偏移,例如 0x42A80(减去基地址0x140000000)
文件偏移(File Offset):数据在文件中的实际位置
转换公式:
File Offset = Section Raw Offset + (RVA - Section Virtual Address)
节表是一个数组,每个条目40字节,描述一个节的信息:
struct SectionHeader {
char Name[8]; // 节名称
DWORD VirtualSize; // 内存中的大小
DWORD VirtualAddress; // 内存中的RVA
DWORD SizeOfRawData; // 文件中的大小
DWORD PointerToRawData; // 文件中的偏移
// ... 其他字段
};
现在我们可以编写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字节码。
为什么程序要加密字节码?这是一种常见的代码保护技术:
防止静态分析:加密后的字节码无法直接理解
增加逆向难度:需要先理解加密算法才能分析VM
对抗反编译:即使提取数据也无法直接使用
通过静态分析程序的解密函数,我们识别出使用了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参数,质量较好。
程序使用LCG生成的伪随机数作为密钥流,与原始数据进行XOR运算:
密文[i] = 明文[i] XOR LOBYTE(Seed[i])
其中:
LOBYTE(Seed[i]):取种子的最低字节(0-255)
XOR:异或运算
为什么用XOR?
XOR运算有一个重要特性:
A XOR B XOR B = A
这意味着加密和解密使用完全相同的算法,只需要用相同的密钥流再次XOR即可。
完整的解密脚本:
#!/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
算法步骤详解:
读取加密数据:将十六进制字符串转为字节数组
初始化种子:设置seed = 0xDEADBEEF
对每个字节:
取种子的最低字节作为密钥
使用XOR解密
更新种子(LCG公式)
返回解密结果
虚拟机(VM)保护是一种代码混淆技术:
定义自定义指令集:不使用x86等真实指令
编写虚拟机解释器:在运行时解释执行虚拟指令
保护关键逻辑:将关键算法编译为虚拟指令
优点:
大幅增加逆向难度
隐藏真实算法逻辑
可以实现代码变形
缺点:
执行效率低
增加程序体积
通过分析解密后的字节码,我们发现每条指令固定为7字节:
+--------+--------+--------+--------------------+
| Opcode | P1 | P2 | Immediate |
| (1字节)| (1字节)| (1字节)| (4字节) |
+--------+--------+--------+--------------------+
字段说明:
Opcode:操作码,决定指令类型(0-11)
P1:参数1,通常是目标寄存器编号
P2:参数2,通常是源寄存器编号
Immediate:32位立即数,小端序存储
通过逆向分析VM解释器函数,识别出12种指令:
| 操作码 | 助记符 | 格式 | 功能说明 |
|---|---|---|---|
| 0 | EXIT | - | 退出虚拟机 |
| 1 | MOV | Rx, INPUT[Ry] | 从输入数组读取数据到寄存器 |
| 2 | MOV | Rx, imm | 加载立即数到寄存器 |
| 3 | MOV | Rx, Ry | 寄存器间复制 |
| 4 | XOR | Rx, Ry | 寄存器间异或:Rx ^= Ry |
| 5 | ADD | Rx, Ry | 寄存器间加法:Rx += Ry |
| 6 | SHL | Rx, imm | 左移:Rx <<= imm |
| 7 | SHR | Rx, imm | 右移:Rx >>= imm |
| 8 | XOR | Rx, imm | 立即数异或:Rx ^= imm |
| 9 | ADD | Rx, imm | 立即数加法:Rx += imm |
| 10 | CMP | Rx, imm | 比较(校验点):检查Rx是否等于imm |
| 11 | INC | Rx | 自增:Rx++ |
编写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指令。
查看反汇编代码的开头部分:
[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码倒序)
继续查看反汇编代码,发现大量重复的指令模式:
[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指令。
通过分析这些指令的模式,识别出这是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;
}
本题的实现有所不同:
DELTA值不同:使用0x7FC3951D而非标准的0x9E3779B9
密钥嵌入代码:密钥以常量形式硬编码
移位量不同:使用<<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
搜索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
对于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轮加密中:
先更新R0:R0_new = R0_old + F(R1_old, k)
再更新R1:R1_new = R1_old + G(R0_new, k)
解密时必须反向操作:
先撤销R1的更新:R1_old = R1_new - G(R0_new, k)
再撤销R0的更新:R0_old = R0_new - F(R1_old, k)
注意顺序和使用的值!
完整的解密脚本:
#!/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()
执行脚本:
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!!}
解密得到的flag可能不正确,原因包括:
解密算法有误
密文提取错误
参数设置不对
最可靠的验证方法是:将明文重新加密,检查是否得到相同的密文。
#!/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()
运行验证脚本:
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!!}
============================================================
验证通过!这证明我们的解密算法完全正确。
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!!}
1. PE文件结构
DOS头、PE头、节表的作用
RVA到文件偏移的转换公式
如何手动解析二进制文件
2. LCG算法
线性同余生成器的数学原理
为什么常用于流加密
XOR加密的对称性
3. 虚拟机保护
VM保护的原理和目的
自定义指令集的设计
如何反汇编虚拟机代码
4. TEA加密算法
Feistel网络结构
迭代加密的思想
如何逆向Feistel结构
5. 逆向工程思维
从整体到局部的分析方法
识别算法特征的技巧
验证结果的重要性
解题过程中编写的脚本:
extract_data.py- PE文件数据提取
decrypt_and_disasm.py- LCG解密和VM反汇编
decrypt_flag.py- TEA解密获取flag
verify_flag.py- 加密验证flag正确性
所有脚本都是纯Python实现,无需额外依赖。
flag{Sh4d0w_0f_Th3_M1m1c_K1ng!!}
要完成这类题目,需要掌握:
二进制文件格式
PE、ELF等可执行文件格式
文件头、节表等结构
虚拟内存映射
密码学基础
对称加密(分组密码、流密码)
常见算法(DES、AES、TEA等)
加密模式(ECB、CBC等)
编程能力
Python字节操作
位运算(AND、OR、XOR、移位)
大小端序处理
逆向工程
IDA Pro、Ghidra等工具使用
汇编语言基础
控制流分析
从简单开始
先做基础的XOR加密题
逐步过渡到复杂算法
理解原理
不要只记脚本模板
深入理解算法原理
动手实践
自己编写所有脚本
尝试优化和改进
建立知识库
整理常见算法特征
记录解题思路
类似的CTF题目类型:
VM保护变种
不同指令集设计
多层VM嵌套
动态生成指令
加密算法变种
RC4、AES的修改版
自定义Feistel结构
混合多种算法
反调试技术
时间检测
调试器检测
代码混淆
想要深入学习可以:
阅读源码
VMProtect、Themida等商业保护
LLVM Obfuscator等开源项目
研究论文
虚拟化保护相关论文
代码混淆技术研究
实践项目
实现简单的VM保护
编写自动化分析工具
本题综合考查了PE文件分析、流加密解密、虚拟机逆向、分组密码分析等多个知识点,是一道优秀的综合性逆向题目。
通过完整的分析过程,我们:
掌握了PE文件的手动解析方法
理解了LCG算法在流加密中的应用
学会了虚拟机保护的分析思路
熟悉了TEA加密算法及其变种
掌握了逆向工程的验证方法
最重要的是,整个过程展示了逆向分析的完整思路:从文件格式分析开始,逐层剥离保护,最终破解核心算法。这种系统性的分析方法,是逆向工程的核心能力。
希望本文能帮助读者理解虚拟机保护和加密算法的逆向分析方法,在CTF比赛和实际工作中有所收获。