c0r3dump CTF writeups

CTF writeups by c0r3dump.

View on GitHub

SpamAndFlags 2020 - OTS

Challenge

That’s right, we have not one, not three, but TWO projects focused on post quantum cryptography. Our newest product will surely make a killing. Unlike measily RSA, we are dead sure you can’t break this one, not even with your fancy quantum computers.

More info on: nc 34.89.64.81 1337

105 points

Solution

When connecting to the server we receive a message: My favorite number is 3417798650350847801. (the number is different each time we connect), its signature, the code used for signing, and the public key.

class OTS:
    def __init__(self):
        self.key_len = 128
        self.priv_key = secrets.token_bytes(16*self.key_len)
        self.pub_key = b''.join([self.hash_iter(self.priv_key[16*i:16*(i+1)], 255) for i in range(self.key_len)]).hex()
        self.pub_key = pub_key

    def hash_iter(self, msg, n):
        assert len(msg) == 16
        assert(n >= 0)
        print('iter', n)
        for i in range(n):
            msg = hashlib.md5(msg).digest()
        return msg

    def wrap(self, msg):
        raw = msg.encode('utf-8')
        assert len(raw) <= self.key_len - 16
        raw = raw + b'\x00'*(self.key_len - 16 - len(raw))
        raw = raw + hashlib.md5(raw).digest()
        return raw

    def sign(self, msg):
        raw = self.wrap(msg)
        signature = b''.join([self.hash_iter(self.priv_key[16*i:16*(i+1)], 255-raw[i]) for i in range(len(raw))]).hex()
        self.verify(msg, signature)
        return signature

    def verify(self, msg, signature):
        raw = self.wrap(msg)
        signature = bytes.fromhex(signature)
        assert len(signature) == self.key_len * 16
        calc_pub_key = b''.join([self.hash_iter(signature[16*i:16*(i+1)], raw[i]) for i in range(len(raw))]).hex()
        assert hmac.compare_digest(self.pub_key, calc_pub_key)

The signing is a simple hash iterator. If our message’s ith byte is n, then the signature’s [i*16,(i+1)*16) bytes will be the result of 255-n times md5 hashing the private key’s [i*16,(i+1)*16) bytes. The only twist is the message we send will be padded to 112 bytes using zeroes and concatenated with the md5 hash of this byte array.

The weakness of an iterator like this, is that if we know the signature of byte n at the ith position then we know the signature of every k <= n byte at the ith position, since sign(k) = hash_iter(private[i*16,(i+1)*16], 255-k) = hash_iter(hash_iter(private[i*16,(i+1)*16], 255-n), n - k). So without the md5 hash at the end, all we would have to do is to find 4 bytes in an interval that are bigger then bytes in flag, replace them with flag and iterate the corresponding parts of the signature.

The “twist” is that our md5 hash has to be smaller at every byte than the md5 hash of the signed message. This can be done by trying many different messages: We lower arbitrary bytes of the message except the flag bytes until we find a hash that is suitable. With 16 bytes the probability of an md5 hash being lower at every byte than the other rarely goes below 1e-10, but we can easily fish for messages from the server where this probability is 1e-7 (the bytes of the signed messages hash are big).

Basically:

The flag is SaF{better_stick_with_WOTS+}.

Files

Other write-ups