These are writeups for BSides Brisbane, which happened on July 4th! I hoped to get this out earlier, but I was flying out of the country right after the conference, and then life happened.
This was my first time on the other end of a CTF, making challenges instead of solving them. I started to think about making these in 2025 after Crikeycon, as there were not many crypto challenges.
As small conference CTFs like this one tend to cater towards beginner players, I wanted to make some challenges that were related to what drew me to cryptography when I was first starting - a lot of real-world exploits on game consoles.
Starlet
On a quick read through of the code, the program gives you the SHA-256 hash of a flag in hex, asks you what the flag is, and only gives you the flag if the hash of the two flags match.
However, the function being used to compare the two hashes is strncmp. The man page for this function mentions that the function is equivalent to:
memcmp(s1, s2, MIN(MIN(strnlen(s1,n),strnlen(s2,n))+1, n))
If you have ever worked with C before, you know that a string contains the characters in the string, then a null byte to show the string is finished. Looking at the output of the program:
I will only show the flag to people who already know it!
The hash of the flag is: 816f0095a0d01aaf28541b830ac4ee0ef97d340e18c2611c6e60fe0ba8598df3
In C, this will be read as 0x81 0x6f 0x00… or a 2 character string! This means that instead of finding a full hash collision, you only need to find a collision of the first 3 bytes.
i = 0
while True:
false_flag = hex(i)[2:]
hash = sha256(false_flag.encode()).digest()
if hash[:3] == hash_bytes[:3]:
break
i += 1
print(false_flag)
Once you have a flag that matches the first 3 bytes of the hash, the program will happily send you the flag you definitely already know :)
Real-world inspiration
This is based of of Trucha, the or signing bug, in the Nintendo Wii! It would verify the hash used in the signature of a disc or program, but used strncmp instead of memcmp, the same as this program. This made it quite easy to fake a signature. The Wiibrew wiki has a good article about it here.
Playshell
The core of this challenge is a nonce-reuse attack for ECDSA. The code used to generate a new signature is:
def sign(message, key, k = randrange(int(curve.p()))):
hash = int.from_bytes(sha256(message.encode()).digest())
signature = key.sign(hash, k)
return (int(signature.r), int(signature.s))
Since the random value is generated in the function header, the same value is used every time. You can also notice this by getting the signature for several different commands - the \(r\) value will always be the same.
If you aren’t familiar with how ECDSA works - the signer takes \(G\), the generator part of the public key, and multiplies the point by \(k\). the \(x\) value of this point \(kG\) becomes the \(r\) value, and \(s\) is calculated as \(k^{-1}(z+rd)\), where \(d\) is the private key.
However, when two of the same keys are used, the \(r\) value will be the same, and when the value of \(k\) and \(x\) is the same for both messages, you can recover the \(k\) value used with a bit of rearranging. Using that, you can recover the private key.
After you have the private key, you can sign and register any command you want… for example, cat flag.txt :)
Real-world inspiration
This exploit was used to reveal the private key used to sign software on the Playstation 3! The PS3 used ECDSA internally to verify that software was legitimate and official - but they reused the same value of k, and in 2010 fail0verflow did a CCC talk on the vulnerability. You can watch the talk here or look at the slides here.
Tegra
Although the program doesn’t let you decrypt any text, the way that it replaces the key is vulnerable! When you provide a new key to encrypt things with, if the key is shorter than 16 bytes, then it keeps the rest of the old key:
def replace_key(old: bytes, new: bytes) -> bytes:
return (old[len(new):] + new).ljust(16, b"\x00")[:16]
Because of this, you can send a 1 byte key and keep 15 bytes of the old key, a 2 byte key and keep 14 bytes… If you send something to encrypt before you overwrite the key each time, once you replace all but one of the bytes, you can try to find what the last byte is by brute forcing that byte until you get a key that decrypts your text.
Repeat this process with the unknown byte for every key, and once you’re finished, you can recover the full key - with a computational complexity of \(2^{12}\), instead of having to brute force the entire 256 bits.
Real-world Inspiration
When writing to the security engine of the Tegra X1 on the Switch, the AES keytable is written to 4 bytes at a time and immediately flushed. This has been used in the past to recover keys from the console. There is a little more information about it on the SwitchBrew wiki, and a paper about the attack here.
Final notes
BSides Brisbane had an AI category and a non-AI category, so my main goal in making these challenges is to get at least one person to try a challenge and become excited about cryptography. This was a massive success - I had a few people come tell me they thought my challenges were cool after the CTF, and one of them has been doing more crypto after that.
As long as people keep learning from my challenges and having fun solving them, I’ll keep writing them - that’s the whole point of CTFs to me :)