ブログ未満のなにか

ブログなのか誰にも分からない

Blaze CTF 2018 blazeme writeup

はじめに

一人writeup advent calendarの1日目です。 1日1問分のwriteupを目標に頑張っていきます。 1日目の問題は、Blaze CTF 2018で出題された「blazeme」。 stack overflowを起点としたkernel exploit問題で初めての人にオススメです。

カーネルの情報 (セキュリティ機構など)

配布されたファイルは以下の通りで、blazeme.cが攻略対象のデバイスソースコードである。 run.shが起動スクリプトになっている。 カーネルのバージョンは4.15で、smepやsmapは無効になっている。

% ls
blazeme.c    bzImage      rootfs.ext2  run.sh

% cat run.sh
#!/bin/sh
/usr/bin/qemu-system-x86_64 -kernel bzImage -smp 1 -hda rootfs.ext2 -boot c -m 64M -append "root=/dev/sda nokaslr rw ip=10.0.2.15:10.0.2.2:10.0.2.2 console=tty1 console=ttyAMA0" -net nic,model=ne2k_pci -net user -nographic
WARNING: Image format was not specified for 'rootfs.ext2' and probing guessed raw.
         Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
         Specify the 'raw' format explicitly to remove the restrictions.
warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]

Welcome to Buildroot
buildroot login: blazeme
Password:
login: can't change directory to '/home/blazeme'
$ uname -a
Linux buildroot 4.15.0 #1 SMP Wed Apr 18 01:12:17 PDT 2018 x86_64 GNU/Linux
$ cat /proc/cpuinfo | grep flags
flags           : fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx lm nopl cpuid pni cx16 hypervisor lahf_lm svm 3dnowprefetch retpoline rsb_ctxsw vmmcall

解析

blazeme_init()で、blazemeデバイスを作成し登録している。 blazeme_fopsはblazemeに対する操作を定義しており、read, write, open, releaseが登録されている。 これらはカーネル空間(カーネルモジュール)の関数なので、ユーザ空間から呼び出すには、

  • blazeme_open() => open('/dev/blazeme', flag, mode);
  • blazeme_close() => cloes(blazeme_fd);
  • blazeme_read() => read(blazeme_fd, buf, size);
  • blazeme_write() => write(blazeme_fd, buf, size);

といった感じになる。(blazeme_fdはopen()で得られたfd、他の引数については適切なものを設定)

blazeme_write()では、kmalloc(64)で取得した領域にユーザ領域からcopy_from_user()で書き込みを行っている。 その後、strncat()でカーネルのスタックへと書き込んでいる。

脆弱性はblazeme_write()のstrncat()でstack overflowとなることである。 今回のカーネルはSLUBアロケータを使用している。 SLUBアロケータでのチャンクはglibc mallocのチャンクでのサイズに相当するメタデータは存在しない。 そのため取得した領域を一杯まで埋めてstrlen()をすると、次のチャンクの内容も巻き込んで長さが求められる。 blazeme_write()でkmalloc()で取得されたポインタはエラーがなければkfree()されないので、複数回blazeme_write()を呼び出すことでチャンクを隣接して配置できる可能性がある。 上手くチャンクが隣接し、blazeme_write()のローカル変数として用意されたスタックのバッファのサイズを超えればstack overflowが発生する。

#include <linux/module.h>
#include <linux/version.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/slab.h>

#define DEVICE_NAME "blazeme"

#define ERR_BLAZEME_OK (1)
#define ERR_BLAZEME_MALLOC_FAIL (2)

#define KBUF_LEN (64)

dev_t dev = 0;
static struct cdev cdev;
static struct class *blazeme_class;

ssize_t blazeme_read(struct file *file, char __user *buf, size_t count,
                                loff_t *ppos);
ssize_t blazeme_write(struct file *file, const char __user *buf,
        size_t count, loff_t *ppos);

int blazeme_open(struct inode *inode, struct file *file);
int blazeme_close(struct inode *inode, struct file *file);

char *kbuf;

struct file_operations blazeme_fops =
{
    .owner           = THIS_MODULE,
    .read            = blazeme_read,
    .write           = blazeme_write,
    .open            = blazeme_open,
    .release         = blazeme_close,
};

ssize_t blazeme_read(struct file *file, char __user *buf, size_t count,
                                loff_t *ppos) {
    int len = count;
    ssize_t ret = ERR_BLAZEME_OK;

    if (len > KBUF_LEN || kbuf == NULL) {
        ret = ERR_BLAZEME_OK;
        goto out;
    }

    if (copy_to_user(buf, kbuf, len)) {
        goto out;
    }

    return (ssize_t)len;

out:
    return ret;
}

ssize_t blazeme_write(struct file *file,
                        const char __user *buf,
                        size_t count, loff_t *ppos) {
    char str[512] = "Hello ";
    ssize_t ret = ERR_BLAZEME_OK;

    if (buf == NULL) {
        printk(KERN_INFO "blazeme_write get a null ptr: buffer\n");
        ret = ERR_BLAZEME_OK;
        goto out;
    }

    if (count > KBUF_LEN) {
        printk(KERN_INFO "blazeme_wrtie invaild paramter count (%zu)\n", count);
        ret = ERR_BLAZEME_OK;
        goto out;
    }

    kbuf = NULL;
    kbuf = kmalloc(KBUF_LEN, GFP_KERNEL);
    if (kbuf == NULL) {
        printk(KERN_INFO "blazeme_write malloc fail\n");
        ret = ERR_BLAZEME_MALLOC_FAIL;
        goto out;
    }

    if (copy_from_user(kbuf, buf, count)) {
        kfree(kbuf);
        kbuf = NULL;
        goto out;
    }

    if (kbuf != NULL) {
        strncat(str, kbuf, strlen(kbuf));
        printk(KERN_INFO "%s", str);
    }

    return (ssize_t)count;

out:
    return ret;
}

int blazeme_open(struct inode *inode, struct file *file) {
    return 0;
}

int blazeme_close(struct inode *inode, struct file *file) {
    return 0;
}

int blazeme_init(void) {
    int ret = 0;

    ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
    if (ret) {
        printk("blazeme_init failed alloc: %d\n", ret);
        return ret;
    }

    memset(&cdev, 0, sizeof(struct cdev));

    cdev_init(&cdev, &blazeme_fops);
    cdev.owner = THIS_MODULE;
    cdev.ops = &blazeme_fops;

    ret = cdev_add(&cdev, dev, 1);
    if (ret) {
        printk("blazeme_init, cdev_add fail\n");
        return ret;
    }

    blazeme_class = class_create(THIS_MODULE, DEVICE_NAME);
    if (IS_ERR(blazeme_class)) {
        printk("blazeme_init, class create failed!\n");
        return ret;
    }

    dev = device_create(blazeme_class, NULL, dev, NULL, DEVICE_NAME);
    if (IS_ERR(&cdev)) {
        ret = PTR_ERR(&cdev);
        printk("blazeme_init device create failed\n");

        class_destroy(blazeme_class);
        cdev_del(&cdev);
        unregister_chrdev_region(&dev, 1);

        return ret;
    }

    return 0;
}

void blazeme_exit(void)
{
    cdev_del(&cdev);
    class_destroy(blazeme_class);
    unregister_chrdev_region(&dev, 1);
}

module_init(blazeme_init);
module_exit(blazeme_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("BLAZECTF 2018 crixer");
MODULE_DESCRIPTION("BLAZECTF CTF 2018 Challenge Kernel Module");

exoloit

今回はcanaryもないので、stack overflowでリターンアドレスを簡単に書き換えることができる。 kaslr、smap、smepが無効となっているので、ret2usrが容易にできる。 ただ今回は練習を兼ねて、stack pivotからのROPを行なった。 切り替え先の偽のスタックをユーザ空間に予め用意してROPガジェットを書き込んでおき、任意のROPに持ち込む。 ROPでは、commit_creds(prepare_kernel_cred(0));を呼んでroot権限を得てから、swapgsとiretqでユーザ空間へ戻っている。 stack pivotのガジェットのアドレスで埋めたバッファで、write()を無限に呼ぶことでstack overflowを起こした。

gist.github.com

f:id:hama7230:20181127021431p:plain

参考リンク