InnoCTF 2019 writeup

2019/07/13-2019/07/14にかけてInnoCTFに参加しました!

 

なんだかチーム登録者以外のメンバーにメールは送られてこないわ,問題サーバはすぐ落とすわ,ごにょごにょ…

 

問題名を覚えてませんがぼくはHex64とIt matchesと猫のやつのPPC3問,レールフェンス暗号のやつのCrypto1問,Quick peekとCrackmeのrev2問を解きました.レールフェンス暗号のやつはソルバーにぶん投げるかテキストエディタで工夫するかすればよいので,省略します.



Hex64

50MBytes以上のデカいテキストファイルが渡されます.見たところbase64エンコードされている文字列なのでまた何度もbase64デコードするのかと思いきや,デコードすると6d6d6d…といったようにascii文字列が16進数のasciiコードでエンコードされた文字列が出てきます.これをasciiデコードするにはxxdというコマンドが便利で,xxd -p -rで通常の文字列に戻せます.これをするとまたbase64エンコードされている文字列が現れます.おそらくcat Hex64.txt | base64 -d | xxd -p -rが1サイクルで,これを何回も繰り返せばフラグを得られると考えました.よしシェルスクリプトの勉強だ!と思いましたが僕にはまだシェルスクリプトは早かったみたいです.

import base64

def xxd(text: str):
    length = len(text)
    ret_text = ""

    for i in range(length // 2):
        ret_text += chr(int(text[2 * i] + text[2 * i + 1], 16))

    return ret_text

def main():
    f = open("Hex64.txt", 'r')
    text = f.read().split()[0]

    while True:
        if text.find('{') > 0:
            break

        text = base64.b64decode(text.encode('utf-8')).decode('utf-8')
        text = xxd(text)

        print("[+] progress: text length = %d" % len(text))

    print(text)

if __name__ == "__main__":
    main()

プログラムでは,ある程度上述のサイクルを繰り返したらフラグが出てくると踏んで,'{'がデコード後文字列に出てきたら終了,としています.progressは,1サイクルごとにテキストの長さを示します.実行してから終了までにおよそ1分かかりました.
f:id:verliezer93764:20190716081115p:plain
InnoCTF{PpC_17_pl34se}

It matches

InnoCTF{noW4y_Y0u_c4N_s0lVe_I7}
InnoCTF{nOw4y_y0U_c4N_soLv3_1T}
InnoCTF{R3gexp_th1S_w0rlD}
・・・というようなテキストファイルが渡されます.本物のフラグは,2単語でできており,1つめは大文字+小文字+数字,2つ目は大文字+数字で構成されているようです.脳筋でやりました.

f = open("matches.txt", 'r')
flag_list = f.readlines()

for flag in flag_list:
    full_content = flag[flag.index('{')+1:flag.index('}')]
    contents = full_content.split('_')

    is_success = True
    if len(contents) != 2:
        is_success = False
    
    for c in contents[0]:
        if not (ord('a') <= ord(c) <= ord('z') or ord('0') <= ord(c) <= ord('9') or ord('A') <= ord(c) <= ord('Z')):
            is_success = False

    for c in contents[1]:
        if not (ord('0') <= ord(c) <= ord('9') or ord('A') <= ord(c) <= ord('Z')):
            is_success = False
    
    if is_success:
        print(flag)
 

実行すると,フラグが見つかりました.InnoCTF{P0werful_REG3XP}

猫のやつ

46470-46479のうちいずれかのポートに接続すると猫のAAと,次にこれを表示するポート番号およびフラグの一部(2文字)が表示されますが,それ以外では,No cats foundと表示されます.つまり,まず猫がいるポート番号を探し,次に移動するポート番号を見て追いかけながらフラグを集める必要があります.猫がポートにとどまっている時間は約5秒で,それも考慮する必要があります.

import socket
import sys
import time

def find_part_flag_and_port(message: str):
    message_split = message.split('\n')

    cologne_index = message_split[0].find(':')
    flag_part = message_split[0][cologne_index+1:]
    cologne_index = message_split[1].find(':')
    next_port = int(message_split[1][cologne_index+1:])
    
    return flag_part, next_port

def main():
    host = "188.130.155.66"

    # search cat
    for port in range(46470, 46480):
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((host, port))

        message = s.recv(1024).decode('utf-8')

        if message.find("No cats here...") == -1:
            print("[+] Cat found at port %d!" % port)
            break
        
        if port == 46479:
            print("[+] Search failed...")
            sys.exit(0)
        print("[+] Cat not found at port %d..." % port)

    print("[+] Capturing the flag...")
    message = s.recv(1024).decode('utf-8')
    s.close()

    message_split = message.split('\n')
    flag_part, next_port = find_part_flag_and_port(message)
    print(flag_part, end="", flush=True)
    time.sleep(5)

    while True:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((host, next_port))

        message = s.recv(1024)  # AA of a cat
        time.sleep(1)
        message = s.recv(1024).decode('utf-8')
        s.close()

        flag_part, next_port = find_part_flag_and_port(message)
        print(flag_part, end="", flush=True)
        time.sleep(3)

if __name__ == "__main__":
    main()

実行結果です.セッション確立までの時間もあるため,このプログラムでは次のポートへ早くいきすぎたりしてNo cats foundになり,splitで1つにしかテキストが分割できずエラーになります.また,cdが被っています.あと少しの工夫でうまくいったんですがね…
f:id:verliezer93764:20190716113647p:plain
InnoCTF{49fe100103cbc466c7ba22c373636a56d}

Quick_peek

32bitのexeファイルです.32bit版のdnSpyで逆コンパイル・解析できます.
Main関数を読むと,どうやらflag3という変数がtrue,すなわちpleaseDoNotLookHere.Nothing_interesting_here()関数の返り値がtrueであればいいようです.
f:id:verliezer93764:20190716121103p:plain
pleaseDoNotLookHere.Nothing_interesting_here()関数を見てみると,bという文字列変数をいじった後に,引数strと比べてbool値を返しています.
f:id:verliezer93764:20190716133045p:plain
赤○の部分はブレークポイントで,dnSpyの上部メニュー画面のStartボタンを押すことによってそこの行で止まります.
f:id:verliezer93764:20190716133721p:plain
入力とフラグの比較をしているのが丸見えです.
InnoCTF{1337_SPAgh377i_CoD3}

Crack me

exeファイルが渡されます.dnSpyは使用できません.
IDAを使用して開くと,サブルーチンがたくさんあります.入力用関数std::cinを探すと,sub_403630にあり,そこで判定を行っているっぽいです.
f:id:verliezer93764:20190717052005p:plain
f:id:verliezer93764:20190717050342p:plain
図中のsub_4032F0関数は,"Invalid key!"と表示してプログラムの終わりに向かう関数です.sub_4032F0関数に分岐する部分で全てそうではない方へと分岐すればよいのですね.しかし,分岐を判断するためのtest命令やcmp命令の前にいちいち独自の関数がありますね.僕もさすがに面倒です.ということで,ollydbgで各test命令にブレークポイントを設置し,入力によりその結果にかかわるレジスタにどのような変化が起こるのかを地道に見ていきました.しかし,プログラム中に"everse"や"fine",'r'や'i'のasciiコードが載っていて,予想はしやすいです.
結果,次のようなことがわかりました.
・正解の文字列は,InnoCTF{hogehohe}のhogehogeの部分
・sub_403310の機能は,引数に指定した文字の文字列におけるインデックスを検索するstrchrと同じであり,入力する文字列の中に'_'が2つ必要
・sub_4033F0の機能はstrlenと同じであり,正解文字列の文字列長は15
・1文字目から7文字目までは"reverse"
・9文字目は'i'
・9文字目と10文字目のasciiコードの和は0xdc,すなわち10文字目は's'
・12文字目から15文字目までは,"fine"を3だけ後ろにシフトした"ilqh"
あとは,'_'を"reverse"と"is"と"ilqh"の間に突っ込むと,"reverse_is_ilqh"になります!(??????)
フラグ出力では地味に'e'を'3'にしていますね.
f:id:verliezer93764:20190717054054p:plain
InnoCTF{InnoCTF{r3v3rs3_is_ilqh}}

おまけ

f:id:verliezer93764:20190717054230p:plain
なるほど,線繋ぎ問題ですね.1T_W4S_GO0DTAC71K5MEQN8,チームメンバーさん,ありがとうございますm(__)m
f:id:verliezer93764:20190717054822p:plain
あれ,いったいなんで通らないのでしょうか…??
f:id:verliezer93764:20190717054926p:plain

:thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face::thinking_face: