TUCTF 2024(開催は 2025 年)に 0nePadding で参加しました。
忙しくて Writeup 書けてないうちにスコアボードが閉鎖してしまっていたのですが、とりあえず解いた問題だけ Writeup を書きます。
もくじ
- Mystery Box(Rev)
- Simple Login(Rev)
- Custom Image Generator(Rev)
- Mystery Presentation(Forensic)
- Packet Detective(Forensic)
- Security Rocks(Forensic)
- まとめ
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?
問題バイナリを実行すると以下のようにゲームをプレイするかを選択するメニューが起動します。
バイナリ解析でこの処理を行っている箇所を特定すると、データ領域にハードコードされたデータを 0x5a で XOR した文字列を Flag として表示する機能を持っていることがわかります。
これを解析すると 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 を表示するための認証パスワードを要求されます。
このパスワードは先頭文字から順に 3 回に分けて評価されていき、最終的にすべての検証に成功した場合に正しい Flag が復号され、表示されます。
最初の検証部分は以下の通り実装されており、文字列 ruint
から始まる場合は次の検証に進むことができます。
続く検証は以下の通り実装されており、各文字を 0x32 で XOR した値がハードコードされた値と一致するかを検証します。
最後の検証では、各文字から 0x54 を引いて 0x1a との MOD をとった値に 0x61 を足した値がハードコードされたものと一致するかを検証しています。
このような条件を満たす文字列は以下のコードで生成できます。
S = "reingvbaonetr"
for s in S:
tmp = ord(s) - 0x61
print(chr(tmp + 0x54),end="")
最終的に、ruinthosepreseX\aZiUTbaXge
というパスワードを入力することで正しい Flag TUCTF{running_through_the_subroutines!!}
を得ることができました。
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 を得ることができます。
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 というフォルダが見つかります。
この中から正しい Flag を取り出すことができます。
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 を平文のまま通信しているストリームを特定できます。
Security Rocks(Forensic)
I shared a super secret message, I hope its secure.
問題バイナリとして与えられた無線通信のパケットキャプチャを解析していきます。
WireShark を使うとこの通信が WPA で暗号化されていそうであることがわかります。
そこで、airodump-ng を使用して WPA 通信のパスワードを解析できるか確認することにしました。
airodump-ng -r dump-05.cap
この結果から、rockyou.txt を使用して辞書攻撃を行うことで、WPA の復号パスワードを特定できます。
aircrack-ng -w /usr/share/wordlists/rockyou.txt -b D8:3A:DD:07:AA:5A dump-05.cap
参考:ja:cracking_wpa [Aircrack-ng]
そこで、WireShark を使用して解析したパスワードから WPA 通信データを復号します。
復号したパケットから、機密情報を含むファイルを通信していたことを確認できます。
このファイルには以下のようなエンコード文字列が記録されていました。
悲しいことに普通に Base64 デコードなどをいくつか試したものの上手くデコードできなかったのですが、チームメンバーが Base62 デコードで Flag を復号できることを見つけてくれたおかげで、正しい Flag が TUCTF{w1f1_15_d3f1n173ly_53cure3}
であることを特定できました。
まとめ
Writeup 書く時間なさすぎてやっつけ仕事になってしまった。。。