All Articles

TUCTF 2024 Writeup

TUCTF 2024(開催は 2025 年)に 0nePadding で参加しました。

忙しくて Writeup 書けてないうちにスコアボードが閉鎖してしまっていたのですが、とりあえず解いた問題だけ Writeup を書きます。

もくじ

Mystery Box(Rev)

Lets play a game!!! You spend hours trying to guess my secret phrase! Can you figure out the secret code and retrieve the flag?

問題バイナリを実行すると以下のようにゲームをプレイするかを選択するメニューが起動します。

image-20250125153407898

バイナリ解析でこの処理を行っている箇所を特定すると、データ領域にハードコードされたデータを 0x5a で XOR した文字列を Flag として表示する機能を持っていることがわかります。

image-20250125153339861

これを解析すると TUCTF{Banana_Socks} が正しい Flag であることを特定できます。

Simple Login(Rev)

Muhahaha, you will never get past my secret defenses. (When done correctly, this challenge returns the flag missing the opening curly brace; this needs to be there for the flag to work, and can be added easily).

問題バイナリを解析すると、始めに TheSuperSecureAdminWhoseSecretWillNeverBeGotten というキーワードが入力されているが動作をチェックする動作を行っていることがわかります。

そして、ユーザが TheSuperSecureAdminWhoseSecretWillNeverBeGotten を入力すると、次は Flag を表示するための認証パスワードを要求されます。

image-20250125160806758

このパスワードは先頭文字から順に 3 回に分けて評価されていき、最終的にすべての検証に成功した場合に正しい Flag が復号され、表示されます。

最初の検証部分は以下の通り実装されており、文字列 ruint から始まる場合は次の検証に進むことができます。

image-20250125160746385

続く検証は以下の通り実装されており、各文字を 0x32 で XOR した値がハードコードされた値と一致するかを検証します。

image-20250125160719757

最後の検証では、各文字から 0x54 を引いて 0x1a との MOD をとった値に 0x61 を足した値がハードコードされたものと一致するかを検証しています。

image-20250125160554006

このような条件を満たす文字列は以下のコードで生成できます。

S = "reingvbaonetr"
for s in S:
    tmp = ord(s) - 0x61
    print(chr(tmp + 0x54),end="")

最終的に、ruinthosepreseX\aZiUTbaXge というパスワードを入力することで正しい Flag TUCTF{running_through_the_subroutines!!} を得ることができました。

image-20250125160501933

Custom Image Generator(Rev)

It’s still a work in progress, but I made my own file format! It is pretty efficient I think, but there may be improvements.

次の問題では、以下のコードで RGB データを独自にエンコーディングしたファイルを生成するコードと、このコードで画像ファイルをエンコードした結果のファイルが与えられます。

from PIL import Image
import numpy as np
import crc8

def main():
    inp = input("""
                Welcome to the TU Image Program
                It can convert images to TIMGs
                It will also display TIGMs

                [1] Convert Image to TIMG
                [2] Display TIMG

                """)
    match inp:
        case "1":
            conv()
        case "2":
            display() #TODO: Add
            '''
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠛⠛⠛⠋⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠙⠛⠛⠛⠿⠻⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀⠀⡀⠠⠤⠒⢂⣉⣉⣉⣑⣒⣒⠒⠒⠒⠒⠒⠒⠒⠀⠀⠐⠒⠚⠻⠿⠿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⠏⠀⠀⠀⠀⡠⠔⠉⣀⠔⠒⠉⣀⣀⠀⠀⠀⣀⡀⠈⠉⠑⠒⠒⠒⠒⠒⠈⠉⠉⠉⠁⠂⠀⠈⠙⢿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⠇⠀⠀⠀⠔⠁⠠⠖⠡⠔⠊⠀⠀⠀⠀⠀⠀⠀⠐⡄⠀⠀⠀⠀⠀⠀⡄⠀⠀⠀⠀⠉⠲⢄⠀⠀⠀⠈⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⠋⠀⠀⠀⠀⠀⠀⠀⠊⠀⢀⣀⣤⣤⣤⣤⣀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠜⠀⠀⠀⠀⣀⡀⠀⠈⠃⠀⠀⠀⠸⣿⣿⣿⣿
⣿⣿⣿⣿⡿⠥⠐⠂⠀⠀⠀⠀⡄⠀⠰⢺⣿⣿⣿⣿⣿⣟⠀⠈⠐⢤⠀⠀⠀⠀⠀⠀⢀⣠⣶⣾⣯⠀⠀⠉⠂⠀⠠⠤⢄⣀⠙⢿⣿⣿
⣿⡿⠋⠡⠐⠈⣉⠭⠤⠤⢄⡀⠈⠀⠈⠁⠉⠁⡠⠀⠀⠀⠉⠐⠠⠔⠀⠀⠀⠀⠀⠲⣿⠿⠛⠛⠓⠒⠂⠀⠀⠀⠀⠀⠀⠠⡉⢢⠙⣿
⣿⠀⢀⠁⠀⠊⠀⠀⠀⠀⠀⠈⠁⠒⠂⠀⠒⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⢀⣀⡠⠔⠒⠒⠂⠀⠈⠀⡇⣿
⣿⠀⢸⠀⠀⠀⢀⣀⡠⠋⠓⠤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠀⠀⠀⠀⠀⠀⠈⠢⠤⡀⠀⠀⠀⠀⠀⠀⢠⠀⠀⠀⡠⠀⡇⣿
⣿⡀⠘⠀⠀⠀⠀⠀⠘⡄⠀⠀⠀⠈⠑⡦⢄⣀⠀⠀⠐⠒⠁⢸⠀⠀⠠⠒⠄⠀⠀⠀⠀⠀⢀⠇⠀⣀⡀⠀⠀⢀⢾⡆⠀⠈⡀⠎⣸⣿
⣿⣿⣄⡈⠢⠀⠀⠀⠀⠘⣶⣄⡀⠀⠀⡇⠀⠀⠈⠉⠒⠢⡤⣀⡀⠀⠀⠀⠀⠀⠐⠦⠤⠒⠁⠀⠀⠀⠀⣀⢴⠁⠀⢷⠀⠀⠀⢰⣿⣿
⣿⣿⣿⣿⣇⠂⠀⠀⠀⠀⠈⢂⠀⠈⠹⡧⣀⠀⠀⠀⠀⠀⡇⠀⠀⠉⠉⠉⢱⠒⠒⠒⠒⢖⠒⠒⠂⠙⠏⠀⠘⡀⠀⢸⠀⠀⠀⣿⣿⣿
⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠑⠄⠰⠀⠀⠁⠐⠲⣤⣴⣄⡀⠀⠀⠀⠀⢸⠀⠀⠀⠀⢸⠀⠀⠀⠀⢠⠀⣠⣷⣶⣿⠀⠀⢰⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠁⢀⠀⠀⠀⠀⠀⡙⠋⠙⠓⠲⢤⣤⣷⣤⣤⣤⣤⣾⣦⣤⣤⣶⣿⣿⣿⣿⡟⢹⠀⠀⢸⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠑⠀⢄⠀⡰⠁⠀⠀⠀⠀⠀⠈⠉⠁⠈⠉⠻⠋⠉⠛⢛⠉⠉⢹⠁⢀⢇⠎⠀⠀⢸⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣀⠈⠢⢄⡉⠂⠄⡀⠀⠈⠒⠢⠄⠀⢀⣀⣀⣰⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⢀⣎⠀⠼⠊⠀⠀⠀⠘⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⡀⠉⠢⢄⡈⠑⠢⢄⡀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠁⠀⠀⢀⠀⠀⠀⠀⠀⢻⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣀⡈⠑⠢⢄⡀⠈⠑⠒⠤⠄⣀⣀⠀⠉⠉⠉⠉⠀⠀⠀⣀⡀⠤⠂⠁⠀⢀⠆⠀⠀⢸⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣄⡀⠁⠉⠒⠂⠤⠤⣀⣀⣉⡉⠉⠉⠉⠉⢀⣀⣀⡠⠤⠒⠈⠀⠀⠀⠀⣸⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣤⣄⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣶⣶⣶⣤⣤⣤⣤⣀⣀⣤⣤⣤⣶⣾⣿⣿⣿⣿⣿
'''

            
        case _:
            return 0
    return 0


def conv():
    file = input("Enter the path to you image you want converted to a TIMG file:\n")
    out = input("Enter the path youd like to write the TIMG to:\n")
    img = Image.open(file)
    w,h = img.size
    write = [b'\x54',b'\x49',b'\x4D',b'\x47',b'\x00',b'\x01',b'\x00',b'\x02']
    for x in w.to_bytes(4):
        write.append(x.to_bytes(1))
    for y in h.to_bytes(4):
        write.append(y.to_bytes(1))
    write.append(b'\x52')
    write.append(b'\x55')
    write.append(b'\x42')
    write.append(b'\x59')



    for i in range(h):
        dat = [b'\x44',b'\x41',b'\x54',b'\x52']
        for j in range(w):
            dat.append(img.getpixel([j,i])[0].to_bytes(1))
        dat.append(getCheck(dat[4:]))
        for wa in dat:
            write.append(wa)

    for i in range(h): 
        dat = [b'\x44',b'\x41',b'\x54',b'\x47']
        for j in range(w):
            dat.append(img.getpixel([j,i])[1].to_bytes(1))
        dat.append(getCheck(dat[4:]))
        for wa in dat:
            write.append(wa)

    for i in range(h): 
        dat = [b'\x44',b'\x41',b'\x54',b'\x42'  ]
        for j in range(w):
            print(img.getpixel([j,i])[2].to_bytes(1))
            dat.append(img.getpixel([j,i])[2].to_bytes(1)) 
        print(dat)  
        dat.append(getCheck(dat[4:]))
        for wa in dat:
            write.append(wa)

    write.append(b'\x44')
    write.append(b'\x41')
    write.append(b'\x54')
    write.append(b'\x45')

    with open(out,"ab") as f:
        for b in write:
            f.write(b)

    return 0  

def getCheck(datr):
    dat = ''
    for w in datr:
        dat+=chr(int.from_bytes(w))
    print(datr      )
    print(dat.encode())
    return int.to_bytes(int(crc8.crc8(dat.encode()).hexdigest(),base=16),1)

if __name__=='__main__':
    main()

このコードでは、単純に画像ファイルの RGB データを独自の形式に置き換えて保存しているだけですので、以下の Solver で簡単に元の画像を復号できます。

from PIL import Image
import numpy as np
import struct
import crc8

def timg_to_jpg(timg_file, output_file):
    try:
        with open(timg_file, "rb") as f:
            data = f.read()

        # Check TIMG header
        if data[:4] != b'TIMG':
            raise ValueError("Invalid TIMG file")

        # Extract width and height
        width = int.from_bytes(data[8:12], "big")
        height = int.from_bytes(data[12:16], "big")

        # Verify header consistency
        if data[16:20] != b'RUBY':
            raise ValueError("Invalid RUBY header")

        # Initialize RGB arrays
        r_channel = np.zeros((height, width), dtype=np.uint8)
        g_channel = np.zeros((height, width), dtype=np.uint8)
        b_channel = np.zeros((height, width), dtype=np.uint8)

        # Parse data sections
        offset = 20  # Start after header
        for color, channel in zip([b'DATR', b'DATG', b'DATB'], [r_channel, g_channel, b_channel]):
            for i in range(height):
                if data[offset:offset+4] != color:
                    raise ValueError(f"Missing {color.decode()} section")
                offset += 4  # Skip section header
                for j in range(width):
                    channel[i, j] = data[offset]
                    offset += 1

                # Skip checksum (1 byte)
                offset += 1

        # Verify footer
        if data[offset:offset+4] != b'DATE':
            raise ValueError("Invalid footer")

        # Combine channels into an image
        rgb_array = np.stack((r_channel, g_channel, b_channel), axis=2)
        img = Image.fromarray(rgb_array, "RGB")

        # Save as JPG
        img.save(output_file, "JPEG")
        print(f"Successfully converted TIMG to {output_file}")

    except Exception as e:
        print(f"Error: {e}")

# Example usage
timg_to_jpg("flag.timg", "output.jpg")

このコードで問題バイナリをデコードすると以下の通り正しい Flag を得ることができます。

image-20250125175912639

Mystery Presentation(Forensic)

We recently got this absolutely non-sensical presentation from a confidential informant, along with a notes that said “The truth hurts boomers, but it’s what on the inside that counts <3”. We can’t make heads or tails of it, but it has to be important! Can you help us out?

問題バイナリとして与えられた Office ファイルを ZIP として解凍すると、中に secret_data.7z というフォルダが見つかります。

image-20250125203721376

この中から正しい Flag を取り出すことができます。

image-20250125203707166

Packet Detective(Forensic)

You are security analyst given a pcap file containing network traffic. Hidden among these packets is a secret flag transmitted. Your task is to analyze the pcap file, filter out common traffic, and pinpoint the packet carrying the hidden flag.

問題バイナリとして与えられたパケットキャプチャの中に存在する大量の TCP 通信データを検索していくと、Flag を平文のまま通信しているストリームを特定できます。

image-20250125204105204

Security Rocks(Forensic)

I shared a super secret message, I hope its secure.

問題バイナリとして与えられた無線通信のパケットキャプチャを解析していきます。

image-20250125204615970

WireShark を使うとこの通信が WPA で暗号化されていそうであることがわかります。

image-20250125205225045

そこで、airodump-ng を使用して WPA 通信のパスワードを解析できるか確認することにしました。

airodump-ng -r dump-05.cap

image-20250125205801267

この結果から、rockyou.txt を使用して辞書攻撃を行うことで、WPA の復号パスワードを特定できます。

aircrack-ng -w /usr/share/wordlists/rockyou.txt -b D8:3A:DD:07:AA:5A dump-05.cap

image-20250125210407740

参考:ja:cracking_wpa [Aircrack-ng]

そこで、WireShark を使用して解析したパスワードから WPA 通信データを復号します。

image-20250125210903482

復号したパケットから、機密情報を含むファイルを通信していたことを確認できます。

image-20250125211341292

このファイルには以下のようなエンコード文字列が記録されていました。

image-20250125211237179

悲しいことに普通に Base64 デコードなどをいくつか試したものの上手くデコードできなかったのですが、チームメンバーが Base62 デコードで Flag を復号できることを見つけてくれたおかげで、正しい Flag が TUCTF{w1f1_15_d3f1n173ly_53cure3} であることを特定できました。

まとめ

Writeup 書く時間なさすぎてやっつけ仕事になってしまった。。。