CTFするぞ

CTF以外のことも書くよ

SECCON 2018 OnlineのWriteup

はじめに

2018年10月27日15:00から28日15:00(JST)にオンラインで開かれたSECCON 2018 Onlineにチームinsecureとして参加しました.結果としては全体で56位でした.

f:id:ptr-yudai:20181028190855p:plain

私が解いたのはUnzip, History, QRChecker, Runme, block, Boguscrypt, mnemonicです.チームメイトの力を借りて解いた部分もありますが,それも含めてWriteupを書こうと思います. 解いた問題のファイルは以下に置いたので,参考にしてください.

seccon2018 - Google ドライブ

チームメイトのふるつきのWriteupはこちら.

furutsuki.hatenablog.com

[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のときは下のように見えますが,

f:id:ptr-yudai:20181028193011p:plain

250x250に縮小すると下のようになります.予想通り大きいQRコードのファインダパターンは消滅しています.

f:id:ptr-yudai:20181028193021p:plain

[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を抽出すると,次のような画像が出てきました. f:id:ptr-yudai:20181028205422p:plain これを送りましたが不正解で,運営に連絡したところもうちょっと頑張れと言われました. 他に手掛かりが無かったので,たぶん内部で文字の順番を入れ替えてるんだろうなぁと思いました. 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でこんな報告が.

f:id:ptr-yudai:20181118191601p:plain

thrustから次のリンクを貰い,これにより解くことができました.

github.com

同じリポジトリに次のようなコードがありました.

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が教えてくれた.

f:id:ptr-yudai:20181028195547p:plain

渡された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の方で決勝出場は確定しているので,あとは決勝に向けて勉強しようと思います. 参加者・運営のみなさん,お疲れ様でした.