ABCTF 2016 write up

ABCTF 2016 write up

ABCTFに、CTF wo Suruというチームで参加しました。
23位で3545ptでした。私は時間があまり取れずちょっとだけしか参加してません。
私は50ptと120ptを通しました。他4人のチームメンバーが強い。すごい。
というわけで120ptだけwrite up書きます。

Zippy - 120pt

問題の概要

問題は名前通りzipファイル。

└─zippy
    │  chunk0.zip
    │  chunk1.zip
    │  chunk2.zip    
    │       ・
    │       ・
    │       ・
    │  chunk52.zip
    │  chunk53.zip

chunk0~53まで連番の54個のzipファイルがあります。

└─chunkN.zip
    │  data.txt         (4byte,encrypted,compressed 0%)

chunkN.zipの中身は全てにおいて4byteのdata.txtだけが存在します。

ヒントとしてzippy.zipにはコメントがついていました。

if you want to find the flag, this hint may be useful: the text files within each zip consist of only "printable" ASCII characters

chunkN.zip内のdata.txtは全て印字可能なASCIIコードで構成されている、らしい

解く

zipといえばpkcrackで既知平文攻撃だよな~、data.txtも4文字で構成されているっぽいので平文をブルートフォースで作ってpkcrackするか~時間かかりそうだな~
と思ってpkcrackで試そうとすると、「平文は13byte以上にしろ」とお叱りを受けた。
改めて調べると、非常に小さなファイルはCRC値から推測できるらしいです。
参考:WEB系情報セキュリティ学習メモ: パスワード付きZipのセキュリティについて

上記の記事によると、zipは中にあるファイルのCRC値を持っていて、それは暗号化がされていない。
非常に小さなファイルのCRCは衝突する可能性が少ないので、予測することが可能ということでした。
なるほどね。 つまり、文字列をブルートフォースで生成し、文字列のCRCとzipの持つdata.txtのCRCを比較すればいいんだな。

zipinfoでCRC-32の値を調べ、zipの該当の値をバイナリエディタで探すと0x0E~0x11の4byteであることがわかった。
この情報を基にPython3でスクリプトを書く。(すごく汚い)

import binascii
import os

flag = False

filelist = os.listdir()
text = open('result.txt', "w")
result = ''


for num in range(0,54):     #chunk0~54まで
    fl = "chunk" + str(num) + ".zip"
    f = open(fl, 'rb')
    print(fl)
    offset = 0x0e
    f.seek(offset)
    zipcrc = binascii.hexlify(f.read(4))
    print (zipcrc)

    for i in range(32,127):     #asciiコード印字可能文字0xE0~0x7Eまで
        for j in range(32,127):
            for k in range(32,127):
                for l in range(32,127):
                    crc = (binascii.crc32(bytearray([i,j,k,l])))
                    crc = binascii.hexlify(crc.to_bytes(4, 'little'))
                    if(zipcrc == crc):
                        byte1 = (i.to_bytes(1, 'little') + j.to_bytes(1, 'little') + k.to_bytes(1, 'little') + l.to_bytes(1, 'little'))
                        result += byte1.decode('ascii')
                        print(result)
                        flag = True
                        break
            if(flag):
                break
        if(flag):
            break
    if (flag):
        print(":)")
        flag = False
    else:
        print(";(")
text.write(result)
print("end")

関数化すりゃいいのにしなかった。(糞)
単純に1つずつ文字列作って比較を行うので実際に動かしてみると案の定遅い。
しかし開催期間が長かったので特に急ぐ必要もないので気にしませんでした。が、終了するまで4時間かかった。
Cythonぐらい使えばよかったね。

このスクリプトでresult.txtが作成されるので、見てみると案の定base64

UEsDBBQDAQAAAJFy1kgWujyNLwAAACMAAAAIAAAAZmxhZy50eHT/xhoeSnjMRLuArw2FXUAIWn8UQblChs4AF1dAnT4nB5hs2SkR4fTfZZRB56Bp/FBLAQI/AxQDAQAAAJFy1kgWujyNLwAAACMAAAAIAAAAAAAAAAAAIIC0gQAAAABmbGFnLnR4dFBLBQYAAAAAAQABADYAAABVAAAAAAA=

こいつをデコードしてやると、flag.txtの入ったzipが現れる。(以下flag.zipとする)

└─flag.zip
    │  flag.txt         (35byte,encrypted,compressed 0%)

35byteか~うーん、CRCで総当たりはキツイな~。その辺に落ちてたzipパスワード総当たりツール使ってもひたすら終わらない・・・。
チームメンバーが辞書攻撃したけどダメだったと言っているし、どうしたもんか・・・。
と、CRC総当たりとパスワード総当たりツールを動かしながら悩んで少し時間溶かしました。(なおその間にCRCが衝突した模様)
よく見ると使ったパスワード総当たりツールが記号を含めない英字だけで回してたので「自分で作った方がいいのでは?」と思い立ち自分でスクリプトを書きました。(もうこの手しか残されていなかった)

import zipfile

def brutepass(num,max, tmp):
    flag = False
    for i in range(32, 127):
        tmp1 = tmp
        tmp1 += i.to_bytes(1, 'little')
        if(num >= max):
            try:
                print(tmp1)
                zipfile.ZipFile("flag.zip").extract("flag.txt",".",tmp1)
                print("OK!!")
                flag = True
                break
            except (RuntimeError, zipfile.BadZipFile):
                pass
        else:
            flag = brutepass(num + 1,max, tmp1)
        if(flag):
            break
    return flag

if(brutepass(0,3,b"")):
    print(":)")
else:
    print(";(")

1桁分ずつ様子見しながらソース弄ってたらまさかの3桁で終わった。
拾ってきたzipパスワード総当たりツールは5桁以上だったし、記号使わないしでそりゃいつまでたっても解凍できないわけだ。
password : z1P

flag.txt : flag{i_z1pp3d_a_zip_w1th_sum_zips}

終わって考えてみれば簡単な問題でしたが、zipの構造について理解が深まってよかったなぁと。
ABCTFは糞雑魚な私でも100pt以上取れたのでとりあえず満足しました。もっと難しい大会でも少しはできるように頑張りたい。