1. はじめに
2021/11/5(金) 20:00 JST ~ 2021/11/7(日) 20:00:00 JST で「WaniCTF 2021」に参加し、途中低迷するも最後頑張ってどうにか 5113 点(得点を得た 330 チーム中 16 位)を獲得しました。


2. 簡易Writeup
問題データや想定解は公式で公開しているので、ここでは自分が使ったソルバだけ紹介します。
2-1. fox (Beginner)
整数をバイトに変換するやつ。
from Crypto.Util.number import *
print(long_to_bytes(19116989514623535769166210117786818367158332986915210065591753844573169066323884981321863605962664727709419615399694310104576887228581060509732286555123028133634836954522269304382229987197
).decode())
FLAG{R1ng_d1n9_ding_d1ng_ding3ring3ding?__Wa_p@_pa_p@_pa_p@_pow?__or_konko-n?}
最初にRSA問やったとき、これで詰まった。ほろ苦い思い出。
2-2. dango (Easy)
XOR の問題。A XOR B XOR A = B (同じものを加えると打ち消しになる)を利用する。串刺しにする感じでダンゴ、なるほど。
from Crypto.Util.number import *
from functools import reduce
def XOR(*X):
xor = lambda A, B: bytes(x ^ y for x, y in zip(A, B))
return reduce(xor, X)
ciphertext = bytes.fromhex("bd35b1c95ee9436db8fad5c3aa493660e606fa4dd7fe171aac75313c18ce5fcf86f0")
A = bytes.fromhex("cae61858ee8c7198632c652fd8416092eb165e2f847f0ebd80637ed0ffd96c6e0359")
B = bytes.fromhex("e6ed8bda14f67343d81830f0f2be3299a97b541db48cfa1873a13e8d774f1e243ce7")
C = bytes.fromhex("319fe8d6cb01539bbcb9ef9f13663d8b6274c50b0ce578c94b7910b3ca785ccea8d4")
pt = XOR(XOR(ciphertext,A),C)
print(pt.decode())
FLAG{dango_sankyodai_dango__-ooo-}
フラグが可愛い。
ちなみに弊チームのダンゴ(注:「0点」の意味)三兄弟は Pwn、Web、Hardware。一人親方でやっていくなら勉強しないといかん。
Sweet Curve (Normal)
やることは楕円曲線における加法演算。
from Crypto.Util.number import *
p = 0x89a4e2c7f834f5fbc6f2a314e373e3723de7df6283c5d97cbca509c61e02965b7ef96efce1d827bfdfa7f21d22803558bb549f9ea15dfe9f47d3976648c55feb
x_P = 0x1e1cba0e07c61cf88e9f23b9859093c33c26cf83bcfb6fe24d7559cd0ea86fb2f144ae643ac5edf6f04ef065dc7c2c18d88ae02843592d5e611029fefc0fece
y_P = 0x198420b30a4330f82380326895d0ac06a1859bc49d45cd4b08021b857d23d515163b9151fbaf7ae5f816d485d129d3b1c4630d1fb45c6790af551428a5c85667
x_Q = 0x7e32edfd7befd8df93d7b738d6a1c95e1cfd56b3a6ccc4a62e4e0ae9059b4903e71fccbe07d8d45c762b4a3ed5c9d1a2505043d033e58adb72191259b81bc47d
y_Q = 0x46016c676585feaf048fff9d5cbb45dbd598c6c4c81694e0881bf110b57012f0bac6eaf7376fee015c8cecba1fc92206ca346f7d72ee1d60f820091c85fa76b3
F = GF(p)
E = EllipticCurve(F,[-1,1])
P = E(x_P,y_P)
Q = E(x_Q,y_Q)
pt = long_to_bytes(int((P + Q)[0])).decode()
print(pt)
FLAG{7h1s_curv3_alw@ys_r3m1nd5_me_0f_pucca}
甘いもの好きなので「Sweet」に釣られて真っ先に解いた。
幸いにもパラメータがフルに与えられたので、SageMath 使ってサクッと解けた(1st Blood)。
AES-NOC (Hard)
実質ボス問。
サーバでは暗号化フラグの入手と、任意平文の暗号化ができる。
汚いソルバご容赦(サーバの戻り値からなにがしかの値を取るときは正規表現とか使って取り出すのが普通でしょうけど、何分競技中なので。。。)
import telnetlib
from Crypto.Util.number import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Util.strxor import strxor
block_size = 16
HOST = "aesnoc.crypto.wanictf.org"
PORT = 50000
def readline():
return tn.read_until(b"\n")
tn = telnetlib.Telnet(HOST, PORT)
iv = bytes.fromhex(readline().decode().strip("iv = ").replace("'",""))
tn.read_until(b"> ")
tn.write(b"1\n")
enc_hex = readline().decode().strip("encrypted_flag = ").replace("'","").replace("\n","")
C_hex =[
enc_hex[i : i + 2 * block_size]
for i in range(0, len(enc_hex), 2 * block_size)
]
pt = pad(b'}', block_size).hex()
for i in range(3):
tmp_pt = pt[0:32]
tn.read_until(b"> ")
tn.write(b"2\n")
tn.read_until(b"Plaintext [hex] > ")
tn.write(tmp_pt.encode() + b"\n")
tmp_ct = readline().decode().strip("ciphertext = ").replace("'","")[0:32]
pt = strxor(iv, strxor(bytes.fromhex(C_hex[3-i]), bytes.fromhex(tmp_ct))).hex() + pt
print(unpad(bytes.fromhex(pt), block_size).decode())
FLAG{Wh47_h4pp3n$1f_y0u_kn0w_the_la5t_bl0ck__?}
「暗号化にはAESを使うこと」みたいな規定がある所で、こういう奴を平気で使ってる所がありそうで怖い。
Flag Service(Very Hard)
「AES-CBC だし、前問が結構ハードだったし、どうせ Padding Oracle Attack だろう」という思い込みで失敗したやつ。
実際、Padding Error が検出できないので POA はムリゲー。そしてよく見ると IV が露出しているので、 IV 書き換えによる先頭 16バイト 改ざんが可能だった。
import requests
import base64
import json
from pwn import xor
URL = 'https://service.crypto.wanictf.org/'
DOMAIN = 'service.crypto.wanictf.org'
block_size = 16
session = requests.Session()
def init(session):
response = session.post(
URL + 'login',
json.dumps({
"username":"x",
}),
headers={'Content-Type': 'application/json'})
cookie_init = session.cookies['token']
return cookie_init
def update_cookie(newvalue):
session.cookies.pop('token')
newvalue = base64.b64encode(newvalue)
session.cookies.set('token', newvalue.decode(), domain=DOMAIN)
return True
initial_cookie = base64.b64decode(init(session))
iv = initial_cookie[:16]
ct = initial_cookie[16:]
m1 = '{"admin": false,'
m1_new = '{"admin": true,'
iv_new = xor(iv, xor(m1, m1_new))
new_cookie = iv_new + ct
update_cookie(new_cookie)
response = session.get(URL)
print(response.text)
FLAG{Fl1p_Flip_Fl1p_Flip_Fl1p____voila!!}
IVは金庫にでもしまっておこう。
3 おわりに
ほかの分野についても少し感想的なものを。
・Forensics
最終的には全完したけど全般的に苦戦。
PolyはVLC Media Playerで強引に再生したら再生できた。VLC Media Player最強。
breakRaidは最後少しエスパー発動した。
・Misc
最後のボス問が解けなかった。
Dockerを使いこなせていなかったので、Docker内のプログラムが実行できず膨大な時間を溶かした。大反省。
nearestはJR西日本115系の40N体質改善車(黄色)がいたので山陽地区(広島近辺)と推察。「駅前商店街」とかでテキトーにググってたら尾道が浮上して確信。
・Pwn
どうにか導入編だけやっつけた感じ。しかも t-cache poisoning 食べ残してるし。
ヘンなところで時間を溶かしてなければもっと頑張れたかもと反省。
いずれにせよ、次回は全完を目指したい。
・Reversing
一応全完したけど、問題数が少なかったからと何とかなったというのが本音。
ボス問(Emotetチックなやつ)は勉強になった。
・Web
相変わらず苦手。しかし「自分で作問して自分で解けばそれなりにできるようになるんじゃね」ということに気付いたので勝利は間近。まじか!?