PWN 新手入门指南:从零到理解本质
写在前面:这份指南的目标是让你真正理解 PWN 是什么,而不是让你背诵一堆技术名词。当你读完这份文档,你应该能回答:我在学什么?为什么要学这些?学完能做什么?
目录
- 第一章:PWN 的本质——你到底在学什么
- 第二章:计算机的真相——程序是如何运行的
- 第三章:漏洞的本质——为什么程序会被攻击
- 第四章:学习路线图——你的升级打怪之路
- 第五章:核心知识点详解
- 第六章:工具链——你的武器库
- 第七章:从 CTF 到真实世界
- 第八章:心态与方法论
第一章:PWN 的本质——你到底在学什么
1.1 一句话定义
PWN = 利用程序的漏洞,让程序执行它本不应该执行的操作。
最常见的目标是:获取 Shell(也就是获得命令行控制权,可以在目标机器上执行任意命令)。
1.2 为什么叫 “PWN”?
“PWN” 来自游戏圈的俚语 “own”(拥有/控制),表示完全控制了对方。在安全领域,PWN 一台机器意味着你获得了它的控制权。
1.3 PWN 的本质是什么?
让我用一个比喻来解释:
想象一个银行金库,有一套严格的安全流程:
- 验证身份
- 检查权限
- 打开特定的保险箱
- 取出/存入物品
- 关闭保险箱
- 记录日志
正常情况:员工按流程操作,一切井然有序。
PWN 做的事:找到流程中的漏洞,比如:
- 发现身份验证可以被绕过(认证绕过)
- 发现可以让系统”忘记”关闭保险箱(内存泄漏)
- 发现可以修改”下一步操作”的指示牌,让员工走向金库总控室(控制流劫持)
在计算机中:
- 银行 = 程序
- 安全流程 = 程序的执行流程
- 保险箱 = 内存中的数据
- 金库总控室 = 系统权限
1.4 PWN 的最终目标
┌─────────────────────────────────────────────────────────────┐
│ PWN 的目标层次 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Level 1: 信息泄露 │
│ └── 读取本不应该读取的数据(密码、密钥、地址) │
│ │
│ Level 2: 拒绝服务 │
│ └── 让程序崩溃,无法正常服务 │
│ │
│ Level 3: 任意代码执行 ⭐ (这是 PWN 的核心目标) │
│ └── 让程序执行你指定的代码(通常是获取 Shell) │
│ │
│ Level 4: 权限提升 │
│ └── 从普通用户变成 root/管理员 │
│ │
└─────────────────────────────────────────────────────────────┘
在 CTF 比赛中,最常见的目标是:
- 获取 Shell,然后读取 flag 文件
- 或者直接利用漏洞读取 flag
第二章:计算机的真相——程序是如何运行的
为什么要学这些? 因为 PWN 的本质是”欺骗”计算机。要欺骗它,你必须先理解它是如何工作的。
2.1 程序的一生
源代码 (.c)
│
▼ [编译器 gcc]
汇编代码 (.s)
│
▼ [汇编器 as]
目标文件 (.o)
│
▼ [链接器 ld]
可执行文件 (ELF)
│
▼ [操作系统加载器]
内存中运行的进程
你需要理解:
- 你写的 C 代码最终变成了什么?(机器指令)
- 程序是如何被加载到内存的?(ELF 格式)
- 程序运行时内存长什么样?(进程内存布局)
2.2 进程内存布局——PWN 的战场
当一个程序运行时,操作系统会给它分配一块内存空间:
高地址
┌─────────────────────────┐
│ 内核空间 │ ← 用户程序不能直接访问
├─────────────────────────┤
│ 栈 (Stack) │ ← 局部变量、函数调用信息
│ ↓ │ 【PWN 重点攻击区域】
│ │
│ ↑ │
│ 堆 (Heap) │ ← 动态分配的内存 (malloc)
├─────────────────────────┤ 【PWN 重点攻击区域】
│ BSS │ ← 未初始化的全局变量
├─────────────────────────┤
│ Data │ ← 已初始化的全局变量
├─────────────────────────┤
│ Text │ ← 程序代码(只读)
└─────────────────────────┘
低地址
关键概念:
- 栈 (Stack):后进先出,存放局部变量和函数调用信息
- 堆 (Heap):动态内存,malloc/free 管理
- 这两个区域是 PWN 的主战场
2.3 函数调用的秘密——栈帧
这是 PWN 最核心的知识点之一,请务必理解透彻。
当你调用一个函数时,栈上会发生什么?
void func(int a, int b) {
int x = 1;
int y = 2;
}
int main() {
func(10, 20);
return 0;
}
调用 func 时的栈布局:
高地址
┌─────────────────────────┐
│ main 的栈帧 │
├─────────────────────────┤
│ 参数 b = 20 │ ← 函数参数(x64 用寄存器传递)
│ 参数 a = 10 │
├─────────────────────────┤
│ 返回地址 (Return Addr) │ ← call 指令自动压入
├─────────────────────────┤ 【这是 PWN 的关键目标】
│ 保存的 rbp (Saved RBP) │ ← push rbp
├─────────────────────────┤
│ 局部变量 x = 1 │
│ 局部变量 y = 2 │
├─────────────────────────┤
│ func 的栈帧 │
└─────────────────────────┘
低地址
核心问题:
- 返回地址告诉 CPU “函数执行完后,下一条指令在哪里”
- 如果我们能修改返回地址,函数返回时就会跳到我们指定的地方!
- 这就是栈溢出攻击的本质
2.4 寄存器——CPU 的工作台
CPU 不直接操作内存,而是通过寄存器这个高速”工作台”。
x86-64 常用寄存器:
| 寄存器 | 用途 | PWN 中的意义 |
|---|---|---|
| RIP | 指令指针,指向下一条要执行的指令 | 控制 RIP = 控制程序流程 |
| RSP | 栈指针,指向栈顶 | 栈操作的基准 |
| RBP | 基址指针,指向当前栈帧的底部 | 定位局部变量 |
| RAX | 返回值 / 系统调用号 | 函数返回值、syscall 编号 |
| RDI | 第 1 个参数 | 函数/syscall 的第 1 个参数 |
| RSI | 第 2 个参数 | 函数/syscall 的第 2 个参数 |
| RDX | 第 3 个参数 | 函数/syscall 的第 3 个参数 |
最重要的一句话:
PWN 的核心目标之一就是控制 RIP(指令指针),让程序跳转到我们想要的地方。
2.5 系统调用——与操作系统对话
用户程序不能直接操作硬件,必须通过系统调用 (syscall) 请求操作系统帮忙。
用户程序 内核
│ │
│ syscall (rax=59, │
│ rdi="/bin/sh", │
│ rsi=0, rdx=0) │
│ ───────────────────────>│
│ │ 执行 execve("/bin/sh", 0, 0)
│ │
│ 返回 Shell │
│ <───────────────────────│
关键系统调用:
| syscall | 编号 | 作用 | PWN 中的用途 |
|---|---|---|---|
| read | 0 | 读取输入 | 读取 payload |
| write | 1 | 输出 | 泄露信息 |
| execve | 59 | 执行程序 | 获取 Shell 的关键 |
| mprotect | 10 | 修改内存权限 | 绕过 NX 保护 |
获取 Shell 的本质:
让程序执行 execve("/bin/sh", NULL, NULL)
第三章:漏洞的本质——为什么程序会被攻击
3.1 漏洞从何而来?
┌─────────────────────────────────────────────────────────────┐
│ 漏洞的根源 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 程序员的错误 │
│ └── 忘记检查边界、错误的类型转换、逻辑错误 │
│ │
│ 2. 语言的特性 │
│ └── C/C++ 不检查数组边界,允许直接操作内存 │
│ │
│ 3. 复杂性 │
│ └── 代码越复杂,越容易出错 │
│ │
│ 4. 信任边界问题 │
│ └── 程序信任了不应该信任的输入 │
│ │
└─────────────────────────────────────────────────────────────┘
3.2 漏洞的分类
漏洞类型
├── 内存破坏类 (Memory Corruption) 【PWN 主战场】
│ ├── 栈溢出 (Stack Overflow)
│ ├── 堆溢出 (Heap Overflow)
│ ├── 格式化字符串 (Format String)
│ ├── Use-After-Free
│ ├── 整数溢出 (Integer Overflow)
│ └── 越界读写 (Out-of-Bounds)
│
├── 逻辑漏洞
│ ├── 条件竞争 (Race Condition)
│ └── 权限检查不当
│
└── 其他
├── 信息泄露
└── 拒绝服务
3.3 漏洞利用的通用模型
无论什么漏洞,利用的本质都是:
┌─────────────────────────────────────────────────────────────┐
│ │
│ 输入恶意数据 ───> 触发漏洞 ───> 改变程序状态 ───> 达成目标 │
│ │
│ (Payload) (Bug) (Corruption) (Shell) │
│ │
└─────────────────────────────────────────────────────────────┘
具体到栈溢出:
输入超长字符串 ──> 覆盖返回地址 ──> 函数返回时跳转到恶意代码 ──> 获取 Shell
第四章:学习路线图——你的升级打怪之路
4.1 完整路线图
╔═══════════════════════════════╗
║ PWN 学习路线图 ║
╚═══════════════════════════════╝
│
┌─────────────────────────────┼─────────────────────────────┐
│ │ │
▼ ▼ ▼
╔═══════════════╗ ╔═══════════════╗ ╔═══════════════╗
║ 基础阶段 ║ ║ 进阶阶段 ║ ║ 高级阶段 ║
║ (1-3个月) ║ ────────> ║ (3-6个月) ║ ────────> ║ (6个月+) ║
╚═══════════════╝ ╚═══════════════╝ ╚═══════════════╝
│ │ │
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ • C 语言基础 │ │ • ROP 技术 │ │ • 内核 PWN │
│ • Linux 基础 │ │ • 格式化字符串 │ │ • 浏览器 PWN │
│ • 汇编语言 │ │ • 堆利用基础 │ │ • 虚拟化逃逸 │
│ • GDB 调试 │ │ • 保护机制绕过 │ │ • 真实漏洞分析 │
│ • 栈溢出基础 │ │ • ret2libc │ │ • 漏洞挖掘 │
└───────────────┘ └───────────────┘ └───────────────┘
4.2 基础阶段详解(1-3个月)
目标:能够独立完成简单的栈溢出题目
4.2.1 C 语言(1-2 周)
你需要掌握:
- 指针的概念和使用
- 数组与内存的关系
- 结构体
- 常见的危险函数:
gets,strcpy,sprintf,scanf
为什么重要:大部分 PWN 题目的目标程序是 C 写的,你需要能看懂代码。
4.2.2 Linux 基础(1 周)
你需要掌握:
- 基本命令:
ls,cd,cat,chmod,file - 文件权限概念
- 进程概念
- 管道和重定向
为什么重要:PWN 的目标通常是获取 Linux Shell。
4.2.3 汇编语言(2-3 周)
你需要掌握:
- 常见指令:
mov,push,pop,call,ret,lea,cmp,jmp - 函数调用约定
- 能读懂简单的汇编代码
为什么重要:程序最终以汇编/机器码运行,你需要理解程序真正在做什么。
推荐学习方式:
不要背指令!通过 GDB 调试实际程序来学习。
看到 mov rax, rbx,就在 GDB 里执行,观察寄存器变化。
4.2.4 GDB 调试(1 周)
你需要掌握:
# 基本命令
r (run) # 运行程序
b *地址 (break) # 设置断点
c (continue) # 继续执行
si (stepi) # 单步执行(进入函数)
ni (nexti) # 单步执行(跳过函数)
x/格式 地址 # 查看内存
p 表达式 # 打印值
info registers # 查看寄存器
bt (backtrace) # 查看调用栈
为什么重要:调试是 PWN 的核心技能,你需要观察程序的实际行为。
4.2.5 栈溢出基础(2-3 周)
学习路径:
Step 1: 理解栈的结构
└── 画出函数调用时的栈布局
Step 2: 手动触发崩溃
└── 输入超长字符串,观察程序崩溃
Step 3: 控制返回地址
└── 精确计算偏移,覆盖返回地址为特定值
Step 4: ret2text
└── 跳转到程序中已有的函数(如 system)
Step 5: ret2shellcode (无 NX)
└── 跳转到自己注入的代码
4.3 进阶阶段详解(3-6个月)
目标:能够绕过常见保护机制,完成中等难度题目
4.3.1 保护机制
| 保护 | 全称 | 作用 | 绕过方法 |
|---|---|---|---|
| Canary | Stack Canary | 检测栈溢出 | 泄露 canary 值 |
| NX | No-Execute | 栈不可执行 | ROP 技术 |
| PIE | Position Independent Executable | 地址随机化 | 泄露地址 |
| ASLR | Address Space Layout Randomization | 内存布局随机化 | 泄露 + 计算偏移 |
| RELRO | Relocation Read-Only | GOT 表只读 | 改用其他攻击面 |
4.3.2 ROP 技术(Return Oriented Programming)
核心思想:既然不能执行自己的代码(NX),就利用程序中已有的代码片段(gadget)。
传统攻击:
返回地址 ──> 你的 shellcode
ROP 攻击:
返回地址 ──> gadget1 ──> gadget2 ──> gadget3 ──> ... ──> 目标函数
│ │ │
pop rdi pop rsi syscall
学习路径:
ret2text → 最简单,跳转到程序自身函数
↓
ret2syscall → 利用 gadget 构造系统调用
↓
ret2libc → 跳转到 libc 中的函数
↓
ret2csu → 利用 __libc_csu_init 的 gadget
↓
SROP → 利用 sigreturn 控制所有寄存器
4.3.3 格式化字符串漏洞
漏洞代码:
printf(user_input); // 危险!
// 应该是 printf("%s", user_input);
能做什么:
- 读取栈上的数据:
%x,%p,%s - 读取任意地址:
%n$s(n 是偏移) - 写入任意地址:
%n
4.3.4 堆利用基础
堆的特点:
- 动态分配(malloc/free)
- 复杂的管理结构(chunk、bins)
- 更多的攻击面
学习路径:
理解 glibc malloc 结构
↓
fastbin attack
↓
unsorted bin attack
↓
tcache attack (新版本 glibc)
↓
house of 系列
4.4 高级阶段(6个月+)
- 内核 PWN:攻击操作系统内核
- 浏览器 PWN:攻击 Chrome/Firefox 等
- 真实漏洞分析:CVE 复现
- 漏洞挖掘:自己发现新漏洞
第五章:核心知识点详解
5.1 栈溢出——PWN 的入门之道
为什么会发生栈溢出?
void vulnerable() {
char buffer[64]; // 只分配了 64 字节
gets(buffer); // 但 gets 不检查长度,可以无限读取
}
利用原理
正常情况:
┌─────────────┐
│ return addr │ = 0x401234 (正常返回地址)
├─────────────┤
│ saved rbp │
├─────────────┤
│ │
│ buffer │ ← 用户输入 "Hello"
│ [64字节] │
└─────────────┘
溢出后:
┌─────────────┐
│ return addr │ = 0xdeadbeef (被覆盖!)
├─────────────┤
│ saved rbp │ = "AAAAAAAA"
├─────────────┤
│ │
│ buffer │ ← 用户输入 "A"*72 + p64(0xdeadbeef)
│ [64字节] │
└─────────────┘
偏移计算方法
方法 1:手动计算
偏移 = buffer大小 + saved_rbp大小
= 64 + 8 = 72 字节
方法 2:使用 pattern(推荐)
# 生成特征字符串
$ cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
# 找到崩溃时 RIP 的值,计算偏移
$ cyclic -l 0x6161616c
72
5.2 ROP 技术——绕过 NX 的艺术
什么是 Gadget?
Gadget 是以 ret 结尾的短代码片段:
; gadget 1: pop rdi; ret
pop rdi
ret
; gadget 2: pop rsi; ret
pop rsi
ret
ROP Chain 构造
目标:执行 system("/bin/sh")
# 栈布局
payload = b'A' * offset # 填充
payload += p64(pop_rdi_ret) # gadget: pop rdi; ret
payload += p64(binsh_addr) # "/bin/sh" 的地址,会被 pop 到 rdi
payload += p64(system_addr) # system 函数地址
执行流程:
1. 函数返回,RIP = pop_rdi_ret
2. 执行 pop rdi,rdi = binsh_addr
3. 执行 ret,RIP = system_addr
4. 执行 system(rdi),即 system("/bin/sh")
5. 获得 Shell!
5.3 格式化字符串——读写任意内存
基本原理
printf 的参数通过栈/寄存器传递:
printf("%d %d %d", a, b, c);
// a, b, c 依次在 rsi, rdx, rcx (x64)
如果只写 printf(user_input),printf 会从栈上读取”参数”:
printf("%p %p %p"); // 会打印栈上的值
利用技巧
泄露栈数据:
%p → 打印一个栈上的值
%n$p → 打印第 n 个"参数"
%s → 把栈上的值当地址,打印该地址的字符串
写入数据:
%n → 把已打印字符数写入地址
%hn → 写入 2 字节
%hhn → 写入 1 字节
5.4 SROP——Sigreturn Oriented Programming
原理
Linux 信号处理机制:
- 信号到来时,内核将所有寄存器保存到用户栈(Signal Frame)
- 执行信号处理函数
- 调用
sigreturn系统调用,从栈上恢复寄存器
攻击思路:伪造 Signal Frame,调用 sigreturn,控制所有寄存器!
frame = SigreturnFrame()
frame.rax = 59 # execve
frame.rdi = binsh_addr # "/bin/sh"
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_addr
5.5 堆利用基础概念
Chunk 结构
┌─────────────────┐
│ prev_size │ ← 前一个 chunk 的大小(如果前一个是 free 的)
├─────────────────┤
│ size │ ← 当前 chunk 大小 + 标志位
├─────────────────┤
│ │
│ user data │ ← malloc 返回的指针指向这里
│ │
└─────────────────┘
常见攻击手法
| 攻击 | 原理 | 效果 |
|---|---|---|
| Fastbin Attack | 修改 fastbin 的 fd 指针 | 分配到任意地址 |
| Unsorted Bin Attack | 利用 unsorted bin 的解链操作 | 向目标地址写入大数 |
| Tcache Poisoning | 修改 tcache 的 next 指针 | 分配到任意地址 |
第六章:工具链——你的武器库
6.1 必备工具
| 工具 | 用途 | 重要程度 |
|---|---|---|
| pwntools | Python 漏洞利用框架 | ⭐⭐⭐⭐⭐ |
| GDB + pwndbg/peda | 调试 | ⭐⭐⭐⭐⭐ |
| IDA Pro / Ghidra | 反汇编、反编译 | ⭐⭐⭐⭐⭐ |
| checksec | 检查保护机制 | ⭐⭐⭐⭐ |
| ROPgadget / ropper | 寻找 gadget | ⭐⭐⭐⭐ |
| one_gadget | 寻找 libc 中的 one gadget | ⭐⭐⭐ |
| LibcSearcher | 根据泄露信息查找 libc 版本 | ⭐⭐⭐ |
6.2 pwntools 快速入门
from pwn import *
# 设置架构
context(arch='amd64', os='linux')
# 连接目标
io = process('./vuln') # 本地
io = remote('ip', port) # 远程
# 接收和发送
io.recv() # 接收数据
io.recvuntil(b'>') # 接收直到某字符串
io.send(b'data') # 发送数据
io.sendline(b'data') # 发送数据 + 换行
# 打包数据
p64(0xdeadbeef) # 64位小端打包
p32(0xdeadbeef) # 32位小端打包
u64(b'\xef\xbe\xad\xde') # 解包
# 获取 Shell
io.interactive()
6.3 GDB + pwndbg 常用命令
# 启动
gdb ./binary
gdb -p PID # 附加到进程
# pwndbg 特有命令
vmmap # 查看内存映射
heap # 查看堆状态
bins # 查看 bins
canary # 显示 canary 值
telescope 地址 # 智能显示内存
cyclic 100 # 生成特征字符串
cyclic -l 0x61616166 # 计算偏移
第七章:从 CTF 到真实世界
7.1 CTF PWN vs 真实漏洞
| 方面 | CTF | 真实世界 |
|---|---|---|
| 目标 | 获取 flag | 获取权限/数据 |
| 程序 | 简化的小程序 | 复杂的大型软件 |
| 漏洞 | 明显且单一 | 隐蔽且可能需要组合 |
| 时间 | 几小时到几天 | 可能几周到几月 |
| 环境 | 已知且固定 | 需要信息收集 |
7.2 学习 CTF PWN 的意义
1. 建立基础知识体系
└── 理解程序运行原理、漏洞成因
2. 锻炼逆向思维
└── 学会从攻击者角度思考
3. 提升调试能力
└── 熟练使用调试工具
4. 为进阶铺路
└── 真实漏洞研究、安全开发
7.3 进阶方向
CTF PWN
│
├──> 漏洞研究
│ └── 分析 CVE、挖掘新漏洞
│
├──> 安全开发
│ └── 编写安全的代码、安全审计
│
├──> 红队/渗透测试
│ └── 实战攻击、提权
│
└──> 安全产品开发
└── 开发防护工具、检测系统
第八章:心态与方法论
8.1 新手常见误区
❌ 误区 1: 追求做题数量,不求理解
✅ 正确: 一道题彻底理解,胜过十道题囫囵吞枣
❌ 误区 2: 只看 WriteUp,不动手
✅ 正确: WriteUp 是参考,必须亲手调试复现
❌ 误区 3: 遇到困难就放弃
✅ 正确: 卡住很正常,这是学习的一部分
❌ 误区 4: 急于求成,跳过基础
✅ 正确: 基础不牢,高级技术学了也用不好
8.2 高效学习方法
┌─────────────────────────────────────────────────────────────┐
│ PWN 学习循环 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 学习理论 ──────> 做题实践 ──────> 总结记录 │
│ ↑ │ │
│ └──────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
具体建议:
每道题都要调试
- 不要只运行 exp,要用 GDB 单步跟踪
- 理解每一步发生了什么
记录学习笔记
- 总结漏洞原理
- 记录踩过的坑
- 建立自己的 exp 模板库
循序渐进
- 先做简单题,建立信心
- 逐步增加难度
- 不要一开始就挑战高难度
善用社区
- CTF Wiki、看雪论坛、CTF Time
- 不懂就问,但先自己思考
8.3 推荐资源
入门书籍/文档:
- 《CTF 竞赛权威指南 PWN 篇》
- CTF Wiki: https://ctf-wiki.org/
- pwn.college: https://pwn.college/
练习平台:
- pwnable.kr
- pwnable.tw
- BUUCTF
- 攻防世界
社区:
- CTF Time
- 看雪论坛
- 各大战队博客
结语
PWN 的学习是一个漫长但有趣的过程。当你第一次成功获取 Shell 时,那种成就感是无与伦比的。
记住:
理解原理比记忆技巧更重要。
每一次失败都是向成功迈进的一步。
保持好奇心,享受这个过程。
祝你在 PWN 的世界里玩得开心!🚀