逆向一个 XOR CrackMe:从侦查到脚本还原

完整的 CrackMe 逆向:编译源码、file/strings 侦查、objdump/Ghidra 定位校验、GDB 动态调试、Python 逆运算还原 flag。
一、现实背景:注册码校验
商业软件用"输入正确序列号才放行"的校验函数保护授权,逆向需要从二进制还原该校验逻辑。CrackMe 是这类校验的简化版,也是逆向入门最好的练习材料。
二、题目源码与编译
出题人视角的 crackme.c:
#include <stdio.h>
#include <string.h>
int check(const char *input) {
char key[] = "k3y";
// key:异或密钥,编译后仍以字符串形式存在二进制里,strings 可见。
unsigned char enc[] = {
0x08,0x47,0x1f,0x10,0x41,0x1c,0x1d,0x56,
0x0b,0x18,0x56,0x26,0x0e,0x5d,0x1e,0x02,
0x5d,0x1c,0x0e,0x41,0x10,0x05,0x54,0x04
};
// enc:把正确 flag 逐字节异或后的密文,硬编码在程序里。
int n = strlen(input);
// strlen:求输入长度(不含结尾 \0)。
if (n != 24) return 0;
// 长度必须是 24,先卡掉大部分错误输入。
for (int i = 0; i < n; i++)
if ((input[i] ^ key[i % 3]) != enc[i]) return 0;
// 逐字节校验:输入第 i 位与 key 循环异或后必须等于密文第 i 位。
// key[i % 3]:i%3 在 0,1,2 之间循环,即 key 循环使用。
return 1;
// 全部通过才返回 1,main 据此打印 Correct!。
}
int main(int argc, char **argv) {
if (argc != 2) {
// argc:命令行参数个数;不带参数时 argc=1,这里要求恰好 2 个。
puts("Usage: ./crackme <flag>");
return 1;
}
if (check(argv[1]))
// argv[1]:命令行第二个参数,即用户输入的 flag。
puts("Correct!");
else
puts("Wrong!");
return 0;
}
编译(解题时只有二进制,没有源码):
gcc -o crackme crackme.c
运行确认行为:
./crackme
输出:Usage: ./crackme <flag>。
./crackme wrongflag123456789012
输出:Wrong!。
三、核心步骤 1:侦查
目的:先确认程序类型、收集可见字符串,缩小分析范围。
思路:strings 能看到 k3y 这样的短密钥(需要 -n 3),但看不到二进制密文数组。若直接看到 flag 明文就是送分题;看不到就继续反汇编。
3.1 file
file crackme
预期输出(Linux ELF):
crackme: ELF 64-bit LSB pie executable, x86-64, dynamically linked, ... not stripped
not stripped:函数名可见。
3.2 strings
注意:strings 默认只显示长度 ≥ 4 的字符串,密钥 k3y 只有 3 个字符,必须加 -n 3:
strings -n 3 crackme
预期输出:
Usage: ./crackme <flag>
Correct!
Wrong!
k3y
k3y 极可能是密钥。enc 数组是二进制数据,strings 显示不出来。
四、核心步骤 2:定位校验函数
目的:找到"比较"发生的代码,确认密钥如何参与运算。
思路:main 里 call check 后紧跟 test eax, eax; je,说明返回值决定对错。于是把目光集中到 check;Ghidra 伪代码把汇编还原成 input[i] ^ key[i%3] != enc[i],逆向思路立刻清晰。
4.1 objdump 看 main
objdump -d -M intel crackme | grep -A 40 '<main>'
预期输出(节选):
0000000000001189 <main>:
1191: cmp edi, 0x2
1194: jne 0x11a0
119e: call 0x1149 <check>
11a3: test eax, eax
11a5: je 0x11b5
call check 说明判断逻辑在 check。
4.2 objdump 看 check
objdump -d -M intel crackme | grep -A 60 '<check>'
预期会看到循环比较:
movzx eax, BYTE PTR [rax] ; input[i]
xor eax, 0x... ; 与 key 异或
cmp al, BYTE PTR [rbp-...] ; 与 enc[i] 比较
je 0x...
4.3 Ghidra 反编译
安装并打开:
apt install openjdk-21-jdk unzip
wget https://github.com/NationalSecurityAgency/ghidra/releases/latest/download/ghidra_*.zip
unzip ghidra_*.zip && cd ghidra_*_PUBLIC && ./ghidraRun
操作:新建项目 → 导入 crackme → 自动分析 → 双击 check。
预期伪代码:
if ((input[i] ^ key[i % 3]) != enc[i]) return 0;
得到逆运算关系:
enc[i] = input[i] ^ key[i % 3]
input[i] = enc[i] ^ key[i % 3]
五、核心步骤 3:GDB 动态调试
gdb ./crackme
(gdb) break check
# 目的:在 check 入口暂停,观察输入参数和密文数据。
预期输出:
Breakpoint 1 at 0x1149
(gdb) run AAAAAAAAAAAAAAAAAAAAAAAA
预期输出:
Breakpoint 1, 0x0000555555555149 in check ()
查看 enc 数组(地址在反汇编中确定):
(gdb) x/24bx 0x5555555551e0
# x:查看内存;/24bx:24 个字节、b 按字节、x 十六进制显示。
预期输出:
0x...: 0x08 0x47 0x1f 0x10 0x41 0x1c 0x1d 0x56 ...
与源码中的 enc 数组一致,确认比较逻辑。
六、核心步骤 4:写脚本还原
目的:把"加密"反过来算:密文与 key 再异或一次即明文。
思路:因为 enc[i] = input[i] ^ key[i%3],两边同时异或 key[i%3] 得 input[i] = enc[i] ^ key[i%3]。异或的自反性是本题全部关键。
enc = [0x08,0x47,0x1f,0x10,0x41,0x1c,0x1d,0x56,
0x0b,0x18,0x56,0x26,0x0e,0x5d,0x1e,0x02,
0x5d,0x1c,0x0e,0x41,0x10,0x05,0x54,0x04]
key = b"k3y" # 从 strings -n 3 找到的密钥
flag = bytes([c ^ key[i % len(key)] for i, c in enumerate(enc)])
# enumerate(enc):逐个取出 (下标 i, 密文字节 c)。
# key[i % len(key)]:i 对 3 取余,让密钥循环(与 C 的 i%3 一致)。
# c ^ key[...]:密文异或密钥 = 明文。
# bytes([...]):把整数列表转成字节串。
print(flag.decode())
# decode():bytes 按 UTF-8 解码成字符串显示。
运行:
python3 solve.py
实际输出:
ctf{reverse_engineering}
验证:
./crackme ctf{reverse_engineering}
实际输出:Correct!。
七、必学工具
| 工具 | 核心功能 | 命令 |
|---|---|---|
| file | 看类型/是否 stripped | file crackme |
| strings | 提取字符串、指定最小长度 | strings -n 3 crackme |
| objdump | 反汇编、看数据段 | objdump -d -M intel crackme、objdump -s -j .rodata crackme |
| GDB | 断点、运行、查看内存 | break check、run xxx、x/24bx addr |
| Ghidra | 反编译伪代码 | ghidraRun |
八、小 CTF 实战:流程复述
strings -n 3 crackme→ 找到密钥k3y。objdump/Ghidra → 确认check里是input[i] ^ key[i%3] == enc[i]。- Python 一行异或还原 →
ctf{reverse_engineering}。 - 运行验证 →
Correct!。
九、小结
XOR CrackMe 的通用解法:找密钥、找密文、再异或一次。逆向题的套路基本都是"侦查 → 定位校验 → 逆运算"三步;后续遇到的壳、混淆只是在中间增加干扰。