WaniCTF 2024 脆弱Writeup
結果
Wani CTF 2024をチームで参加してきました。
36位/1475チームでした!
解けなかった問題も含めて振り返ります。(追記予定)
Crypto
beginners_rsa (121pt 530 solves Beginner)✅
問題
RSAの素数が5つあるという問題。
p = getPrime(64)
q = getPrime(64)
r = getPrime(64)
s = getPrime(64)
a = getPrime(64)
n = p*q*r*s*a
e = 0x10001
FLAG = b'FLAG{This_is_a_fake_flag}'
m = bytes_to_long(FLAG)
enc = pow(m, e, n)
n = 317903423385943473062528814030345176720578295695512495346444822768171649361480819163749494400347
e = 65537
enc = 127075137729897107295787718796341877071536678034322988535029776806418266591167534816788125330265
解法
factordbでnを素因数分解できた。
p = 9953162929836910171
q = 11771834931016130837
r = 12109985960354612149
s = 13079524394617385153
a = 17129880600534041513
素数5つのバージョンなど知らなかったが、勘でRSAの逆算をさせてみたらできてしまった。
phi = (p - 1) * (q - 1) * (r - 1) * (s - 1) * (a - 1)
d = pow(e, -1, phi)
m = pow(enc, d, n)
flag = long_to_bytes(m)
FLAG{S0_3a5y_1254!!}
beginners_aes (125pt 453 solves Beginner)✅
問題
AESのkeyとivの最後の一文字がランダムでわからないという問題。
key = b'the_enc_key_is_'
iv = b'my_great_iv_is_'
key += urandom(1)
iv += urandom(1)
cipher = AES.new(key, AES.MODE_CBC, iv)
FLAG = b'FLAG{This_is_a_dummy_flag}'
flag_hash = hashlib.sha256(FLAG).hexdigest()
msg = pad(FLAG, 16)
enc = cipher.encrypt(msg)
enc = b'\x16\x97,\xa7\xfb_\xf3\x15.\x87jKRaF&"\xb6\xc4x\xf4.K\xd77j\xe5MLI_y\xd96\xf1$\xc5\xa3\x03\x990Q^\xc0\x17M2\x18'
flag_hash = 6a96111d69e015a07e96dcd141d31e7fc81c4420dbbef75aef5201809093210e
解法
ランダムでわからない部分を総当たりでdecryptする。
flag_hashのハッシュが与えられているということは総当たりしろってことですね。
for i in range(256):
for j in range(256):
key = key_prefix + bytes([i])
iv = iv_prefix + bytes([j])
try:
cipher = AES.new(key, AES.MODE_CBC, iv)
dec = unpad(cipher.decrypt(enc), 16)
if hashlib.sha256(dec).hexdigest() == flag_hash:
print(f'Decrypted FLAG: {dec}')
break
except ValueError:
continue
FLAG{7h3_f1r57_5t3p_t0_Crypt0!!}
replacement (126pt 431 solves Easy)✅
問題
秘密の文章がmd5のハッシュで暗号化されている。
enc = []
for char in cal:
x = ord(char)
x = hashlib.md5(str(x).encode()).hexdigest()
enc.append(int(x, 16))
[265685380796387128074260337556987156845, 75371056103973480373443517203033791314, 330443362254714811278522520670919771869, 127044987962124214100696270195559210814, 75371056103973480373443517203033791314, 57512852240092789512489991536185408584, 330443362254714811278522520670919771869, 301648155472379285594517050531127483548,
# 以下略
解法
文字列を全てmd5にして、テーブルで置換するという作戦。
まず、コードをそのまま流用してテーブルを書き出す。
cal = string.printable
enc = []
for char in cal:
x = ord(char)
x = hashlib.md5(str(x).encode()).hexdigest()
enc.append(int(x, 16))
for e, c in zip(enc, cal):
print(c, e)
0 133164625458131999197654962205376583862
1 324787361952219506718126426467652498112
2 256249087116017178292043637459846421514
3 53459933652527578064242465506376923016
4 204791166937441563272975036703176244680
5 287344569722037931951990749151720527344
6 221342036062217931314813830374780339254
...
このテーブルに対して、ヒットしたものを置換する
(シェル芸ごめん)
$ cat my_diary_11_8_Wednesday.txt | tr "," "\n" | xargs -I{} grep {} rep.csv | cut -f 1 -d " " | tr -d "\n"
Wednesday,11/8,clearskies.Thismorning,Ihadbreakfastatmyfavoritecafe.Drinkingthefreshlybrewedcoffeeandsavoringthewarmbutterytoastisthebest.Changingthesubject,Ireceivedanemailtodaywithsomethingratherpeculiarinit.Itcontainedamysteriousmessagethatsaid"Thisisasecretcode,sopleasedon'ttellanyone.FLAG{13epl4cem3nt}".Howstrange!Gureisya
Easy calc (197pt 95 solves Easy)
問題
FLAG = os.getenvb(b"FLAG", b"FAKE{THIS_IS_NOT_THE_FLAG!!!!!!}")
def encrypt(m: bytes, key: int) -> bytes:
iv = os.urandom(16)
key = long_to_bytes(key)
key = md5(key).digest()
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
return iv + cipher.encrypt(m)
def f(s, p):
u = 0
for i in range(p):
u += p - i
u *= s
u %= p
return u
p = getPrime(1024)
s = random.randint(1, p - 1)
A = f(s, p)
ciphertext = encrypt(FLAG, s).hex()
p = 108159532265181242371960862176089900437183046655107822712736597793129430067645352619047923366465213553080964155205008757015024406041606723580700542617009651237415277095236385696694741342539811786180063943404300498027896890240121098409649537982185247548732754713793214557909539077228488668731016501718242238229
A = 60804426023059829529243916100868813693528686280274100232668009387292986893221484159514697867975996653561494260686110180269479231384753818873838897508257692444056934156009244570713404772622837916262561177765724587140931364577707149626116683828625211736898598854127868638686640564102372517526588283709560663960
ciphertext = '9fb749ef7467a5aff04ec5c751e7dceca4f3386987f252a2fc14a8970ff097a81fcb1a8fbe173465eecb74fb1a843383'
解法
解けなかったので、復習しました。
数式を展開する。s^p
があるからsについて解けないじゃんって諦めていた。フェルマーの小定理を使っていいんだ。
fについては以下のように書ける。
def f(s, p):
return s * pow(1 - s, -1, p) % p
sについて解くと s = A / (1 + A)
s = A * pow(1 + A, -1 , p) % p
iv = bytes.fromhex(ciphertext[:32])
ciphertext = bytes.fromhex(ciphertext[32:])
key = long_to_bytes(s)
key = md5(key).digest()
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
print(cipher.decrypt(ciphertext).decode())
dance (205pt 85 solves Normal)✅
問題
ログインして暗号化できるシステムのPythonコードが与えられる(一部抜粋)
def Register():
global d
username = input('Enter username: ')
if username in d:
print('Username already exists')
return
dt_now = datetime.datetime.now()
minutes = dt_now.minute
sec = dt_now.second
data1 = f'user: {username}, {minutes}:{sec}'
data2 = f'{username}'+str(random.randint(0, 10))
d[username] = make_token(data1, data2)
print('Registered successfully!')
print('Your token is:', d[username])
return
class MyCipher:
def __init__(self, key: bytes, nonce: bytes):
self.key = key
self.nonce = nonce
self.counter = 1
self.state = List[F2_32]
def __quarter_round(self, a: F2_32, b: F2_32, c: F2_32, d: F2_32):
a += b; d ^= a; d <<= 16
c += d; b ^= c; b <<= 12
a += b; d ^= a; d <<= 8
c += d; b ^= c; b <<= 7
return a, b, c, d
def __Qround(self, idx1, idx2, idx3, idx4):
self.state[idx1], self.state[idx2], self.state[idx3], self.state[idx4] = \
self.__quarter_round(self.state[idx1], self.state[idx2], self.state[idx3], self.state[idx4])
def __update_state(self):
for _ in range(10):
self.__Qround(0, 4, 8, 12)
self.__Qround(1, 5, 9, 13)
self.__Qround(2, 6, 10, 14)
self.__Qround(3, 7, 11, 15)
self.__Qround(0, 5, 10, 15)
self.__Qround(1, 6, 11, 12)
self.__Qround(2, 7, 8, 13)
self.__Qround(3, 4, 9, 14)
username = 'gureisya'
ciphertext = '061ff06da6fbf8efcd2ca0c1d3b236aede3f5d4b6e8ea24179'
解法
使われている暗号について調べてみると、ChaCha20という暗号そのものであった。
参考:https://tex2e.github.io/blog/crypto/chacha20poly1305
tokenを作るところをよく見ると、60x60x11通りしか存在しないことがわかる。
data1 = f'user: {username}, {minutes}:{sec}'
data2 = f'{username}'+str(random.randint(0, 10))
d[username] = make_token(data1, data2)
ということで、tokenを全て求めて、復号する。
ちなみに、ChaCha20のdecryptが記述されていないが、encryptと同じ処理をすることで復号できる。
tokens = []
for minutes in range(60):
for sec in range(60):
for randnum in range(11):
data1 = f'user: {username}, {minutes}:{sec}'
data2 = f'{username}'+str(randnum)
tokens.append(make_token(data1, data2))
for token in tokens:
cipherbytes = bytes.fromhex(ciphertext)
sha256 = hashlib.sha256()
sha256.update(token.encode())
key = sha256.hexdigest()[:32]
nonce = token[:12]
cipher = MyCipher(key.encode(), nonce.encode())
decrypted = cipher.encrypt(cipherbytes)
if b'FLAG' in decrypted:
print(decrypted)
FLAG{d4nc3_l0b0t_d4nc3!!}
speedy (235pt 60 solves Hard) ✅
問題
オリジナルであろうブロック暗号について、解読する問題。
def rotl(x, y):
x &= 0xFFFFFFFFFFFFFFFF
return ((x << y) | (x >> (64 - y))) & 0xFFFFFFFFFFFFFFFF
class MyCipher:
def __init__(self, s0, s1):
self.X = s0
self.Y = s1
self.mod = 0xFFFFFFFFFFFFFFFF
self.BLOCK_SIZE = 8
def get_key_stream(self):
s0 = self.X
s1 = self.Y
sum = (s0 + s1) & self.mod
s1 ^= s0
key = []
for _ in range(8):
key.append(sum & 0xFF)
sum >>= 8
self.X = (rotl(s0, 24) ^ s1 ^ (s1 << 16)) & self.mod
self.Y = rotl(s1, 37) & self.mod
return key
def encrypt(self, pt: bytes):
ct = b''
for i in range(0, len(pt), self.BLOCK_SIZE):
ct += long_to_bytes(self.X)
key = self.get_key_stream()
block = pt[i:i+self.BLOCK_SIZE]
ct += bytes([block[j] ^ key[j] for j in range(len(block))])
return ct
暗号化処理
s0 = bytes_to_long(os.urandom(8))
s1 = bytes_to_long(os.urandom(8))
cipher = MyCipher(s0, s1)
secret = b'FLAG{'+b'*'*19+b'}'
pt = pad(secret, 8)
ct = cipher.encrypt(pt)
暗号化結果
ct = b'"G:F\xfe\x8f\xb0<O\xc0\x91\xc8\xa6\x96\xc5\xf7N\xc7n\xaf8\x1c,\xcb\xebY<z\xd7\xd8\xc0-\x08\x8d\xe9\x9e\xd8\xa51\xa8\xfbp\x8f\xd4\x13\xf5m\x8f\x02\xa3\xa9\x9e\xb7\xbb\xaf\xbd\xb9\xdf&Y3\xf3\x80\xb8'
解法
8文字を1ブロックとして、4つのブロックで暗号化されている。
それぞれのブロックにおいて使われる変数をx_i, y_i, key_i、暗号化文をc_i、平文をm_iと留守。
コードをよく読むと、以下の手がかりが得られた。
- c1~c4は出力結果ctに含まれている
- x1~x4も出力結果ctに含まれている
- y_i→yi-1が求められる(xi-1は判明しているため)
- 平文m4は
b'}\x07\x07\x07\x07\x07\x07\x07'
である - key_i→y_iが求められる(xiは判明しているため)
つまり、地道に計算していけば、y_iが求まり、keyも平文も求めることができる。
key4を求める
c4 = b'\xb9\xdf&Y3\xf3\x80\xb8'
m4 = b'}\x07\x07\x07\x07\x07\x07\x07'
key4 = bytes([c4[j] ^ m4[j] for j in range(8)])
key4->y4
x4 = 190182108432150461
sum = key4[::-1] # 逆順
y4 = (bytes_to_long(sum) - x4) & 0xFFFFFFFFFFFFFFFF
# y4 = 13611085980422777095
yiを求める
# y4 -> y3
y3 = cp.rotl(y4, 27)
y3 ^= x3
# y3 -> y2
y2 = cp.rotl(y3, 27)
y2 ^= x2
# y2 -> y1
y1 = cp.rotl(y2, 27)
y1 ^= x1
decrypt
これまで求めた値に対して、decrypt(x1, y1, c1)みたいに計算すれば、各ブロックの平文がもとまる。
def decrypt(x, y, c):
s0 = x
s1 = y
sum = (s0 + s1) & 0xFFFFFFFFFFFFFFFF
s1 ^= s0
key = []
for _ in range(8):
key.append(sum & 0xFF)
sum >>= 8
m = bytes([c[j] ^ key[j] for j in range(len(c))])
print(m)
FLAG{x013_ro74te_5hif7!!}
Many Xor Shift (307pt 29 solves Normal)
問題
FLAG = b'FAKE{XXXXXXXXXXXXXXXXXXXXXX}'
N = 7
M = 17005450388330379
WORD_SIZE = 32
WORD_MASK = (1 << WORD_SIZE) - 1
def encrypt(m):
state = [int.from_bytes(m[i:i+4]) for i in range(0, len(m), 4)]
assert len(state) == N
def xor_shift():
nonlocal state
t = state[0] ^ ((state[0] << 11) & WORD_MASK)
for i in range(N-1):
state[i] = state[i+1]
state[-1] = (state[-1] ^ (state[-1] >> 19)) ^ (t ^ (t >> 8))
for _ in range(M):
xor_shift()
return state
N = 7
M = 17005450388330379
WORD_SIZE = 32
state = [1927245640, 871031439, 789877080, 4042398809, 3950816575, 2366948739, 935819524]
解法
解けなかったので、後日復習しました。
Xorshiftという擬似乱数生成らしい。
公式Writeup
(編集中)
uf
問題
解けなかったので、後日復習
解法
解けなかったので、後日復習
公式Writeup
Forensics
I_wanna_be_a_streamer (169pt 144 solves Easy)✅
問題
ストリーミングした通信のpcapファイルが与えられる
解法
RCPのパケットから、映像の取得を試みた。
チームメンバーが以下のような参考を教えてくれた。
- https://fumimaker.net/entry/2021/03/17/215110
- https://community.cisco.com/t5/tkb-コラボレーション-ドキュメント/gstreamer-で-wireshark-の-rtp-パケットからビデオを再生する方法/ta-p/3162522
記事を参考に、GStreamerを使って以下のコマンドを実行すると動画が生成できた。
C:\gstreamer\1.0\msvc_x86_64\bin\gst-launch-1.0.exe -m -v filesrc location=input.pcap ! pcapparse src-port=22000 dst-port=59974 ! application/x-rtp,media=video,clock-rate=90000 ! rtpjitterbuffer ! rtph264depay ! avdec_h264 ! videoscale ! video/x-raw, width=1280, height=720 ! videoconvert ! x264enc ! mp4mux ! filesink location=output.mp4
src-port=22000 dst-port=59974 は、WiresharkからTelephony>RTP>RTP Streamsから確認。
おわりに
チーム「脆弱エンジニア(full_weak_engineer)」では、SECCON CTF決勝進出を目指してCTFに参加しています!
チームでCTFをやりたいメンバーを常に募集しています。(Pwn, Rev担当大歓迎)
YouTubeの概要欄にあるDiscordリンクから、気軽にご参加ください!
Discussion