In this challenge we are told to retrieve a flag from a vulnerable server. We are given the files contract.pcapng with network traffic and server.py with a simple python TCP server implementation. We are also given the command nc contract.vuln.icec.tf 6002 which tells us the address and port we can use to communicate with the server.

The pcapng file consists mostly of packets used for DNS resolution and TCP handshaking. Only packet 6, 18 and 20 contain an actual payload. Packets 6 and 18 were sent from the client to the server and contain a payload of the format: “command:<192 bytes of hexadecimal characters>.”. with the commands help and time sent in packet 6 and 18 respectively. Packet 20 seems to be sent as a response to packet 18 since it contains only a string with the date and time. Let’s look at the server code and see what the server does with the data it receives:

    def handle(self):
        signal.alarm(5)
        d = self.rfile.readline().strip()
        try:
            msg, sig = d.split(b":")
        except ValueError as e:
            self.wfile.write(b"bad command\n")
            return
        if not self.verify(msg, sig):
            self.wfile.write(b"bad signature\n")
            return

        self.run_command(msg)

We see that server tries to split the data at the colon character and upon succes seemingly performs a verification on it. The help or time strings we saw earlier would be assigned to the msg variable. The hexadecimal string we saw gets assigned to the sig variable indicating it is likely going to be used as a signature. If the verification succeeds it passes msg as an argument to the run_command function seen below:

 
    def run_command(self, msg):
        cmd, *args = msg.split()
        if cmd == b"read":
            try:
                with open(args[0], "rb") as f:
                    self.wfile.write(f.read())
            except:
                self.wfile.write("\n")
        elif cmd == b"time":
            self.wfile.write(datetime.datetime.strftime(datetime.datetime.now(), "%Y-%m-%d %H:%M:%S").encode("utf8"))
        elif cmd == b"help":
            self.wfile.write(help_string)
        else:
            self.wfile.write(b"bad command\n")

After reading this function it becomes clear that the challenge reduces to finding the correct signature to send along with the read command and hopefully use it to read a file with our flag from the server. We can get more insight into the type of verification used by looking at the verification function:

 
    def verify(self, msg, sig):
        try:
            return vk.verify(unhexlify(sig), msg, hashfunc=hashlib.sha256)
        except:
            return False

We see that the object vk is used for verification which is created at the beginning of the file using the following code:

 
PUBLIC_KEY = """
-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEgTxPtDMGS8oOT3h6fLvYyUGq/BWeKiCB
sQPyD0+2vybIT/Xdl6hOqQd74zr4U2dkj+2q6+vwQ4DCB1X7HsFZ5JczfkO7HCdY
I7sGDvd9eUias/xPdSIL3gMbs26b0Ww0
-----END PUBLIC KEY-----
"""
vk = VerifyingKey.from_pem(PUBLIC_KEY.strip()) 

The class VerifyingKey is imported from the ecdsa library which is a python implementation of the ECDSA digital signature algorithm. When reading through the library and the wikipedia page it becomes clear that a signature actually consists of an r,s pair. The VeryfyingKey class and its counterpart SigningKey are defined in the keys.py. We see that the sign function calls another function called sign_digest which takes an r,s pair and encodes it in a string. Considering the format of the data sent to the server and the api presented by this library through the key classes defined in this file, the client must have hexlified and appended this pair to the command and a colon. The server then unhexlifies the signature after which it uses the command and signature for verification. After peeling away some layers of abstraction we can find that the actual ECDSA signing algorithm that returns this pair is implemented as the sign function in the ecdsa.py file:

 
  def sign(self, hash, random_k):
    """Return a signature for the provided hash, using the provided
    random nonce.  It is absolutely vital that random_k be an unpredictable
    number in the range [1, self.public_key.point.order()-1].  If
    an attacker can guess random_k, he can compute our private key from a
    single signature.  Also, if an attacker knows a few high-order
    bits (or a few low-order bits) of random_k, he can compute our private
    key from many signatures.  The generation of nonces with adequate
    cryptographic strength is very difficult and far beyond the scope
    of this comment.
    May raise RuntimeError, in which case retrying with a new
    random value k is in order.
    """

    G = self.public_key.generator
    n = G.order()
    k = random_k % n
    p1 = k * G
    r = p1.x()
    if r == 0:
      raise RuntimeError("amazingly unlucky random number r")
    s = (numbertheory.inverse_mod(k, n) *
         (hash + (self.secret_multiplier * r) % n)) % n
    if s == 0:
      raise RuntimeError("amazingly unlucky random number s")
    return Signature(r, s)

We see that in order to compute a signature the functions needs four values: hash,k,G and self.secret_multiplier. Here hash is just the hash of the data we wanna sign and G is the so called elliptic curve base point which is part of the public key. What is unknown to us is k and self.secret_multiplier which is the actual private key. If we could find these we could sign our message and be able to use the servers read command. The first thing to ask is if ECDSA has any known vulnerabilities that we could exploit. After doing some research on ECDSA I found this pdf: 1780_27c3_console_hacking_2010.pdf. On slide 124 and 125 it talks about the possibility to compute m and k incase the same m is used to compute each signature. After comparing we see that the m and k in the slides correspond to the variable k and the private key self.secret_multiplier in the code. Another thing to note is that existence of this vulnerability would imply that the r in the r,s pair would be the same each time. After inspecting the signatures in the pcapng file we see that this is indeed the case! Both signatures start with the string: “c0e1fc4e3858ac6334cc8798fdec40790d7ad361ffc691c26f2902c41f2b7c2fd1ca916de687858953a6405423fe156c” : ).

The difficult part was to implement the code to retrieve the variables k and the private key and sign a new message. Please see the ecdsa_ps3.py which is the python script I wrote for this. During writing it I realised that the verification algorithm used by the ecdsa library must have some functions that extract the G we need from the public key. I used these functions to implement my own function get_G. To retrieve k and and the private key i wrote the functions get_k and get_privkey. These functions implement the ideas conveyed in the slides. For clarity I wrote an improved version of the mathematical derivations in latex.



Here the last implications in both derivations are true because in the ECDSA algorithm pk and k are chosen to be between 1 and n - 1. Let’s turn to the main code:

 
PUBLIC_KEY = """
-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEgTxPtDMGS8oOT3h6fLvYyUGq/BWeKiCB
sQPyD0+2vybIT/Xdl6hOqQd74zr4U2dkj+2q6+vwQ4DCB1X7HsFZ5JczfkO7HCdY
I7sGDvd9eUias/xPdSIL3gMbs26b0Ww0
-----END PUBLIC KEY-----
"""
G = get_G(PUBLIC_KEY)

hash1 = int(hashlib.sha256('time').hexdigest(),16)
hash2 = int(hashlib.sha256('help').hexdigest(),16)
hash3 = int(hashlib.sha256('read flag.txt').hexdigest(),16)
sig1 ="c0e1fc4e3858ac6334cc8798fdec40790d7ad361ffc691c26f2902c41f2b7c2fd1ca916de687858953a6405423fe156c0cbebcec222f83dc9dd5b0d4d8e698a08ddecb79e6c3b35fc2caaa4543d58a45603639647364983301565728b504015d"
sig2 ="c0e1fc4e3858ac6334cc8798fdec40790d7ad361ffc691c26f2902c41f2b7c2fd1ca916de687858953a6405423fe156cfd7287caf75247c9a32e52ab8260e7ff1e46e55594aea88731bee163035f9ee31f2c2965ac7b2cdfca6100d10ba23826"
r1,s1 =  sigdecode_string(unhexlify(sig1), G.order())
r2,s2 =  sigdecode_string(unhexlify(sig2), G.order())

k = get_k(s1, s2, hash1, hash2, G)
private_key = get_privkey(k, s1, hash1, r1, G)
r3, s3 =  sign(hash3, k, private_key, G)

print 'read flag.txt:' + hexlify(sigencode_string(r3, s3, G.order()))

First we extract G from the public key. Then we create the three hashes, extract the r,s pairs from the signatures of the two sent messages and use these to retrieve the k and private_k variables. Along with the message we want to send and G these are then passed as arguments to the sign function. This function is simply a modified version of the sign function in ecdsa.py that allows G and k be passed as arguments. The r, s pair then gets encoded into a byte string, hexlified and appended to our message to conform to the format the server expects. Finally we open up a terminal and type nc contract.vuln.icec.tf 6002 after which we paste the generated and signed message and hit enter. The server responds with: IceCTF{a_f0rged_signatur3_is_as_g00d_as_a_real_1} as expected.