防衛省サイバーコンテスト2024 writeup

防衛省サイバーコンテスト2024に参加してみました。

prtimes.jp

防衛省とサイバーコンテストという組み合わせに興味を惹かれ、CTF自体初めてでしたが、勢いで参加してしまいました。

CTFにおいてはWriteupを公開することがあるらしく、参加要領でも「開催時間中の解法の公開は禁止(コンテスト終了後の Writeup 公開は歓迎)」とされていたので、アウトプットと備忘も兼ねて、解けたものについて解法を書いていきます。

あまり多くは解けませんでしたし、解いたものにも力技がいくつかありますが、ご笑覧ください。

Crypto

Information of Certificate(10)

Easy.crt ファイルは自己署名証明書です。証明書の発行者 (Issuer) のコモンネーム (CN) 全体を flag{} で囲んだものがフラグです。

opensslを使うといいらしい。

$ openssl x509 -in Easy.crt -text -noout
Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number: 2024 (0x7e8)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = XX, ST = Some-State, L = Nowhere, O = Invalid, OU = Invalid, CN = QRK7rNJ3hShV.vlc-cybercontest.invalid, emailAddress = user@QRK7rNJ3hShV.vlc-cybercontest.invalid
...

Missing IV(20)

NoIV.bin ファイルは、128bit AES の CBC モードで暗号化した機密ファイルですが、困ったことに IV (初期化ベクトル) を紛失してしまいました。このファイルからできる限りのデータを復元し、隠されているフラグを抽出してください。暗号鍵は 16 進数表記で 4285a7a182c286b5aa39609176d99c13 です。

暗号化鍵がわかっているため、暗号文を素の(ECBモードの) AESで復号できる。ブロックごとに復号したものとその一個前のブロックとをXORしてやれば、IVが必要な最初以外は復元できる。

encs = [] # 後から考えれば、わざわざencsに格納しなくても、最初から読み込んだ順に処理すればよかった

with open(f'NoIV.bin','rb') as fr:    
    cnt = 1
    read_data=fr.read(16)
    while(read_data and cnt < 10000):
        cnt += 1
        encs.append(read_data)
        read_data=fr.read(16)

c = AES.new(
    bytes.fromhex("4285a7a182c286b5aa39609176d99c13"),
    AES.MODE_ECB,
)


decs = []
for i in range(1, len(encs)):
    enc_block = encs[-i]
    enc_block_before = encs[- i - 1]
    
    x = c.decrypt(enc_block)
    decs.append(strxor(x, enc_block_before))

with open("result2.odt", "wb") as fw:
    for d in decs[::-1]:
        fw.write(d)

出てきたヘッダをバイナリで開くと mimetypeapplication/vnd.oasis.opendocument.textなるものが出てきたので、ググるOpenDocument Text であることがわかる。

あとは適当なodtファイルからヘッダの欠けている部分を補完してやればwordで開けるようになる。……といいつつこのファイルを開いたら「修復しますか?」と出てきて、補完ステップいらない疑惑が。(後で試したら実際いらなかった)

フラグ獲得。

参考資料

この二つの記事が非常にわかりやすかった。

zenn.dev

yocchin.hatenablog.com

平文1ブロック目 ^ IV --(AES暗号)--> 暗号1ブロック目

平文2ブロック目 ^ 暗号1ブロック目 --(AES暗号)--> 暗号2ブロック目

Forensics

NTFS Data Hide(10)

NTFSDataHide フォルダに保存されている Sample.pptx を利用して、攻撃者が実行予定のスクリプトを隠しているようです。 仮想ディスクファイル NTFS.vhd を解析して、攻撃者が実行しようとしているスクリプトの内容を明らかにしてください。

ファイルイメージを解析するツールとして autopsy なるものがあるらしい。これでNTFS.vhdを解析してみたら、なんかSample.pptx:script とかいうファイルが出てきた。base64を見つけたのでこれをデコードしてフラグ獲得。

ただ原理はわかっていない。:scriptってなんなんだろう。

NTFS Data Delete(10)

NTFSFileDelete フォルダにフラグを記載した txt ファイルを保存したのですが、どうやら何者かによって消されてしまったようです。

問題「NTFS Data Hide」に引き続き、仮想ディスクファイル NTFS.vhd を解析して、削除された flag.txt に書かれていた内容を見つけ出してください。

「削除されたファイル」にflag.txtがあったので、ここからフラグを回収。

flag{resident_in_mft}

My Secret(30)

問題「HiddEN Variable」に引き続き、メモリダンプファイル memdump.raw を解析して、秘密(Secret)を明らかにしてください。

問題の順序はこれが最後だけど、メモリダンプを解析するツール Volatility3 をポチポチ触ってたらクリティカルなコマンドを打ってしまったのでこっちがさきに解けてしまった。

最初Volatile2 を使うも、profileの特定のためのimageinfoがうまくいかなかったし、stringsの出力を見てもWindowsであること以上はわからなかったので断念。Volatile 3のほうがよいという情報を見たこともありVolatile3に移行。

適当にコマンドを試す中で ./vol.py -f ../memdump.raw windows.cmdline とか打ったら

5516 7z.exe 7z x -pY0uCanF1ndTh1sPa$$w0rd C:\Users\vauser\Documents\Secrets.7z -od:\

とかいう死ぬほど怪しいログを見つけてしまった。どうやらこれは起動中のコマンドの引数を表示してくれるらしいので、次の目標がSecrets.7zを見つけることになる。

file系ということでfilescanとfiledumpを試す。まずはfilescanから。そのままだと大量に出てくるので grep で絞り込みをかける。

$ ./vol.py -f ../memdump.raw windows.filescan | grep Secret
0xe206bba6b1d0.0\Users\vauser\Documents\Secrets.7z      216
0xe206bbc4a2f0  \Users\vauser\Documents\Secrets.7z      216

filedumpの--virt_addrに与えるのはこれか。--physaddrと迷ったけどこっちで決め打ち。

$ ./vol.py -f ../memdump.raw windows.dumpfiles --virtaddr 0xe206bbc4a2f0

lsすると

file.0xe206bbc4a2f0.0xe206bbabada0.SharedCacheMap.Secrets.7z.dat
file.0xe206bbc4a2f0.0xe206bbabada0.SharedCacheMap.Secrets.7z.vacb

なる物々しいファイル名が出てくるが、datのほうを普通にSecrets.7zにリネームしてOK。あとは最初に出てきた7z.exeの引数にあったパスワードを使って解凍するとSecrets.rtfが出てきてフラグ回収。

このときはcatで中を見たから気が付かなかったが、後でwordで開いてみたらflagは丁寧に白文字になって隠されていた。

参考資料

qiita.com

blog.hamayanhamayan.com

jpn.nec.com

HiddEN Variable(20)

NTFSFileDelete フォルダにフラグを記載した txt ファイルを保存したのですが、どうやら何者かによって消されてしまったようです。 このメモリダンプが取得された環境にはフラグが隠されています。 memdump.raw を解析して、フラグを見つけ出してください。

モリダンプファイルのダウンロードはこちら: メモリダンプは展開すると 4.5GB 程度になるので注意してください。

「取得された 環境 には」だし、ENvironment Variable か。環境変数を取得するコマンドがヘルプになくてわからなかった(ので飛ばしていた)が、ググるwindows.envarsなるコマンドがあることが分かった。

すごい量のログが出てくるが、

FLAG   BDkPUNzMM3VHthkj2cVEjdRBqTJcfLMJaxT9si67RgJZ45PS

なるものを発見。

flag{BDkPUNzMM3VHthkj2cVEjdRBqTJcfLMJaxT9si67RgJZ45PS} は通らなかったのでもうひとひねり必要だった。base64でもなかったので、ほかにこの文字列が現れる場所があるのか、そもそもこのFLAGSは本当にフラグなのか、と思っていろいろコマンドを打ってはみたが、結局CyberChefでMagicしてみたらflagが出てきてしまった。

NTFS File Rename(20)

NTFSFileRename フォルダに保存されている Renamed.docx は、以前は別のファイル名で保存されていました。

問題「NTFS File Delete」に引き続き、仮想ディスクファイル NTFS.vhdを解析して、 Renamed.docx の元のファイル名を明らかにしてください。

Autopsyを探し回っても出てこなかったので、resident_in_mftって言ってたし出てこないかなーくらいのつもりでNTFS.vhdをstringsにかける。有意な情報は出てこない。バイナリエディタで見る。有意っぽい文字列が、間にゼロをはさみつつ登場していることがわかる。だからstringsに出てこなかったのか。

ただそれ以外のところで0のバイトが大量に出てきすぎて、スクロールしてもスクロールしても終わらず、諦める。

いっそpythonでゼロになってるバイトを取り除くとスクロールしやすくならないかな?って思って実装。これをバイナリエディタで見るとフラグが出てきた。……たぶん想定解じゃないよなあ。

Miscellaneous

Une Maison (10)

画像 maison.jpg の中にフラグが隠されています。探してみてください。

Forensics入門(CTF) #CTF - Qiita

この記事の手法をひととおり試してみたものの当たらなかった。しかし「意外と一番重要かもしれない」というこの記事の導きに従い、画像検索で元ファイルを探して、gimpで重ねて見比べると画像の一部がバーコードになっていることがわかった。これを読み込むとフラグ獲得。

String Obfuscation (10)

難読化された Python コード string_obfuscation.py ファイルからフラグを抽出してください。

import sys

if len(sys.argv) < 2:
    exit()

KEY = "gobbledygook".replace("b", "").replace("e", "").replace("oo", "").replace("gk", "").replace("y", "en")
FLAG = chr(51)+chr(70)+chr(120)+chr(89)+chr(70)+chr(109)+chr(52)+chr(117)+chr(84)+chr(89)+chr(68)+chr(70)+chr(70)+chr(122)+chr(109)+chr(98)+chr(51)

if sys.argv[1] == KEY:
    print("flag{%s}" % FLAG)

if文消したら普通に動いた。

Where Is the Legit Flag?(20)

fakeflag.py を実行しても偽のフラグが出力されてしまいます。難読化されたコードを解読し、本物のフラグを見つけ出してください。

exec(chr(105)+chr(109)+chr(112)+chr(111)+chr(114)+chr(116)+chr(32)+chr(122)+chr(108)+chr(105)+chr(98)+chr(44)+chr(32)+chr(98)+chr(97)+chr(115)+chr(101)+chr(54)+chr(52))
TANAKA = "eJyNVG1320QT/Z5z8h+GhNYvcR35JZZdaCGhT6D0gQTiFKjjlpU0ljZe7272xYpoy2/vrJRA+MA56IOPrJ29e+feO7sP84JJ2CohsEqYELBlktsCWM64tA6E3+gKEjSm6u/uXBzPz+AZtBY/vRx91Unffv908vOrw9PXz7/E23h/nf2mtp9/Gz05fn9zbv8sB18f/P7DWa9o/5/1f/Hf6KMlhzfJ9YvZ/x4NKzk185PNF6vud3uf/Xjx0eV/PLsUvz4ev/tw1bq6au3u7MNxorYIK5Yi4K0WRAhWyoAuKstTJiDDlFuuZB9C9WvOwEq2RpBsg2CUlxk4Is5XPIXEMGubwlNqVpVc5mB9nqN1BAG2LjeYM5OFpRVumCAUTPF+31yVtAhb+oB0OLcsN4ikjUTmCih8jqCVoSODUpdvLl+9JK0W8fhJdBD1dnfg7pnG3UGPS9ceT7vdQYdW9uFstQLtjVYWQTBiwiwYb6hJ65jDDUpHoPcIYfP03ahTo4yG/Sg8zb/WaNwKkPel8QQeQ3R7etqLh/CB3qKoF8/gbfO2mBwtF9GypvDCm9D4WipHbYsKLCP1S4MuLTADmzISw6gyiHGP3h52euMY+ArmxpNLguhHNY/B8JBaG0TwCAaDnjJZOy1MezjpPCQ3ig6O7pQ4HHYJa9adLQMXOBfeglMIFp0jH0pOCm8ZBZJSialrHIGLQJECnFmwBQqSvqk0zLkKtFGZT5GEo9Iz7yzPSF3MLUhynYw0NpximLzxXISmWchCU39soWRiDZqRHE04eF64lRfAsi0n2JrCCdaomlXBowBGKU0qMtFQHNYYpmYfzgPzBAu25SHAiv65Jk1esoT6K9TmDhCON4psoLhT7FO1aXKfKhnOqR3ykjwq6Zs3pslFG8K+hqXVzKzJLWVSmuJ6gqxWQY7cMF0fEqRvtWjLpSTIJr3XWFKo00Jp6oXoZaiRVqklmh8RNAy7+uHnWhGhf33ai7/9DQ5xWfeRlJiA4wiKkl544yjYoZu7S2XBl38h/Ldd4SbglAZoJu3hoRRHDs9hHA+nT/9Bhp7EIFs//OhoRoej8WQSQxemo3h69HBV02mu7Q5H46M4no46tUPzgqTOC7jxiBIytiF6YAXXGk1Ve8YMt3WQls2OkyqEKQyzUXRBhYwqT83QQKGjJVtQbVN6pike3CFUoVIijV7SZMx6wk/CjUzXcfCxIbe3Eip/P91e46z0MtGz6fjjHmHt7nwCLpe/Qg=="
TAKAHASHI = [0x7a,0x7a,0x7a,0x12,0x18,0x12,0x1d,0x12,0x07,0x7b,0x36,0x37,0x3c,0x30,0x36,0x37,0x67,0x65,0x31,0x7d,0x67,0x65,0x36,0x20,0x32,0x31,0x7b,0x20,0x20,0x36,0x21,0x23,0x3e,0x3c,0x30,0x36,0x37,0x7d,0x31,0x3a,0x3f,0x29,0x7b,0x30,0x36,0x2b,0x36]
exec(bytes([WATANABE ^ 0b01010011 for WATANABE in reversed(TAKAHASHI)]))

実行すると出てくるのはflog{8vje9wunbp984} だが、これは当然通らない。

とりあえずexecの中身を確認。 1個目は import zlib, base64 。2個目はexec(zlib.decompress(base64.b64decode(TANAKA)))'。もう一回execの中身を確認。すると、大量のコメントの中に

SATO = ... 
SUZUKI = ...
(''.join([SATO[i] for i in SUZUKI]))
print("flog{8vje9wunbp984}")

というコードが埋もれているのを発見。3行目をprintすればflagが出てくる。

Utter Darkness(20)

画像ファイル darkness.bmp に隠されているフラグを見つけてください。

真っ黒。実は真っ黒に見えて画素値1/255で書いてたりしない?と思ってpythonで読み込んで画素値を調べてみるも、全部ゼロ。 ただバイナリを見ると全部同じ値というわけではない。???と思ってbmpのヘッダを解読すると、どうやら画素を表現する値(0/1)は、直接色を指定しているわけではなく、ヘッダにあるパレットの番号を表しており、これによって色を指定しているらしい。そのパレットの色が0も1も真っ黒(0x000000)になっていたことが原因だった。片方を白(0xFFFFFF)に書き換えたらフラグが出てきた。

参考資料

www.setsuki.com

Serial Port Signal(30)

Tx.csv は、とあるシリアル通信の内容を傍受し、電気信号の Hi, Low をそれぞれ数字の 1 と 0 に変換したものです。通信内容を解析してフラグを抽出してください。

中身はmicroseconds(20μs刻み)とlogic(0/1)の対応表。どっかで見たことあるなと思いつつ「シリアル通信 Tx」などと検索するとUARTが出てくる。学部の実験でやったところだ!!!!!

0/1の切り替わりが最短でも100μsほど空いていたことから、baud rate は9600だろうとあたりをつけてuartのシミュレータをpythonで書く。いろいろネットで検索したけど実装の詳細がよくわからず、授業で配布された資料を引っ張ってきて書いた。やっぱりわかりやすかった。送信が7bitであることとparity bit があることが最初わかっておらず、結果がちょっとずれて原因の特定に時間を食った。

def get_logic(df, t):
    ret_df = df[(df["microseconds"] <= t)]["logic"]
    if(len(ret_df) == 0):
        return 1
      
    return int(ret_df.iloc[-1])

baudrate = 9600
dt = 1000 * 1000 / baudrate 
t = 5460 # 最初のlow bit の開始時刻
t += dt / 4


while(t < 40940):
    while(get_logic(df, t) == 1):
        t += dt
    s = ""
    for i in range(7):
        t += dt
        s = str(get_logic(df, t)) + s
    t += dt # parity
    t += dt
    if (get_logic(df, t) != 1):
        print(f"ERROR on t = {t}")
    print(chr(int(s, 2)))

Hello UART: synt{IjUZC5TD} Hell

やっぱUARTか。rot13かな?とあたりをつけたらやっぱりrot13だった。

flag{VwHMP5GQ}

参考資料

www.rohde-schwarz.com

パリティビットの存在と、データが8bitでない場合があることは知らなかった。

Network

FileExtract(10)

添付の FileExtract.pcapng ファイルからフラグを見つけ出し、解答してください。

Network解けたのはこれだけだった。

pcapngを解析するためにはWiresharkがよいらしい。眺めると、FTPでs3cr3t.zipを送っているログを発見。ファイルを書き出す方法を発見したのでs3cr3t.zipを落として展開。

……パスワードに阻まれる。ログのFTPをもう一回探すと

FTP Request: Pass br2fWWJjjab3

なるものを見つける。これを打ち込むと解凍に成功。パスを取得。

その時は何も考えなかったけど、FTPのログインパスワードとzipのパス同じってことだろうか。実質PPAP

参考資料

unit42.paloaltonetworks.jp

Programming

Logistic Map(10)

下記のロジスティック写像について、x_0 = 0.3 を与えた時の x_9999 の値を求め、小数第7位までの値を答えてください(例:flag{0.1234567})。なお、値の保持と計算には倍精度浮動小数点数を使用してください。 x_{n+1} = 3.99 x_n (1 - x_n)

double x = 0.3;
for(int i = 0; i < 9999; i++){
    x = (double) 3.99 * x * ((double) 1 - x);
}
cout << std::setprecision(7) << x;

Randomness Extraction(20)

ファイル random.dat は一様でない乱数生成器の出力ですが、一部にフラグが埋め込まれています。フォン・ノイマンランダムネスエクストラクターを適用してフラグを抽出してください。

フォン・ノイマンランダムネスエクストラクター、初耳だった。カタカナで検索しても出てこないが、英語で検索をかけると出てくる。

2bitずつ読み込んで

src[2*i] src[2*i+1] 操作
0 0 skip
0 1 dst[j]=0; j++;
1 0 dst[j]=1; j++;
1 1 skip

みたいな処理をするらしい。

最初、これを6~7回くらい繰り返し適用したらflagになるのかな~とか思ってたけど何も出てこず、一旦飛ばしていた。最後30分くらいで見直したら1回目の適用結果(のバイナリ)にflagが普通に紛れ込んでいた。

参考資料

prefetch.eu

XML Confectioner(20)

添付の sweets.xml には、多数の sweets:batch 要素が含まれています。これらの中から、下記の条件すべてを満たすものを探してください。

少なくとも二つの子要素 sweets:icecream が含まれる 子要素 sweets:icecream には icecream:amount 属性の値が 105g を下回るものがない 子要素 sweets:candy の candy:weight 属性の値の合計が 28.0g 以上である 子要素 sweets:candy の candy:shape 属性が 5 種類以上含まれる cookie:kind 属性が icing でありかつ cookie:radius 属性が 3.0cm 以上の子要素 sweets:cookie を少なくとも一つ含む フラグは、条件を満たす sweets:batch 要素内において、最も cookie:radius 属性が大きな sweets:cookie 要素の内容に書かれています。

xml.etree.ElementTree でひたすら処理する。

tree = et.parse("sweets.xml")
root = tree.getroot()

ans = []
for child in root:
    icecream = []
    candy = []
    cookie = []
    for c2 in child:
        if ("icecream" in c2.tag) :
            icecream.append(c2)
        if ("candy" in c2.tag) :
            candy.append(c2)
        if ("cookie" in c2.tag) :
            cookie.append(c2)
    
    if len(icecream) < 2:
        continue
    
    prefix_icecream = "{http://xml.vlc-cybercontest.com/icecream}"
    prefix_candy = "{http://xml.vlc-cybercontest.com/candy}"
    prefix_cookie = "{http://xml.vlc-cybercontest.com/cookie}"
    
    
    skipFlg = False
    for i in icecream:
        if float(i.attrib[prefix_icecream + "amount"][:-1]) < 105:
            skipFlg = True
            break
    if skipFlg: continue
    
    s = 0
    shapes = set()
    for c in candy:
        s += float(c.attrib[prefix_candy + "weight"][:-1])
        shapes.add(c.attrib[prefix_candy + "shape"])
    if s < 28.0:
        continue
    if(len(shapes) < 4):
        continue
    
    for c in cookie:
        if (c.attrib[prefix_cookie + "kind"] == "icing" and
            c.attrib[prefix_cookie + "radius"][:-2] >= "3.0"):
            ans.append(child)
            break

for a in ans:
    max_rad = -1
    max_rad_c = None
    for c in a:
        if "cookie" in c.tag:
            if max_rad < float(c.attrib[prefix_cookie+"radius"][:-2]):
                max_rad =  float(c.attrib[prefix_cookie+"radius"][:-2])
                max_rad_c = c

print(max_rad_c.tag)
print(max_rad_c.attrib)

Trivia(各10点)

Q. Advanced Encryption Standard (AES) は、公募によって策定された標準暗号です。 現在採用されているアルゴリズムの候補名は何だったでしょうか?

A. Rijndael

Q. 最も番号が若い CVE レコードのソフトウェアパッケージにおいて、脆弱性が指摘された行を含むソースファイル名は何でしょう?

CVEレコードの一覧がだいぶ見つけづらかったが、 https://www.cve.org/Downloads を発見すれば最も若い番号がCVE-1999-0001であることがわかる。画面上部の検索窓に打ち込めば答えが見つかる。

A. ip_input.c

Q. 多要素認証に使われる本人確認のための3種類の情報の名前は何でしょう?それぞれ漢字2文字で、50音の辞書順で並べて「・」で区切ってお答えください。

A. 所持・生体・知識

Web

Browsers Have Local Storage(10)

http://10.10.10.30 にアクセスしてフラグを見つけ出し、解答してください。

右クリックメニューからInspectを選び、Storage > Local Storage を見るとフラグが見つかる。

結果・感想

Webやネットワークがほとんど解けなかったのが少し悔しいです。

一日丸々費やしてかなり疲れましたが、Google検索と地道な調査と閃きで進んでいくのが楽しかったです。

解けなかった問題や、解けても力技になった問題は、他の方のwriteupを見るなどして学習してみたいと思いました。