个人学习 Pwn 思路:从基础到漏洞利用的进阶路径

建立自己的SOP(标准作业程序)

Pwn 的核心是**"信息收集 -> 寻找漏洞 -> 构造利用链 -> 编写 Exploit"**。


第一步:基础侦察(知己知彼)------ 搞清楚"这是什么程序"

把题目附件下载下来后,第一件事不是开 IDA,而是用命令行工具摸底。

  1. file [文件名]:
    • 看架构 :是 32 位(i386/386)还是 64 位(x86-64)?这决定了你后续写 exp 用 p32 还是 p64,以及传参方式(栈传参 vs 寄存器传参)。
    • 看类型:是动态链接(dynamically linked)还是静态链接(statically linked)?
    • 看符号:是否被脱去符号表(stripped)?如果没脱(not stripped),IDA 里能看到函数名,会简单很多。
  1. file ./stack
    • 这是你输入的命令。file 是一个用来识别文件类型的命令,./stack 是你要检查的目标文件。
  1. ./stack: ELF 32-bit LSB executable
    • ELF: 这是文件格式。说明它是一个 Linux 系统下的可执行文件(类似于 Windows 下的 .exe)。
    • 32-bit: 这是一个 32位 程序。在做 Pwn 题时,这非常重要,因为 32 位和 64 位的漏洞利用方法(比如传参方式、寄存器用法)是完全不同的。
    • LSB executable: 意思是"最低有效位(Little-Endian)"可执行文件。这决定了多字节数据在内存中的存储顺序(小端序),写 exploit 脚本(如使用 pwntools)时需要注意这一点。
  1. Intel i386, version 1 (SYSV)
    • Intel i386: 指的是它运行在 Intel x86 架构的 CPU 上。
    • version 1 (SYSV): 使用 System V 的 ABI 标准。
  1. dynamically linked
    • 意思是动态链接 。这个程序本身不包含它需要的所有库代码(比如 printf 函数的代码),而是需要在运行时去系统里寻找并加载共享库(通常是 libc.so.6)。这意味着你可能需要泄露 libc 的地址来进行利用。
  1. interpreter /lib/ld-linux.so.2
    • 这是动态链接器的路径,系统通过它来加载程序运行所需的依赖库。
  1. for GNU/Linux 2.6.32
    • 这是编译该程序时使用的内核版本信息。
  1. BuildID[sha1]=...
    • 这是一个唯一的构建 ID,类似于文件的指纹。
  1. not stripped(最关键的信息之一)
    • 没有被剥离符号表 。这是一个非常好的消息!意味着程序里保留了函数名和变量名(比如 main、vuln、gets 等)。
    • 如果你用 IDA Pro 或 Ghidra 反编译它,你能直接看到清晰的函数名,这会大大降低逆向分析的难度。如果显示 stripped,就说明符号被去除了,分析起来会比较痛苦。
  1. checksec --file=[文件名](或者用 pwntools 里的 checksec):
    • 这一项极其重要!决定了你的攻击方向:
    • NX(栈不可执行):如果开启(Enabled),你就不能直接往栈里写 shellcode 执行了,必须用 ROP(返回导向编程)或者 ret2libc。
    • PIE(地址随机化):如果开启,程序每次运行的基址都不一样,你需要先泄漏地址(Leak)。
    • Canary(栈保护):如果开启,你不能随便溢出覆盖返回地址,需要先泄漏 Canary 或者想办法绕过。
    • RELRO:影响能不能改写 GOT 表(通常开启 Partial 或 Full)。
  1. Arch: i386-32-little
    • 确认了架构是 32位 小端序。和你之前用 file 命令看到的一致。在写 exploit 脚本时,记得打包地址要用 p32()。
  1. RELRO: Partial RELRO (部分 RELRO)
    • RELRO 全称是 Relocation Read-Only(重定位只读)。它用于保护全局偏移表(GOT 表)。
    • Partial RELRO 意味着 GOT 表是可写的。在高级利用中,如果有格式化字符串漏洞,你可以尝试覆写 GOT 表里的函数地址来劫持程序执行流(比如把 printf 的 GOT 表地址改成 system 的地址)。对于基础栈溢出影响不大。
  1. Stack: No canary found (无栈保护)
    • Canary 是一种放在栈上的随机值。如果发生栈溢出覆盖了返回地址,Canary 的值就会被破坏,程序会直接崩溃报错,从而阻止漏洞利用。
    • No canary found是个天大的好消息! 这意味着你可以毫无顾忌地往栈里写入超长数据,直接把返回地址覆盖成你想要的地址(比如后门函数的地址)。
  1. NX: NX enabled (栈不可执行)
    • NX (No-eXecute) 意为数据不可执行。系统把内存分为了代码段和数据段,栈属于数据段,开启了 NX 意味着你不能把 Shellcode 放在栈上然后让程序跳过去执行。
    • 既然不能直接执行栈上的代码,我们就需要用到 ROP(面向返回编程) 技术,或者调用程序自带的函数(比如 system("/bin/sh"))。
  1. PIE: No PIE (0x8048000) (无地址随机化)
    • PIE 会使得程序每次运行时的代码基地址都随机变化(类似 ASLR 的一种)。
    • No PIE也是个天大的好消息! 这意味着程序加载的基地址是固定的,就在 0x8048000。你可以直接在反编译工具(如 IDA)里找到 main 函数的地址、后门函数的地址,然后在 exp 中直接硬编码这些地址,不用担心每次运行地址都会变。
  1. Stripped: No (符号表未剥离)
    • 和之前一样,意味着你可以清楚地看到各种函数名,大大降低了逆向分析的难度。
  1. 运行一下程序:
    • ./[文件名] 看看它长啥样。是经典的菜单题?还是简单的输入输出题?如果是远程题,用 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函数开始顺藤摸瓜。

  1. 找输入点(找危险函数):
    • 在 IDA 左侧的 Imports(导入表) 或 Functions(函数列表) 里找有没有危险函数:
      • gets():绝对危险,无限长度输入,必溢出。
      • read(0, buf, size)/ fgets(buf, size, stdin):看 size 是不是比 buf 实际大小大。如果是,就是溢出。
      • scanf("%s", buf)/ strcpy/ strcat:同样不限制长度。
  1. 确定溢出偏移量(Offset):
    • 双击进入那个有漏洞的函数。
    • 看 char buf[x] 的大小。
    • 双击汇编里的 buf变量 ,看它距离栈底(RBP 或 EBP)多远。比如显示 [rbp-20h],那就是 32 字节(0x20)。
    • 算偏移 :如果程序是 32 位,偏移 = 数组大小 + 4;如果是 64 位,偏移 = 数组大小 + 8(覆盖 Saved RBP)。(更稳妥的方法是后面用 pwntools 的 cyclic 工具实测)。

用 pwntools cyclic 配合 GDB 实测偏移量(推荐做法) :IDA 里算出来的偏移是静态估算,遇到编译器优化、结构体对齐或局部变量顺序变化时可能不准,最稳妥的方式是用 cyclic 生成规律字符串,让程序崩溃后反查覆盖返回地址的位置。完整流程如下:

  1. 生成规律字符串 :在终端执行 cyclic 100,会输出一段 100 字节、每 4 字节都唯一的字符串(32 位程序按 4 字节对齐,64 位按 8 字节)。
bash 复制代码
$ cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
  1. 用 GDB 运行程序并喂入该字符串 :启动 gdb ./stack(建议装好 pwndbg 或 gef),运行程序,在输入点粘贴上面那段字符串。
bash 复制代码
$ gdb ./stack
pwndbg> run
# 程序提示输入时,粘贴 cyclic 100 的输出并回车
  1. 崩溃后查看 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)。

  1. 用 cyclic -l 反查偏移 :把崩溃时 EIP 的值(去掉 0x)传给 cyclic -l,它会告诉你这个片段在字符串中的起始下标,也就是覆盖到返回地址所需的填充长度。
bash 复制代码
$ cyclic -l 0x6161616c
44

结果 44 就是偏移量,说明 payload 需要先填 44 个垃圾字节,接下来的 4 个字节才会覆盖到返回地址。64 位程序同理,只是传入的是 8 字节的 RIP 值,例如 cyclic -l 0x616161616161616c。

  1. 与 IDA 静态计算结果互相验证 :回到 IDA,若 buf 显示为 [ebp-0x28],则数组大小 0x28 = 40 字节,32 位程序偏移 = 40 + 4(覆盖 Saved EBP)= 44,与 cyclic 实测结果完全一致,说明静态分析正确,可以放心写 exp。若两者不一致,通常有以下原因,需要以 cyclic 实测值为准 :

    复制代码
    <pre>

    <pre>

    <ul>

  2. IDA 里看到的 buf 不是真正被溢出的那个缓冲区,或者中间还夹着其它局部变量;

  3. 编译器做了栈对齐或变量重排,实际栈帧布局与伪代码顺序不同;

  4. 64 位程序忘记把 Saved RBP 的 8 字节算进去,或漏算了栈对齐填充。

验证通过后,就可以把 offset = 44 直接写进 exploit 脚本,配合前面 IDA 中找到的后门函数或 system 地址构造 payload 了。

  1. 找"惊喜"(后门函数):
    • 在左侧函数列表里,有没有类似 backdoor、get_shell、system 的直接利用函数?
    • 如果有,并且你能控制返回地址,覆盖返回地址为后门函数(Ret2Text),直接通关。
  1. 找可用的字符串:
    • 按 Shift + F12 打开字符串窗口,找找有没有 /bin/sh。如果有,记下地址。
  1. setvbuf(stdout, 0, 2, 0);等 :这是 CTF 题目的常规操作,用来关闭缓冲区。这样你 print 的内容能立刻显示出来,不用等程序结束才输出。
  2. puts("stack happy!");等:这就是你刚才运行程序时看到的输出。
  3. pwnme();:这是破局的核心! main 函数自己什么都没干,只是调用了一个名为 pwnme() 的函数。
  4. puts("\nExiting");:程序正常结束。

既然核心逻辑在 pwnme() 里,我们就要进去看。注意看左侧的函数列表,你能清晰地看到 pwnme、 fgets、 puts、 system这几个极其敏感的函数。 因为题目没有去掉符号(Not stripped),这简直是送分。


第三步:动态调试与构造思路(排兵布阵)

在动手写 payload 之前,先看一张「保护项 → 打法」的分流图。拿到 checksec 结果后,按图中的判断节点依次走一遍,就能快速锁定该用哪种利用方式:

flowchart TD A[checksec 结果] --> B{Canary 开启?} B -- 否 --> C{PIE 开启?} B -- 是 --> D[先 Leak Canary 或绕过] D --> C C -- 否 --> E{程序内有后门 / system + /bin/sh?} C -- 是 --> F[先 Leak 程序基址] F --> E E -- 有 --> G[NX 开启?] E -- 没有 --> H[NX 开启?] G -- 否 --> I[Ret2Text 或 ret2shellcode] G -- 是 --> J[Ret2Text 复用已有函数] H -- 否 --> K[ret2shellcode 直接执行栈上代码] H -- 是 --> L[Ret2libc 先 Leak libc 再二次溢出] L --> M{RELRO 状态?} J --> M M -- Partial / 无 --> N[有格式化字符串等写能力时可考虑 GOT 覆写] M -- Full --> O[放弃 GOT 覆写,走栈上 ROP]

如何按图索骥选择路线:

  1. 先看 Canary :这是第一道分水岭。若 No canary found,可以直接溢出覆盖返回地址,路线最省事;若开启,则必须先想办法 Leak 出 Canary 的值(如格式化字符串、栈上残留输出)或找到不经过 Canary 的覆盖点,否则溢出只会触发 __stack_chk_fail 崩溃。
  2. 再看 PIE :No PIE 时程序基址固定(32 位通常为 0x8048000),IDA 里看到的函数地址可以直接硬编码;PIE enabled 时每次运行基址随机,必须先通过 puts/write 等输出函数 Leak 一个程序内地址,算出基址后再加偏移。
  3. 然后看有没有现成的"捷径" :如果程序里存在后门函数,或同时存在 system 与 /bin/sh 字符串,就走 Ret2Text,把返回地址直接改成目标函数地址;如果没有,就进入 Ret2libc 分支,先 Leak libc 中某个函数的真实地址,算出 system 与 /bin/sh 的地址,再二次溢出。
  4. NX 决定能不能写 Shellcode :NX disabled 时可以把 Shellcode 放到栈上再跳过去执行(ret2shellcode);NX enabled 时栈不可执行,只能复用已有代码,即 Ret2Text / Ret2libc / ROP。
  5. 最后看 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)
  • 场景 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()

💡 新手避坑小建议:

  1. 多看 WP,但要看思路 :不要直接抄 exp,看别人怎么用 checksec,怎么用 GDB 找偏移,怎么找 gadget(比如用 ROPgadget --binary ./pwn --only "pop|ret")。
  2. 善用 pwn cyclic:算不准偏移,就在本地跑的时候扔一个 cyclic(100) 进去,看崩溃时的寄存器值,秒算偏移。
  3. 先通本地,再打远程 :本地调试环境(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 道简单的题目后,这些词你就自然而然地记住了。

相关推荐
甲维斯4 分钟前
Workbuddy+HY4 略超预期!
人工智能
Nuanyt33 分钟前
LeetCode Hot 100 链表专题(五)· 2 题(Java)
java·算法·leetcode·链表
天远API41 分钟前
零信任架构实战:基于天远运营商三要素详版V即时版构建自动化消费电子大促电商平台高价值权益秒杀与优惠券防羊毛高并发实名核验网关
运维·人工智能·架构·自动化
朱涛的自习室1 小时前
212 个需求、22 万行代码:Agent 接管开源项目的 19 天
android·前端·人工智能
美林数据Tempodata1 小时前
高职大数据技术专业升级的六个落地样本:从产业链需求库到实训室数据链路
大数据·人工智能·数字化转型·产教融合·ai教育
aZhe的全栈知识分享1 小时前
AI 还原 Figma 效果总达不到预期?先别急着让它写代码
前端·后端·面试
review445431 小时前
Codex Hooks 入门实战:生命周期事件、matcher 与5分钟跑通一个钩子
前端·agent·vibecoding
函数小陈1 小时前
阿里云部署DSH与RAM完成运维
前端·后端
美好世界1 小时前
OpenAI Codex 源码深度剖析(三):执行引擎与内核沙箱深度剖析
人工智能