# SecurinetsCTF2025
> Nhan_laptop|
> ---
> This wu for 2 chall Fl1pperZer0+1 - Securinets, both are unintended.
> The main idea is recover the state of MT19937 for predicting the next state - private_key through the author's mistake in implementation.
## Fl1pperZer0
chall:
:::spoiler
```python!
from Crypto.Util.number import long_to_bytes, bytes_to_long, inverse
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from fastecdsa.curve import P256 as EC
from fastecdsa.point import Point
import os, random, hashlib, json
from secret import FLAG
class SignService:
def __init__(self):
self.G = Point(EC.gx, EC.gy, curve=EC)
self.order = EC.q
self.p = EC.p
self.a = EC.a
self.b = EC.b
self.privkey = random.randrange(1, self.order - 1)
self.pubkey = (self.privkey * self.G)
self.key = os.urandom(16)
self.iv = os.urandom(16)
def generate_key(self):
self.privkey = random.randrange(1, self.order - 1)
self.pubkey = (self.privkey * self.G)
def ecdsa_sign(self, message, privkey):
z = int(hashlib.sha256(message).hexdigest(), 16)
k = random.randrange(1, self.order - 1)
r = (k*self.G).x % self.order
s = (inverse(k, self.order) * (z + r*privkey)) % self.order
return (r, s),k
def ecdsa_verify(self, message, r, s, pubkey):
r %= self.order
s %= self.order
if s == 0 or r == 0:
return False
z = int(hashlib.sha256(message).hexdigest(), 16)
s_inv = inverse(s, self.order)
u1 = (z*s_inv) % self.order
u2 = (r*s_inv) % self.order
W = u1*self.G + u2*pubkey
return W.x == r
def aes_encrypt(self, plaintext):
cipher = AES.new(self.key, AES.MODE_GCM, nonce=self.iv)
ct, tag = cipher.encrypt_and_digest(plaintext)
return tag + ct
def aes_decrypt(self, ciphertext):
tag, ct = ciphertext[:16], ciphertext[16:]
cipher = AES.new(self.key, AES.MODE_GCM, nonce=self.iv)
plaintext = cipher.decrypt_and_verify(ct, tag)
return plaintext
def get_flag(self):
key = hashlib.sha256(long_to_bytes(self.privkey)).digest()[:16]
cipher = AES.new(key, AES.MODE_ECB)
encrypted_flag = cipher.encrypt(pad(FLAG.encode(), 16))
return encrypted_flag
if __name__ == '__main__':
print("Welcome to Fl1pper Zer0 – Signing Service!\n")
S = SignService()
signkey = S.aes_encrypt(long_to_bytes(S.privkey))
print(f"Here is your encrypted signing key, use it to sign a message : {json.dumps({'pubkey': {'x': hex(S.pubkey.x), 'y': hex(S.pubkey.y)}, 'signkey': signkey.hex()})}")
while True:
print("\nOptions:\n \
1) sign <message> <signkey> : Sign a message\n \
2) verify <message> <signature> <pubkey> : Verify the signed message\n \
3) generate_key : Generate a new signing key\n \
4) get_flag : Get the flag\n \
5) quit : Quit\n")
try:
inp = json.loads(input('> '))
if 'option' not in inp:
print(json.dumps({'error': 'You must send an option'}))
elif inp['option'] == 'sign':
msg = bytes.fromhex(inp['msg'])
signkey = bytes.fromhex(inp['signkey'])
sk = bytes_to_long(S.aes_decrypt(signkey))
(r, s),k = S.ecdsa_sign(msg, sk)
print(json.dumps({'r': hex(r), 's': hex(s),'sk':k}))
elif inp['option'] == 'verify':
msg = bytes.fromhex(inp['msg'])
r = int(inp['r'], 16)
s = int(inp['s'], 16)
px = int(inp['px'], 16)
py = int(inp['py'], 16)
pub = Point(px, py, curve=EC)
verified = S.ecdsa_verify(msg, r, s, pub)
if verified:
print(json.dumps({'result': 'Success'}))
else:
print(json.dumps({'result': 'Invalid signature'}))
elif inp['option'] == 'generate_key':
S.generate_key()
signkey = S.aes_encrypt(long_to_bytes(S.privkey))
print("Here is your *NEW* encrypted signing key :")
print(json.dumps({'pubkey': {'x': hex(S.pubkey.x), 'y': hex(S.pubkey.y)}, 'signkey': signkey.hex()}))
elif inp['option'] == 'get_flag':
encrypted_flag = S.get_flag()
print(json.dumps({'flag': encrypted_flag.hex()}))
elif inp['option'] == 'quit':
print("Adios :)")
break
else:
print(json.dumps({'error': 'Invalid option'}))
except Exception:
print(json.dumps({'error': 'Oops! Something went wrong'}))
break
```
:::
Overview:
At first glance, we will receive signkey:
$$
\text{signkey = AES-GCM.encrypt(private_key)}
$$
In this chall, we have 3 options:
- **Sign:** User allowed to sign user_message with signkey with the following process:
\begin{array}{c}
\text{AES-GCM(signkey)}\\
\downarrow\\
\text{Sign(user_message)}\\
\downarrow\\
r = (k\ (\ = random(1,E.order()-1)\ ) * G).x \mod{E.order}\\
s = k^{-1} *( hash(message) + r * private) \mod{E.order}
\end{array}
- And what'll happend if we can get the list of `k = random(1,E.order()-1)` from `private_key = 0 `.
- **Verify:** just verifying ( I did not use it so skipping it in this blog ).
- **Generate_key**: server will reset the private key and give signkey to us.
- ->> How can i project the next private key???
- **Get_flag**:
$$
ct = AES-ECB(key=private-key,\ flag)
$$
### First step: Recovering the E_k(Y_0) of Tag formula.
> Note: I'll give you the formula and the way to get E_k(Y_0), I'll not explain clearly how it works, you can see the general concept in https://frereit.de/aes_gcm/#gcm-authentication .
The vulneribility of this server is the mode AES_GCM within `reuse key - nonce`, take a look at [this blog](https://frereit.de/aes_gcm/#gcm-authentication), this vulner can leak the `GHASH()` to get `E_k(Y_0)` which can bypass the GCM authentication, and the formula Tag - authentication tag (the formula will be different without AHEAD in this chall) is:
\begin{array}{c}
\text{Tag} = C_{1}\cdot H^{3} \oplus C_{0}\cdot H^{2} \oplus L\cdot H \oplus E_k(Y_0)
\in \mathrm{GF}(2^{128}) \\
\text{where +, *, ^} \text{ stand for } \oplus,\ \times,\ \text{power in } \mathrm{GF}(2^{128}).
\\
\left\{
\begin{aligned}
T_1 &= C_{1_1}\cdot H^3 + C_{1_0}\cdot H^2 + L\cdot H + E_k(Y_0)\\
T_2 &= C_{2_1}\cdot H^3 + C_{2_0}\cdot H^2 + L\cdot H + E_k(Y_0)\\
T_3 &= C_{3_1}\cdot H^3 + C_{3_0}\cdot H^2 + L\cdot H + E_k(Y_0)
\end{aligned}
\right. \\
\Rightarrow
\left\{
\begin{aligned}
T_1 - T_2 &= H^3\cdot (C_{1_1}-C_{2_1}) + H^2\cdot (C_{1_0}-C_{2_0})\\
T_2 - T_3 &= H^3\cdot (C_{2_1}-C_{3_1}) + H^2\cdot (C_{2_0}-C_{3_0})
\end{aligned}
\right. \\
\text{Let }
\delta_1 = C_{1_1}-C_{2_1},\
\delta_2 = C_{1_0}-C_{2_0},\
\delta_3 = C_{2_1}-C_{3_1},\
\delta_4 = C_{2_0}-C_{3_0}.
\\
\Rightarrow
\left\{
\begin{aligned}
T_1 - T_2 &= H^2\cdot(\delta_1 H + \delta_2)\\
T_2 - T_3 &= H^2\cdot(\delta_3 H + \delta_4)
\end{aligned}
\right.
\\
\text{Define } T \;=\; \dfrac{T_1-T_2}{T_2-T_3}.
\quad\text{Then}
\quad
T \;=\; \dfrac{\delta_1 H + \delta_2}{\delta_3 H + \delta_4}.
\\
\text{Solve for } H:\qquad
T(\delta_3 H + \delta_4) = \delta_1 H + \delta_2
\end{array}
\begin{aligned}
&\; T\delta_3 H + T\delta_4 = \delta_1 H + \delta_2 \\
\Longrightarrow\;& (T\delta_3 - \delta_1)\,H = \delta_2 - T\delta_4 \\
\Longrightarrow\;&
\boxed{\, H = \dfrac{\delta_2 - T\delta_4}{T\delta_3 - \delta_1}\,}
\end{aligned}
scripts:
:::spoiler
```python!
def split_tag_ct(hexs):
b = bytes.fromhex(hexs)
tag, ct = b[:16], b[16:]
assert len(ct) == 32 # 2 block
c1, c2 = int.from_bytes(ct[:16],'big'), int.from_bytes(ct[16:],'big')
T = int.from_bytes(tag,'big')
return T, c1, c2
T1,c1_1,c1_2 = split_tag_ct(signkey)
T2,c2_1,c2_2 = split_tag_ct(gen_key())
T3,c3_1,c3_2 = split_tag_ct(gen_key())
x = GF(2)["x"].gen()
gf2e = GF( 2**128 , name = "y" , modulus = x**128 + x**7 + x**2 + x + 1 )
# https://github.com/jvdsn/crypto-attacks/blob/master/attacks/gcm/forbidden_attack.py
def _to_gf2e(n):
return gf2e([(n >> i) & 1 for i in range(127, -1, -1)])
def _from_gf2e(p):
n = p._integer_representation()
ans = 0
for i in range(128):
ans <<= 1
ans |= ((n >> i) & 1)
return ans
T1 = _to_gf2e(T1); T2 = _to_gf2e(T2); T3 = _to_gf2e(T3)
C1 = _to_gf2e(c1_1); C2 = _to_gf2e(c1_2); C2_1 = _to_gf2e(c2_1)
C2_2 = _to_gf2e(c2_2);C3_1 = _to_gf2e(c3_1); C3_2 = _to_gf2e(c3_2)
delta1 = C1 + C2_1; delta2 = C2 + C2_2;
delta3 = C2_1 + C3_1; delta4 = C2_2 + C3_2
T12 = T1 + T2
T23 = T2 + T3
T = T12 / T23
H = (delta2 - T*delta4) / (T*delta3 - delta1)
H_int = _from_gf2e(H)
Lbits = 256
S = T1 + C1*(H**3) + C2*(H**2) + _to_gf2e(Lbits)*H
assert T12 == (C1 + C2_1)*(H**3) + (C2 + C2_2)*(H**2)
S = _from_gf2e(S)
S = int(S)
```
:::
Do you consider that what'll i do with the E_k(Y_0)??
### Recovering MT19937's state and predicting the next state - the next private_key.
> You can see the core attack - break Pyrandom in https://rbtree.blog/posts/2021-05-18-breaking-python-random-module/
The next step is getting the list of `k = random( 1,E.order -1 )` ( state - random in Python ) to estimate the next state.
The previous `S - E_k(Y_0)` (just recovered) can help us get k.
If you send only `signkey = tag and message`, the server will calculate:
\begin{aligned}
tag,\ ct &:= \texttt{signkey[:16]} \;\|\; \texttt{signkey[16:]} \\
\Rightarrow\ ct & = \text{""} \Leftrightarrow \text{user_private_key} =0 \\
\Rightarrow\ s &\equiv k^{-1}\cdot \operatorname{hash}(m) \pmod{E.\mathrm{order}} \\
\Rightarrow\ k &\equiv s^{-1} \cdot \operatorname{hash}(m) \pmod{E.\mathrm{order}}
\end{aligned}
Currently, we can get a list of random-k, so we just get enough samples to recover the state of MT19937.
Let see my workflow on it:
:::spoiler
```python!
import random,copy
from gf2bv import LinearSystem
from gf2bv.crypto.mt import MT19937 #https://github.com/maple3142/gf2bv
lin = LinearSystem([32] * 624)
mt = lin.gens()
rng = MT19937(mt)
bs = 32
q = 115792089210356248762697446949407573529996955224135760342422259061068512044369
rr = random.Random()
for i in range(4):
print(rr.randrange(1,q-1))
print("===")
assert rr.randrange(1,q-1) == cc.getrandbits(256)+1
rng.getrandbits(256)
zeros = [rng.getrandbits(256) ^(rr.randrange(1,q-1)-1) for _ in range(300)]
sol = lin.solve_one(zeros)
rng = MT19937(sol)
pyrand = rng.to_python_random()
for _ in range(4+ 300 ):
pyrand.getrandbits(256)
```
:::
Note:
- we need run 4 times before getting samples because:
- 1st: server random -> getting private_key
- 2,3,4th : we get the signkeys.
- And getrandbits(256) + start = randrange(1,q-1 ( 256bits)) ( where start $\in$ (1,2,3,...)) in this chall start = 1 like:
- 
- 
If you confused why 300 samples can recover the MT19937-state instead of another number, because we lost 4 times getrandbits(256) $\sim$ 4 * 8 (state) = 32 state ~ 1024 freedom bit -> so we need at 300 ($\sim$ 76800 bits equations) samples to recover all state ( I tried many times to get the number of samples ).
scripts:
:::spoiler
```python!
def sign(m,signkey):
(r.recvuntil("5"))
(r.recvuntil("> "))
r.sendline(json.dumps({
"option":"sign",
"msg": m.hex(),
"signkey": signkey.hex()
}))
resp = r.recvline().strip().decode()
resp = r.recvline().strip().decode()
resp = eval(resp)
return int(resp['r'],16), int(resp['s'],16)
from gf2bv import LinearSystem
from gf2bv.crypto.mt import MT19937
lin = LinearSystem([32] * 624)
mt = lin.gens()
rng = MT19937(mt)
for _ in range(4):
rng.getrandbits(256)
zeros = []
import time,tqdm
for i in tqdm.tqdm(range(300)):
m = b'123'
h = hash(m)
r1,s1= sign(m,long_to_bytes(S))
k = int((h*inverse(s1,q))%q)
zeros.append(rng.getrandbits(256) ^(k-1))
sol = lin.solve_one(zeros)
rng = MT19937(sol)
pyrand = rng.to_python_random()
```
:::
### Get_flag
:::spoiler
```python!
r.recv()
r.sendline(json.dumps({
"option":"generate_key"
}))
print(r.recv().decode())
print(r.recv().decode())
r.sendline(json.dumps({
"option": "get_flag",
}))
resp = r.recvline().strip().decode()
resp = r.recvline().strip().decode()
print(resp)
resp = eval(resp)
flag = resp['flag']
flag = bytes.fromhex(flag)
key = pyrand.randrange(1,q-1)
key = hashlib.sha256(long_to_bytes(key)).digest()[:16]
cipher = AES.new(key, AES.MODE_ECB)
decrypted_flag = cipher.decrypt(flag)
print(decrypted_flag)
#Securinets{bea0c8b66714035aaa7e7035868dd58ac229399449b663da96cf637f2ced3d84}
```
:::
### The intended solution.
How about the bit-state of private_key, what can we do with them.
If you flip the ith-bit ( by $\pm 2^{i}$ ) ( i $\in$ [0,private_key.bit_length()]), you can see a rule as follows:
- Assume that: ith-bit is '0' and we want to check it so let's flip to '1' by $\dotplus 2^{i}$:
\begin{array}{c}
\text{ith-bit = 0 / private_key} + 2^{i} \rightarrow \text{ith-bit = 1} \\
\downarrow \\
\text{Forging the signkey:} \\
\left\{
\begin{aligned}
C_{new} &= 2^i + C = C1||C2 \\
Tag_{new} &= C_1 \cdot H^3 + C_2 \cdot H^2 + L \cdot H + E_k(Y_0) )\ \ \ \ \text{(all operations are performed in } \mathbb{F}_{2^{128}})
\end{aligned}
\right.\\
\downarrow\\
\text{Getting signature to check bit-state:}\\
\\
\left\{
\begin{aligned}
r_{new} &= (\ \text{radom-k} \cdot G \ ).x \mod{E.order} \\
s_{new} &= k^{-1}\cdot (\ \text{hash(message)}+ r_{new}\cdot \text{new_private_key} \ ) \mod {E.order}
\end{aligned}
\right.\\
\downarrow\\
\text{Calculating PublicKey-P}_{new} = P+2^i\cdot G =(\text{private_key +}2^i)\cdot G\\
\downarrow\\
\text{Verifying new P & (r, s): }\\
\text{response = 1}\rightarrow \text{ith-bit = 0 else 1 }
\end{array}
::: spoiler
```python!
from Crypto.Util.number import long_to_bytes, bytes_to_long, inverse
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from pwn import *
from fastecdsa.curve import P256 as EC
from fastecdsa.point import Point
import os, random, hashlib, json
# from secret import FLAG
from sage.all import *
FLAG = b"aaaaaaaaaaaaaaaa"
def hash(m):
return int(hashlib.sha256(m).hexdigest(), 16)
"""
r = kG.x
s = k^-1(hash(m) + r*priv) mod n
s * k = hash(m) + r*priv mod n
k - h*s^-1 - r*s^-1*d = 0 mod n
"""
# r = process(['python3','chall.py'],level = 'debug')
r = process(['python3','chall.py'])
# r = remote('flipper.p2.securinets.tn',6000)
def sign(m,signkey):
r.sendline(json.dumps({
"option":"sign",
"msg": m.hex(),
"signkey": signkey.hex()
}))
resp = r.recvline().strip().decode()
print(resp)
resp = eval(resp)
return int(resp['r'],16), int(resp['s'],16)
def gen_key():
r.sendline(json.dumps({"option":"generate_key"}))
r.recvuntil("encrypted signing key :")
resp = r.recvline().strip().decode()
resp = r.recvline().strip().decode()
print(resp)
# exit()
resp = eval(resp)
return (resp['signkey'])
E = EllipticCurve(GF(EC.p), [EC.a, EC.b])
q = EC.q
r.recvuntil(" use it to sign a message : ")
resp = r.recvline().strip().decode()
resp = eval(resp)
signkey =(resp['signkey'])
def split_tag_ct(hexs):
b = bytes.fromhex(hexs)
tag, ct = b[:16], b[16:]
assert len(ct) == 32 # 2 block
c1, c2 = int.from_bytes(ct[:16],'big'), int.from_bytes(ct[16:],'big')
T = int.from_bytes(tag,'big')
return T, c1, c2
T1,c1_1,c1_2 = split_tag_ct(signkey)
T2,c2_1,c2_2 = split_tag_ct(gen_key())
T3,c3_1,c3_2 = split_tag_ct(gen_key())
# exit()
x = GF(2)["x"].gen()
gf2e = GF( 2**128 , name = "y" , modulus = x**128 + x**7 + x**2 + x + 1 )
def _to_gf2e(n):
return gf2e([(n >> i) & 1 for i in range(127, -1, -1)])
def _from_gf2e(p):
n = p._integer_representation()
ans = 0
for i in range(128):
ans <<= 1
ans |= ((n >> i) & 1)
return ans
"""
# T = C1 * H^3 + C2 * H^2 + L*H + E(y0)
T1 = c1_1 * H^3 + c1_2 * H^2 + L*H + E(y0)
T2 = c2_1 * H^3 + c2_2 * H^2 + L*H + E(y0)
T3 = c3_1 * H^3 + c3_2 * H^2 + L*H + E(y0)
T1 + T2 = (c1_1 + c2_1) * H^3 + (c1_2 + c2_2) * H^2
= delta_C1 * H^3 + delta_C2 * H^2
T12 = H2 ( delta_C1 * H + delta_C2)
T2 + T3 = (c2_1 + c3_1) * H^3 + (c2_2 + c3_2) * H^2
= delta_C3 * H^3 + delta_C4 * H^2
T23 = H2 ( delta_C3 * H + delta_C4)
T12/T23 = (delta_C1 * H + delta_C2)/(delta_C3 * H + delta_C4)
T' * (delta_C3 * H + delta_C4) = (delta_C1 * H + delta_C2)
T' * delta_C3 * H + T' * delta_C4 = delta_C1 * H + delta_C2
=>
"""
T1 = _to_gf2e(T1)
T2 = _to_gf2e(T2)
T3 = _to_gf2e(T3)
C1 = _to_gf2e(c1_1)
C2 = _to_gf2e(c1_2)
C2_1 = _to_gf2e(c2_1)
C2_2 = _to_gf2e(c2_2)
C3_1 = _to_gf2e(c3_1)
C3_2 = _to_gf2e(c3_2)
delta1 = C1 + C2_1
delta2 = C2 + C2_2
delta3 = C2_1 + C3_1
delta4 = C2_2 + C3_2
T12 = T1 + T2
T23 = T2 + T3
T = T12 / T23
H = (delta2 - T*delta4) / (T*delta3 - delta1)
H_int = _from_gf2e(H)
print(f'{H_int = }')
Lbits = 256
S = T1 + C1*(H**3) + C2*(H**2) + _to_gf2e(Lbits)*H
assert T12 == (C1 + C2_1)*(H**3) + (C2 + C2_2)*(H**2)
S = _from_gf2e(S)
S = int(S)
def sign(m,signkey):
(r.recvuntil("5"))
(r.recvuntil("> "))
r.sendline(json.dumps({
"option":"sign",
"msg": m.hex(),
"signkey": signkey.hex()
}))
resp = r.recvline().strip().decode()
print(resp)
# resp = r.recvline().strip().decode()
resp = eval(resp)
return int(resp['r'],16), int(resp['s'],16), resp['sk']
def forge_public_key(c):
c1,c2 = c[:16], c[16:]
x = int.from_bytes(c1,'big')
y = int.from_bytes(c2,'big')
x = _to_gf2e(x)
y = _to_gf2e(y)
Tag = x * H**3 + y * H**2 + _to_gf2e(Lbits)*H + _to_gf2e(S)
Tag = _from_gf2e(Tag)
Tag = long_to_bytes(Tag)
return Tag + c
r.sendline(json.dumps({"option":"generate_key"}))
r.recvuntil("encrypted signing key :")
resp = r.recvline().strip().decode()
resp = r.recvline().strip().decode()
print(resp)
# exit()
resp = eval(resp)
x = resp['pubkey']['x']
y = resp['pubkey']['y']
x = int(x,16)
y = int(y,16)
pub = Point(x,y,curve=EC)
signkey = resp['signkey']
signkey = bytes.fromhex(signkey)
tag, ct = signkey[:16], signkey[16:]
def verify(m,_r,s,px,py):
r.recvuntil("5")
r.recvuntil("> ")
r.sendline(json.dumps({
"option":"verify",
"msg": m.hex(),
"r": hex(_r),
"s": hex(s),
'px': hex(px),
'py': hex(py)
}))
resp = r.recvline().strip().decode()
# resp = r.recvline().strip().decode()
resp = eval(resp)
return resp['result']
tmp = ''
for i in range(0,256):
shift = (1 << i)
print(shift)
ct_new = xor(ct, shift.to_bytes(32,'big'))
forged_signkey = forge_public_key(ct_new)
m = b'12'
_r,s,sk = sign(m,forged_signkey)
P_new = pub - shift * EC.G
res = verify(m,_r,s,P_new.x,P_new.y)
if res == 'Success':
tmp = '1' + tmp
continue
P_new = pub + shift * EC.G
res = verify(m,_r,s,P_new.x,P_new.y)
if res == 'Success':
tmp = '0' + tmp
continue
assert tmp.zfill(256) == bin(sk)[2:].zfill(256)
r.recvuntil("5")
r.recvuntil("> ")
r.sendline(json.dumps({"option":"get_flag"}))
resp = r.recvline().strip().decode()
resp = eval(resp)
encrypted_flag = bytes.fromhex(resp['flag'])
key = int(tmp.zfill(256),2)
key = long_to_bytes(key)
key = hashlib.sha256(key).digest()[:16]
cipher = AES.new(key, AES.MODE_ECB)
flag = cipher.decrypt(encrypted_flag)
flag = flag.rstrip(b'\x04')
print(f'Flag: {flag.decode()}')
```
:::
## Fl1pperZer1
chall:
:::spoiler
```python!
from sage.all import *
from Crypto.Util.number import long_to_bytes, bytes_to_long, inverse
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from fastecdsa.curve import P256 as EC
from fastecdsa.point import Point
import os, random, hashlib, json
# load('secret.sage')
Flag = "FLAG{REDACTED_FOR_PRIVACY}"
class SecureSignService:
def __init__(self):
self.G = Point(EC.gx, EC.gy, curve=EC)
self.order = EC.q
self.p = EC.p
self.a = EC.a
self.b = EC.b
self.privkey = random.randrange(1, self.order - 1)
self.pubkey = (self.privkey * self.G)
self.key = os.urandom(16)
self.iv = os.urandom(16)
def split_privkey(self, privkey):
shares = []
coeffs = [privkey]
for _ in range(3):
coeffs.append(random.randrange(1, self.order))
P = PolynomialRing(GF(self.order),'x')
x = P.gen()
poly = sum(c*x**i for i, c in enumerate(coeffs))
for x in range(1, 5):
y = poly(x=x)
shares.append((x, y))
return shares
def reconstruct_privkey(self, shares):
P = PolynomialRing(GF(self.order),'x')
x = P.gen()
reconst_poly = P.lagrange_polynomial(shares)
return int(reconst_poly(0))
def shares_encrypt(self, shares):
return [self.aes_encrypt(long_to_bytes(int(s[1]))).hex() for s in shares]
def shares_decrypt(self, shares):
return [(x+1, bytes_to_long(self.aes_decrypt(bytes.fromhex(y)))) for x, y in enumerate(shares)]
def generate_key(self):
self.privkey = random.randrange(1, self.order - 1)
self.pubkey = (self.privkey * self.G)
def ecdsa_sign(self, message, privkey):
z = int(hashlib.sha256(message).hexdigest(), 16)
k = random.randrange(1, self.order - 1)
r = (k*self.G).x % self.order
s = (inverse(k, self.order) * (z + r*privkey)) % self.order
return (r, s), k
def ecdsa_verify(self, message, r, s, pubkey):
r %= self.order
s %= self.order
if s == 0 or r == 0:
return False
z = int(hashlib.sha256(message).hexdigest(), 16)
s_inv = inverse(s, self.order)
u1 = (z*s_inv) % self.order
u2 = (r*s_inv) % self.order
W = u1*self.G + u2*pubkey
return W.x == r
def aes_encrypt(self, plaintext):
cipher = AES.new(self.key, AES.MODE_GCM, nonce=self.iv)
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
return tag + ciphertext
def aes_decrypt(self, ciphertext):
tag, ct = ciphertext[:16], ciphertext[16:]
cipher = AES.new(self.key, AES.MODE_GCM, nonce=self.iv)
plaintext = cipher.decrypt_and_verify(ct, tag)
return plaintext
def get_flag(self):
key = hashlib.sha256(long_to_bytes(self.privkey)).digest()[:16]
cipher = AES.new(key, AES.MODE_ECB)
encrypted_flag = cipher.encrypt(pad(FLAG.encode(), 16))
return encrypted_flag
if __name__ == '__main__':
print("Welcome to Fl0pper Zer1 – Secure Signing Service!\n")
S = SecureSignService()
signkey = S.shares_encrypt(S.split_privkey(S.privkey))
print(f"Here are your encrypted signing key shares, use them to sign a message : {json.dumps({'pubkey': {'x': hex(S.pubkey.x), 'y': hex(S.pubkey.y)}, 'signkey': signkey})}")
while True:
print("\nOptions:\n \
1) sign <message> <signkey> : Sign a message\n \
2) verify <message> <signature> <pubkey> : Verify the signed message\n \
3) generate_key : Generate new signing key shares\n \
4) get_flag : Get the flag\n \
5) quit : Quit\n")
try:
inp = json.loads(input('> '))
if 'option' not in inp:
print(json.dumps({'error': 'You must send an option'}))
elif inp['option'] == 'sign':
msg = bytes.fromhex(inp['msg'])
signkey = inp['signkey']
sk = S.reconstruct_privkey(S.shares_decrypt(signkey))
(r, s),k = S.ecdsa_sign(msg, sk)
print(json.dumps({'r': hex(r), 's': hex(s), 'k': (k)}))
elif inp['option'] == 'verify':
msg = bytes.fromhex(inp['msg'])
r = int(inp['r'], 16)
s = int(inp['s'], 16)
px = int(inp['px'], 16)
py = int(inp['py'], 16)
pub = Point(px, py, curve=EC)
verified = S.ecdsa_verify(msg, r, s, pub)
if verified:
print(json.dumps({'result': 'Success'}))
else:
print(json.dumps({'result': 'Invalid signature'}))
elif inp['option'] == 'generate_key':
S.generate_key()
signkey = S.shares_encrypt(S.split_privkey(S.privkey))
print("Here are your *NEW* encrypted signing key shares :")
print(json.dumps({'pubkey': {'x': hex(S.pubkey.x), 'y': hex(S.pubkey.y)}, 'signkey': signkey}))
elif inp['option'] == 'get_flag':
encrypted_flag = S.get_flag()
print(json.dumps({'flag': encrypted_flag.hex()}))
elif inp['option'] == 'quit':
print("Adios :)")
break
else:
print(json.dumps({'error': 'Invalid option'}))
except Exception as e:
print(json.dumps({'error': 'Oops! Something went wrong'}))
break
```
:::
Solve:
There is the same mistake in implementation so we can use the way to solve above chall to do this chall.
However, the encrypt data is different.
:::spoiler
```python!
def split_privkey(self, privkey):
shares = []
coeffs = [privkey]
for _ in range(3):
coeffs.append(random.randrange(1, self.order))
P = PolynomialRing(GF(self.order),'x')
x = P.gen()
poly = sum(c*x**i for i, c in enumerate(coeffs))
for x in range(1, 5):
y = poly(x=x)
shares.append((x, y))
return shares
def reconstruct_privkey(self, shares):
P = PolynomialRing(GF(self.order),'x')
x = P.gen()
reconst_poly = P.lagrange_polynomial(shares)
return int(reconst_poly(0))
def shares_encrypt(self, shares):
return [self.aes_encrypt(long_to_bytes(int(s[1]))).hex() for s in shares]
signkey = S.shares_encrypt(S.split_privkey(S.privkey))
```
:::
It use the mixture SSS_s to encrypt data, but we can only send Tag to get the private_key = 0 to get k - random.
scripts:
:::spoiler
```python!
<Recovering E_k(y_0)>
tmp = [long_to_bytes(S).hex() for x in range(4)]
def sign(m,signkey):
print(r.recv().decode())
r.sendline(json.dumps({
"option":"sign",
"msg": m.hex(),
"signkey": signkey
}))
resp = r.recvline().strip().decode()
resp = r.recvline().strip().decode()
resp = eval(resp)
return (int(resp['r'],16), int(resp['s'],16))
from gf2bv import LinearSystem
from gf2bv.crypto.mt import MT19937
lin = LinearSystem([32] * 624)
mt = lin.gens()
rng = MT19937(mt)
for _ in range(4):
rng.getrandbits(256)
zeros = []
import time,tqdm
def hash(m):
return int(hashlib.sha256(m).hexdigest(), 16)
for i in tqdm.tqdm(range(300)):
m = b'123'
h = hash(m)
(r1,s1)= sign(m,tmp)
k = int((h*inverse(s1,q))%q)
zeros.append(rng.getrandbits(256) ^(k-1))
sol = lin.solve_one(zeros)
rng = MT19937(sol)
pyrand = rng.to_python_random()
for _ in range(4+ 300):
pyrand.getrandbits(256)
<Getting Flag>.
```
:::
### Intended solution:
> Resource: https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing#Reconstruction
**Recall:** The formula of Shamir's secret sharing (this chall):
\begin{aligned}
f(x) & = \text{private_key} +k_1\cdot x + k_2\cdot x^2 + k_3\cdot x^3\ | \text{ki = random}\\
& = y_0 \cdot l_0(x) + y_1 \cdot l_1(x) + y_2 \cdot l_2(x) + y_3 \cdot l_3(x) \\
& = \text{private_key | at x = 0}
\end{aligned}
We only know the x-coordinate so it easy to see that we must use the 2nd-formula and because the properties of Lagrange basis polynomials, we can not directly flip bit, but we can modify one of these coefficient of f(x) :
\begin{array}{c}
f(0) = (y_0 + 2^i ) \cdot l_0(0) + y_1 \cdot l_1(0) + y_2 \cdot l_2(0) + y_3 \cdot l_3(0) \\
= \text{private_key}+2^i \cdot l_0(0)\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \\
\downarrow\\
\text{Getting new signature}\\
\downarrow\\
\text{Calculate: }\ P_{new} = P + 2^i\cdot l_0 *Q
\end{array}
The next steps are similar with the previous chall.
:::spoiler
```python!
from sage.all import *
from Crypto.Util.number import long_to_bytes, bytes_to_long, inverse
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from fastecdsa.curve import P256 as EC
from fastecdsa.point import Point
from pwn import *
import os, random, hashlib, json
# r = process(['python3', 'chall.py'], level='debug')
# r = process(['python3', 'server.py'], level='debug')
# r = process(['python3', 'server.py'])
ok = 1
r = process(['python3', 'chall.py'])
# nc flopper.p2.securinets.tn 6002p
# r = remote("flopper.p2.securinets.tn", 6002)
r.recvuntil(" use them to sign a message : ")
resp = r.recvline().strip().decode()
resp = eval(resp)
px = int(resp['pubkey']['x'],16)
py = int(resp['pubkey']['y'],16)
E = EllipticCurve(GF(EC.p), [EC.a, EC.b])
P = E(px, py)
G = E(EC.gx, EC.gy)
signkey =(resp['signkey'])
q = EC.q
def split_tag_ct(hexs):
b = bytes.fromhex(hexs)
tag, ct = b[:16], b[16:]
assert len(ct) == 32 # 2 block
c1, c2 = int.from_bytes(ct[:16],'big'), int.from_bytes(ct[16:],'big')
T = int.from_bytes(tag,'big')
return T, c1, c2, ct
T1,c1_1,c1_2,c1 = split_tag_ct(signkey[0])
T2,c2_1,c2_2,c2 = split_tag_ct(signkey[1])
T3,c3_1,c3_2,c3 = split_tag_ct(signkey[2])
T4,c4_1,c4_2,c4 = split_tag_ct(signkey[3])
x = GF(2)["x"].gen()
gf2e = GF( 2**128 , name = "y" , modulus = x**128 + x**7 + x**2 + x + 1 )
def _to_gf2e(n):
return gf2e([(n >> i) & 1 for i in range(127, -1, -1)])
def _from_gf2e(p):
n = p._integer_representation()
ans = 0
for i in range(128):
ans <<= 1
ans |= ((n >> i) & 1)
return ans
T1 = _to_gf2e(T1)
T2 = _to_gf2e(T2)
T3 = _to_gf2e(T3)
T4 = _to_gf2e(T4)
C1 = _to_gf2e(c1_1)
C2 = _to_gf2e(c1_2)
C2_1 = _to_gf2e(c2_1)
C2_2 = _to_gf2e(c2_2)
C3_1 = _to_gf2e(c3_1)
C3_2 = _to_gf2e(c3_2)
C4_1 = _to_gf2e(c4_1)
C4_2 = _to_gf2e(c4_2)
delta1 = C1 + C2_1
delta2 = C2 + C2_2
delta3 = C2_1 + C3_1
delta4 = C2_2 + C3_2
T12 = T1 + T2
T23 = T2 + T3
T = T12 / T23
H = (delta2 - T*delta4) / (T*delta3 - delta1)
H_int = _from_gf2e(H)
Lbits = 256
L = _to_gf2e(Lbits)
S = T1 + C1*(H**3) + C2*(H**2) + _to_gf2e(Lbits)*H
# print(S)
assert C4_1 * H**3 + C4_2 * H**2 + _to_gf2e(Lbits)*H + S == T4
q = EC.q
def lagrange_basis(idx):
n = 1
d = 1
for i in range(1,5):
if i == idx:
continue
n = n * (0- i )
d = d * (idx - i )
return n//d
lbasis = []
for i in range(1,5):
lbasis.append(lagrange_basis(i))
print(lbasis)
r.recvuntil('5')
r.recvuntil('> ')
r.sendline(json.dumps({
"option":"generate_key",
}))
resp = r.recvline().strip().decode()
resp = r.recvline().strip().decode()
resp = eval(resp)
x,y = int(resp['pubkey']['x'],16), int(resp['pubkey']['y'],16)
pub = Point(x,y, curve=EC)
signkey = (resp['signkey'])
t1,c1 = signkey[0][:32], bytes.fromhex(signkey[0][32:])
t2,c2 = signkey[1][:32], bytes.fromhex(signkey[1][32:])
t3,c3 = signkey[2][:32], bytes.fromhex(signkey[2][32:])
t4,c4 = signkey[3][:32], bytes.fromhex(signkey[3][32:])
def forge_public_key(c):
c1,c2 = c[:16], c[16:]
x = int.from_bytes(c1,'big')
y = int.from_bytes(c2,'big')
x = _to_gf2e(x)
y = _to_gf2e(y)
Tag = x * H**3 + y * H**2 + _to_gf2e(256)*H + (S)
Tag_int = _from_gf2e(Tag)
Tag_bytes = Tag_int.to_bytes(16, 'big')
return Tag_bytes + c
tmp =''
listy = []
for j in range(4):
tmp = ''
for i in range(256):
shift = (1 << i)
ct = bytes.fromhex(signkey[j][32:])
c_new = xor(ct, shift.to_bytes(32,'big'))
c_new = forge_public_key(c_new)
fsign = []
for t in range(4):
if t == j:
fsign.append(c_new.hex())
else:
fsign.append(signkey[t])
r.recvuntil('5')
r.recvuntil('> ')
m = b'12'
r.sendline(json.dumps({
"option":"sign",
"msg": m.hex(),
"signkey": fsign
}))
resp = r.recvline().strip().decode()
# print(resp)
resp = eval(resp)
# exit()
_r, s= int(resp['r'],16), int(resp['s'],16)
# priv = resp['priv']
shares = resp['shares']
# exit()
if lbasis[j]<0:
shift = -shift
P_new = pub - (shift*lbasis[j]) * EC.G
r.recvuntil('5')
r.recvuntil('> ')
r.sendline(json.dumps({
"option":"verify",
"msg": m.hex(),
"r": hex(_r),
"s": hex(s),
'px': hex(P_new.x),
'py': hex(P_new.y)
}))
resp = r.recvline().strip().decode()
resp = eval(resp)
tmp1 = resp['result']
if lbasis[j]>0:
if resp['result'] == 'Success':
tmp = '1'+tmp
else:
tmp = '0'+tmp
else :
if resp['result'] == 'Success':
tmp = '0'+tmp
else:
tmp = '1'+tmp
print(tmp)
print(bin(int(shares[j],16))[2:])
print()
listy.append(int(tmp,2))
print(listy)
F = GF(EC.q)
P = PolynomialRing(F, 'x')
x = P.gen()
points = [(i+1, F(listy[i])) for i in range(4)]
poly = P.lagrange_polynomial(points)
flag_int = int(poly(0))
print(r.recv(1024).decode())
r.sendline(json.dumps({"option": "get_flag"}))
resp = r.recvline().strip().decode()
resp = eval(resp)
ct = bytes.fromhex(resp['flag'])
key = hashlib.sha256(long_to_bytes(flag_int)).digest()[:16]
cipher = AES.new(key,AES.MODE_ECB)
decrypted_flag = cipher.decrypt(ct)
print(decrypted_flag)
```
:::
## Exclusive
The core attack is you can decrypt the second ciphertext as follow:
\begin{array}{c}
P_2^* =W\text{[:len(data)%16]} = (D_K(C_2+T_2) + T_2 )\text{[:len(data)%16]}\\
P_1^* = D_K(C_2||W\text{[len(data)%16:]} + T_1) +T_1
\\ \downarrow \\
\text{let C2= 15* 'A', so you know the (15*'A'}+Block{Flag_{idex}})
\end{array}
And the first byte each block we can not recover so, brute each flag into server.
find the last block:
:::spoiler
```python!
from pwn import *
from random import *
import string
# r = process(['python3.10', 'chall.py'],level = 'debug')
r = process(['python3.10', 'chall.py'])
r.recvuntil(b'> ')
r.sendline(b'5')
r.recvuntil(b'Your clue : ')
clue = r.recvline().strip().decode()
clue = bytes.fromhex(clue)
clue = [clue[i:i+16] for i in range(0, len(clue), 16)]
res = b''
for i in range(15,-1,-1):
pay = clue[0] + b'A'*i
r.recvuntil(b'> ')
r.sendline(pay.hex())
r.recvuntil(b'Exclusive content : ')
check = r.recvline().strip().decode()
paylist = []
for a in range(256):
tmp = b'A'*i + bytes([a]) + res
tmp = tmp.hex()
tmp = tmp + '\n'
paylist.append(tmp)
payload = ''.join(paylist)
r.send(payload)
for j in range(256):
r.recvuntil(b'Exclusive content : ')
line = r.recvline().strip().decode()
if check in line:
print("oops")
res = bytes([j]) + res
print(res)
print(res)
```
:::
find 4 - first block:
:::spoiler
```python!
from pwn import *
from random import *
import string
# r = process(['python3.10', 'chall.py'],level = 'debug')
r = process(['python3.10', 'chall.py'])
r.recvuntil(b'> ')
r.sendline(b'5')
r.recvuntil(b'Your clue : ')
clue = r.recvline().strip().decode()
clue = bytes.fromhex(clue)
clue = [clue[i:i+16] for i in range(0, len(clue), 16)]
res = b''
for i in range(15,-1,-1):
pay = clue[1] + b'A'*i
r.recvuntil(b'> ')
r.sendline(pay.hex())
r.recvuntil(b'Exclusive content : ')
check = r.recvline().strip().decode()
paylist = []
for a in range(256):
tmp = b'A'*i + bytes([a]) + res
tmp = tmp.hex()
tmp = tmp + '\n'
paylist.append(tmp)
payload = ''.join(paylist)
r.send(payload)
for j in range(256):
r.recvuntil(b'Exclusive content : ')
line = r.recvline().strip().decode()
if check in line:
print("oops")
res = bytes([j]) + res
print(res)
print(res)
```
:::
## References:
[1] https://frereit.de/aes_gcm/
[2] https://github.com/jvdsn/crypto-attacks/blob/master/attacks/gcm/forbidden_attack.py