从零到一:AttackLab缓冲区溢出攻击实战全解析 1. 缓冲区溢出攻击基础入门第一次接触缓冲区溢出攻击时我完全被那些专业术语吓到了。什么栈帧、返回地址、ROP链听起来就像天书一样。但当我真正动手操作后才发现这些概念其实就像搭积木一样简单直观。缓冲区溢出本质上就是数据装多了。想象你有一个容量固定的水杯缓冲区如果一直往里倒水输入数据超过容量的部分就会溢出来流到不该去的地方。在计算机中这个不该去的地方往往就是存储着关键程序信息的内存区域。AttackLab实验中的getbuf函数就是典型的缓冲区漏洞案例unsigned getbuf() { char buf[BUFFER_SIZE]; Gets(buf); return 1; }这个函数使用不安全的Gets方法读取输入它不会检查输入长度是否超过缓冲区大小BUFFER_SIZE。就像服务员不问你要多少水直接往杯子里倒直到你说停为止 - 但攻击者永远不会说停。在x86-64架构中栈是从高地址向低地址增长的。当函数被调用时会在栈上分配空间存储局部变量如buf然后是保存的寄存器值最后是返回地址。攻击者精心构造的超长输入可以覆盖这个返回地址从而控制程序执行流程。我第一次尝试phase1时用objdump反汇编ctargetobjdump -S ctarget ctarget.s在ctarget.s中查找getbuf函数发现它分配了0x2840字节的缓冲区空间。这意味着我们需要构造一个至少48字节的字符串40字节填满缓冲区 8字节覆盖返回地址。2. 阶段一最简单的返回地址劫持phase1是整个实验的Hello World它教会我们最基本的攻击模式 - 通过溢出修改返回地址。目标是将getbuf的返回地址从原本的test函数改为touch1函数。具体操作就像玩填字游戏先用40个任意字符填满buf缓冲区我习惯用00接着写入touch1的地址000000000040185d注意x86是小端序所以要倒着写5d 18 40 00 00 00 00 00把这段十六进制码保存为phase_1.txt然后用实验提供的hex2raw工具转换./hex2raw phase_1.txt test.txt ./ctarget test.txt -q看到Touch1!: You called touch1的瞬间我激动得差点从椅子上跳起来。这种亲手操控程序执行流程的感觉比看十篇理论文章都来得深刻。调试时有个实用技巧在gdb中设置断点观察栈变化gdb ./ctarget (gdb) break *getbuf (gdb) run -q test.txt (gdb) x/20x $rsp这个命令可以查看栈内存的前20个字能清晰看到我们的攻击字符串是如何覆盖返回地址的。3. 阶段二注入可执行代码phase2增加了难度要求不仅要跳转到touch2还要传递参数。这就需要在栈上注入可执行代码就像在数据区偷偷藏了个小程序。x86-64架构中第一个参数通过rdi寄存器传递。所以我们的攻击代码需要将cookie值如0x5134f5ad存入rdi跳转到touch2地址000000000040188b汇编代码attack1.s大致长这样mov $0x5134f5ad, %rdi push $0x40188b ret编译后用objdump查看机器码gcc -c attack1.s objdump -d attack1.o关键是要确定这段代码在栈上的位置。通过gdb调试在getbuf函数内打印$rsp的值就是buf的起始地址。把这个地址作为返回地址程序就会执行我们注入的代码。这里有个坑现代系统默认开启了NX不可执行栈保护但ctarget特意关闭了这个保护所以我们的代码才能执行。实际环境中这种直接注入代码的方式已经很难奏效了。4. 阶段三传递字符串参数phase3要求传递字符串形式的cookie作为参数。这就像phase2的升级版需要考虑字符串存储位置和内存布局。首先要把cookie 0x5134f5ad转换成ASCII码35 31 33 34 66 35 61 64注意末尾还要加\0。字符串可以放在getbuf的栈帧上方也就是test的栈帧里这样不会被后续操作覆盖。攻击代码需要计算字符串地址通常是getbuf的rsp 偏移量将地址存入rdi跳转到touch3通过gdb调试我发现test的栈帧起始于getbuf的rsp0x28。所以字符串可以放在rsp0x30处对应的攻击代码mov $0x5562fce8, %rdi # 字符串地址 push $0x4019a2 # touch3地址 ret这个阶段最考验耐心因为地址计算必须精确到字节。我失败了七八次才发现问题出在字符串末尾忘了加\0终止符。5. ROP攻击原理与实践phase4和phase5引入了ROPReturn-Oriented Programming技术这是现代绕过NX保护的经典方法。它就像用乐高积木拼装程序 - 从现有代码中找出有用的片段gadget通过ret指令把它们串起来。每个gadget通常以ret0xc3结尾形如58 pop %rax c3 ret这样的代码片段可以从farm.c提供的机器码中挖掘。使用objdump反汇编rtarget后在start_farm和end_farm之间搜索首先找pop %rax的gadget机器码58 c3然后找mov %rax,%rdi的gadget48 89 c7 c3最后跳转到touch2构造ROP链就像写购物清单[填充40字节] [gadget1地址] # pop %rax [cookie值] # 会被pop到rax [gadget2地址] # mov %rax,%rdi [touch2地址]实际调试时我花了三小时才找到可用的gadget组合。关键技巧是在gdb中单步执行观察每个gadget执行后寄存器的变化。6. 高级ROP链构造技巧phase5是终极挑战需要构造更复杂的ROP链来传递字符串参数。由于ASLR地址随机化的关系我们无法直接知道字符串在栈上的绝对地址必须通过相对计算得到。解决方案是用mov %rsp,%rax获取当前栈指针通过加法计算字符串偏移量lea指令最终传递到rdi从farm中挖掘的gadget链如下401ad1: mov %rsp,%rax 401a82: lea (%rdi,%rsi,1),%rax # 需要设置rsi为偏移量 401a4d: mov %rax,%rdi字符串偏移量需要精心计算。在我的实验中字符串距离rsp初始位置0x37字节所以构造[填充40字节] [gadget1] # mov %rsp,%rax [gadget2] # lea (%rdi,%rsi,1),%rax [占位符] # 实际是pop %rsi的gadget [0x37] # 偏移量 [gadget3] # mov %rax,%rdi [cookie字符串]这个阶段让我深刻理解了ROP的精髓 - 就像玩俄罗斯方块要把各种形状的gadget完美拼接才能达成目标。每次失败后调整gadget顺序和参数的过程就是对计算机系统理解不断加深的过程。