新春杯CTF逆向工程题目解析与教学文档
本教学文档旨在详细解析“新春杯CTF”逆向工程(RE)题目的解题思路、涉及的技术点和详细操作步骤。文档基于提供的文章内容整理,力求全面、深入,适合有一定逆向基础的学习者参考。
题目总览与资源
- 比赛名称:新春杯CTF
- 题目类型:逆向工程(Reverse Engineering)
- 题目打包文件:
逆向.zip - 文件下载链接:
https://pan.baidu.com/s/1p_XcsKWhNLwbiF0hkpSrcg?pwd=aaaa
题目共四道,难度和考察点递进。
第一题:签到
考察点:Python打包的Exe程序逆向处理流程。
核心步骤:
- 解包Exe:使用PyInstaller提取工具(
pyinstxtractor.py)对题目提供的exe文件进行解包,得到.pyc字节码文件。 - 环境配置:解包工具可能需要特定的Python环境(如3.11)。可使用Conda创建独立环境:
conda create -n py311 python=3.11 -y - 反编译PyC:使用
.pyc反编译工具(如pycdc.exe)或在线网站(例如https://tool.lu/pyc/)将.pyc文件反编译为Python源代码。 - 分析源码:反编译后的关键源码如下:
#!/usr/bin/env python # Version: Python 3.11 a = '53 44 50 43 53 45 43 7b 72 65 76 65 72 73 65 5f 71 69 61 6e 5f 64 61 6f 7d 0a' correct_flag = bytes.fromhex(a).decode('utf-8').strip() user_input = input('please input your flag: ') if user_input == correct_flag: print('great') return None print('wrong') #SDPCSEC{reverse_qian_dao} - 获取Flag:变量
a是以空格分隔的十六进制字符串。将其拼接并转换即可得到Flag。
Flag:SDPCSEC{reverse_qian_dao}
知识点:PyInstaller打包机制、.pyc文件结构、Python字节码反编译。
第二题:Ez_VM
考察点:C++编写的自定义虚拟机(VM)逆向分析。
程序逻辑概述:
main函数负责初始化虚拟机上下文(结构体vm_ctx_t)和指令码,然后调用verify函数进行校验。verify函数是虚拟机的解释器,循环执行指令码,根据执行结果设置标志位ok。- 最终根据
ok标志位输出成功或失败信息。
关键数据结构(还原后):
typedef struct vm_ctx_t
{
unsigned char *code_ptr; // +0x00,指令指针
int ip; // +0x08,指令索引
unsigned char regs[4]; // +0x0C,4个寄存器
char *input_ptr; // +0x10,输入指针
int in_pos; // +0x18,输入位置
unsigned char ok; // +0x1C,校验标志位
unsigned char _pad1D[3]; // +0x1D,填充
} vm_ctx_t;
逆向分析要点:
- 指令分析:
verify函数中的关键指令是0x20,其后跟两个字节[A][B],含义为:regs[A] ^= B。这是一个简单的异或操作指令。 - 动态验证:
main函数中复杂的代码实际上是在“填充”一段固定的指令序列(code_ptr指向的数据)。解题的关键是识别出这段指令序列,并模拟执行,得到用于异或比较的正确数据。 - 解题脚本:通过分析,可以提取出硬编码的指令数据(文中以
aWPvjL表示),并对每个字节异或0x11后得到Flag。
Flag:需要运行脚本获取。aWPvjL = (b"w}pvj" + bytes([0x27]) + b"#&(<(#)!<\"#&" + bytes([0x27]) + b"< %$ %l") assert len(aWPvjL) == 27 flag = bytes([b ^ 0x11 for b in aWPvjL]).decode("ascii") print(flag)
知识点:虚拟机逆向、结构体还原、自定义指令集分析、动态内存操作分析。
第三题:baby_rc4
考察点:UPX魔改壳与RC4流密码算法魔改识别。
解题步骤:
- 脱壳:题目使用了魔改的UPX壳,需要将壳头特征序列(例如
55 50 58 21)修复为标准UPX头,然后使用UPX官方工具或修改后的脱壳机进行脱壳。 - 识别算法:脱壳后分析主逻辑,识别出是RC4算法,但最后一步的加密/解密操作被修改。
- 标准RC4回顾:
- KSA(密钥调度算法):利用密钥
key对256字节的S盒进行初始置换。S = list(range(256)) j = 0 for i in range(256): j = (j + S[i] + key[i % len(key)]) & 0xFF S[i], S[j] = S[j], S[i] - PRGA(伪随机生成算法):生成密钥流。
i = j = 0 for each byte output: i = (i + 1) & 0xFF j = (j + S[i]) & 0xFF S[i], S[j] = S[j], S[i] t = (S[i] + S[j]) & 0xFF keystream_byte = S[t] - 加密/解密:
cipher_byte = plain_byte ^ keystream_byte。
- KSA(密钥调度算法):利用密钥
- 魔改点分析:题目中的魔改在于,最后不是用异或,而是用加法(
cipher_byte = (plain_byte + keystream_byte) & 0xFF)。因此解密时需对应使用减法(或加法的逆运算)。 - 编写解密脚本:
Flag:运行脚本后输出。key = b"happynewyear" cipher = bytes([0x19,0x9D,0xDF,0x8D,0xED,0x42,0xAB,0x52,0x7E,0x25,0xC8,0xD2,0x48,0x56,0xEA,0xAB,0x60,0x94,0xCF,0x7E,0x66,0x4D,0x1C,0xE2,0x62,0x21,0xC7,0xAE,0xFD,0x39]) # KSA (保持不变) S = list(range(256)) j = 0 for i in range(256): j = (j + S[i] + key[i % len(key)]) & 0xFF S[i], S[j] = S[j], S[i] # PRGA + 魔改解密 (plain = cipher - keystream) i = j = 0 plain = bytearray() for c in cipher: i = (i + 1) & 0xFF j = (j + S[i]) & 0xFF S[i], S[j] = S[j], S[i] k = S[(S[i] + S[j]) & 0xFF] plain.append((c - k) & 0xFF) # 注意:此处是减法 print(plain.decode())
知识点:UPX壳与魔改修复、RC4算法原理与识别、密码算法魔改分析。
第四题:WebAssembly
考察点:WebAssembly(Wasm)模块逆向、自定义加密算法(Feistel网络、xorshift64* PRNG)、哈希算法(FNV-1a)。
题目架构:
这是一个前端登录验证系统,包含HTML、JS和核心的Wasm模块。
index.html:用户界面,引入crypto-js.js(用于计算展示用的SHA1 ticket)和release.js(Wasm加载桥接)。release.wasm:核心校验逻辑,导出authenticate函数。- 流程:用户输入
user和pass-> JS调用Wasm的authenticate-> Wasm内部校验 -> 返回结果和消息(包含Flag)。
Wasm内部校验逻辑详解:
校验分为三个主要阶段,必须全部通过才能获取Flag。
阶段一:用户名验证(FNV-1a哈希)
- 使用32位FNV-1a哈希算法计算用户名的哈希值。
- 必须等于固定值
0x477DDD25。 - 算法实现:
uint32_t fnv1a32(const char *s) { uint32_t h = 2166136261u; // 0x811C9DC5 while (*s) { h ^= (uint8_t)(*s); h *= 16777619u; // 0x01000193 s++; } return h; } - 解题:需要暴力破解或逆向找到一个字符串,使其FNV-1a哈希等于
0x477DDD25。
阶段二:密码验证(Feistel网络变换)
- 密钥派生:使用用户名(上一步验证通过的)派生8个64位的轮密钥(
keys[0..7])。- 初始化一个64位状态
s = 0xC3A5C85C97CB3127。 - 遍历用户名每个字节,用特定公式更新
s。 - 循环8轮,每轮用
s经过变换并乘以常数得到keys[r]。
- 初始化一个64位状态
- Feistel加密:将用户输入的16字节密码视为一个128位的数据块,分为左右各8字节(
L0,R0)。- 进行8轮Feistel变换:
L_{i+1} = R_i,R_{i+1} = L_i ^ round_f(R_i, keys[i])。 round_f是一个自定义的轮函数,包含加、循环移位、异或、乘等操作。
- 进行8轮Feistel变换:
- 结果比对:经过8轮变换后,输出的16字节结果(
mixed)必须等于程序中硬编码的16字节TARGET常量。
- 解题:
- 方法A(正向):已知正确的
user,可以计算出keys。目标是找到一个16字节的pass,使得经过Feistel加密后等于TARGET。这需要逆向Feistel网络(实现feistel_inv_from_target函数)或暴力破解。 - 方法B(直接获取Flag):如果目的是直接拿到Flag而不破解密码,可以绕过此步骤,但需要理解后续的Flag生成逻辑。
- 方法A(正向):已知正确的
阶段三:Flag生成(流密码解密)
仅当阶段二验证通过(即mixed == TARGET)时执行。
- 生成种子(Seed):
seed = mixed[0..7](小端加载为64位整数) ^ keys[0] ^ 0xA5A5A5A5A5A5A5A5。 - 生成密钥流:以
seed为初始状态,使用xorshift64* 伪随机数生成器生成任意长度的密钥流。- 迭代公式:
x ^= (x >> 12); x ^= (x << 25); x ^= (x >> 27); x *= 2685821657736338717; - 每次迭代产生一个64位随机数,按小端序拆分为8字节密钥流。
- 迭代公式:
- 解密Flag:将生成的39字节密钥流与程序中硬编码的39字节
FLAG_CIPHERTEXT进行逐字节异或,得到明文的Flag。
- 解题(直接法):
- 破解出正确的
user(满足哈希条件)。 - 由于
TARGET是已知常量,且keys可由user算出,因此可以直接计算出正确的seed:seed = TARGET[0..7] ^ keys[0] ^ 0xA5A5A5A5A5A5A5A5。 - 使用此
seed运行xorshift64*生成39字节密钥流。 - 用密钥流异或
FLAG_CIPHERTEXT得到Flag。
- 破解出正确的
关键代码与Exp思路:
文档中提供了大量的伪代码和结构定义,是编写解题脚本(Exp)的基础。例如,需要实现fnv1a32、derive_keys_from_user、rotl64、round_f、feistel_inv、xorshift64star等函数。
整体依赖关系:
user -> keys & 哈希校验
keys + pass -> mixed (需==TARGET)
mixed + keys[0] -> seed
seed -> keystream
keystream ^ FLAG_CIPHERTEXT -> flag
知识点:WebAssembly逆向工具使用(如wasm2c、wasm-decompile)、FNV哈希算法、Feistel密码结构、xorshift系列伪随机数生成器、Wasm与JavaScript的交互(导入导出表、内存操作)。
总结与工具链
- 工具汇总:PyInstaller提取器、pycdc、UPX、Wasm逆向套件(wasm2wat、wasm-decompile)、IDA Pro(用于分析C++ VM)、调试器。
- 核心技能:静态分析、动态调试、算法识别与还原、密码学基础、对不同文件格式(PE、Wasm)和语言(Python、C++)的逆向能力。
- 学习路径:建议从简单的打包程序逆向开始,逐步过渡到虚拟机、壳保护、密码算法和Wasm等更复杂的领域。
通过系统性地练习以上题目,可以全面锻炼逆向工程中的各项核心能力。