TRX CTF2026 Writeup
はじめに
これはチーム Vongole bianco、4/25 04:00 ~ 4/27 04:00開催のTRX CTF 2026に関するWriteupです。
チームメンバーは
・Wing2C1
・Lqd0f
になります。Lqd0fは解けた問題がありませんので今回彼のはありません。かくいう話、私も1問しか解けなかったです :(
Category, Quizの順に書いていきます。
Sanity
Sanity Check
Description

素直に入力します。
flag
TRX{DAJE_ROMA}
Crypto
HBKG
Description
HB key generator? I wonder what HB stands for...
Attachments
crypto_hbkg.zip
唯一解けたやつです。
まず、重要なファイルを提示します。
import random
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
TIMEOUT = 60
FLAG = "TRX{fake_flag}"
def generate_rotation_matrix():
theta = pi * random.randint(1, 2^16) / random.randint(1, 2^16)
phi = pi * random.randint(1, 2^16) / random.randint(1, 2^16)
v = vector([cos(theta)*sin(phi), sin(theta)*sin(phi), cos(phi)])
theta = random.randint(1, 359)*pi/180
u = v / v.norm()
K = matrix([
[0, -u[2], u[1]],
[u[2], 0, -u[0]],
[-u[1], u[0], 0]
])
R = matrix.identity(3) + sin(theta)*K + (1 - cos(theta))*(K*K)
return R, v
def get_tangent(point, axis):
return point.cross_product(axis).simplify_full()
def check_on_sphere(point):
return (point.norm()^2 - 1).simplify_full().n() == 0
def read_point():
print("Insert a point")
x, y, z = var('x y z')
v = vector([x, y, z])
values = [SR(input("x: ")), SR(input("y: ")), SR(input("z: "))]
v = vector([x.subs({x: values[0]}), y.subs({y: values[1]}), z.subs({z: values[2]})])
if not check_on_sphere(v):
print("point not on the sphere")
exit()
return v
def rotate(v, R):
return v*R
def cipher(v, axis):
t = get_tangent(v, axis)
k = t*random.randint(2^127, 2^128)
data = ','.join([str(c) for c in k])
h = hashlib.sha256(data.encode()).digest()
aes = AES.new(h, AES.MODE_ECB)
ciphertext = aes.encrypt(pad(FLAG.encode(), AES.block_size))
return ciphertext.hex()
def print_menu():
print("Welcome to HB Key Generator!")
print("1) Rotate a point")
print("2) Encrypt")
def main():
R, axis = generate_rotation_matrix()
can_rotate = 0
print_menu()
while True:
choice = int(input("> "))
if choice == 1:
if can_rotate < 2:
can_rotate += 1
print(rotate(read_point(), R))
else:
print("can't rotate anymore")
exit()
elif choice == 2:
print(cipher(read_point(), axis))
exit()
else:
print("Invalid choice")
exit()
if __name__ == "__main__":
signal.alarm(TIMEOUT)
main()
これが暗号化するためのファイルです。SageMath(以下Sage)というものがあります。
Pythonをベースとした数学の幅広い処理を扱うソフトウェアです。
これがサーバー上で動いているためそれに接続して挙動を見て解読します。
FLAG = "TRX{fake_flag}"
より、配布ファイルにあるフラグではないことは明らかです。
さて、このファイルの流れを解説します。
1. R(回転行列)と axis(回転軸ベクトル)を生成
2. ループ開始
2.1 ユーザーの点を R で回転させて表示(最大2回まで)
2.2 ユーザーの点と axis から暗号化したフラグを出力
ここから重要な関数を解説していく。
generate_rotation_matrix()
ランダムな角度
次に
コード内で
最後に
read_point()
位置ベクトルを入力する。単位球上の点でなかったらエラーを返す。
rotate(v, R)
位置ベクトルを回転行列
cipher(v, axis)
入力された位置ベクトルと(1)のベクトル
引数の
このコードには暗号化する際の致命的な脆弱性がある。それは、cipher関数において
rotateする際に、例として1,0,0を順に入力すると
((cos(31/180pi) +
1)cos(3058/57645pi)sin(2312/7501pi)^2sin(3058/57645pi)/(cos(3058/57645p
i)^2sin(2312/7501pi)^2 + sin(2312/7501pi)^2sin(3058/57645pi)^2 +
cos(2312/7501pi)^2) +
cos(2312/7501pi)sin(31/180pi)/sqrt(cos(3058/57645pi)^2sin(2312/7501pi)^
2 + sin(2312/7501pi)^2sin(3058/57645pi)^2 + cos(2312/7501pi)^2), -
(cos(3058/57645pi)^2sin(2312/7501pi)^2/(cos(3058/57645pi)^2sin(2312/750
1pi)^2 + sin(2312/7501pi)^2sin(3058/57645pi)^2 + cos(2312/7501pi)^2) +
cos(2312/7501pi)^2/(cos(3058/57645pi)^2sin(2312/7501pi)^2 +
sin(2312/7501pi)^2sin(3058/57645pi)^2 + cos(2312/7501pi)^2))
(cos(31/180pi) + 1) + 1, (cos(31/180pi) +
1)cos(2312/7501pi)sin(2312/7501pi)sin(3058/57645pi)/(cos(3058/57645pi)^
2sin(2312/7501pi)^2 + sin(2312/7501pi)^2sin(3058/57645pi)^2 +
cos(2312/7501pi)^2) -
cos(3058/57645pi)sin(2312/7501pi)sin(31/180pi)/sqrt(cos(3058/57645pi)^2
sin(2312/7501pi)^2 + sin(2312/7501pi)^2sin(3058/57645pi)^2 +
cos(2312/7501pi)^2))
と返ってくるとしよう。数式のままで返ってくることから角度が窃取できる。
角度がそのまま出てくる理由は、
ここで、
theta = random.randint(1, 359)*pi/180
phi = pi * random.randint(1, 2^16) / random.randint(1, 2^16)
theta = pi * random.randint(1, 2^16) / random.randint(1, 2^16)
(1)の式から、
よって、読み取った角度、例でいうならx,y,zを順に
x:cos(3058*pi/57645)sin(2312*pi/7501)
y:sin(3058*pi/57645)sin(2312*pi/7501)
z:cos(2312*pi/7501)
と入力すると
cb1cca360b02a47fe8f9a59b7331d491c3298f75420e91a775c43d2fc68f0619329e280
4293a2adb7188716be74601f2d14f6a59c3b6179bdb0c943421a770d0c71784c7f43c94
7e19ef4e00e9654919f612cdead1daf549fa5047b0b21032d3
が固定で返ってくる。あとはこれを"0,0,0"でデコードするとflagが返ってくる。
flag
TRX{y0u_c4n7_c0mb_TheHairyCoconut_https://en.wikipedia.org/wiki/Hairy_ball_theorem}
おわりに
決勝戦がイタリアで開催されるという情報しか知らずにでたらめに実力試しとして応募しました。
TRX CTFについて、公式Discordのアナウンスに以下の記述があります。
Welcome to the 2nd edition of TRX CTF!
TRX CTF is a Jeopardy-style competition featuring challenges in Web, Pwn, Rev, Crypto, Misc, and Blockchain.
The top 8 eligible teams will qualify for the on-site finals to be held in Italy this autumn.
Jeopardy-styleとは、一般的なCTFの形式で様々なカテゴリを好きな順に解いていくCTFです。
第二回目のCTFだそうで、新興のようです。
そんでもって、舐めてかかったらほぼ解けずに終わりました。伸びしろがあるってことですね!
Discussion