PwnMe CTF 2025
Solution for Crypto challs
Easy_difficult
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from Crypto.Util.number import getPrime, long_to_bytes
from Crypto.Util.Padding import pad, unpad
from Crypto.Cipher import AES
from hashlib import sha256
import os
# generating strong parameters
flag = b"REDACTED"
p = getPrime(1536)
g = p-1
a = getPrime(1536)
b = getPrime(1536)
A = pow(g, a, p)
B = pow(g, b, p)
assert pow(A, b, p) == pow(B, a, p)
C = pow(B, a, p)
# print(C)
# Encrypting my message
key = long_to_bytes(C)
key = sha256(key).digest()[:16]
cipher = AES.new(key, AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(flag, AES.block_size))
print(f"{p = }")
print(f"{g = }")
print("ciphertext =", ciphertext.hex())
After checking C
value I can see that it is g
.
1
2
C = g
key = long_to_bytes(C)
Square Power
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from Crypto.Util.number import getStrongPrime
from math import gcd
from random import randint
from typing import Tuple
from Crypto.Cipher import AES
from hashlib import sha256
flag = b"PWNME{xxxxxxxxxxxxxxxxxxxxxxxxx}"
def generate_primes() -> int:
p = getStrongPrime(512)
q = getStrongPrime(512)
while gcd(p*q, (p-1)*(q-1)) != 1:
p = getStrongPrime(512)
q = getStrongPrime(512)
return p*q
def generate_public_key() -> Tuple[int, int]:
n = generate_primes()
k = randint(2, n-1)
while gcd(k, n) != 1:
k = randint(2, n-1)
g = 1 + k * n
return n, g, k
n, g, k = generate_public_key()
a = randint(2, n-1)
b = randint(2, n-1)
A = pow(g, a, n*n)
B = pow(g, b, n*n)
assert A == (1 + k*n*a )%(n**2)
secret_key = pow(B, a, n*n)
def encrypt(m: bytes, secret_key: int) -> str:
hash_secret_key = sha256(str(secret_key).encode()).digest()
cipher = AES.new(hash_secret_key, AES.MODE_ECB)
return cipher.encrypt(m).hex()
print(f"{n = }")
print(f"{g = }")
print(f"{k = }")
print(f"{A = }")
print(f"{B = }")
print(f'enc = "{encrypt(flag, secret_key)}"')
A = (1 + k*n*a) % (n**2)
=> (A - 1) * inverse(k, n) //n = a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from Crypto.Util.number import getStrongPrime
from math import gcd
from random import randint
from typing import Tuple
from Crypto.Cipher import AES
from hashlib import sha256
n = 130480001264795511204952981970554765286628282097708497573805562495761746956689294837477924716000173700265689121058390655726461662172763702188805523675445230642476356316152454104476211773099930843629048798894397653741145611772970364363628025189743819724119397704649989182196725015667676292311250680303497618517
g = 14232999694821698106937459755169111250723143832548091913379257481041382160905011536064172867298828679844798321319150896238739468953330826850323402142301574319504629396273693718919620024174195297927441113170542054761376462382214102358902439525383324742996901035237645136720903186256923046588009251626138008729683922041672060805697738869610571751318652149349473581384089857319209790798013971104266851625853032010411092935478960705260673746033508293802329472778623222171537591292046922903109474029045030942924661333067125642763133098420446959785042615587636015849430889154003912947938463326118557965158805882580597710148
k = 109081848228506024782212502305948797716572300830339785578465230204043919222714279516643240420456408658167645175971167179492414538281767939326117482613367750888391232635306106151999375263906703485783436272382449557941704742019717763385971731987034043089865070488786181508175732060731733665723128263548176110391
A = 10331979810348166693003506393334562363373083416444082955583854323636220335613638441209816437198980825253073980493123573286927762799807446436773117670818921078297923733365129554252727963674496148945815529457095198387555733553703069705181377382893601879633657895337279524071439340411690401699779320407420258592904893010800421041848764790649945309236525529148459624417556599146885803882692326627657181584151248747924080070945415558421472606778565193931117263508570619290441914589981949634553417159683167906276897159926442471600725573380647253372071392282203683205441190912735696337884772579017885457286629133944441076065
B = 4081342267323018166249607688978380665241423816957875747125328810958590656153973787783246867777679461978030117454679495989870502705358238920918102708702013201363687875430336612386215884751792630402395947375495263771248401103245739000962715422458344125251671671250588124240486938525081520695571867300148511333511433839123962435025865462662009339451634433842267524048553313626315201481951251476302835595998914217740184369102003837614515913319042566394680732429410107620067602633793215206219823499602447575406162296590635685877032818801721681953430382920303700518722500790613216329394164889181089201919505288870098353385
enc = "abd9dd2798f4c17b9de4556da160bd42b1a5e3a331b9358ffb11e7c7b3120ed3"
from Crypto.Util.number import*
# A%n**2 // n
a = ( A - 1 ) *inverse(k, n**2) %(n**2) //n
print(a)
secret_key = pow(B, a, n*n)
hash_secret_key = sha256(str(secret_key).encode()).digest()
cipher = AES.new(hash_secret_key, AES.MODE_ECB)
flag = cipher.decrypt(bytes.fromhex(enc))
print(flag.decode())
# PWNME{Thi5_1s_H0w_pAl1ier_WorKs}
my_zed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from openzedlib import openzed
import os
import zlib
from flag import FLAG
file = openzed.Openzed(b'zed', os.urandom(16), 'flag.txt', len(FLAG))
file.encrypt(FLAG)
file.generate_container()
with open(f"{file.filename}.ozed", "wb") as f:
f.write(file.secure_container)
This is a custom encryptation
1
2
3
4
5
6
7
8
def encrypt(self, data):
cipher = AES_CBC_ZED(self.user, self.password)
# print(f"{cipher.key.hex() = }")
self.encrypted = cipher.encrypt(data)
self.encrypted = zlib.compress(self.encrypted) # just for the lore
return self.encrypted
Take a closer look on cipher AES_CBC_ZED
I notice that
1
2
3
4
5
6
7
8
9
10
11
class AES_CBC_ZED:
def __init__(self, user, password):
self.user = user
self.password = password
self.derive_password()
self.generate_iv()
...
def derive_password(self):
for i in range(100):
self.key = sha256(self.password).digest()[:16]
key
= sha256(password)
and we get metadata["password_hash"] = sha256(self.password).hexdigest()
so password_hash
is the key value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class AES_CBC_ZED:
def __init__(self, user, password):
self.user = user
self.key = password
def decrypt(self, ciphertext: bytes):
# TODO prendre un iv déjà connu en paramètre ?
plaintext = b""
ecb_cipher = AES.new(key=self.key, mode=AES.MODE_ECB)
iv = ciphertext[:16]
ciphertext = ciphertext[16:]
for pos in range(0, len(ciphertext), 16):
chunk = ciphertext[pos:pos+16]
# AES CFB for the last block or if there is only one block
if len(ciphertext[pos+16:pos+32]) == 0 :
#if plaintext length <= 16, iv = self.iv
if len(ciphertext) <= 16 :
prev=iv
# else, iv = previous ciphertext
else:
prev=ciphertext[pos-16:pos]
prev = ecb_cipher.encrypt(prev)
plaintext += xor(prev, chunk)
# AES CBC for the n-1 firsts block
elif not plaintext:
xored = ecb_cipher.decrypt(chunk)
plaintext += bytes(xor(xored, iv))
else:
xored = ecb_cipher.decrypt(chunk)
plaintext += bytes(xor(xored, ciphertext[pos-16:pos]))
return plaintext
file = openzed.Openzed()
data = {"user": "zed", "password_hash": "b3a97eb583db5a940c0705e6450b81f4d702a9122d7342a25768e3d75be739be", "filename": "flag.txt", "size": 63}
pwd = bytes.fromhex(data["password_hash"])[:16]
enc = open("flag.txt.ozed", "rb").read()[300+4:]
ciphertxt = zlib.decompress(enc)
cipher = AES_CBC_ZED("zed", pwd)
flag = (cipher.decrypt(ciphertxt))
print(flag.decode())
# PWNME{49e531f28d1cedef03103af6cec79669_th4t_v3Ct0r_k1nd4_l3aky}
Better My Zed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
@app.route('/encrypt_flag/', methods=['GET'])
def encrypt_flag():
#chiffrer avec des creds random
file = openzed.Openzed(USER, KEY, 'flag.txt.ozed')
file.encrypt(FLAG.encode())
file.generate_container()
#tricking flask into thinking the data come from a file
encrypted = io.BytesIO(file.secure_container)
return send_file(
encrypted,
mimetype='text/plain',
as_attachment=False,
download_name='flag.txt.ozed'
)
@app.route('/encrypt/', methods=['POST'])
def encrypt_file():
if request.form["username"] and request.form["password"]:
username = request.form["username"].encode()
password = bytes.fromhex(request.form["password"])
else:
username = USER
password = KEY
if request.form["iv"] :
try :
iv = request.form["iv"]
except:
return "Please submit iv hex encoded"
else:
iv = None
if not request.files or not request.files["file"].filename:
return "Please upload a file"
filename = request.files["file"].filename
file_to_encrypt = request.files['file']
data = file_to_encrypt.stream.read()
file = openzed.Openzed(username, password, filename, iv)
file.encrypt(data)
file.generate_container()
encrypted = io.BytesIO(file.secure_container)
return send_file(
encrypted,
mimetype='text/plain',
as_attachment=False,
download_name=filename+".ozed"
)
@app.route('/decrypt/', methods=['POST'])
def decrypt_file():
if not request.form["username"] :
return "Please submit an username"
if not request.form["password"]:
return "Please submit a password"
try :
bytes.fromhex(request.form["password"])
except:
return "Please submit the password hex encoded"
if not request.files or not request.files["file"].filename:
return "Please upload a file"
username = request.form["username"].encode()
password = bytes.fromhex(request.form["password"])
filename = request.files["file"].filename
file_to_decrypt = request.files['file']
data = file_to_decrypt.stream.read()
file = openzed.Openzed(username, password, filename)
file.secure_container = data
decrypted = file.decrypt_container(file.secure_container)
decrypted = io.BytesIO(decrypted["data"])
return send_file(
decrypted,
mimetype='text/plain',
as_attachment=False,
download_name=filename+".dec"
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def encrypt(self, plaintext: bytes):
iv = self.iv
ciphertext = b""
ecb_cipher = AES.new(key=self.key, mode=AES.MODE_ECB)
for pos in range(0, len(plaintext), 16):
chunk = plaintext[pos:pos+16]
# AES CFB for the last block or if there is only one block
if len(plaintext[pos+16:pos+32]) == 0 :
#if plaintext length <= 16, iv = self.iv
if len(plaintext) <= 16 :
prev=iv
# else, iv = previous ciphertext
else:
prev=ciphertext[pos-16:pos]
prev = ecb_cipher.encrypt(prev)
ciphertext += xor(chunk, prev)
# AES CBC for the n-1 firsts block
elif not ciphertext:
xored = bytes(xor(plaintext, iv))
ciphertext += ecb_cipher.encrypt(xored)
else:
xored = bytes(xor(chunk, ciphertext[pos-16:pos]))
ciphertext += ecb_cipher.encrypt(xored)
return ciphertext
We get three methods encrypt flag
, encrypt
, decrypt
and I notice this condition assert len(FLAG) <= 16
so because length of flag are smaller 16 bytes so it will go to CFB mode
The IV goes through block encryption and xor with pt
-> ct
so from there if we get encrypt of data
= enc_data
. Then data
$\oplus$ enc_data
= keystream
and that keystream
shares same for the encryption of flag because of the similar of IV => flag
= data
$\oplus$ enc_data
$\oplus$ enc_flag
PWNME{zEd_15_3z}
vending_machine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#!/usr/bin/env python3
from tinyec.tinyec.ec import SubGroup, Curve
from Crypto.Util.Padding import pad
from Crypto.Cipher import AES
from json import loads, dumps
from hashlib import sha3_256
from random import choice
from os import urandom
# from flag import FLAG
FLAG = "PWNSEC{TEST_FLAG}"
import secrets
import time
class SignatureManager:
def __init__(self):
# FRP256v1 Parameters
self.p = 0xf1fd178c0b3ad58f10126de8ce42435b3961adbcabc8ca6de8fcf353d86e9c03
self.a = 0xf1fd178c0b3ad58f10126de8ce42435b3961adbcabc8ca6de8fcf353d86e9c00
self.b = 0xee353fca5428a9300d4aba754a44c00fdfec0c9ae4b1a1803075ed967b7bb73f
self.Gx = 0xb6b3d4c356c139eb31183d4749d423958c27d2dcaf98b70164c97a2dd98f5cff
self.Gy = 0x6142e0f7c8b204911f9271f0f3ecef8c2701c307e8e4c9e183115a1554062cfb
self.n = 0xf1fd178c0b3ad58f10126de8ce42435b53dc67e140d2bf941ffdd459c6d655e1
self.h = 1
subgroup = SubGroup(self.p, (self.Gx, self.Gy), self.n, self.h)
self.curve = Curve(self.a, self.b, subgroup, name="CustomCurve")
self.P = self.curve.g
self.d = int.from_bytes(urandom(32), "big")
while self.d >= self.n:
self.d = int.from_bytes(urandom(32), "big")
self.Q = self.d * self.P
self.salt = int.from_bytes(urandom(32), "big") % self.n
def inverse(self, a, n):
return pow(a, -1, n)
def gen_sign(self, m: bytes, alea_1, alea_2):
a = int(alea_1)
b = int(alea_2)
assert a**2 < b**2 < 2**120 - 1
c = (hash(a) - hash(b)) * int.from_bytes(urandom(32), "big") ^ 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
randomized_main_part_l = 249
randomized_part = ""
for _ in range(256 - randomized_main_part_l):
randomized_part += choice(bin(c).split("0b")[1])
parity = int(randomized_part, 2) % 2
randomized_part = bin(self.salt ^ int(randomized_part, 2))[-(256 - randomized_main_part_l):]
k = 0xFF000000000000000000000000000000000000000000000000000000000000FF ^ int(randomized_part + bin(secrets.randbits(randomized_main_part_l)).split("0b")[1].zfill(randomized_main_part_l) if parity else bin(secrets.randbits(randomized_main_part_l)) + randomized_part, 2)
e = int.from_bytes(sha3_256(m).digest(), "big")
R = k * self.P
r = R.x % self.n
assert r != 0
s = (self.inverse(k, self.n) * (e + self.d * r)) % self.n
return r, s, int.from_bytes(m, "big"), k
def verify(self, m: bytes, r: int, s: int):
e = int.from_bytes(sha3_256(m).digest(), "big")
assert 0 < r < self.n and 0 < s < self.n
w = self.inverse(s, self.n)
u1 = (e * w) % self.n
u2 = (r * w) % self.n
P_ = u1 * self.P + u2 * self.Q
return r == P_.x % self.n
class Server:
def __init__(self):
self.signature_manager = SignatureManager()
self.credits = 1
self.signatures = []
self.credit_currency = 0
key = sha3_256(self.signature_manager.d.to_bytes(32, "big")).digest()[:16]
self.iv = urandom(16)
cipher = AES.new(key, IV=self.iv, mode=AES.MODE_CBC)
self.encrypted_flag = cipher.encrypt(pad(FLAG.encode(), 16)).hex()
self.used_credit = 0
def show_credits(self):
return {"credits": self.credits}
def show_currency(self):
return {"currency": self.credit_currency}
def get_encrypted_flag(self):
return {"encrypted_flag": self.encrypted_flag, "iv": self.iv.hex()}
def get_new_signatures(self, alea_1, alea_2):
if self.credits > 0:
self.credits -= 1
self.used_credit += 1
new_signatures = []
for i in range(10):
m = sha3_256(b"this is my lovely loved distributed item " + str(i+10*self.used_credit).encode()).digest()
r,s,_, k = self.signature_manager.gen_sign(m, alea_1, alea_2)
new_signatures.append((r, s, k))
self.signatures.append((m.hex(), r, s))
# ...Yeah, it's long, but it's just like vending machines... the cans take forever to drop, it's maddening...
time.sleep(90)
return {"signatures": new_signatures}
else:
return {"error": "Not enough credits."}
def verify_proof_of_ownership(self, owner_proofs):
owner_proofs = [tuple(item) for item in owner_proofs]
if len(set(owner_proofs)) != self.credit_currency:
return False
for owner_proof in owner_proofs:
if not self.signature_manager.verify(bytes.fromhex(owner_proof[0]), owner_proof[1], owner_proof[2]) or owner_proof in self.signatures:
return False
return True
def buy_credit(self, owner_proofs):
if self.verify_proof_of_ownership(owner_proofs):
self.credits += 1
# each credit cost more and more proofs to ensure you are the owner
self.credit_currency += 5
return {"status": "success", "credits": self.credits, "credit_currency": self.credit_currency}
else:
return {"error": f"You need {self.credit_currency} *NEW* signatures to buy more credits."}
def main():
server = Server()
print("Welcome to the signatures distributor, this is what you can do:")
print("1. Show credits")
print("2. Show currency")
print("3. Get encrypted flag")
print("4. Get signatures")
print("5. Buy credit")
print("6. Exit")
while True:
try:
command = loads(input("Enter your command in JSON format: "))
if "action" not in command:
print({"error": "Invalid command format."})
continue
action = command["action"]
if action == "show_credits":
print(server.show_credits())
elif action == "show_currency":
print(server.show_currency())
elif action == "get_encrypted_flag":
print(server.get_encrypted_flag())
elif action == "get_signatures":
if "alea_1" not in command or "alea_2" not in command:
print({"error": "Invalid command format."})
alea_1 = command["alea_1"]
alea_2 = command["alea_2"]
print(server.get_new_signatures(alea_1, alea_2))
elif action == "buy_credit":
if "owner_proofs" not in command:
print({"error": "Invalid command format."})
print(server.buy_credit(command["owner_proofs"]))
elif action == "exit":
print({"status": "Goodbye!"})
breakass
else:
print({"error": "Unknown action."})
except Exception as e:
print({"error": str(e)})
if __name__ == "__main__":
main()
Take a closer look on this
1
2
3
4
5
6
7
8
9
10
11
12
13
def gen_sign(self, m: bytes, alea_1, alea_2):
a = int(alea_1)
b = int(alea_2)
assert a**2 < b**2 < 2**120 - 1
c = (hash(a) - hash(b)) * int.from_bytes(urandom(32), "big") ^ 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
randomized_main_part_l = 249
randomized_part = ""
for _ in range(256 - randomized_main_part_l):
randomized_part += choice(bin(c).split("0b")[1])
parity = int(randomized_part, 2) % 2
randomized_part = bin(self.salt ^ int(randomized_part, 2))[-(256 - randomized_main_part_l):]
k = 0xFF000000000000000000000000000000000000000000000000000000000000FF ^ int(randomized_part + bin(secrets.randbits(randomized_main_part_l)).split("0b")[1].zfill(randomized_main_part_l) if parity else bin(secrets.randbits(randomized_main_part_l)) + randomized_part, 2)
This is how nonces were created from 2 parts randomized_part
and secret.randbits(249)
. After doing some google search I have found this property of hash in python: hash(-1) == hash(2)
means that c = 111...111
-> randomized_part
will always be fix (time for bruteforce) for every nonces created for ECDSA. This makes a weakness: Biased Nonce on ECDSA.
On another problem that how can we create more new signatures to get about 60 signatures for LLL solving. That is (m, r, -s) also a valid signature (Read more on this)
Here is the solve.py