import os,random,sys,string from hashlib import sha256 import SocketServer import signal from FLAG import flag from gmpy2 import invert from Crypto.Util.number import bytes_to_long, long_to_bytes
q = 0xdd7860f2c4afe6d96059766ddd2b52f7bb1ab0fce779a36f723d50339ab25bbd a = 0x4cee8d95bb3f64db7d53b078ba3a904557425e2a6d91c5dfbf4c564a3f3619fa b = 0x56cbc73d8d2ad00e22f12b930d1d685136357d692fa705dae25c66bee23157b8 zero = (0,0)
defadd(p1,p2): if p1 == zero: return p2 if p2 == zero: return p1 (p1x,p1y),(p2x,p2y) = p1,p2 if p1x == p2x and (p1y != p2y or p1y == 0): return zero if p1x == p2x: tmp = (3 * p1x * p1x + a) * invert(2 * p1y , q) % q else: tmp = (p2y - p1y) * invert(p2x - p1x , q) % q x = (tmp * tmp - p1x - p2x) % q y = (tmp * (p1x - x) - p1y) % q return (int(x),int(y))
defmul(n,p): r = zero tmp = p while0 < n: if n & 1 == 1: r = add(r,tmp) n, tmp = n >> 1, add(tmp,tmp) return r
Px = 0xb55c08d92cd878a3ad444a3627a52764f5a402f4a86ef700271cb17edfa739ca Py = 0x49ee01169c130f25853b66b1b97437fb28cfc8ba38b9f497c78f4a09c17a7ab2 P = (Px,Py)
classTask(SocketServer.BaseRequestHandler): defproof_of_work(self): random.seed(os.urandom(8)) proof = "".join([random.choice(string.ascii_letters+string.digits) for _ in range(20)]) digest = sha256(proof).hexdigest() self.request.send("sha256(XXXX+%s) == %s\n" % (proof[4:],digest)) self.request.send("Give me XXXX:") x = self.request.recv(10) x = x.strip() if len(x) != 4or sha256(x+proof[4:]).hexdigest() != digest: returnFalse returnTrue
defrecvall(self, sz): try: r = sz res = "" while r > 0: res += self.request.recv(r) if res.endswith("\n"): r = 0 else: r = sz - len(res) res = res.strip() except: res = "" return res.strip("\n")
self.dosend("Welcome to the ECDH System.\n") self.dosend("The params are: \n") self.dosend("q: " + str(q) + "\n") self.dosend("a: " + str(a) + "\n") self.dosend("b: " + str(b) + "\n") self.dosend("P: " + pointToString(P) + "\n") self.dosend("Q: " + pointToString(Q) + "\n") self.exchange() for _ in range(90): self.dosend("Tell me your choice:\n") choice = self.recvall(9) if choice == "Exchange": self.exchange() elif choice == "Encrypt": self.encrypt() elif choice == "Backdoor": self.backdoor() else: self.dosend("No such choice!\n") self.dosend("Bye bye~\n") self.request.close() except: self.dosend("Something error!\n") self.request.close()
defpad(self,m): pad_length = q.bit_length()*2 - len(m) for _ in range(pad_length): m.insert(0,0) return m
defencrypt(self): self.dosend("Give me your message(hex):\n") msg = self.recvall(150) data = [int(i) for i in list('{0:0b}'.format(bytes_to_long(msg.decode("hex"))))] enc = [data[i] ^ self.key[i%len(self.key)] for i in range(len(data))] result = 0 for bit in enc: result = (result << 1) | bit result = long_to_bytes(result).encode("hex") self.dosend("The result is:\n") self.dosend(result + "\n")
defpointToKeys(self,p): x = p[0] y = p[1] tmp = x << q.bit_length() | y res = self.pad([int(i) for i in list('{0:0b}'.format(tmp))]) return res
defexchange(self): self.dosend("Give me your key:\n") self.dosend("X:\n") x = int(self.recvall(80)) self.dosend("Y:\n") y = int(self.recvall(80)) key = (x,y) result = mul(self.secret,key) self.key = self.pointToKeys(result) self.dosend("Exchange success\n")
defbackdoor(self): self.dosend("Give me the secret:\n") s = self.recvall(80) if int(s) == self.secret: self.dosend('Wow! How smart you are! Here is your flag:\n') self.dosend(flag) else: self.dosend('Sorry you are wrong!\n') exit(0)
if __name__ == "__main__": HOST, PORT = "0.0.0.0", 8848 server = ForkedServer((HOST, PORT), Task) server.allow_reuse_address = True server.serve_forever()
So the server is defining a custom elliptic curve:
y^2 = x^3 + ax + b over Fq
where:
1 2 3
a = 0x4cee8d95bb3f64db7d53b078ba3a904557425e2a6d91c5dfbf4c564a3f3619fa b = 0x56cbc73d8d2ad00e22f12b930d1d685136357d692fa705dae25c66bee23157b8 q = 0xdd7860f2c4afe6d96059766ddd2b52f7bb1ab0fce779a36f723d50339ab25bbd
And a generator P = (x, y) with:
1 2
x = 0xb55c08d92cd878a3ad444a3627a52764f5a402f4a86ef700271cb17edfa739ca y = 0x49ee01169c130f25853b66b1b97437fb28cfc8ba38b9f497c78f4a09c17a7ab2
The main iter of the server is:
Ask for a Proof of Work where we need to find an easy collision on sha256.
Generate a random secret between (0, q).
Compute Q = P * secret.
Exchange operation.
For 90 times 1 operation between Exchange, Encrypt, Backdoor.
Code of Exchange:
1 2 3 4 5 6 7 8 9 10
defexchange(self): self.dosend("Give me your key:\n") self.dosend("X:\n") x = int(self.recvall(80)) self.dosend("Y:\n") y = int(self.recvall(80)) key = (x,y) result = mul(self.secret,key) self.key = self.pointToKeys(result) self.dosend("Exchange success\n")
So the server asks the coordinate of a point (let’s call this point G), and then compute key as key = G * secret.
Code of Encrypt:
1 2 3 4 5 6 7 8 9 10 11
defencrypt(self): self.dosend("Give me your message(hex):\n") msg = self.recvall(150) data = [int(i) for i in list('{0:0b}'.format(bytes_to_long(msg.decode("hex"))))] enc = [data[i] ^ self.key[i%len(self.key)] for i in range(len(data))] result = 0 for bit in enc: result = (result << 1) | bit result = long_to_bytes(result).encode("hex") self.dosend("The result is:\n") self.dosend(result + "\n")
The server asks for a message and then xor our message with the key. If we xor back the result with our message we obtain the key.
Code of Backdoor:
1 2 3 4 5 6 7 8 9
defbackdoor(self): self.dosend("Give me the secret:\n") s = self.recvall(80) if int(s) == self.secret: self.dosend('Wow! How smart you are! Here is your flag:\n') self.dosend(flag) else: self.dosend('Sorry you are wrong!\n') exit(0)
So if we guess/find correctly the secret we can get the flag.
However finding k from Q, P where Q = P * k is known as the discrete logarithm problem and is very hard to compute in polynomial time. (In our case k is the secret).
I found this paper which describe various attacks on ECC, the one I was interested in is the Smart’s attack which is possible when the order of the curve is equal to q.
We can test this in sage:
1 2 3 4 5 6 7
a = 0x4cee8d95bb3f64db7d53b078ba3a904557425e2a6d91c5dfbf4c564a3f3619fa b = 0x56cbc73d8d2ad00e22f12b930d1d685136357d692fa705dae25c66bee23157b8 q = 0xdd7860f2c4afe6d96059766ddd2b52f7bb1ab0fce779a36f723d50339ab25bbd E = EllipticCurve(GF(q), [a, b]) print(E.order()) print(q) print(E.order() == q)
We tried to set the point (0, 0) during the exchange and then encrypting a random message. We saw that the ciphertext was equal to the message, so the key was set to 0. Then I wanted to try some basic multiplications on sage using the point (0, 1) but:
1 2
G = E([0, 1]) TypeError: Coordinates [0, 1, 1] do not define a point on Elliptic Curve defined by y^2 = x^3 + 34797263276731929172113534470189285565338055361167847976642146658712494152186*x + 39258950039257324692361857404792305546106163510884954845070423459288379905976 over Finite Field of size 100173830297345234246296808618734115432031228596757431606170574500462062623677
Right! The point (0, 1) and (0, 0) are not on the curve!
So what?
Well we can use the invalid curve attack to find the secret!
The server fails to validate that the point is on the curve, so the custom points have different order, and we can easily compute the discrete logarithm since the order of the new curve is very small.
sage: E = EllipticCurve(GF(q), [a, 10]) # Define custom curve with b = 10 sage: E Elliptic Curve defined by y^2 = x^3 + 34797263276731929172113534470189285565338055361167847976642146658712494152186*x + 10 over Finite Field of size 100173830297345234246296808618734115432031228596757431606170574500462062623677 sage: primes = prime_factors(E.order()) sage: primes [2, 7, 43, 41600427864346027510920601585853037970001163068300848417938134241287552041] sage: prime = 7# We want to generate a point which has order 7
sage: G = E.gen(0) * int(E.order() / prime) sage: G # Base point of the curve (23800613574902822049829027077760684224803559019003054675408332590180803344147 : 4985144212684407923452143080073709767923132482274656621285455512784711650956 : 1)
sage: for i in range(10): ....: print(G * i) ....:
We chose as order 7, and then we computed a generator G of the curve. Finally we have computed G * i for every i in range(10).
We can see that after the firsts 7 multiplication, the points are repeating. This is because we are in a cyclic group.
How can this help?
Well in this case computing discrete log is very easy because there are few possible cases (7).
If we now submit this point in the Exchange to the server and then we retrieve the key it’s trivial to find the secret?
Well no. What we will found is not the secret because the secret is between (0, q) and is presumably around 250/255 bits long and not between 0 and 7.
What we can do is to try retrieving various pair (dlog, prime) and create a system of linear congruences that are solvable using the Chinese Remainder Theorem.
dlog is the result of the discrete logarithm and prime is the order of the generator.
import sys from pwn import * from Crypto.Util.number import bytes_to_long as bl from factordb.factordb import FactorDB from Crypto.Util.number import long_to_bytes as lb from sage.all import * import string
# Too lazy to use itertools defcrack(to_crack): alphabet = string.digits + string.ascii_letters for x in alphabet: for y in alphabet: for z in alphabet: for w in alphabet: key = x+y+z+w + secret _hash = hashlib.sha256(key.encode()).hexdigest() if _hash == to_crack: return x + y + z + w
defxor_string(s1, s2): return bytes(x ^ y for x, y in zip(s1, s2))
defpad(m): pad_length = 256*2 - len(m) for _ in range(pad_length): m.insert(0,0) return m
defpoint_to_keys(p): x = p[0] y = p[1] tmp = x << 256 | y res = pad([int(i) for i in list('{0:0b}'.format(tmp))]) return res
defbytes_to_bit(a): l = [] for b in a: c = bin(b)[2:].rjust(8, '0') for d in c: l.append(int(d)) return l
defkeys_to_point(k): y = int(''.join(str(i) for i in k[-256:]), 2) x = int(''.join(str(i) for i in k[:256]), 2) return (x, y)
a = 0x4cee8d95bb3f64db7d53b078ba3a904557425e2a6d91c5dfbf4c564a3f3619fa b = 0x56cbc73d8d2ad00e22f12b930d1d685136357d692fa705dae25c66bee23157b8 q = 0xdd7860f2c4afe6d96059766ddd2b52f7bb1ab0fce779a36f723d50339ab25bbd
pp = [] dlog = [] blacklist_dlog = [30, 42] for i in range(10):
# Exchange like a beast print("\n\nEXCHANGE\n") # G # Q = G * secret b = randint(10, 50) print(f"b: {b}") E = EllipticCurve(GF(q), [a, b]) print(f"Curve: {E}") order = E.order() print("computing prime factors") primes = prime_factors(order) valid = [] # 12298541720533 for p in primes: if p <= 122985417205330: valid.append(p) prime = valid[-1:][0] pp.append(prime) print(f"prime order: {prime}") print("computing generator") G = E.gen(0) * int(order / prime) G = read_sage_point(str(G)) if i != 0: conn.recvuntil(b'choice:\n') conn.sendline(b'Exchange') conn.recvuntil(b'X:') conn.sendline(str(G[0])) conn.recvuntil(b'Y:') conn.sendline(str(G[1]))
# Solve DLOG muthafucka conn.recvuntil(b'Tell me your choice:') conn.sendline('Encrypt') conn.recvuntil('Give me your message(hex):') to_cipher = "" to_cipher += 'f1' for _ in range(63): to_cipher += '41' conn.sendline(to_cipher)
The main problem was time. The server has a connection timeout of 5 minutes and we got 3 main bottlenecks:
Computing prime factors of the custom curve.
Find a generator of the curve.
Solving dlog.
What we did was:
Use factordb to factorize the order of the curve.
Cache various generator using an external script.
Solve the discrete logarithm in a separate process.
Exploit
Generator script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
generators = {}
for i in range(2, 50): if generators.get(i, None) isNone: E = EllipticCurve(GF(q), [a, i]) order = E.order() f = FactorDB(order) f.connect() primes = f.get_factor_list() valid = 0 for p in primes: if p <= 122985417205330: valid = p prime = valid G = E.gen(0) * int(order / prime) G = read_sage_point(str(G)) generators[i] = (G[0], G[1], prime) print(generators)
import sys from pwn import * from Crypto.Util.number import bytes_to_long as bl from factordb.factordb import FactorDB from Crypto.Util.number import long_to_bytes as lb from sage.all import * import multiprocessing import string import time
# Too lazy to use itertools defcrack(to_crack): alphabet = string.digits + string.ascii_letters for x in alphabet: for y in alphabet: for z in alphabet: for w in alphabet: key = x+y+z+w + secret _hash = hashlib.sha256(key.encode()).hexdigest() if _hash == to_crack: return x + y + z + w
defxor_string(s1, s2): return bytes(x ^ y for x, y in zip(s1, s2))
defpad(m): pad_length = 256*2 - len(m) for _ in range(pad_length): m.insert(0,0) return m
defpoint_to_keys(p): x = p[0] y = p[1] tmp = x << 256 | y res = pad([int(i) for i in list('{0:0b}'.format(tmp))]) return res
defbytes_to_bit(a): l = [] for b in a: c = bin(b)[2:].rjust(8, '0') for d in c: l.append(int(d)) return l
defkeys_to_point(k): y = int(''.join(str(i) for i in k[-256:]), 2) x = int(''.join(str(i) for i in k[:256]), 2) return (x, y)
a = 0x4cee8d95bb3f64db7d53b078ba3a904557425e2a6d91c5dfbf4c564a3f3619fa b = 0x56cbc73d8d2ad00e22f12b930d1d685136357d692fa705dae25c66bee23157b8 q = 0xdd7860f2c4afe6d96059766ddd2b52f7bb1ab0fce779a36f723d50339ab25bbd
manager = multiprocessing.Manager() crt_solver = manager.dict() processes = [] for i in range(len(list(generators.keys()))):
# Exchange like a beast b = list(generators.keys())[i] E = EllipticCurve(GF(q), [a, b]) G = generators[b][:2] print("\n\nEXCHANGE\n") print(f"trying b: {b}")
if i != 0: conn.recvuntil(b'choice:\n') conn.sendline(b'Exchange') conn.recvuntil(b'X:') conn.sendline(str(G[0])) conn.recvuntil(b'Y:') conn.sendline(str(G[1]))
# Solve DLOG muthafucka conn.recvuntil(b'Tell me your choice:') conn.sendline('Encrypt') conn.recvuntil('Give me your message(hex):') to_cipher = "" to_cipher += 'f1' for _ in range(63): to_cipher += '41' conn.sendline(to_cipher)
# Useless but fun for _ in range(6): conn.recvuntil(b'choice:\n') conn.sendline(b'Exchange') conn.recvuntil(b'X:') conn.sendline(str(1)) conn.recvuntil(b'Y:') conn.sendline(str(1)) time.sleep(30)