In the challenge we get two files. First one contains properly looking Schnorr signature code and the second one is the server code.
The application generates a new private-public key for the server and then in look:
- asks us for our public key
- ask for action to perform
We can perform 3 actions:
- Send
DEPOSIT
message signed with our private key and server will verify this signature agains the public key we send initially. - SEND
WITHDRAW
message signed with our private key and with server private key, and server will verify this with our public key combined with server public key (in this context it means our ECC Public Key Point will be added to the server Point). - As for server public key.
There are 2 key points to notice here:
- In given connection (so while server maintains his private-public key pair) we can perform multiple operations and each one of the for
different
public key. This is because public key is requested every time. - Adding and subtracting points on Elliptic Curve is not a difficult operation. It's "division" that is the hard operation that security is based on. This means if we have points
P
andQ
it's very simple to calculate pointS
such thatP+S = Q
. Subtraction is simply addition of point withy
coordinate negated, soS = P-Q = (xp,yp)+(xq,-yq)
The layout of the attack is pretty simple:
- Generate some private-public keypair for ourselves (
P
andpP
). - Send our public key
P
and request server public key, let's call it pointQ
. - Send our public key
P
and sendDEPOSIT
message signed with our private keypP
. - Calculate point
S = P-Q = (xp,yp)+(xq,-yq)
- Send point
S
as our public key and sendWITHDRAW
operation signed with our private keypP
. - Now the server will add their public key
Q
to our public keyS
to verify our message. ButS+Q = P-Q+Q = P
, our public key, for whichpP
is the corresponding private key. - The message will be properly verified and we will get back the flag.
We implement this by:
def main():
host = "tcp.realworldctf.com"
# host = "localhost"
port = 20014
s = nc(host, port)
data = receive_until(s, "\n").strip()
prefix = data[-16:]
print(data)
print(prefix)
res = breakPoW(prefix)
s.sendall(res)
sk, pk = generate_keys()
real_key_message = (str(pk[0]) + "," + str(pk[1]))
send(s, base64.b64encode(real_key_message))
send(s, base64.b64encode("1"))
deposit_signature = schnorr_sign("DEPOSIT", sk)
send(s, base64.b64encode(deposit_signature))
send(s, base64.b64encode(real_key_message))
send(s, base64.b64encode("3"))
data = receive_until_match(s, "one of us: .*\n")
serverPk = re.findall("one of us: (.*)\n", data)[0].replace("(", "").replace(")", "").replace("L", "")
serverPk = (int(serverPk.split(",")[0]), int(serverPk.split(",")[1]))
Q = point_add(pk, (serverPk[0], (-serverPk[1]) % p))
fake_key_message = (str(Q[0]) + "," + str(Q[1]))
send(s, base64.b64encode(fake_key_message))
withdraw_signature = schnorr_sign("WITHDRAW", sk)
send(s, base64.b64encode("2"))
send(s, base64.b64encode(withdraw_signature))
interactive(s)
main()
And we get back the flag: rwctf{P1Ain_SChNorr_n33Ds_m0re_5ecur1ty!}