discord -> announcement -> 第一條訊息
SECCON{JPY's_drop_makes_it_a_good_deal_to_go_to_the_Finals}
:::spoiler server.py
#!/usr/bin/env python3.9
import os
FLAG = os.getenv("FLAG", "FAKECON{*** REDUCTED ***}").encode()
def check():
try:
filename = input("filename: ")
if open(filename, "rb").read(len(FLAG)) == FLAG:
return True
except FileNotFoundError:
print("[-] missing")
except IsADirectoryError:
print("[-] seems wrong")
except PermissionError:
print("[-] not mine")
except OSError:
print("[-] hurting my eyes")
except KeyboardInterrupt:
print("[-] gone")
return False
if __name__ == '__main__':
try:
check = check()
except:
print("[-] something went wrong")
exit(1)
finally:
if check:
print("[+] congrats!")
print(FLAG.decode())
:::
可以看到,在這題中會要求輸入檔案路徑,並會嘗試讀取檔案,並比對檔案內容是否是 flag 的內容
不過,查看提供的 docker 相關設定檔,沒有看到有 mount 其他檔案,所以應該沒有其他檔案有 flag 的內容
以下是其他人的解法
可以看到,當發生其他特別的錯誤時,會在 main 中進入到 except 區塊並輸出一些提示字後執行 exit(1) 的指令,而從一些外部資料可知 python 中的 exit() 並非強制程式停止,而是產生 SystemExit exception
而因為 python finally 的特性,當在 try 或 except 中發生的 exception 沒有被 handle 時,會在執行完 finally 後才被 raise
因此,我們這邊的 exit 不會馬上被執行,而會在執行完 finally 進行檢查並印出提示文字及 flag 資訊後才執行
另外還有一些問題,要怎麼產生 exception 並被 raise 出來,以及當 raise 出來要怎麼通過檢查
第一個問題,可以看到因為 FileNotFoundError、IsADirectoryError、 ... 等都已經在 check 中被 handle 住,無法使用這些 exception,在經過嘗試後發現當檔案名稱是 Null byte 時會 raise 出 ValueError,不會被 handle 住
而第二個問題,可以發現程式中 main 的部分有變數名稱重用的問題,也就是 check = check() 這行,因此檢查時檢查的變數為 check,而由於在 check 函式中發生了 exception,導致在 check = check() 這行不會進行賦值,而 python 中有奇怪特性是當 x 是一個 function 時, x != true 但 if(x) 能成功
因此在輸入 filename 為 null byte 的情況下,會發生 exception 且 check = check() 不會進行賦值,此時的 check 變數型態為 function,因此 if(check) 成功進入,拿到 flag
SECCON{exit_1n_Pyth0n_d0es_n0t_c4ll_exit_sysc4ll}
:::spoiler index.js
const app = require("express")();
const FLAG = process.env.FLAG ?? "SECCON{dummy}";
const PORT = 3000;
app.get("/", (req, res) => {
console.log(req.query);
req.query.proxy.includes("nginx")
? res.status(400).send("Access here directly, not via nginx :(")
: res.send(`Congratz! You got a flag: ${FLAG}`);
});
app.listen({ port: PORT, host: "0.0.0.0" }, () => {
console.log(`Server listening at ${PORT}`);
});
:::
:::spoiler default.conf
server {
listen 8080 default_server;
server_name nginx;
location / {
set $args "${args}&proxy=nginx";
proxy_pass http://web:3000;
}
}
:::
可以看到,當使用 nginx 進入時,會自動帶上 proxy=nginx 的參數,而程式中會判斷是否有這一項參數,如果沒有的話就會吐出 flag
經測試發現,似乎當參數超過 1000 個的時候,後面剩下的參數就會被忽略
查詢文件後,發現是 express 框架中預設使用 qs 模組作為 query string 的 parser,而其預設限制即為 1000 個參數
因此,透過以下程式,即可獲得 flag
requests.get('http://skipinx.seccon.games:8080/?proxy=' + 'a=3&'*1000).text
SECCON{sometimes_deFault_options_are_useful_to_bypa55}
:::spoiler problem.py
from Crypto.Util.number import *
from Crypto.Random import *
from flag import flag
p = getPrime(512)
q = getPrime(512)
r = getPrime(512)
n = p * q * r
e = 2 * 65537
assert n.bit_length() // 8 - len(flag) > 0
padding = get_random_bytes(n.bit_length() // 8 - len(flag))
m = bytes_to_long(padding + flag)
assert m < n
c1p = pow(p, e, n)
c1q = pow(q, e, n)
cm = pow(m, e, n)
c1 = (c1p - c1q) % n
c2 = pow(p - q, e, n)
print(f"e = {e}")
print(f"n = {n}")
# p^e - q^e mod n
print(f"c1 = {c1}")
# (p-q)^e mod n
print(f"c2 = {c2}")
# m^e mod n
print(f"cm = {cm}")
:::
已知 e, n, c1, c2, cm,關係式如下
要求得 p, q,可觀察 c1 和 c2 的式子,發現以下關係
$\begin{aligned} c2 - c1 &\equiv (p-q)^e - (p^e - q^e) \ & \equiv (p^e - {e \choose 1} p^{e-1} q + {e \choose 2} p^{e-2} q^2 - ... - {e \choose e-1} p q^{e-1} + q^e) - (p^e - q^e) \ & \equiv - {e \choose 1} p^{e-1} q + {e \choose 2} p^{e-2} q^2 - ... - {e \choose e-1} p q^{e-1} + q^e + q^e \ & \equiv k_1 \times q \end{aligned}$
$\begin{aligned} c2 + c1 &\equiv (p-q)^e + (p^e - q^e) \ & \equiv (p^e - {e \choose 1} p^{e-1} q + {e \choose 2} p^{e-2} q^2 - ... - {e \choose e-1} p q^{e-1} + q^e) + (p^e - q^e) \ & \equiv p^e - {e \choose 1} p^{e-1} q + {e \choose 2} p^{e-2} q^2 - ... - {e \choose e-1} p q^{e-1} + p^e \ & \equiv k_2 \times p \end{aligned}$
其中,$k_1$ 和
因此可透過 GCD 的方式得出 p 和 q,即
而 r 值亦可知,參數如下:
p = 7572427786695057270624844967644562609112132599800420296747189080920032359205995588384031542287784540006438555802994008688795974493684400576592403320929717
q = 8609258896430210586523688955272794335561428099377427081622836355194006054569349679983850344916908011330202034512905353365631416251631307084038768336538857
r = 9018251874561850467651399512661829039310834429345808807288228370045576292997274498659156953954383290793552486677903139680704353709352146165598701061994853
進一步亦可得出
不過此題的
參考此文章,先將
此外,亦可計算
而在數學上有名為 Quadratic residue 的特性,當模數
不過此題的
而現在有了
完整腳本如下:
import Crypto.Util.number as cn
from sage.all import *
with open("output.txt") as fh:
data = fh.readlines()
for d in data:
exec(d)
q = cn.GCD(c2-c1, n)
p = cn.GCD(c2+c1, n)
assert (pow(p, e, n) - pow(q, e, n))%n == c1
assert pow(p-q, e, n) == c2
r = n // p // q
assert (p * q * r) == n
assert cn.GCD(p, q) == 1 and cn.GCD(p, r) == 1 and cn.GCD(r, q) == 1
phi = (p-1) * (q-1) * (r-1)
# not invertable :(
# GCD(e, phi) != 1
# https://tttang.com/archive/1504/
t = cn.GCD(e, phi)
assert t == 2
e_bar = e // t
d_bar = pow(e_bar, -1, phi)
m_t = pow(cm, d_bar, n)
# m_t = m^2 % n
# m_t_p = m_t % p = m^2 % p
# m_t_q = m_t % q = m^2 % q
# m_t_r = m_t % r = m^2 % r
m_t_p = m_t % p
m_t_q = m_t % q
m_t_r = m_t % r
# m_p = Tonelli-shanks(m_t_p, p)
# m_q = Tonelli-shanks(m_t_q, q)
# m_r = Tonelli-shanks(m_t_r, r)
def Tonelli_Shanks(n, p):
assert cn.isPrime(p)
assert pow(n, (p-1)//2, p) == 1
if (p % 4 == 3):
return pow(n, (p+1)//4, p)
assert (p % 4) == 1
S = 0
Q = p-1
while (Q % 2 == 0):
Q //= 2
S += 1
z = 2
while(pow(z, (p-1)//2, p) != p-1):
z += 1
M = S
c = pow(z, Q, p)
t = pow(n, Q, p)
R = pow(n, (Q+1)//2, p)
r = 0
while(True):
if(t == 0):
return 0
elif(t == 1):
return R
else:
i = 1
t2 = pow(t, 2, p)
for i in range(1, M):
if((t2-1) % p == 0):
break
t2 = pow(t2, 2, p)
b = pow(c, pow(2, M-i-1), p)
M = i
c = pow(b, 2, p)
t = (t * c) % p
R = (R * b) % p
m_p = Tonelli_Shanks(m_t_p, p)
m_q = Tonelli_Shanks(m_t_q, q)
m_r = Tonelli_Shanks(m_t_r, r)
for i in range(2**3):
mm_p,mm_q,mm_r = m_p,m_q,m_r
if(i % 2 == 1):
mm_p = (-mm_p)%p
if((i >> 1) % 2 == 1):
mm_q = (-mm_q)%q
if((i >> 2) % 2 == 1):
mm_r = (-mm_r)%r
flag = CRT([mm_p,mm_q,mm_r],[p,q,r])
print(cn.long_to_bytes(flag))
SECCON{being_able_to_s0lve_this_1s_great!}
另一個類似的解法 (使用 sage 自動解 $x^2 \equiv n\ (mod\ p)$)
import Crypto.Util.number as cn
from sage.all import *
with open("output.txt") as fh:
data = fh.readlines()
for d in data:
exec(d)
q = cn.GCD(c2-c1, n)
p = cn.GCD(c2+c1, n)
assert (pow(p, e, n) - pow(q, e, n))%n == c1
assert pow(p-q, e, n) == c2
r = n // p // q
assert (p * q * r) == n
assert cn.GCD(p, q) == 1 and cn.GCD(p, r) == 1 and cn.GCD(r, q) == 1
phi = (p-1) * (q-1) * (r-1)
# not invertable :(
# GCD(e, phi) != 1
# https://tttang.com/archive/1504/
t = cn.GCD(e, phi)
assert t == 2
e_bar = e // t
d_bar = pow(e_bar, -1, phi)
m_t = pow(cm, d_bar, n)
# m_t = m^2 % n
POLp = PolynomialRing(GF(p), 'pp')
POLq = PolynomialRing(GF(q), 'qq')
POLr = PolynomialRing(GF(r), 'rr')
pp = POLp.gen()
qq = POLq.gen()
rr = POLr.gen()
f_p = pp**2 - m_t # pp^2 = m_t
f_q = qq**2 - m_t # qq^2 = m_t
f_r = rr**2 - m_t # rr^2 = m_t
pps = f_p.roots()
qqs = f_q.roots()
rrs = f_r.roots()
for i in range(len(pps)):
for j in range(len(qqs)):
for k in range(len(rrs)):
flag = CRT([int(pps[i][0]), int(qqs[j][0]), int(rrs[k][0])], [p, q, r])
print(cn.long_to_bytes(flag))
首先丟到 ghidra 翻譯,翻出來的 code 很噁心,不過可以看出是會將輸入字串與一些數字運算做 xor 後最終進行比對
稍微看了一下 asm 後,發現 xor 的 key 在指令 0x001250 (在 gdb 上對應的位置是 0x555555555250) 時的 AL 可以看到,直接使用動態分析方式 break 在那邊查看 key
發現 key 是 Welcome to SECCON 2022
解密腳本:
# 591E2320202F2004 2B2D3675357F1A44 0736506D035A1711 362B470401093C15 380a41
enc = b"\x04\x20\x2f\x20" + b"\x20\x23\x1e\x59" + \
b"\x44\x1a\x7f\x35" + b"\x75\x36\x2d\x2b" + \
b"\x11\x17\x5a\x03" + b"\x6d\x50\x36\x07" + \
b"\x15\x3c\x09\x01" + b"\x04\x47\x2b\x36" + \
b"\x41\x0a\x38"
key = b"Welcome to SECCON 2022"
flag = [0 for _ in range(len(enc))]
for i,e in enumerate(enc):
flag[i] = e ^ key[i%len(key)]
flag = [f for f in flag]
print(bytes(flag))
SECCON{y0u_f0und_7h3_baby_flag_YaY}
這題蠻有趣的,思路應該是解掉一個函式就好了
eguite::Crackme::onclick::hb69201652eb2ef3b
應該可以用推的推出來Licence或跳轉到輸出Flag的地方,不過時間不夠沒解出來。
幫補個找到的 writeup
input[19] == '-' and input[26] == '-' and input[33] == '-'
from z3 import *
x, y, z, w = BitVecs('x y z w', 64)
solve(x + y == 0x8B228BF35F6A, y + z == 15172161, z + w == 4199291551, w + x == 0x8B238557F7C8, y ^ z ^ w == 4184371021)SECCON{8b228b98e458-5a7b12-8d072f-f9bf1370}



















