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实际操作一遍:

栈溢出调试核心流程 栈溢出调试核心流程 函数调用 栈帧分配(局部变量+EBP+返回地址) 输入溢出(覆盖返回地址) GDB调试(查看寄存器/内存) 控制EIP → 劫持程序流程 调试要点 1. disassemble 看函数布局 2. b *ret_addr 下断点 3. r 运行并输入payload 4. x/8wx $esp 看栈顶 5. p $eip 确认劫持 6. si 单步跟踪验证 常见坑: · 忘记算EBP占位 · 地址大小端搞反 · 栈对齐问题

1.6 本章小结

栈溢出不是魔法,就是一层窗户纸。你只要记住三点:

  1. 栈布局——局部变量、EBP、返回地址,从低到高排排坐
  2. 溢出点——gets、strcpy这些不检查长度的函数就是突破口
  3. 调试方法——GDB里盯紧EIP和栈顶,一切尽在掌握