Curvy Decryptor
473 points
Alice has hidden 2 flags in this challenge. And even though she is willing to decrypt most ciphers, she has some basic saveguards against stealing flags.
Please submit flag1 here.
nc 52.59.124.14 10005
#!/usr/bin/env python3
importosimportsysimportstringfromCrypto.UtilimportnumberfromCrypto.Util.numberimportbytes_to_long,long_to_bytesfromCrypto.CipherimportAESfrombinasciiimporthexlifyfromecimport*fromutilsimport*fromsecretimportflag1,flag2#P-256 parameters
p=0xffffffff00000001000000000000000000000000ffffffffffffffffffffffffa=-3b=0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604bn=0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551curve=EllipticCurve(p,a,b,order=n)G=ECPoint(curve,0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296,0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5)d_a=bytes_to_long(os.urandom(32))P_a=G*d_aprintable=[ord(char.encode())forcharinstring.printable]defencrypt(msg:bytes,pubkey:ECPoint):x=bytes_to_long(msg)y=modular_sqrt(x**3+a*x+b,p)m=ECPoint(curve,x,y)d_b=number.getRandomRange(0,n)return (G*d_b,m+(pubkey*d_b))defdecrypt(B:ECPoint,c:ECPoint,d_a:int):ifB.inforc.inf:returnb''returnlong_to_bytes((c-(B*d_a)).x)defloop():print('I will decrypt anythin as long as it does not talk about flags.')balance=1024whileTrue:print('B:',end='')sys.stdout.flush()B_input=sys.stdin.buffer.readline().strip().decode()print('c:',end='')sys.stdout.flush()c_input=sys.stdin.buffer.readline().strip().decode()B=ECPoint(curve,*[int(_)for_inB_input.split(',')])c=ECPoint(curve,*[int(_)for_inc_input.split(',')])msg=decrypt(B,c,d_a)ifb'ENO'inmsg:balance=-1else:balance-=1+len([cforcinmsgifcinprintable])ifbalance>=0:print(hexlify(msg))print('balance left: %d'%balance)else:print('You cannot afford any more decryptions.')returnif__name__=='__main__':print('My public key is:')print(P_a)print('Good luck decrypting this cipher.')B,c=encrypt(flag1,P_a)print(B)print(c)key=long_to_bytes((d_a>>(8*16))^(d_a&0xffffffffffffffffffffffffffffffff))enc=AES.new(key,AES.MODE_ECB)cipher=enc.encrypt(flag2)print(hexlify(cipher).decode())try:loop()exceptExceptionaserr:print(repr(err))
Curvy Decryptor part 1
The solution of Curvy Decryptor part 1 is to find out flag1
Curvy Decryptor part 2
The solution of Curvy Decryptor part 1 is to find out flag2
The flag2 appears to be AES encrypted with a key which is
The xor of most significant and least significant 64 bits of the 128 bits private key d_a
So in order to recover the flag2 we will need to break the ECC and recover d_a
indeed look like a standard curve P-256 (note that a = -3 is equivalent to a = p - 3)
So there doesnt appear to be any standard weakness based on weak curve parameters.
The private key d_a is initialized to be per-instance system-random 128 bit number
And the Public key P_a is simply G*d_a
The msg is encoded as the x coordinate of the point, the corresponding y is found
so as to find the point on the curve to generate the point m
A secure random number d_b in range (0 - curve order) is generated as the nonce,
The points G*d_b and m + (pubkey * d_b) are returned
The main loop of the program just repeatedly asks for input of two EC points B and c and tries to decrypt it with the servers private key d_a
BUT
if the decryption contains b'ENO' i.e the start of the flag, it exits.
Otherwise it decreases the balance proportionate to the number of printable characters in the decryption.
So If we directly input the B and c corresponding to the flag1, it will abort and hence no flags for us :’(
But it doesnt care what points we ask it to decrypt.
So what if we try to decrypt the points B, c + A
As long as we know A, we can always get the point from the x coordinate, and subtract A from it to get the original point from the curve for the sake of simplicity, we can even pick it to be G
or we can even try decrypting the points B + A, c which will lead to
if we choose A to be G or -G, we will end up with the point flag - P_a or flag + P_a and since we even know P_a, it will also work
With a high probability, we wont observe any b'ENO' in the resulting point, and if we do, we can always pick countless possibilities of A to make it work
fromecimport*fromutilsimport*importpwnp=0xffffffff00000001000000000000000000000000ffffffffffffffffffffffffa=-3b=0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604bn=0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551curve=EllipticCurve(p,a,b,order=n)G=ECPoint(curve,0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296,0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5)HOST,PORT="52.59.124.14",10005REM=pwn.remote(HOST,PORT)REM.recvline()# My public key is:
pubkey=REM.recvline().strip()[6:-1].split(b',')P_a=ECPoint(curve,int(pubkey[0]),int(pubkey[1]))REM.recvline()# Good luck decrypting this cipher.
B_text=REM.recvline().strip()[6:-1].split(b',')B=ECPoint(curve,int(B_text[0]),int(B_text[1]))c_text=REM.recvline().strip()[6:-1].split(b',')c=ECPoint(curve,int(c_text[0]),int(c_text[1]))flag2_enc=bytes.fromhex(REM.recvline().strip().decode())REM.recvline()# I will decrypt anythin as long as it does not talk about flags.
defget_decryption(B,c):REM.sendline("{},{}".format(B.x,B.y))REM.sendline("{},{}".format(c.x,c.y))status=REM.recvline()ifb'cannot afford'instatus:return-1,Nonebalance=int(REM.recvline().strip().split(b': ')[-1])returnbalance,bytes.fromhex(status.strip()[6:-1].decode())bal,BG=get_decryption(B,c+G)BG_int=int.from_bytes(BG)y=modular_sqrt(BG_int**3+a*BG_int+b,p)# getting the valid y coordinate for the x
point_BG1=ECPoint(curve,BG_int,y)point_BG2=-point_BG1print(int.to_bytes((point_BG1-G).x,32,'big'))print(int.to_bytes((point_BG2-G).x,32,'big'))
Note that we only get the x coordinate of the given point lifting the point would
result in two points and we will need to try with both of them (x,y) and (x,-y)
The last part will also work with
Python
1
2
3
4
5
6
7
bal,BG=get_decryption(B-G,c)BG_int=int.from_bytes(BG)y=modular_sqrt(BG_int**3+a*BG_int+b,p)# getting the valid y coordinate for the x
point_BG1=ECPoint(curve,BG_int,y)point_BG2=-point_BG1print(int.to_bytes((point_BG1-P_a).x,32,'big'))print(int.to_bytes((point_BG2-P_a).x,32,'big'))
And we get the flag to part 1
b'\x00\x00ENO{ElGam4l_1s_mult1pl1cativ3}'
Looking closely at the ECPoint class, one would note that on the initialization of the point with arbitrary x, y coordinates, it works as usual and doesnt check whether the supplied x, y satisfy the curve equation y2=x3+ax+bmodp
This leads to an interesting vulnerability aka Invalid Curve Attack
Which can be noted by the facts that
The point addtion of two points P and Q over the curve y2=x3+ax+bmodpif P=Q is independent of both the curve parameters a and b
The point doubling i.e P=Q is just dependent on P and Q and a but not on b again
This means that the group addition operation is independent of the parameter b, the number of points in the group of some point P is just dependent on P and a but independent of b
So if we choose a curve C′ with parameter b′ and pick a valid point P′ on it, and run the point addition over the original curve C, the order of point P′ when used on C will be the same as order of point P′ when used on C′
This implies that we are not stuck with the original prime order of P-256, but we can vary b=−3 such that the order of the curve C′ has small factors. We can then easily find points P′ with those small factors as their order by using the fact that -
If G′ is the generator of C′ with order o=f1f2f3...fn, the point G′∗(o/f1) will have the order f1
Once we have sufficient number of small orders, we can take a chinese remainder theorem over them to recover the original private key d_a
To find the order of curve C′ and the corresponding generators, we can utilize the greate library of sagemath
Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
defget_invalid_curves(a,b,n,cutoff=10**5):factors,total,i={},0,0whiletotal<n*2:i+=1try:E=EllipticCurve(GF(p),[a,i])order=E.order()n_facs=order.factor()exceptArithmeticError:#the parameter i defines a singular curve
continueforprime,powerinn_facs:ifprime>cutoff:# dont take any factors bigger than it
breakgen=E.gen(0)*(order//prime)factors[prime]=[int(gen[0]),int(gen[1]),i]total*=primeprint(i,total)# key : [gen_x, gen_y, b']
returnfactors