PWN 新手入门指南:从零到理解本质

写在前面:这份指南的目标是让你真正理解 PWN 是什么,而不是让你背诵一堆技术名词。当你读完这份文档,你应该能回答:我在学什么?为什么要学这些?学完能做什么?


目录


第一章:PWN 的本质——你到底在学什么

1.1 一句话定义

PWN = 利用程序的漏洞,让程序执行它本不应该执行的操作。

最常见的目标是:获取 Shell(也就是获得命令行控制权,可以在目标机器上执行任意命令)。

1.2 为什么叫 “PWN”?

“PWN” 来自游戏圈的俚语 “own”(拥有/控制),表示完全控制了对方。在安全领域,PWN 一台机器意味着你获得了它的控制权。

1.3 PWN 的本质是什么?

让我用一个比喻来解释:

想象一个银行金库,有一套严格的安全流程:

  1. 验证身份
  2. 检查权限
  3. 打开特定的保险箱
  4. 取出/存入物品
  5. 关闭保险箱
  6. 记录日志

正常情况:员工按流程操作,一切井然有序。

PWN 做的事:找到流程中的漏洞,比如:

  • 发现身份验证可以被绕过(认证绕过)
  • 发现可以让系统”忘记”关闭保险箱(内存泄漏)
  • 发现可以修改”下一步操作”的指示牌,让员工走向金库总控室(控制流劫持)

在计算机中

  • 银行 = 程序
  • 安全流程 = 程序的执行流程
  • 保险箱 = 内存中的数据
  • 金库总控室 = 系统权限

1.4 PWN 的最终目标

┌─────────────────────────────────────────────────────────────┐
│                      PWN 的目标层次                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Level 1: 信息泄露                                          │
│   └── 读取本不应该读取的数据(密码、密钥、地址)                   │
│                                                             │
│   Level 2: 拒绝服务                                          │
│   └── 让程序崩溃,无法正常服务                                  │
│                                                             │
│   Level 3: 任意代码执行 ⭐ (这是 PWN 的核心目标)                │
│   └── 让程序执行你指定的代码(通常是获取 Shell)                  │
│                                                             │
│   Level 4: 权限提升                                          │
│   └── 从普通用户变成 root/管理员                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

在 CTF 比赛中,最常见的目标是:

  1. 获取 Shell,然后读取 flag 文件
  2. 或者直接利用漏洞读取 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 信号处理机制:

  1. 信号到来时,内核将所有寄存器保存到用户栈(Signal Frame)
  2. 执行信号处理函数
  3. 调用 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 学习循环                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│         学习理论 ──────> 做题实践 ──────> 总结记录           │
│              ↑                              │               │
│              └──────────────────────────────┘               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

具体建议

  1. 每道题都要调试

    • 不要只运行 exp,要用 GDB 单步跟踪
    • 理解每一步发生了什么
  2. 记录学习笔记

    • 总结漏洞原理
    • 记录踩过的坑
    • 建立自己的 exp 模板库
  3. 循序渐进

    • 先做简单题,建立信心
    • 逐步增加难度
    • 不要一开始就挑战高难度
  4. 善用社区

    • CTF Wiki、看雪论坛、CTF Time
    • 不懂就问,但先自己思考

8.3 推荐资源

入门书籍/文档

练习平台

  • pwnable.kr
  • pwnable.tw
  • BUUCTF
  • 攻防世界

社区

  • CTF Time
  • 看雪论坛
  • 各大战队博客

结语

PWN 的学习是一个漫长但有趣的过程。当你第一次成功获取 Shell 时,那种成就感是无与伦比的。

记住:

理解原理比记忆技巧更重要。
每一次失败都是向成功迈进的一步。
保持好奇心,享受这个过程。

祝你在 PWN 的世界里玩得开心!🚀