31C3 CTF: Crypto 10 "hwaes" writeup

This was a nice little crypto challenge related to AES key expansion. The description says

We implemented aes in hardware and saved a lot of memory. Feel free to use our online aes encryption service to secure your data.

Additionally, we were provided with a Python server script and an IP/port pair. The server provides the following options:

self.commands = {"help": (self.help, "show this help"),
                "setkey": (self.set_key, "Set AES key"),
                "getkey": (self.get_key, "Set AES key"),
                "encrypt": (self.encrypt, "Encrypt with AES"),
                "flag": (self.flag, "Encrypt flag"),

Besides that, the script mainly consists of interface code to communicate with an AES hardware module, essentially passing the [gs]etkey and encrypt commands to the module and echoing back the results to the socket.

Of course, our attention immediately shifted to the flag command, which does

def flag(self, paran):

This command encrypts the flag with a pseudorandom key and prints the ciphertext. For obtaining the key, we tried the getkey command, but after some (unsuccessful) attempts to decrypt the flag, we noticed the following:

Welcome to the online AES encryption service
encrypt deadbeefdeadbeefdeadbeefdeadbeef

After encryption, the key is changed! So the key that getkey after a flag outputs is actually different from the original random key. Since all relevant operations are hidden in the hardware module, there seemed to be no way to figure out what happened, except… when pasting b4ef5bcb3e92e21123e951cf6f8f188e (the new key that results from the all-zero key!) into Google, one finds this page, providing test vectors for AES key expansion. Therefore (and by re-reading the challenge description talking about memory usage), one could guess that getkey leaks the last round key used in the encryption.

It is well-known that AES key expansion is a simple, invertible operation. We used the aes128_key_schedule_inv_round function from this file (thanks!) to actually perform the inversion and obtain the original key. For convenience, we then used Python to decrypt the flag:

$ ./pwn
last round key: 941e0a5f628be029f9496b4d1a430155
original key: 323ae8281ce4492246c63d968011bfd3
$ python2
>>> from Crypto.Cipher import AES
>>> aes = AES.new('323ae8281ce4492246c63d968011bfd3'.decode('hex'), AES.MODE_ECB)
>>> aes.decrypt('aaad9c5376ee2f20175cbec0a91b47d3e5956f2948468fe9deb61564642018f6320b63df16502fcd408ac7cdb8a78751'.decode('hex'))
>> 'The flag is 31C3_0748a7b8be603056aa9c391e !\n\x00\x00\x00\x00'

(As a side note: It is not clear to me how encrypting multiple blocks in sequence without re-initializing the key register results in ECB mode. It should rather have led to each block’s key being the last round key from the previous block, but ECB mode using a single key worked just fine to decrypt the flag. I assume this is due to some hacks from the CTF organizers to avoid having to use real hardware AES modules.)