This task has two parts to it, the first part is the signer. You can send a message to the server and it will sign it and send it back. The second part is the verifier. The verifier will take the message and the signature of the message and verify they match.
Lets take a look at the signer code:
import socketserver
import random
import ecdsa
key = open("secp256k1-key.pem").read()
sk = ecdsa.SigningKey.from_pem(key)
def sony_rand(n):
return random.getrandbits(8*n).to_bytes(n, "big")
def sign(data):
if data == b"admin":
raise ValueError("Not Permitted!")
signature = sk.sign(data, entropy=sony_rand)
return signature
class TCPHandler(socketserver.StreamRequestHandler):
def handle(self):
data = self.rfile.readline().strip()
try:
signature = sign(data).hex()
self.wfile.write(b"Your token: " + data + b"," + signature.encode())
except ValueError as ex:
self.wfile.write(b"Invalid string submitted: " + str(ex).encode())
if __name__ == '__main__':
server = socketserver.ForkingTCPServer(("0.0.0.0", 10101), TCPHandler)
server.serve_forever()
Basic info we can get from it:
- Every request will call
sign(our_data)
sign()
will check that our input is NOT "admin", and will sign- It's using some sort or rand function for the entropy of the signer
- The ecdsa curve they are using is "secp256k1"
- We can send it any message (besides "admin") and get the signature of the message
Now lets take a look at the verifier:
import socketserver
import ecdsa
import pyjokes
from flag import FLAG
key = open("pub.pem").read()
vk = ecdsa.VerifyingKey.from_pem(key)
def valid_signature(msg, sig):
try:
vk.verify(sig, msg)
return True
except ecdsa.BadSignatureError:
return False
class TCPHandler(socketserver.StreamRequestHandler):
def handle(self):
data = self.rfile.readline().strip()
user, signature = data.split(b",")
sig = bytes.fromhex(signature.decode())
try:
if valid_signature(user, sig):
if user == b"admin":
self.wfile.write(b"Hello admin! Here is your flag: " + FLAG)
else:
self.wfile.write(pyjokes.get_joke().encode())
else:
self.wfile.write(b"Invalid signature!")
except Exception as ex:
self.wfile.write(b"Something went wrong!")
if __name__ == '__main__':
server = socketserver.ForkingTCPServer(("0.0.0.0", 10100), TCPHandler)
server.serve_forever()
Basic info we can get from it:
- The handler will try to first
valid_signature
valid_signature
will use the msg and signature you provided and verify they match- Checks that the message you send equals "admin" before sending the flag
Since we cannot send "admin" to the signer I had to do some research into how ecdsa works.
An ECDSA signature is a pair of integers (r,s)
.
The ECDSA signature algorithm works like so:
e = H(m)
where H is a hashing function (i.e sha1, sha256)- Pick a random
k
such that0 < k < n-1
- Compute
(x,y) = kG
where G is the prime order of curve r = x mod n
s = inverse(k)*(z+r*d) mod n
where d is a private key integer and z is the leftmost bits of e- Send
(m,r,s)
The ECDSA verification algorithm works like so:
e = H(m)
w = inverse(s) mod n
u_1 = zw mod n
andu_2 = zw mod n
(x,y) = u_1*G + u_2*Q
where Q = d x Q- If
r
is congruent withx mod n
we know the signature is valid
When I send the signer different strings to sign i noticed it would always send something with the prefix "13d8f71de2338048bcddd4846ea9762fa022172b6602f269c519892d8bf7e94f".... If we think back to how the ECDSA signature looks (r,s)
we can see that r is not changing. This means we know that r = x mod n
has to always be the same. From this information we can figure out that they are using the same "random" k
everytime. So now lets make two request to the server with different m's. The signer will send us back (m1,r,s1) and (m2,r,s2)
.
Since we know k is constant we can easily solve for it with the following equation:
k = (H(m1) - h(M2)) / (s1 - s2)
We then can solve for what x was:
x = (k*s1 - h(m1)) / r
With k and x known we can now start writing the script to sign "admin" and send to the server.
# https://crypto.stackexchange.com/questions/57846/recovering-private-key-from-secp256k1-signatures
import ecdsa, hashlib
from ecdsa.numbertheory import inverse_mod
from ecdsa.ecdsa import Signature
from ecdsa import SigningKey, VerifyingKey, der
from pwn import *
curve = ecdsa.SECP256k1
text_to_sign = b"admin"
hash_algorithm = hashlib.sha1
def get_key_from_hash():
m_hash1 = '21298df8a3277357ee55b01df9530b535cf08ec1'
sig1_hex = '13d8f71de2338048bcddd4846ea9762fa022172b6602f269c519892d8bf7e94f77608e0387a7ba5392bd1e2b4ded1048133fb584b7686233af00a6e7c5d427e7'
m_hash2 = 'c692d6a10598e0a801576fdd4ecf3c37e45bfbc4'
sig2_hex = '13d8f71de2338048bcddd4846ea9762fa022172b6602f269c519892d8bf7e94fdcb6d55b347bfbe8c6a37e2b7c6ca764d7bd07f52d56df2ff80df7a59cbe51ec'
m_hash1 = int(m_hash1, 16)
r = int(sig1_hex[:len(sig1_hex)//2], 16)
sig1 = int(sig1_hex[len(sig1_hex)//2:], 16)
m_hash2 = int(m_hash2, 16)
sig2 = int(sig2_hex[len(sig2_hex)//2:], 16)
print("m_hash1 = " + hex(m_hash1))
print("sig1 = " + hex(sig1))
print("m_hash2 = " + hex(m_hash2))
print("sig2 = " + hex(sig2))
print("r = " + hex(r))
r_i = inverse_mod(r, curve.order)
m_h_diff = (m_hash1 - m_hash2) % curve.order
for k_try in (sig1 - sig2, sig1 + sig2, -sig1 - sig2, -sig1 + sig2):
k = (m_h_diff * inverse_mod(k_try, curve.order)) % curve.order
s_E = (((((sig1 * k) % curve.order) - m_hash1) % curve.order) * r_i) % curve.order
key = SigningKey.from_secret_exponent(s_E, curve=curve, hashfunc=hash_algorithm)
if key.get_verifying_key().pubkey.verifies(m_hash1, Signature(r, sig1)):
print("ECDSA Private Key = " + "".join("{:02x}".format(c) for c in key.to_string())) # If we got here we found a solution
return key
def sign_text(priv_key):
sk = ecdsa.SigningKey.from_string(priv_key.to_string(), curve=curve)
vk = sk.get_verifying_key()
sig = sk.sign(text_to_sign)
signed_message = "".join("{:02x}".format(c) for c in sig)
return "{},{}".format(text_to_sign.decode("utf-8"), signed_message)
def send_message(s_m):
target = remote('chal.cybersecurityrumble.de', 10100)
print("Sending '{}'".format(s_m))
target.sendline(s_m)
target.interactive()
signed_message = sign_text(get_key_from_hash())
print(send_message(signed_message))
Once you run this script the server will print "Hello admin! Here is your flag: CSR{m33333333333p}".
flag = CSR{m33333333333p}