文章首发于安全客 ,本文由安全客原创发布
转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/201454
安全客 – 有思想的安全新媒体

0x01 前言

由于关于Kernel安全的文章实在过于繁杂,本文有部分内容大篇幅或全文引用了参考文献,若出现此情况的,将在相关内容的开头予以说明,部分引用参考文献的将在文件结尾的参考链接中注明。

Kernel的相关知识以及栈溢出在Kernel中的利用已经在Kernel Pwn 学习之路(一)给予了说明,本文主要介绍了Kernel中更多的利用思路以及更多的实例。

【传送门】:Kernel Pwn 学习之路(一)

0x02 关于x64下内核gdb连接失败的解决方案

我们在用GDB调试x64内核时可能会回显Remote 'g' packet reply is too long:的错误,形如:

image-20200320192124742

那么在网上查到的大多数解决方案都是使用源码重编译安装GDB,然后修改remote.c,将其从

if (buf_len > 2 * rsa->sizeof_g_packet)
    error (_("Remote 'g' packet reply is too long: %s"), rs->buf);

修改为:

if (buf_len > 2 * rsa->sizeof_g_packet) {
//error (_("Remote 'g' packet reply is too long: %s"), rs->buf);
    rsa->sizeof_g_packet = buf_len ;
    for (i = 0; i < gdbarch_num_regs (gdbarch); i++) {
        if (rsa->regs->pnum == -1)
            continue;
        if (rsa->regs->offset >= rsa->sizeof_g_packet)
            rsa->regs->in_g_packet = 0;
        else
            rsa->regs->in_g_packet = 1;
    } 
}

但事实上我们只需要在连接前使用GDB命令设置架构即可成功连接:

set architecture i386:x86-64:intel

0x03 关于4.15.*以上内核中kallsyms的新保护

首先,我们知道在/proc/kallsyms函数中将存放了大量关键的函数的真实地址,这无疑是十分危险的,而低版本内核也提供了一些保护措施如kptr_restrict保护,但是在4.15.*以上内核中,内核新增了一个保护机制,我们首先来跟进/source/kernel/kallsyms.c

/*
 * We show kallsyms information even to normal users if we've enabled
 * kernel profiling and are explicitly not paranoid (so kptr_restrict
 * is clear, and sysctl_perf_event_paranoid isn't set).
 *
 * Otherwise, require CAP_SYSLOG (assuming kptr_restrict isn't set to
 * block even that).
 */
int kallsyms_show_value(void)
{
    switch (kptr_restrict) {
    case 0:
        if (kallsyms_for_perf())
            return 1;
    /* fallthrough */
    case 1:
        if (has_capability_noaudit(current, CAP_SYSLOG))
            return 1;
    /* fallthrough */
    default:
        return 0;
    }
}

可以发现,在4.15.*以上内核中,kptr_restrict只有01两种取值,此处我们不对kptr_restrict=1的情况分析,继续跟进kallsyms_for_perf():

static inline int kallsyms_for_perf(void)
{
#ifdef CONFIG_PERF_EVENTS
    extern int sysctl_perf_event_paranoid;
    if (sysctl_perf_event_paranoid <= 1)
        return 1;
#endif
    return 0;
}

这里看到了,我们要同时保证sysctl_perf_event_paranoid的值小于等于1才可以成功的查看/proc/kallsyms,而在默认情况下,这个标志量的值为2

0x04 劫持重要结构体进行攻击

劫持tty struct控制程序流程

ptmx设备是tty设备的一种,当使用open函数打开时,通过系统调用进入内核,创建新的文件结构体,并执行驱动设备自实现的open函数。

我们可以在/source/drivers/tty/pty.c中找到它的相关实现(Line 786):

/**
 *  ptmx_open       -   open a unix 98 pty master
 *  @inode: inode of device file
 *  @filp: file pointer to tty
 *
 *  Allocate a unix98 pty master device from the ptmx driver.
 *
 *  Locking: tty_mutex protects the init_dev work. tty->count should
 *      protect the rest.
 *      allocated_ptys_lock handles the list of free pty numbers
 */

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结构体的申请在Line 47,通过tty_init_dev(ptm_driver, index);来实现的,那么经过交叉引用的查看可以发现这个函数在/source/drivers/tty/tty_io.c#L1292中实现:

struct tty_struct *tty_init_dev(struct tty_driver *driver, int idx)
{
    struct tty_struct *tty;
    int retval;

    /*
     * First time open is complex, especially for PTY devices.
     * This code guarantees that either everything succeeds and the
     * TTY is ready for operation, or else the table slots are vacated
     * and the allocated memory released.  (Except that the termios
     * may be retained.)
     */

    if (!try_module_get(driver->owner))
        return ERR_PTR(-ENODEV);

    tty = alloc_tty_struct(driver, idx);
    if (!tty) {
        retval = -ENOMEM;
        goto err_module_put;
    }

    tty_lock(tty);
    retval = tty_driver_install_tty(driver, tty);
    if (retval < 0)
        goto err_free_tty;

    if (!tty->port)
        tty->port = driver->ports[idx];

    WARN_RATELIMIT(!tty->port,
            "%s: %s driver does not set tty->port. This will crash the kernel later. Fix the driver!n",
            __func__, tty->driver->name);

    retval = tty_ldisc_lock(tty, 5 * HZ);
    if (retval)
        goto err_release_lock;
    tty->port->itty = tty;

    /*
     * Structures all installed ... call the ldisc open routines.
     * If we fail here just call release_tty to clean up.  No need
     * to decrement the use counts, as release_tty doesn't care.
     */
    retval = tty_ldisc_setup(tty, tty->link);
    if (retval)
        goto err_release_tty;
    tty_ldisc_unlock(tty);
    /* Return the tty locked so that it cannot vanish under the caller */
    return tty;

err_free_tty:
    tty_unlock(tty);
    free_tty_struct(tty);
err_module_put:
    module_put(driver->owner);
    return ERR_PTR(retval);

    /* call the tty release_tty routine to clean out this slot */
err_release_tty:
    tty_ldisc_unlock(tty);
    tty_info_ratelimited(tty, "ldisc open failed (%d), clearing slot %dn",
                 retval, idx);
err_release_lock:
    tty_unlock(tty);
    release_tty(tty, idx);
    return ERR_PTR(retval);
}

继续分析可以发现程序在Line 17通过alloc_tty_struct(driver, idx);来分配一个tty_struct结构体,经过交叉引用的查看可以发现这个函数在/source/drivers/tty/tty_io.c#L2800中实现:

struct tty_struct *alloc_tty_struct(struct tty_driver *driver, int idx)
{
    struct tty_struct *tty;

    tty = kzalloc(sizeof(*tty), GFP_KERNEL);
    if (!tty)
        return NULL;

    kref_init(&tty->kref);
    tty->magic = TTY_MAGIC;
    tty_ldisc_init(tty);
    tty->session = NULL;
    tty->pgrp = NULL;
    mutex_init(&tty->legacy_mutex);
    mutex_init(&tty->throttle_mutex);
    init_rwsem(&tty->termios_rwsem);
    mutex_init(&tty->winsize_mutex);
    init_ldsem(&tty->ldisc_sem);
    init_waitqueue_head(&tty->write_wait);
    init_waitqueue_head(&tty->read_wait);
    INIT_WORK(&tty->hangup_work, do_tty_hangup);
    mutex_init(&tty->atomic_write_lock);
    spin_lock_init(&tty->ctrl_lock);
    spin_lock_init(&tty->flow_lock);
    spin_lock_init(&tty->files_lock);
    INIT_LIST_HEAD(&tty->tty_files);
    INIT_WORK(&tty->SAK_work, do_SAK_work);

    tty->driver = driver;
    tty->ops = driver->ops;
    tty->index = idx;
    tty_line_name(driver, idx, tty->name);
    tty->dev = tty_get_device(tty);

    return tty;
}

程序最终的分配函数是kzalloc函数,该函数定义在/source/include/linux/slab.h#L686

/**
 * kzalloc - allocate memory. The memory is set to zero.
 * @size: how many bytes of memory are required.
 * @flags: the type of memory to allocate (see kmalloc).
 */
static inline void *kzalloc(size_t size, gfp_t flags)
{
    return kmalloc(size, flags | __GFP_ZERO);
}

可以看到,最后实际上还是调用了kmalloc函数。(关于kmalloc函数使用的slab分配器将会在之后的文章中给予说明)

kmalloc函数定义在/source/include/linux/slab.h#L487

/**
 * kmalloc - allocate memory
 * @size: how many bytes of memory are required.
 * @flags: the type of memory to allocate.
 *
 * kmalloc is the normal method of allocating memory
 * for objects smaller than page size in the kernel.
 *
 * The @flags argument may be one of:
 *
 * %GFP_USER - Allocate memory on behalf of user.  May sleep.
 *
 * %GFP_KERNEL - Allocate normal kernel ram.  May sleep.
 *
 * %GFP_ATOMIC - Allocation will not sleep.  May use emergency pools.
 *   For example, use this inside interrupt handlers.
 *
 * %GFP_HIGHUSER - Allocate pages from high memory.
 *
 * %GFP_NOIO - Do not do any I/O at all while trying to get memory.
 *
 * %GFP_NOFS - Do not make any fs calls while trying to get memory.
 *
 * %GFP_NOWAIT - Allocation will not sleep.
 *
 * %__GFP_THISNODE - Allocate node-local memory only.
 *
 * %GFP_DMA - Allocation suitable for DMA.
 *   Should only be used for kmalloc() caches. Otherwise, use a
 *   slab created with SLAB_DMA.
 *
 * Also it is possible to set different flags by OR'ing
 * in one or more of the following additional @flags:
 *
 * %__GFP_HIGH - This allocation has high priority and may use emergency pools.
 *
 * %__GFP_NOFAIL - Indicate that this allocation is in no way allowed to fail
 *   (think twice before using).
 *
 * %__GFP_NORETRY - If memory is not immediately available,
 *   then give up at once.
 *
 * %__GFP_NOWARN - If allocation fails, don't issue any warnings.
 *
 * %__GFP_RETRY_MAYFAIL - Try really hard to succeed the allocation but fail
 *   eventually.
 *
 * There are other flags available as well, but these are not intended
 * for general use, and so are not documented here. For a full list of
 * potential flags, always refer to linux/gfp.h.
 */
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
    if (__builtin_constant_p(size)) {
        if (size > KMALLOC_MAX_CACHE_SIZE)
            return kmalloc_large(size, flags);
#ifndef CONFIG_SLOB
        if (!(flags & GFP_DMA)) {
            int index = kmalloc_index(size);

            if (!index)
                return ZERO_SIZE_PTR;

            return kmem_cache_alloc_trace(kmalloc_caches[index],
                    flags, size);
        }
#endif
    }
    return __kmalloc(size, flags);
}

我们现在只需要明确,kmalloc其实是使用slab/slub分配器,现在多见的是slub分配器。这个分配器通过一个多级的结构进行管理。首先有cache层,cache是一个结构,里边通过保存空对象,部分使用的对象和完全使用中的对象来管理,对象就是指内存对象,也就是用来分配或者已经分配的一部分内核空间。

slab分配器严格按照cache去区分,不同cache的无法分配在一页内,slub分配器则较为宽松,不同cache如果分配相同大小,可能会在一页内。

那么我们若能通过UAF漏洞劫持一个tty_struct我们就能劫持其内部的所有函数指针,进而控制程序流程。

关于tty_struct的定义位于/source/include/linux/tty.h#L282

struct tty_struct {
    int magic;
    struct kref kref;
    struct device *dev;
    struct tty_driver *driver;
    const struct tty_operations *ops;
    int index;

    /* Protects ldisc changes: Lock tty not pty */
    struct ld_semaphore ldisc_sem;
    struct tty_ldisc *ldisc;

    struct mutex atomic_write_lock;
    struct mutex legacy_mutex;
    struct mutex throttle_mutex;
    struct rw_semaphore termios_rwsem;
    struct mutex winsize_mutex;
    spinlock_t ctrl_lock;
    spinlock_t flow_lock;
    /* Termios values are protected by the termios rwsem */
    struct ktermios termios, termios_locked;
    struct termiox *termiox;    /* May be NULL for unsupported */
    char name[64];
    struct pid *pgrp;       /* Protected by ctrl lock */
    struct pid *session;
    unsigned long flags;
    int count;
    struct winsize winsize;     /* winsize_mutex */
    unsigned long stopped:1,    /* flow_lock */
              flow_stopped:1,
              unused:BITS_PER_LONG - 2;
    int hw_stopped;
    unsigned long ctrl_status:8,    /* ctrl_lock */
              packet:1,
              unused_ctrl:BITS_PER_LONG - 9;
    unsigned int receive_room;  /* Bytes free for queue */
    int flow_change;

    struct tty_struct *link;
    struct fasync_struct *fasync;
    wait_queue_head_t write_wait;
    wait_queue_head_t read_wait;
    struct work_struct hangup_work;
    void *disc_data;
    void *driver_data;
    spinlock_t files_lock;      /* protects tty_files list */
    struct list_head tty_files;

#define N_TTY_BUF_SIZE 4096

    int closing;
    unsigned char *write_buf;
    int write_cnt;
    /* If the tty has a pending do_SAK, queue it here - akpm */
    struct work_struct SAK_work;
    struct tty_port *port;
} __randomize_layout;

我们接下来重点关注tty_struct -> ops,它的类型是const struct tty_operations,这个结构体的定义位于/source/include/linux/tty_driver.h#L253

struct tty_operations {
    struct tty_struct * (*lookup)(struct tty_driver *driver,
            struct file *filp, int idx);
    int  (*install)(struct tty_driver *driver, struct tty_struct *tty);
    void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
    int  (*open)(struct tty_struct * tty, struct file * filp);
    void (*close)(struct tty_struct * tty, struct file * filp);
    void (*shutdown)(struct tty_struct *tty);
    void (*cleanup)(struct tty_struct *tty);
    int  (*write)(struct tty_struct * tty,
              const unsigned char *buf, int count);
    int  (*put_char)(struct tty_struct *tty, unsigned char ch);
    void (*flush_chars)(struct tty_struct *tty);
    int  (*write_room)(struct tty_struct *tty);
    int  (*chars_in_buffer)(struct tty_struct *tty);
    int  (*ioctl)(struct tty_struct *tty,
            unsigned int cmd, unsigned long arg);
    long (*compat_ioctl)(struct tty_struct *tty,
                 unsigned int cmd, unsigned long arg);
    void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
    void (*throttle)(struct tty_struct * tty);
    void (*unthrottle)(struct tty_struct * tty);
    void (*stop)(struct tty_struct *tty);
    void (*start)(struct tty_struct *tty);
    void (*hangup)(struct tty_struct *tty);
    int (*break_ctl)(struct tty_struct *tty, int state);
    void (*flush_buffer)(struct tty_struct *tty);
    void (*set_ldisc)(struct tty_struct *tty);
    void (*wait_until_sent)(struct tty_struct *tty, int timeout);
    void (*send_xchar)(struct tty_struct *tty, char ch);
    int (*tiocmget)(struct tty_struct *tty);
    int (*tiocmset)(struct tty_struct *tty,
            unsigned int set, unsigned int clear);
    int (*resize)(struct tty_struct *tty, struct winsize *ws);
    int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
    int (*get_icount)(struct tty_struct *tty,
                struct serial_icounter_struct *icount);
    void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
    int (*poll_init)(struct tty_driver *driver, int line, char *options);
    int (*poll_get_char)(struct tty_driver *driver, int line);
    void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
    const struct file_operations *proc_fops;
} __randomize_layout;

通常,我们希望劫持ioctl这个函数指针。

分类: CTF

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注