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):
self._set_key(os.urandom(16))
self.println(self._encrypt(FLAG).encode("hex").strip())
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 getkey 00000000000000000000000000000000 encrypt deadbeefdeadbeefdeadbeefdeadbeef f3321c62ed192c3f56618d0d4b3869f7 getkey b4ef5bcb3e92e21123e951cf6f8f188e
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.)