CTF 逆向入门:侦查、反汇编与 GDB 调试

CTF 逆向入门

完整演示逆向三板斧:file/strings 侦查、objdump 反汇编定位校验函数、GDB 动态调试,并举例一个CrackMe。

14th Aug 2026

5 min read

一、现实背景:为什么需要逆向

商业软件只发布编译后的二进制文件,不发布源码。恶意软件分析、漏洞研究、授权校验分析都依赖逆向。CTF 逆向题最常见的形式:程序读取一个输入,判断对不对,对就输出 flag;不给你源码,只给你编译好的文件。

目标程序示例:

$ ./checkflag
Usage: ./checkflag <flag>
$ ./checkflag test
Wrong!

解题目标:通过分析二进制,找出正确输入。

二、环境准备

在 Linux(Kali/Ubuntu)上:

apt install file binutils gdb ltrace strace

本文以编译后的 checkflag 为例,逐步演示。

三、核心步骤 1:侦查

目的:在不运行程序的情况下,先收集"程序是什么、里面写了什么字符串"。

思路:程序类型决定用什么工具(ELF 用 objdump,PE 用其他);字符串往往直接暴露提示、密钥甚至 flag。先做最便宜的三件事,通常能直接定位简单题。

3.1 file:看文件类型

输入:

file checkflag

预期输出:

checkflag: ELF 64-bit LSB pie executable, x86-64, dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, not stripped

重点看最后:not stripped 表示符号表还在,函数名(main、check)可见;如果是 stripped,函数名被删,难度更高。

目的:确认我们面对的是 ELF 还是 PE、多少位、是否被 strip。这决定后续用 objdump 还是其他工具,以及能否直接看到函数名。

3.2 strings:提取字符串

输入:

strings checkflag

预期输出:

/lib64/ld-linux-x86-64.so.2
Usage: ./checkflag <flag>
Correct!
Wrong!

三个提示字符串都出来了,但还没有 flag。注意 strings 默认只显示长度 ≥ 4 的字符串,短字符串用 -n 指定:

strings -n 3 checkflag

如果程序用 XOR 加密了 flag,strings 只能看到提示,看不到明文,需要继续分析。

目的:把文件里所有可打印字符串列出来,找"提示/密钥/硬编码密文"。

思路:输出里的 Usage: ./checkflag <flag>、Correct!、Wrong! 说明这是个接收参数的校验程序;如果看到可疑短串(如 k3y),先用 -n 3 确认——可能正是加密密钥。

3.3 运行程序观察行为

./checkflag test

输出:Wrong!。

./checkflag AAAAAAAAAAAAAAAAAAAAAAAA

输出:Wrong!。行为确认:带一个参数,对就 Correct,错就 Wrong。

目的:确认输入输出格式(参数个数、提示文字),为后续在反汇编里找"打印 Correct 的分支"做准备。

四、核心步骤 2:反汇编

目的:找到程序"判断对错"的那段代码,弄清校验逻辑。

思路:程序要么比较字符串(strcmp),要么逐字节运算(XOR 等)。反汇编里找 call(函数调用)和 je/jne(条件跳转)就能定位判断点。

4.1 objdump 看 main

输入:

objdump -d -M intel checkflag

输出很长,用 grep 只看 main:

objdump -d -M intel checkflag | grep -A 40 '<main>'

预期输出(节选):

0000000000001189 <main>:
    1189:  push   rbp
    118a:  mov    rbp, rsp
    1191:  cmp    edi, 0x2
    1194:  jne    0x11a0
    119e:  call   0x1149 <check>
    11a3:  test   eax, eax
    11a5:  je     0x11b5
    11a7:  lea    rdi, [rip+0x2ea]
    11ae:  call   puts@plt

逐行解释:

指令含义
cmp edi, 0x2比较参数个数 argc 是否为 2
call check调用校验函数 check
test eax, eax检查返回值是否为 0
je 0x11b5为 0 则跳到打印 Wrong 的地方

结论:逻辑在 check 函数里。

怎么想到看 check:call check 之后紧跟 test eax, eax 和 je,这是典型的"函数返回结果决定分支"结构——check 的返回值非 0 才打印 Correct。所以下一步只看 check。

4.2 看 check

objdump -d -M intel checkflag | grep -A 50 '<check>'

如果看到:

call   strcmp@plt
test   eax, eax
je     ...

说明是字符串比较,flag 就藏在 .rodata 里。

验证方式:call strcmp@plt 前面通常有一条 lea rsi, [rip+...] 把要比较的字符串地址装进 rsi。记下那个地址,去 .rodata 里找内容即可。

4.3 看只读数据段

objdump -s -j .rodata checkflag

预期输出:

...
 402000 636f6e67726174756c6174696f6e73 00   "congratulations"

ASCII 十六进制 63 6f 6e 67... 解出来就是 congratulations。

为什么 flag 会在 .rodata:字符串常量编译后存放在只读数据段 .rodata,objdump -s 直接按字节显示,右侧还附了 ASCII 明文。

4.4 用 Ghidra 反编译(更省力)

Ghidra 是 NSA 开源的免费反编译器。安装:

apt install openjdk-21-jdk unzip
wget https://github.com/NationalSecurityAgency/ghidra/releases/latest/download/ghidra_*.zip
unzip ghidra_*.zip
cd ghidra_*_PUBLIC && ./ghidraRun

操作步骤:

  1. File → New Project → 命名。
  2. 把 checkflag 拖进项目,双击导入。
  3. 等待自动分析完成。
  4. 左侧 Symbol Tree 双击 main,右侧 Decompiler 显示伪代码。

预期伪代码:

if (strcmp(argv[1], "congratulations") == 0)
    puts("Correct!");
else
    puts("Wrong!");

flag 直接可见。

五、核心步骤 3:GDB 动态调试

静态分析看不懂时,让程序跑起来。

目的:在真实执行现场观察"比较发生时寄存器里到底装了什么"。

思路:下断点停在 check 入口 → 带参数运行 → 单步走到 call strcmp → 查看 rsi(第二个参数)指向的字符串,那就是程序期望的 flag。

gdb ./checkflag

进入 GDB 交互后:

5.1 下断点

(gdb) break check

预期输出:

Breakpoint 1 at 0x1149

5.2 带参数运行

(gdb) run test

预期输出:

Breakpoint 1, 0x0000555555555149 in check ()

程序停在 check 入口。

5.3 查看即将执行的指令

(gdb) x/20i $rip

输出 check 函数开头的 20 条汇编。

5.4 单步并观察比较

(gdb) stepi

反复执行直到看到 call strcmp@plt。然后:

(gdb) x/s $rsi

预期输出:程序正在比较的字符串,即 flag。

补充说明:x/s 的 s 表示按字符串显示;$rsi 是 x86-64 的第 2 个参数寄存器,strcmp(a, b) 的 b 就存在这里。

补充:如果嫌 GDB 麻烦,ltrace 直接跟踪库调用:

ltrace ./checkflag test

预期输出中会出现:

strcmp("test", "congratulations") = 1

flag 一目了然。

ltrace 的原理:它拦截并打印程序对共享库函数的调用,包括参数值。遇到 strcmp 类题目比 GDB 快得多,但不能用来调试自己写的代码。

六、必学工具

工具核心功能命令示例
file看类型、位数、是否 strippedfile checkflag
strings提取字符串、指定最小长度、管道过滤strings checkflag、strings -n 3 a、strings a | grep flag
objdump反汇编、看数据段、Intel 语法objdump -d -M intel a、objdump -s -j .rodata a
GDB断点、运行、看内存/寄存器break check、run、x/20i $rip

另:Ghidra 负责把汇编还原成伪代码;ltrace/strace 负责快速跟踪函数与系统调用。

七、小 CTF 实战:一个 XOR 加密的 CrackMe

题目:crackme,校验函数把输入和密钥异或后与密文比较。

第一步:侦查

目的:先找密钥与提示。

思路:strings 默认漏掉短串,所以用 -n 3;看到 k3y 猜测是 XOR 密钥,接下来去确认校验函数。

strings -n 3 crackme

预期输出中有 k3y——疑似密钥。

第二步:反编译 check

目的:确认密钥如何使用、密文数组是什么。

验证方式:伪代码 input[i] ^ key[i%3] != enc[i] 说明每一位输入与 key 循环异或后要和 enc 相等——逆向就是再异或一次。

用 Ghidra 打开,check 伪代码:

if ((input[i] ^ key[i % 3]) != enc[i]) return 0;

第三步:写脚本逆运算

目的:把"加密"反过来算,得到明文。

思路:异或的自反性 A^B=C => C^B=A。对密文数组的每一位,用 key 按位置循环异或,即还原明文。

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 找到的密钥
flag = bytes([c ^ key[i % 3] for i, c in enumerate(enc)])
# bytes([...]):把列表里的整数逐个转成字节,组成 bytes 对象。
# 列表推导式含义:对 enc 的第 i 个字节 c,与 key 的第 (i % 3) 个字节异或。
# i % 3 让密钥循环使用(0,1,2,0,1,2,...),与 C 源码 key[i % 3] 完全对应。
# 异或结果就是明文字节;因为 enc[i] = flag[i] ^ key[i%3],
# 所以 flag[i] = enc[i] ^ key[i%3]。
print(flag.decode())
# decode():把 bytes 按 UTF-8 解码成字符串显示,方便直接看到 flag。

输出:

ctf{reverse_engineering}

第四步:验证

./crackme ctf{reverse_engineering}

输出:Correct!。

八、小结

逆向入门三步:file/strings 侦查 → objdump/Ghidra 定位校验函数 → GDB 验证。大部分入门题要么字符串直接可见,要么只是一个异或,逆运算一行脚本即可解决。