1. 栈溢出原理与基础调试
栈溢出,说白了就是往函数栈里塞了太多东西,把不该碰的内存给踩了。我刚开始学Pwn那会儿,觉得这玩意儿特玄乎,后来用GDB一步步跟下来,才发现其实就那么回事儿——你只要搞懂函数调用时栈长什么样、数据怎么放的,溢出攻击的思路就清晰了。
1.1 函数调用栈布局
每次调用一个函数,系统都会在栈上给它分配一块“临时工位”,这就是栈帧。一个典型的栈帧长这样:
| 栈地址(高→低) | 内容 | 说明 |
|---|---|---|
| 高地址 | 调用者栈帧 | 调用函数的局部变量和状态 |
| ↑ | 返回地址 (Return Address) | 函数结束后跳回哪里 |
| ↑ | 保存的 EBP / RBP | 上一帧基址指针 |
| ↑ | 局部变量区 | 当前函数的局部变量 |
| 低地址 | 缓冲区 (Buffer) | 比如 char buf[64] |
嗯,这里要注意:栈是从高地址向低地址生长的。你声明一个局部数组,索引0在低地址,索引n-1在高地址。而返回地址,就在局部变量区的“头顶”上。
核心记忆点:缓冲区越界写入时,数据从低地址往高地址覆盖。只要写够长度,就能一路捅到返回地址。
1.2 缓冲区溢出原理
为什么会有溢出?因为像 strcpy、gets、sprintf 这些C标准库函数,根本不检查目标缓冲区有多大。你输入100个字符,它照单全收,剩下的就溢到栈上别的地方去了。
我印象很深,有一次在项目中审计一个网络服务,发现开发者用了 gets(buf) 读用户输入。我当时就想,这简直是给攻击者铺红地毯啊。果然,顺着那条路,一个精心构造的payload就能改写返回地址,劫持程序流程。
溢出的典型后果:
- 覆盖局部变量——改变程序逻辑
- 覆盖保存的EBP——破坏栈回溯
- 覆盖返回地址——控制EIP/RIP,实现代码执行
避坑指南:我曾经在调试一个32位程序时,以为填充到返回地址只需要64字节,结果忘了考虑EBP占的4字节,导致payload一直没生效。记住:局部变量区 + 保存的EBP + 返回地址,这三段一个都不能少算。
1.3 使用GDB进行基础调试
GDB是Pwn手的瑞士军刀。我个人习惯用 pwndbg 或 gef 插件,它们能自动高亮关键信息,省不少事。但基础命令你得先练熟。
1.3.1 启动与反汇编
# 编译一个带符号的测试程序
gcc -m32 -fno-stack-protector -z execstack -no-pie -g -o vuln vuln.c
# 启动GDB
gdb ./vuln
# 查看main函数的汇编
(gdb) disassemble main
# 设置断点在函数入口
(gdb) b *main
# 运行
(gdb) r
1.3.2 查看寄存器与内存
程序断下来后,我最先看的就是寄存器:
# 查看所有寄存器
(gdb) info registers
# 重点关注
(gdb) p $eip # 当前指令指针
(gdb) p $esp # 栈顶指针
(gdb) p $ebp # 栈基址指针
# 查看栈内存(从esp开始,看32字节)
(gdb) x/8wx $esp
# 查看指定地址的内存
(gdb) x/16gx 0xffffd500
为什么会这样看?因为溢出攻击的本质就是:你控制了栈上的数据,最终要反映到EIP上。所以每次调试,我都会盯着EIP和栈顶那几行数据。
1.3.3 单步跟踪
# 单步执行一条指令
(gdb) si
# 单步跳过函数调用
(gdb) ni
# 执行到返回
(gdb) finish
# 查看函数调用栈
(gdb) backtrace
我的调试习惯:先用 disassemble 看清函数布局,然后在 ret 指令处下断点。执行到那里时,看一眼栈顶的值——那就是即将被赋给EIP的地址。如果那个地址是你输入的payload的一部分,恭喜,你已经成功劫持了控制流。
1.4 实战:手把手看一个溢出
假设我们有这么个漏洞函数:
void vulnerable() {
char buf[64];
gets(buf); // 危险!
}
编译后用GDB加载,输入一串 AAAA 加一些 BBBB,然后看崩溃:
(gdb) r
Starting program: ./vuln
AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEEFFFFFFFFFF
Program received signal SIGSEGV, Segmentation fault.
0x46464646 in ?? ()
看到 0x46464646 了吗?那是 FFFF 的ASCII码。说明我们输入的 F 覆盖了返回地址,程序试图跳转到 0x46464646 执行,自然就崩了。
这时候你只需要把 FFFF 替换成一个有效的地址(比如某个 jmp esp 的gadget地址),就能让程序跳转到你控制的shellcode上。
1.5 本章知识结构图
下面这张图把栈溢出调试的核心流程串起来了,建议你对照着GDB实际操作一遍:
1.6 本章小结
栈溢出不是魔法,就是一层窗户纸。你只要记住三点:
- 栈布局——局部变量、EBP、返回地址,从低到高排排坐
- 溢出点——gets、strcpy这些不检查长度的函数就是突破口
- 调试方法——GDB里盯紧EIP和栈顶,一切尽在掌握