この記事はKogakuin Univ Advent Calendar 2019 - Adventar2日目です。
↑まだがら空きなのでよければご参加ください
metarinです。
CTFのWriteup以外では初記事です。
さて、先日大学のサークル(のようなもの)でゲームデータのアーカイブファイルの展開問題をやったらだれも正解してくれなかったので、その問題の紹介をしようと思います。
ただ、普通に実行ファイルを解析して展開してもつまらないので、あえてアーカイブファイルから得られる情報のみで展開してみます
問題ファイルはこちらからどうぞ
このファイルの中にあるmetarinctf{で始まるflagをゲットできれば正解です
さあ、みんなもPCを用意してやってみよう!
回答(解凍)
実行ファイルを読まずに行うデータ展開作業は多数の推測のもとに成り立っています。気を強く持って作業しましょう
とりあえずバイナリエディタでq.mpfを開いてみます
すると、真っ先に/metarin.pngと/flag.txtの2つのファイル名のような文字列が目に付きます
この2つのファイル名の間に0埋めされている場所があるでファイル1つあたりのデスクリプタが固定長であると推測できます
その長さは/metarin.pngから/flag.txtが始まるまでのサイズを数えてみると72Bだと分かります
次にこの72Bに焦点を当てて調べてみます
デスクリプタには大抵の場合、
- ファイル名
- サイズ
- オフセット
の3つが含まれています
これらのサイズが分かれば暗号化や圧縮が成されていないアーカイブファイルの場合は展開できますし、そうでない場合もファイルの境界線を特定することができます
サイズとオフセットはint型(=4B)で入っていることが多いのでファイル名は72-(4*2)=64Bであると推測できます
各デスクリプタのファイル名を除く8Bを見るとそれぞれ
- /metarin.png
5D 0C 00 00 00 00 00 00 - /flag.txt
0B 01 00 00 5D 0C 00 00
となっています
1番目のファイルである/metarin.pngのオフセットは他のどのファイルよりも小さくなる筈なので、末尾4Bがオフセット、そして残りがサイズであると分かります
これで各ファイルの位置が分かったので、試しにmetarin.pngの部分を切り出して開いてみます
しかし、エラーが出てしまいました
また、flag.txtに関しても文字化けしているファイルが出てきてしまいました
どうやら何かしらの暗号化か圧縮が施されているようです
うさみみハリケーンのファイルデータ抽出にかけてもflag.txtのflagの部分が引っかかっただけなので、既存の圧縮方式ではないようです
実装のコスト的に圧縮を独自実装することはあまりないので、この手のデータによくある4B単位でのxorだと仮定して差分平文攻撃を行ってみます
復号されたflag.txtの先頭11Bはmetarinctf{であることがflagの条件から分かっています
なので暗号化された後の先頭4B1B 5C D9 BFとの差分を取ることで鍵76 39 AD DEを取得できます
しかし、その次の4Bにさっきの鍵を適用しても期待された出力であるrincが出てきません
鍵を変化させているのでしょう。なのでここでも同じようにrincと42 AD D5 9Cで差分を取って鍵を割りだします
そして、前後の鍵の変化量を取り、その変化量0x210e8abaを用いて8~12Bの復号に用いる鍵を推測してみます
前の鍵0xffbbc430+変化量0x210e8aba % 2^32 で得られた鍵0x20ca4eeaで復号を行います
これでtf{で始まるデータが得られれば成功です
Pythonでやってみると
>>> (int.from_bytes(b'\xEA\x4E\xCA\x20', 'little') ^ int.from_bytes(b'\x9e\x28\xb1\x50', 'little')).to_bytes(4, 'little')
b'tf{p'
となり、見事復号して正しいデータを手に入れることに成功しました
あとはいままで調べたことを用いて後続のデータを復号するだけです
それを行うPythonスクリプトがこちら(未テスト)
import math
key = 0xdead3976
key_df = 0x20ca4eea
f = open("flag.txt", 'rb').read()
o = open("flag_dec.txt", 'wb')
for i in range(math.ceil(len(f) / 4)):
o.write((int.from_bytes(f[i*4:(i+1)*4]) ^ key, 'little').to_bytes(4, 'little'), 4)
if len(f) % 4 != 0:
o.write(f[-1*(len(f)%4):], len(f)%4)
まとめ
無事アーカイブファイルのみでの展開を行うことが出来ました
しかし、実行ファイルが入手可能な場面においては素直にGhidraとかIDAに投げた方が楽だと思います