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

XOR CrackMe 完整实操

完整的 CrackMe 逆向:编译源码、file/strings 侦查、objdump/Ghidra 定位校验、GDB 动态调试、Python 逆运算还原 flag。

14th Aug 2026

5 min read

一、现实背景:注册码校验

商业软件用"输入正确序列号才放行"的校验函数保护授权,逆向需要从二进制还原该校验逻辑。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看类型/是否 strippedfile 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 实战:流程复述

  1. strings -n 3 crackme → 找到密钥 k3y。
  2. objdump/Ghidra → 确认 check 里是 input[i] ^ key[i%3] == enc[i]。
  3. Python 一行异或还原 → ctf{reverse_engineering}。
  4. 运行验证 → Correct!。

九、小结

XOR CrackMe 的通用解法:找密钥、找密文、再异或一次。逆向题的套路基本都是"侦查 → 定位校验 → 逆运算"三步;后续遇到的壳、混淆只是在中间增加干扰。