はじめに
2018年10月27日15:00から28日15:00(JST)にオンラインで開かれたSECCON 2018 Onlineにチームinsecureとして参加しました.結果としては全体で56位でした.
私が解いたのはUnzip, History, QRChecker, Runme, block, Boguscrypt, mnemonicです.チームメイトの力を借りて解いた部分もありますが,それも含めてWriteupを書こうと思います. 解いた問題のファイルは以下に置いたので,参考にしてください.
チームメイトのふるつきのWriteupはこちら.
[Forensics 101] Unzip
問題文: Unzip flag.zip. 問題ファイル: unzip.zip_26c0cb5b40e9f78641ae44229cda45529418183f
makefile.shを見ると,zipのパスワードはtime関数で得られたタイムスタンプのようです.
$ cat makefile.sh echo 'SECCON{'`cat key`'}' > flag.txt zip -e --password=`perl -e "print time()"` flag.zip flag.txt
unzipコマンドで作成時刻が見れるので,このタイムスタンプ周辺でブルートフォースアタックします. タイムスタンプは次のように確認できます.
$ unzip -l flag.zip Archive: flag.zip Length Date Time Name --------- ---------- ----- ---- 32 10-27-2018 00:10 flag.txt --------- ------- 32 1 file $ date +%s -d "2018-10-27 00:10" 1540566600
次のpythonコードを使いました.
import zipfile f = zipfile.ZipFile("./flag.zip") time = 1540566600 while True: try: f.extractall(pwd=str(time)) break except KeyboardInterrupt, e: break except: pass time += 1
実行するとすぐに解凍が完了します.
$ cat flag.txt SECCON{We1c0me_2_SECCONCTF2o18}
[Forensics 145] History
問題文: History Check changed filename. file:J.zip_4c7050d70c9077b8c94ce0d76effcb8676bed3ba 問題ファイル: J.zip_4c7050d70c9077b8c94ce0d76effcb8676bed3ba
Jという名前の謎のファイルが渡されます.とりあえず見てみると,ファイル名がUnicodeでたくさん記載されています.
$ hexdump -C J 00000000 60 00 00 00 02 00 00 00 55 ed 00 00 00 00 23 00 |`.......U.....#.| 00000010 61 08 00 00 00 00 01 00 d0 73 3f 01 00 00 00 00 |a........s?.....| 00000020 a4 df f6 d3 62 49 d1 01 00 01 00 00 00 00 00 00 |....bI..........| 00000030 00 00 00 00 20 00 00 00 22 00 3c 00 6e 00 67 00 |.... ...".<.n.g.| 00000040 65 00 6e 00 5f 00 73 00 65 00 72 00 76 00 69 00 |e.n._.s.e.r.v.i.| 00000050 63 00 65 00 2e 00 6c 00 6f 00 63 00 6b 00 00 00 |c.e...l.o.c.k...| 00000060 60 00 00 00 02 00 00 00 a7 56 00 00 00 00 01 00 |`........V......| ...
まず,フラグが書かれてそうなファイル名を探すと,SECという文字でSEC.txtがヒットしました.
$ strings -t x -e l J | grep SEC ... 3c1faa <SECURITY.LOG1 3c2002 <SECURITY.LOG2 3ccf42 <SEC.txt 3ccf92 <SEC.txt 3ccfe2 <SEC.txt 3cd472 <SEC.txt 3e4dca <SECURITY.LOG1 3e4e22 <SECURITY 3e508a <SECURITY.LOG2 ...
このあたりをバイナリエディタで見ましたが,特にファイル内容が書かれているわけでもなかったので,今度は拡張子txtを探します.
... 3c13aa <logfile.txt.0 3c8392 <logfile.txt.0 3ccf42 <SEC.txt 3ccf92 <SEC.txt 3ccfe2 <SEC.txt 3cd472 <SEC.txt 3cd4c2 <CON{.txt 3cd512 <CON{.txt 3cee22 <CON{.txt 3cee72 <F0r.txt 3ceec2 <F0r.txt 3d085a <tktksec.txt 3d08b2 <tktksec.txt 3d090a <tktksec.txt 3d0ab2 <F0r.txt 3d0b02 <ensic.txt 3d0b52 <ensic.txt 3d0cca <ensic.txt 3d0d1a <s.txt 3d0d62 <s.txt 3d28f2 <s.txt 3d293a <_usnjrnl.txt 3d2992 <_usnjrnl.txt 3d2cea <_usnjrnl.txt 3d2d42 <2018}.txt 3d2d92 <2018}.txt 3e59da <logfile.txt.0
ファイル名がフラグの欠片になっていることが分かります.したがって,フラグは「SECCON{F0rensics_usnjrnl2018}」です. ということで,フラグが出るまで分かりませんでしたが,NTFSの$UsnJrnl領域のデータなのでしょう.
[QR 222] QRChecker
問題文: QR Checker http://qrchecker.pwn.seccon.jp/ 問題ファイル: qr.cgi_93bb1a11da93ab2a50e61c7da1e62b34d316bc9b
QRコードを読み取ってくれるのですが,与えた画像は500x500, 250x250, 100x100, 50x50に拡大・縮小され,それぞれ読み取られます. 読み取った結果,全体で2通り以上のデータが読み込めればフラグを表示してくれます. このとき,リーダは1つのサイズの画像から1つのデータを読み取るので,確実にフラグを得たければ1つのサイズに対して1つのデータが得られるように作る必要があります. 縮小するとき等間隔のピクセルデータのみが残り,他の部分の情報は失われるので,縮小により新しいQRを出現させることができます. この方法の良いところは,縮小時に大きいQRコードのファインダパターンが消失するので,1つのサイズに対して有効なQRコードを1つだけ存在させられるという点です.
import qrcode from PIL import Image def add_margin(pil_img, top, right, bottom, left, color): width, height = pil_img.size new_width = width + right + left new_height = height + top + bottom result = Image.new(pil_img.mode, (new_width, new_height), color) result.paste(pil_img, (left, top)) return result qr = qrcode.QRCode( box_size = 4, version = 20, error_correction = qrcode.constants.ERROR_CORRECT_H, border = 0 ) qr.add_data("A") qr.make(fit = True) img = qr.make_image(fill_color = "black", back_color = "white") img = add_margin(img, 0, 500 - img.size[0], 500 - img.size[0], 0, (255)) qr = qrcode.QRCode( box_size = 1, version = 1, error_correction = qrcode.constants.ERROR_CORRECT_H, border = 0 ) qr.add_data("B") qr.make(fit = True) img2 = qr.make_image(fill_color = "black", back_color = "white") img2 = add_margin(img2, 5, 5, 5, 5, (255)) for y in xrange(1, img2.size[0]): for x in xrange(1, img2.size[0]): img.putpixel((x*2+1, y*2+1), img2.getpixel((x, y))) img.resize((500, 500)).save("QR500.png") img.resize((250, 250)).save("QR250.png") img.resize((100, 100)).save("QR100.png") img.resize((50, 50)).save("QR50.png")
500x500のときは下のように見えますが,
250x250に縮小すると下のようになります.予想通り大きいQRコードのファインダパターンは消滅しています.
[Reversing 102] Runme
問題文: Run me. 問題ファイル: runme.exe_b834d0ce1d709affeedb1ee4c2f9c5d8ca4aac68
引数を1文字ずつチェックして,間違いがあれば終了するプログラムです. 1文字チェックするごとにcallして次のチェックに移るので確認が面倒です. 動的解析で愚直に1文字ずつ確認していったところ,次のような引数であればOKのようです.
C:\Temp\SECCON2018Online.exe SECCON{Runn1n6_P47h
ということで,フラグは「SECCON{Runn1n6_P47h」です. こういうのをちゃんとプログラムに解析させるのが強い人なんだろうなぁ.
[Reversing 362] block
問題文: BREAK THE BLOCK! Hint: Real answer flag is not easy to get it, if your post flag is incorrect, it is not real one. Please try to analyze more. 問題ファイル: block.apk_f2f0a7d6a3b3e940ca7cd5a3f7c5045eb57f92cf
Unity製のandroidアプリで,実行すると正方形の後ろでフラグっぽいのが高速回転しています.
Unity3D Unpackerでassetを抽出すると,次のような画像が出てきました.
これを送りましたが不正解で,運営に連絡したところもうちょっと頑張れと言われました.
他に手掛かりが無かったので,たぶん内部で文字の順番を入れ替えてるんだろうなぁと思いました.
il2cppを解析するのも面倒なので放置していたのですが,「34CH+3R」は「CH34+3R」なのでは,と思いついて「SECCON{Y0U_4R3_34CH+3R?}」を
「SECCON{Y0U_4R3_CH34+3R?}」とか「SECCON{4R3_Y0U_CH34+3R?}」に変えて送ってみたら,後者が正解でした.
29チームしか解いてないけど同じようなことした人は他にもいると確信しています.
[Crypto 162] Boguscrypt
問題文: Boguscrypt Hey, Can you decrypt the file? 問題ファイル: Boguscrypt.zip_3d8f4d6495e291543d48fcbdaccecf7127d16fae
flag.txt.encryptedと,それをデコードするdecというプログラムがあり,さらに謎のchallenge.pcapが存在します. decの処理を読み,pythonに直すと次のようになっていました.
with open("flag.txt.encrypted", "rb") as f: encrypted = f.read() output = "" for (i, e) in enumerate(encrypted): d = ord(e) ^ ord(a[i % len(a)]) output += chr(d) print output
ここで,aはgethostbyaddrで得たホスト名(を反転したもの)です. pcapにエンコード時のホスト名が無いかを探すと,DNSパケットに書いてありました.
00000000 af 5e 01 00 00 01 00 00 00 00 00 00 01 32 01 30 .^...... .....2.0 00000010 01 30 03 31 32 37 07 69 6e 2d 61 64 64 72 04 61 .0.127.i n-addr.a 00000020 72 70 61 00 00 0c 00 01 rpa..... 00000000 af 5e 85 80 00 01 00 01 00 01 00 02 01 32 01 30 .^...... .....2.0 00000010 01 30 03 31 32 37 07 69 6e 2d 61 64 64 72 04 61 .0.127.i n-addr.a 00000020 72 70 61 00 00 0c 00 01 c0 0c 00 0c 00 01 00 09 rpa..... ........ 00000030 3a 80 00 18 16 63 75 72 31 30 75 73 34 6e 64 6c :....cur 10us4ndl 00000040 30 6e 67 68 30 73 74 6e 34 6d 33 00 c0 12 00 02 0ngh0stn 4m3..... 00000050 00 01 00 09 3a 80 00 0b 09 6c 6f 63 61 6c 68 6f ....:... .localho 00000060 73 74 00 c0 58 00 01 00 01 00 09 3a 80 00 04 7f st..X... ...:.... 00000070 00 00 01 c0 58 00 1c 00 01 00 09 3a 80 00 10 00 ....X... ...:.... 00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 ........ .......
ということで,これを使えばデコードできます.
with open("flag.txt.encrypted", "rb") as f: encrypted = f.read() a = "cur10us4ndl0ngh0stn4m3"[::-1] output = "" for (i, e) in enumerate(encrypted): d = ord(e) ^ ord(a[i % len(a)]) output += chr(d) print output
この反転処理を忘れていて,チームに上手くいかなかった報告をしたところ,ふるつきが光速で修正して一足先に答えられてしまいました.
$ python solve.py SECCON{This flag is encoded by bogus routine}
[Crypto 260] mnemonic
問題文: Read me. 問題ファイル: mnemonic.txt
32バイトの16進数文字,謎のひらがな羅列,64バイトの16進数文字が記載されたリストのセットが渡されます. 最初はまったく分からず,mnemonicなので日本語の文字コードから0x3000を引くと機械語になるのでは,とか考えてやっていました. するとthrustからslackでこんな報告が.
thrustから次のリンクを貰い,これにより解くことができました.
同じリポジトリに次のようなコードがありました.
vcoin/mnemonic-test.js at master · vertcoin-project/vcoin · GitHub
どうやら渡されたmnemonic.txtにはentropy, phrase, passphraseが記載されているようです. 今回は,entropyの先頭と一部欠落したphraseからentropyを全部戻すという問題のようです. mnemonic-test.jsに
assert.bufferEqual(mnemonic.getEntropy(), entropy);
というものがあったので,MnemonicクラスのgetEntropyメソッドを探すことにしました. vcoinはbcoinのforkだったので,元のリポジトリに検索をかけて次のコードを見つけました.
bcoin/mnemonic.js at 85ed59c842f2aa4c1a8154d74a9cad7696cdfee3 · bcoin-org/bcoin · GitHub
このコードを読んでいると,getEntropyはどうでもよくて,fromPhraseが鍵であることが分かりました. これはphraseからentropyを求めるメソッドなので,まさに今回探しているものです. fromPhraseをpythonで書き直すを次のようになります.
# coding: utf-8 import json import math from words import wordlist n = 1 data = json.load(open("mnemonic.txt", "rb"))["japanese"] entropy = data[n][0].decode("hex") bits = len(entropy) * 8 password = data[n][2] words = data[n][1].split(u" ") for i in range(len(words)): words[i] = words[i].encode("utf-8") wbits = len(words) * 11 cbits = wbits % 32 bits = wbits - cbits size = int(math.ceil(wbits / 8)) data = [0 for i in range(size)] for i in xrange(len(words)): word = words[i] index = wordlist.index(word) for j in range(11): pos = i * 11 + j bit = pos % 8 octo = (pos - bits) / 8 val = (index >> (10 - j)) & 1 data[octo] |= val << (7 - bit) cbytes = int(math.ceil(cbits / 8)) entropy = data[1:len(data) - cbytes + 1] ans = "" for c in entropy: ans += hex(c)[2:] print(ans)
なお,words.pyには日本語の単語一覧が書かれています.これはyoshikingが教えてくれた.
渡されたmnemonic.txtの1つ目や2つ目のphraseから正しくentropyが求められました. 3つ目のphraseは最初の1つが欠落しているので,wordlistから順番に当てはめて,求めたentropyの先頭がmnemonic.txtのものと一致すれば出力します.
# coding: utf-8 import json import math from words import wordlist from pwn import * n = 2 data = json.load(open("mnemonic.txt", "rb"))["japanese"] #entropy = data[n][0].decode("hex") #bits = len(entropy) * 8 #password = data[n][2] words = data[n][1].split(u" ") for i in range(len(words)): words[i] = words[i].encode("utf-8") for target in wordlist: words[0] = target wbits = len(words) * 11 cbits = wbits % 32 bits = wbits - cbits size = int(math.ceil(wbits / 8)) data = [0 for i in range(size)] for i in xrange(len(words)): word = words[i] index = wordlist.index(word) for j in range(11): pos = i * 11 + j bit = pos % 8 octo = (pos - bits) / 8 val = (index >> (10 - j)) & 1 data[octo] |= val << (7 - bit) cbytes = int(math.ceil(cbits / 8)) entropy = data[1:len(data) - cbytes + 1] ans = "" for c in entropy: ans += hex(c)[2:].zfill(2) if ans[:3] == "c0f": print(ans) print(md5sumhex(ans)) break
答えはentropyのmd5なので,「SECCON{cda2cb1742d1b6fc21d05c879c263eec}」となります.
$ python solve4.py c0f4d6b07a192ac251d4ee2a34d5f1977d549a2e6d7cbaf9b09485b379cd3f70 cda2cb1742d1b6fc21d05c879c263eec
感想
チーム内ではForensicsやStegano, Pwnを担当していましたが,Pwn全く手が出なかったのは反省です. Mediaに時間を取られすぎたせいで,せっかくshooterのSQLiまでたどり着いたのに時間が足りませんでした. (捨てる問題と解ける問題を見極められる力が足りない.) でもチーム内では活躍できたと思うし,担当のForensicsとQRを瞬殺できたのは個人的には満足です. KOSEN SECCONの方で決勝出場は確定しているので,あとは決勝に向けて勉強しようと思います. 参加者・運営のみなさん,お疲れ様でした.