InterKosenCTF Writeup
InterKosenCTF_2019
2019年8月11日から8月12日にかけて開催されたInterKosenCTFにチーム./vespiaryで参加しました。順位は得点を入れたチームに限定すると4位/91チームでした。
チームメイトが低レイヤー(Pwn, Rev)を並行してやってくれたのと私が寝てる間にWebを全部解いてくれたおかげで満足行く順位に達せました。
この記事以外にもGitHubにもソースコード付きでWriteupをあげています。
なお、公式の皆さんが全問Writeupを書いています(https://hackmd.io/@theoldmoon0602/H1QJUrgVr)
Challenges
challenges | genre | score |
---|---|---|
Hugtto! | Forensics | 238 |
Kurukuru Shuffle | Crypto | 200 |
Temple of Time | Forensics | 285 |
Lost World | Forensics | 303 |
Pascal Homomorphicity | Crypto | 333 |
saferm | Forensics | 434 |
Hugtto! (InterKosenCTF)
既存のツールを使うだけで解決するようなStegoでは無く独自形式のStego。埋め込む色はランダムで決定するが問題のソースを読むとシード値がコード実行時の時間だったのでファイルが作成された時間から少しずつずらすことで正しいシードを使うことができると思われる。
score | genre |
---|---|
238 | Forensics |
添付ファイル
steg.py
: steganographyを行うファイルsteg_emiru.png
: steg.pyによって生成された画像ファイル
手順
- 乱数によって画素のどの色に情報を仕込むか決まるがその仕込み方は
r = (r & 0xFE) | bin_flag[i % len(bin_flag)]
のように一定(r
は赤色、g
は緑色、b
は青色に関する値が入っている) - ここで8bit整数と
0xFE
の論理積を取るとこの整数は末尾1桁が0になる。 - また、
bin_flag[i]
は0
か1
であるので上記値は0|0 = 0
か0|1 = 1
となりbin_flag[i]
の値に依存する - 従ってどの色に仕込まれたのかさえ判明すれば
bin_flag[i]
の値を復元可能。復元方法は単純に色の値の最下位ビットの偶奇である random.seed(int(datetime.now().timestamp()))
よりこのファイルが作られる直前の時刻をシードとして乱数を生成していることからファイル生成時刻のUNIX時間、int(datetime(2019, 8, 6, 11, 44, 18).timestamp())
からこれより少し小さい値をシードとして乱数を生成するとこのファイルが作られた時と同じ埋め込み方が可能だと考えられる- 1秒ずつ早い時間にしていったところ3秒早い時刻で復号したところフラグのようなものが表示された
使用コード
from PIL import Image from datetime import datetime import random def bit_array_to_string(bit_array): try: if len(bit_array) != 8: raise ValueError("argument 'bit_array' must be 8 length" ) except ValueError as e: print(e) bit_str = "".join(list(map(str, bit_array))) return chr(int(bit_str, 2)) if __name__ == '__main__': img = Image.open("./steg_emiru.png") new_img = Image.new("RGB", img.size) w, h = img.size # 誤差が1sぐらいあるかもしれない seed_time = int(datetime(2019, 8, 6, 11, 44, 15).timestamp()) print(seed_time) random.seed(seed_time) i = 0 bit_array = [] current_bit_array = [] for x in range(w): for y in range(h): r, g, b = img.getpixel((x, y)) rnd = random.randint(0, 2) if rnd == 0: current_bit_array.append(r % 2) elif rnd == 1: current_bit_array.append(g % 2) elif rnd == 2: current_bit_array.append(b % 2) i += 1 if i % 8 == 0: bit_array.append(current_bit_array) current_bit_array = [] str_arr = [] for i, arr in enumerate(bit_array): if i < 100: str_arr.append(bit_array_to_string(list(reversed(arr)))) print("".join(str_arr))
Flag
KosenCTF{Her_name_is_EMIRU_AISAKI_who_is_appeared_in_Hugtto!PreCure}
KuruKuru Shuffle
3つのパラメータa, b, k
によって文字数回分の転置規則(何回目にどの文字とどの文字を転置するか)が定まる。これらのパラメータはどれもrange(len(flag))
の範囲内であるため、全通り考えたとしても高々10万通りとちょっと。よって総当りが可能でると考えられる。
score | genre |
---|---|
200 | Crypto |
添付ファイル
shuffle.py
: 暗号化する際のソースコードencrypted
: 暗号化されたファイル
手順
- 各パラメータ毎にどの文字とどの文字を何回目で置換するかを配列で格納する
- 各転置規則に対してそれを逆順に実行して復号を行う
- 復号結果に
KosenCTF{
を含むかを確認する
使用コード
from copy import copy def make_st_list(a, b, k, L): _list = [] i = k for _ in range(L): s = (i + a) % L t = (i + b) % L i = (i + k) % L _list.append([s, t]) return _list def dec(_enc_list, _st_list): for _ in range(len(_st_list)): st = _st_list.pop() s = st[0] t = st[1] _enc_list[s], _enc_list[t] = _enc_list[t], _enc_list[s] return "".join(_enc_list) if __name__ == '__main__': enc = "1m__s4sk_s3np41m1r_836lly_cut3_34799u14}1osenCTF{5sKm" L = len(enc) enc_list = list(enc) for a in range(L): for b in range(L): for k in range(1, L): st_list = make_st_list(a, b, k, L) tmp_enc_list = copy(enc_list) dec_str = dec(tmp_enc_list, st_list) if dec_str[0:9] == "KosenCTF{": print(dec_str)
Flag
KosenCTF{us4m1m1_m4sk_s3np41_1s_r34lly_cut3_38769915}
Lost World
vdiを渡されそれをVirtualbox等で読み込ませるとログインパスワードを要求される。ログインパスワード自体を入手するのは骨が折れそうなので内部の暗号化されたログイン情報が入っている/etc/shadow
を書き換えてログインできるようにする。なお、フラグ自体はログイン成功時のメッセージに入っているらしくログインに成功した後にdmesg | grep KosenCTF{
で抽出が可能。
score | genre |
---|---|
303 | Forensics |
添付ファイル
- ディスクイメージ(.vdi)
手順
- 下記参考文献に従って
root:
でヒットする箇所をこちらで用意した鍵に置き換える(今回は下記参考文献の物を流用したが、皆さんがお使いのLinuxの/etc/shadow
を流用して問題ないと思われる) - VirtualBoxで書き換え後のイメージを読み込み、起動
- 用意したパスワードを入力し
dmesg | grep KosenCTF{
をしてフラグを入手
Flag
KosenCTF{u_c4n_r3s3t_r00t_p4ssw0rd_1n_VM}
参考文献
write-ups/Hack.lu CTF 2015/Dr.Bob https://github.com/RandomsCTF/write-ups/tree/master/Hack.lu%20CTF%202015/Dr.%20Bob%20%5Bforensics%5D%20(150)
補足
後から運営の皆さんのTwitterを見て知ったのだが次のような非想定解があったらしい
lost worldはフラグを出力してるのがカーネルモジュールだから一回起動するとkern.logに記録されてstringsで解けるのか。bashrcとかに書くと取り出して解析する人が出そうだからカーネルモジュールにしたのが裏目に出たな...... #InterKosenCTF
— ptr-yudai (@ptrYudai) August 13, 2019
pascal homomorphicity
Paillier暗号で使われている合同式を利用した問題。最初にフラグを暗号化した数字が表示されその後ユーザー入力で渡された数字を暗号化する。求めるKeyが指数となっているが、nが判明しているなら、剰余を考えると離散対数問題を解かなくてもkeyを求めることができる。nはこちらからの入力によって求めることができるのでそれを利用すれば良い。
score | genre |
---|---|
333 | Crypto |
添付ファイル
- サーバー側で動いてるコード
方針
二項定理よりであることがわかる。これを使えばである時にとがわかっていればを求めることができる(逆も同様)。
今回のプログラムではサーバー側に小さい数字を渡すと、警告と共にの長さを教えてくれて、であったことからの条件が適用されるので剰余を考慮しなくても解くことができる。
手順
- 鯖に接続すると
pow(1 + n, key, n**2)
が表示される - 数値を入力すると
pow(1 + n, int(input), n**2)
が表示されるのでを利用しn
を導出する n
が求まったので最初に表示されたpow(1 + n, key, n**2)
からkey
を導出できる
当然ながらPythonを利用して解きましたがPythonターミナル上で全てを行ったので使用コードはありません。
Flag
KosenCTF{Th15_15_t00_we4k_p41ll1er_crypt05y5tem}
参考文献
saferm
最初Forensics(ディスクダンプの解析), 中盤Rev(抽出したバイナリの解析), 終盤Forensics(Zipのファイル構造から複合キーを割り出す)の3段構え複合問題。Revパートについてはチームメイトが秒殺してくれたので説明を割愛。
score | genre |
---|---|
434 | Forensics, Reversing |
添付ファイル
- ディスクダンプ
手順
- FTK Imagerにこのイメージを読ませると
saferm
というバイナリと削除されたdocument.zip
というファイルが見つかる saferm
をリバーシングすると(ここはチームメイトがやってくれた、感謝)、64bitのキーを用いて暗号化(encrypted[index] = file[index] ^ key[index % 64])
してから元のファイルを削除していることがわかる。- FTK Imagerは削除済みのファイルも吸い出せるので唯一削除済みと表示されたファイルである
document.zip
を抽出する。 - ここでzipファイルのシグネチャは頭4バイトは確定しており残りの4バイトもバージョン情報(単にdeflate圧縮であれば
14 00
)と圧縮形式ごとに用いる特殊ビット(特にオプション指定が無ければ00 00
)であることから容易に候補を絞ることができる
※この候補を絞る過程ではファイル名がdocument.pdf
であるという仮定も用いた - これにより幾つかキーの候補を絞り復号化を行うとあるキーでzipが開くことができ、中に入っていた
document.pdf
を見るとフラグが(但し、手元のアーカイバでは開かずチームメイトが使っているツールでは開くことができた、感謝)
※公式Writeupによれば暗号化の際、末尾の64bitに満たない部分は暗号化されないのでここだけ元のバイト列のままで良いらしい
使用コード
過去に出場したCTFでXORによる暗号化を実装していたので(https://github.com/Xornet-Euphoria/HSCTF_6/tree/master/Hidden_Flag)キーを変えて流用
if __name__ == '__main__': key = [46, 87, 173, 46, 255, 200, 202, 73] f = open('document.zip', 'rb') enc_bytes = f.read() f.close() f = open('dec.zip', 'wb') for i, byte in enumerate(enc_bytes): f.write((int(byte) ^ key[i % len(key)]).to_bytes(1, 'big')) f.close()
Flag
KosenCTF{p00r_shr3dd3r}
補足
公式Writeupによればディスクダンプの解析にThe Sleuth Kitというものを使っていた。復習時に使ってみたところ、割と使いやすかったので今後はこのような解析をする際に頻繁に使っていきたい
Temple of Time
ForensicsメインだがWebの知識も要求される複合問題。前述のsafermもそうだが強引な複合問題では無く非常に良問だった。
パケットを解析するとどうやらSQLインジェクションの痕跡が見える。それもTime-BasedのブラインドSQLインジェクションだと思われる。攻撃者は1文字ずつ情報を探索しているので調査している文字が切り替わったタイミングのパケットを読めば攻撃者が判明させた文字がわかる。
score | genre |
---|---|
285 | Forensics, Web |
添付ファイル
- pcapng
手順
- Wiresharkにpcapngを読ませ、プロトコルをHTTPに絞る
GET /index.php?portal=%27OR%28SELECT%28IF%28ORD%28SUBSTR%28%28SELECT+password+FROM+Users+WHERE+username%3D%27admin%27%29%2C1%2C1%29%29%3D48%2CSLEEP%281%29%2C%27%27%29%29%29%23 HTTP/1.1
というリクエストからTime-Based SQL injectionを行っているように思われる
なお、これをデコードするとGET /index.php?portal='OR(SELECT(IF(ORD(SUBSTR((SELECT+password+FROM+Users+WHERE+username='admin'),1,1))=48,SLEEP(1),'')))# HTTP/1.1
であることからこれは1文字目が文字コードで48('0')であるかを判定していると思われる- 該当パケットを解析しやすいようにCSV形式で出力する
IF
の条件はusername
がadmin
のレコードのpassword
のある位置の文字コードがn(上記例では48)であるかであり、このnをパケットを送るごとに1ずつ変化させているSUBSTR
の引数に注目するとあるパケットとパケットの間で引数が変わっているが、この代わり目である位置の文字コードが判明したことになるのでそこのパケットを抽出する- 抽出したパケットから文字コードの部分を読んで文字に戻し、繋げるとフラグが現れる
使用コード
ここでは5. の抽出だけ行いフラグの復旧は人力で行ったので割愛、6.も自動化した方がWriteupとして見栄えが良かったと反省
import csv if __name__ == '__main__': payloads = [] with open('export.csv') as f: reader = csv.reader(f) for row in reader: payload = row[6] if payload[0:10] == "GET /index": payloads.append(payload) index = 1 for j, payload in enumerate(payloads): search_word = "GET /index.php?portal=%27OR%28SELECT%28IF%28ORD%28SUBSTR%28%28SELECT+password+FROM+Users+WHERE+username%3D%27admin%27%29%2C" + str(index) if search_word not in payload: print(payloads[j - 1]) index += 1
Flag
KosenCTF{t1m3_b4s3d_4tt4ck_v31ls_1t}
感想
2ヶ月ぶりのCTFということと寝不足もあり開始6時間ぐらいはゴミみたいなミス(コードの文法エラー等)を繰り返して絶望していましたが、Temple of Timeのようなmedium問が解けた辺りから楽しくなり全体を通して良いCTFでした。作問者の方が色々なCTFで上位に入賞しているということもあり少なくとも自分が解いた問題に関しては解きづらい(≠難しい)問題は無く全体的に良問が多かったです。
冬にも同じ運営で難易度の高いCTFを開催するらしいので精進してまた参加したいです。
自身の課題として、ツールで解決できるところを人力でやったり、低レイヤーを全部チームメイトに任せたりと色々残りましたが、本当に良いCTFでした。運営の皆さん、本当にありがとうございました。