4.11 利用 mprotect 修改栈权限
mprotect 函数
mprotect 函数用于设置一块内存的保护权限(将从 start 开始、长度为 len 的内存的保护属性修改为 prot 指定的值),函数原型如下所示:
#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);prot 的取值如下,通过
|可以将几个属性结合使用(值相加):PROT_READ:可写,值为 1
PROT_WRITE:可读, 值为 2
PROT_EXEC:可执行,值为 4
PROT_NONE:不允许访问,值为 0
需要注意的是,指定的内存区间必须包含整个内存页(4K),起始地址 start 必须是一个内存页的起始地址,并且区间长度 len 必须是页大小的整数倍。
如果执行成功,函数返回 0;如果执行失败,函数返回 -1,并且通过 errno 变量表示具体原因。错误的原因主要有以下几个:
EACCES:该内存不能设置为相应权限。这是可能发生的,比如 mmap(2) 映射一个文件为只读的,接着使用 mprotect() 修改为 PROT_WRITE。
EINVAL:start 不是一个有效指针,指向的不是某个内存页的开头。
ENOMEM:内核内部的结构体无法分配。
ENOMEM:进程的地址空间在区间 [start, start+len] 范围内是无效,或者有一个或多个内存页没有映射。
当一个进程的内存访问行为违背了内存的保护属性,内核将发出 SIGSEGV(Segmentation fault,段错误)信号,并且终止该进程。
例题
例题来自 2020 安网杯,pwn1 是相对简单对栈溢出,pwn2 在此基础上增加了 mprotect 的运用,同时还是一个静态编译的程序。下载地址
先来看 pwn1,这是一个 64 位的动态链接程序,开启了 Partial RELRO 和 NX。系统层面 ASLR 也是开启的。
主函数 main() 先调用 write() 打印字符串,然后进入存在栈溢出漏洞的 vul() 函数,read(0, &buf, 0x100uLL) 读入最多 0x100 字节到 0x80 大小的缓冲区。
总体思路就是栈溢出控制返回地址,执行 one-gadget。因此,我们还需要泄漏 libc 地址,程序里有 write() 函数可以利用。exp 如下所示:
pwn2 是一个 64 位的静态链接程序,开启了 Partial RELRO 和 NX。
由于静态链接程序的执行不再需要 libc,因此 ret2libc 类型的攻击手段就失效了,需要考虑注入 shellcode,但是又开启了 NX 保护,这时就需要使用本节所讲的 mprotect() 函数修改栈的可执行权限。
可以在程序里找到关键函数 _dl_make_stack_executable(),该函数内部调用了 mprotect(v3, dl_pagesize, (unsigned int)_stack_prot):
构造方法是在进入 _dl_make_stack_executable 函数之前,将全局变量 _stack_prot 设置为 7(可读可写可执行),同时将 rdi 设置为全局变量 __libc_stack_end 的值。如下所示:
调用 mprotect 前:
调用 mprotect 后,可以看到 0x00007ffef00bb000 到 0x00007ffef00bc000 的栈内存已经是 rwx 权限了:
接下来程序跳到 vul 函数,读入 shellcode 到栈上并执行,即可获得 shell。exp 如下所示:
参考资料
Last updated
Was this helpful?