# SWSEC LAB1 Writeup
###### tags: `swsec` `writeup`
<style>
p:has(img) {
text-align: center;
}
</style>
## COR
triLFSR 是一種 Geffe generator,其輸出為 $x_1$ ? $x_2$ : $x_3$
![](https://hackmd.io/_uploads/HkRg8c0Wa.png =150x220)
但比較輸出與 $x_2$、$x_3$ 的關係後,可以發現輸出有 75% 的機率分別跟 $x_2$、$x_3$ 相同,即當我們猜測的初始 state 是對的,output 會有 75% 是相同的。
因為 tap 已知,因此我們可以分別對 $x_2$ 與 $x_3$ 初始 state 進行爆破,需要爆破的數量從 $2^{19 + 23 + 27}$ 降為 $2^{19} + 2^{23} + 2^{27}$。
因為題目是先直接輸出,我們可以取前 `len(state)` 個 bit 直接做 flip 來回推可能的 state (75% 是對的,只要 flip 25% 的 bit 就有可能猜到)。
`COR/brute_COR.py`
```python
def brute(output, bits):
for i in range(bits):
for t in itertools.combinations(range(bits),i):
state = brute_sub(output, bits, t)
if state:
return state
def brute_sub(output, bits, t):
state = output[:bits]
for idx in t:
state[idx] = int(not state[idx]) # bit flip
lfsr = LFSR([0, 1, 2, 5], state)
tmp = [lfsr.getbit() for _ in range(FLAG_OFFSET)]
union = sum(map(lambda x: int(x[0] == x[1]), zip(output, tmp)))
if union >= FLAG_OFFSET * 0.7:
return state
```
以上的 `brute()` 會透過 `itertools.combination` 產生要做 bit flipping 的 index,再傳給 `brute_sub()` 來測試這個猜測是否正確。
由於可能不能整除導致誤差,因此取 0.7 而非 0.75 作為 threshold。
在爆破完 $x_2$ 與 $x_3$ 的 state 後,最後再進行 $x_1$ 的爆破即可。
`COR/brute_COR.py`
```python
FLAG_OFFSET = 200
# lfsr3
lfsr3_state = brute(output, 27)
# lfsr 2
lfsr2_state = brute(output, 23)
for n in tqdm.tqdm(range(1 << 19)):
lfsr3 = LFSR([0, 1, 2, 5], lfsr3_state)
lfsr2 = LFSR([0, 1, 2, 5], lfsr2_state)
state = list(map(int, f"{n:>019b}"))
lfsr1 = LFSR([0, 1, 2, 5], state)
lfsr = triLFSR(lfsr1, lfsr2, lfsr3)
for gt in output[:FLAG_OFFSET]:
b = lfsr.getbit()
if b != gt:
break
else:
flag = [b ^ lfsr.getbit() for b in output[FLAG_OFFSET:]]
break
```
![](https://hackmd.io/_uploads/rklI1Avfa.png)
FLAG: `FLAG{Corre1ati0n_Attack!_!}`
## POA
Padding Oracle $O_{p}$ (server) 可以幫我們檢查 padding 是否正確。
在 AES 的 CBC 模式中,前一塊的密文 (或是 IV) 會先與明文 XOR 再進行加密:
![](https://hackmd.io/_uploads/Bkae-lJGT.png =350x)
而 CBC 解密時,則是會在解密區塊後,跟前一塊的密文 (或是 IV) XOR 得到明文:
![](https://hackmd.io/_uploads/Syp7bl1fa.png =350x)
這代表解密輸出的明文能透過前一塊的密文 (或是 IV) 來控制,若 $O_p$ 會在 padding 錯誤時噴 error,便可用前一塊的密文 (或是 IV) 來猜測原本區塊解密出的結果,最後與原始密文 (或是 IV) XOR 即可得到真正的明文。
使用以上方法,可以一次解一個 block。從 block 尾端往前,一個一個 byte 暴力猜,當 padding error 未發生,即可判斷已經成功將其 XOR 為正確的 padding,因此將 padding byte $\oplus$ 猜的 byte $=$ 原本的 IV,再將 IV $\oplus$ 原始密文則得到原始明文。
![](https://hackmd.io/_uploads/ryvjITvGp.png =350x)
`POA/brute.py`
```python
def brute_block(iv: bytes, block: bytes) -> bytes:
# tmp == data before xor
tmp = [0x00] * BLOCK_SIZE
for i in tqdm.tqdm(range(BLOCK_SIZE)):
idx = -(i + 1)
brute_byte(iv, tmp, block, idx)
return bytes(xor_array(tmp, iv))
def brute_byte(iv: bytes, tmp: List[int], block: bytes, idx: int):
for i in range(128):
tmp[idx] = i ^ iv[idx] ^ 0x80
iv_bytes = bytes(tmp)
r.sendline((iv_bytes + block).hex().encode())
res = r.recvline()
# padding correct: [0x80][0x00]...[0x00]
# leave index > idx as original bytes
if b"wrong" not in res:
tmp[idx] ^= 0x80
break
else:
# 0x80 case
tmp[idx] = 0x80 ^ iv[idx]
def padding_oracle(data: bytes) -> bytes:
blocks = [data[i:i+BLOCK_SIZE] for i in range(0, len(data), BLOCK_SIZE)]
plaintext = b""
for i in range(1, len(blocks)):
iv_block = blocks[i-1]
block = blocks[i]
print("Block", i)
plaintext += brute_block(iv_block, block)
return plaintext
```
因為此題的 padding 格式是 `<oooooo[0x80][0x00]...[0x00]> (find first [0x80])`,且明文是 ASCII,每個 byte 皆在 0 ~ 127 之間,因此只需要暴力炸 (0 ~ 127) $\oplus$ 0x80 (小於 0x80 => MSB 為 0,XOR 0x80 後 MSB 會被設成 1)。
炸第一個 byte 時,要讓 XOR 輸出 0x80,炸後續 byte 後則要使正在炸的 byte 為 0x80,其後的則為 0x00,所以將結果 XOR 0x80 則取得 IV,另 IV XOR 解密輸出為 0x00,因此 IV 同時等於明文。
另外若炸 0 ~ 127 都噴錯,則代表原本 IV $\oplus$ 密文 = 0x80,所以 flag 為 IV $\oplus$ 0x80。
![](https://hackmd.io/_uploads/rJvhATvfp.png)
FLAG: `FLAG{pAdd1NG_0rAcL3_A77aCK}`
## LSB
這題是一個 LSB Oracle $O_{LSB}$,會根據使用者所輸入的明文加密後,輸出 密文 % 3 的結果。
假設明文為 $m$,其實可將其拆為 $m = 3y_1 + x_0$,$x_0$ 即為我們可以取得的輸出。
$3^{-1} m = y_1 + 3^{-1} x_0 = (3y_2 + x_1) + 3^{-1} x_0 \quad (\text{mod} \quad n)$,將 $3^{-1} m = (3^{-1})^e c$ 作為輸入可以得到 $r = [x_1 + 3^{-1} x_0]_{\mod n} \mod 3$。此時 $x_0$ 與 $3^{-1} \mod n$ 皆已知,因此可推論 $x_1$。
一直持續算下去,則可得到 $x_k$, $x_{k-1}$, ..., $x_0$,並將 $m = \sum^k_{i=0} 3^{i} x_i$ 還原出來。當 $3^k > n$ 時即可終止。
`LSB/brute.py`
```python
mod = 3
mod_inv = pow(mod, -1, n)
mod_inv_e = pow(mod, -e, n)
flag_long = 0
exponential = 1
remain = 0
while exponential <= n:
r.sendline(str(enc).encode())
res = int(r.recvline().decode().strip())
x = (res - remain) % mod
flag_long += x * exponential
enc = (enc * mod_inv_e) % n
remain = (mod_inv * (remain + x)) % n
exponential *= mod
```
![](https://hackmd.io/_uploads/HyJOk0Df6.png)
FLAG: `FLAG{Viycx_qsklsjgmeld_fgd_spkgjo}`
## dlog
此題可自行輸入 $p$ 與 $g$,題目會回傳 $g^{flag} \mod p$。
要解出 flag,則此題實際上為一個 Discrete Logarithm Problem (DLP)
當 $p$ 的 order ($\phi(p)$) 是 smooth 的 (由許多小質數相乘),則可透過 Pohlig-Hellman 將一個大的 DLP 化為數個 order 小的 DLP,此時可用大步小步等算法來快速計算小 order DLP 的解。
![](https://hackmd.io/_uploads/rkthxk_M6.png =400x)
最後再透過 CRT 將這些解出來的 $x_i$ 算出 $x\mod\phi(p)$,即為 flag。
![](https://hackmd.io/_uploads/Sk0mKCDMa.png =400x)
`dlog/exp.py`
```python
from sage.all import *
from Crypto.Util.number import getPrime, isPrime
def gen_smooth_number(bits):
while True:
N = 2
while (l := N.bit_length()) <= bits:
if l == bits and isPrime(N+1):
return N
N *= getPrime(10)
N = gen_smooth_number(1024) + 1
F = GF(N, modulus="primitive")
g = F.gen()
print(f"{N=}, {g=}")
m = get_result(N, g)
print(f"{m=}")
FLAG = discrete_log( F(m), F(g) )
```
若使用 Sage,其有內建 `discrete_log` 函式,因此不須自行實作 Pohlig-Hellman 與大步小步算法。
![](https://hackmd.io/_uploads/H1CaQ0wGT.png)
FLAG: `FLAG{YouAreARealRealRealRealDiscreteLogMaster}`
## signature
此題為 ECDSA 的簽章 server,可以傳送非指定字串的明文給他簽章,或是驗證簽章。
```python
if option == '1':
msg = input('What do you want? ')
if msg == 'Give me the FLAG.':
print('No way!')
else:
h = sha256(msg.encode()).digest()
k = k * 1337 % n
sig = prikey.sign(bytes_to_long(h), k)
print(f'sig = ({sig.r}, {sig.s})')
```
其中可以發現不同次加密的 $k$ 互相有關係。
假設訊息相同 ($H$ 相同),兩次得到的 $s_1$, $s_2$ 分別如下:
$$
s_1 = k^{–1} (H + d \times r_1) \mod n \\
s_2 = (1337k)^{–1} (H + d \times r_2) \mod n
$$
移項後得到
$$
k = s_1^{–1}H + s_1^{–1} \times d \times r_1 \mod n \\
1337k = s_2^{–1}H + s_2^{–1} \times d \times r_2 \mod n \\
1337 (s_1^{–1}H + s_1^{–1} \times d \times r_1) = s_2^{–1}H + s_2^{–1} \times d \times r_2 \mod n
$$
因此
$$
1337 s_1^{–1}H - s_2^{–1}H = d [(s_2^{–1} \times r_2) - 1337(s_1^{–1} \times r_1)] \mod n \\
d = \frac{1337 s_1^{–1}H - s_2^{–1}H}{[(s_2^{–1} \times r_2) - 1337(s_1^{–1} \times r_1)]} \mod n
$$
解開 $d$ 後即可自行簽署任意簽章。
![](https://hackmd.io/_uploads/rkOfE0wGp.png)
FLAG: `FLAG{EphemeralKeyShouldBeRandom}`
## coppersmith
Flag 在加密前先 prepend 了一段很長的已知 padding,且加密的 $e$ 很小 ($e = 3$)。
此時可令明文 $m = a + x_0, x_0 \leq R$,密文 $c = m^e = (a+x_0)^e$
$x_0$ 為 $f(x) = (a+x)^3 - c$ 的整數根。
![](https://hackmd.io/_uploads/S1zUjk_Ga.png =450x)
$f(x)$ 的根不好解,而透過 Coppersmith's Method,建構較容易解,且有相同根的 $Q(x)$。
設 $s(x) = c_3$, $t(x) = c_2x^2 + c_1x + c_0$
得到 $Q(x) = c_3(x^3 + 3ax^2 + 3a^2x + (a_3 - c)) + c_2Nx^2 + c_1Nx + c_0N, \deg(Q) <= 3$
且 $Q(x_0) \leq Q(R) = c_3(R^3 + 3aR^2 + 3a^2R + (a_3 - c)) + c_2NR^2 + c_1NR + c_0N$
![](https://hackmd.io/_uploads/HyCWA1ufT.png =450x)
因為 $Q(R)$ 足夠小,因此透過 LLL,則可在線性時間內解出長度最短的 vector ,且值為 $[Q_3R^3, Q_2R^2, Q_1R, Q_0]$,因此得到 Q 的各項係數
![](https://hackmd.io/_uploads/BkLphJ_Gp.png =450x)
此時解 $Q(x) = Q_3x^3 + Q_2x^3 + Q_1x^3 + Q_0$ 的根則可得到 $f(x)$ 的根 $x_0$,也就是 flag。
已知 flag 長度 30 bytes,意即 $a = \text{padding} \times (30 \times 8)$
`coppersmith/exp.py`
```python
a = bytes_to_long(padding + b"\x00" * LEN)
R = 1 << (LEN * 8)
basis = [
[R**3, 3 * a * R**2, 3 * a ** 2 * R, a**3 - ct],
[0, n * R ** 2, 0, 0],
[0, 0, n * R, 0],
[0, 0, 0, n]
]
L = matrix(basis).LLL()
v = L[0]
F.<x> = PolynomialRing(ZZ)
Q = (v[0] // R**3) * x**3 + (v[1] // R**2) * x ** 2 + (v[2] // R) * x + v[3]
result = Q.roots()
flag = long_to_bytes(result[0][0]).decode()
```
LLL 解開係數後,建立一個在 $\mathbb Z$ 下的多項式 $Q(x)$,呼叫 `Q.roots()` 找其解得到 flag。
![](https://hackmd.io/_uploads/ryl-E0DGT.png)
FLAG: `FLAG{RandomPaddingIsImportant}`