NoName was a task in the VolgaCTF 2020 Qualifier
I have Noname; I am but two days old.
And included where two files,
encrypted contained a base64 encoded string(which we’ll come back to later), which was created with the included
from Crypto.Cipher import AES from secret import flag import time from hashlib import md5 key = md5(str(int(time.time()))).digest() padding = 16 - len(flag) % 16 aes = AES.new(key, AES.MODE_ECB) outData = aes.encrypt(flag + padding * hex(padding)[2:].decode('hex')) print outData.encode('base64')
Now let’s analyze this code and highlight what it’s doing. Let’s first look at our key.
key = md5(str(int(time.time()))).digest()
The important thing to take away here, it that we’re using
int(time.time()) to generate our md5.
This means we’re using the epoch timestamp of right now, which looks something like this
This key is then used 2 lines later to create our aes variable. At this point we already had a good idea of how to create our key.
Now let’s look at the next line.
padding = 16 - len(flag) % 16
This line basically takes the length of the flag, which we don’t know, modulo 16, and whatever is the result of that is being deducted from 16. Our first thought was, that our padding could be anything between 1 through 16. But for this to make sense you have to take another line into consideration.
outData = aes.encrypt(flag + padding * hex(padding)[2:].decode('hex')).
This is where things get interesting.
hex(padding)[2:] takes our padding, which we just said could be between 1-16, converts it into hex, and then cuts off the first 2 characters.
This would mean that
hex(1) would result in
0x1, then with the first two characters cut off this would be just
Fair enough, but where’s the issue?
The thing is,
.digest('hex') expects 2 characters from us. You see where this is going now? No number between 1-15 will result in anything larger than 2 characters if converted to hex in this way.
Thus our padding must be 16, which is the case when the length of our flag is a multiple of 16.
With this knowledge we were able to reconstruct the procedure in a bottom up order, starting by decoding the given string with
.decode('base64'), which was the result of the
aes.encrypt(flag + padding * hex(padding)[2:].decode('hex')) call.
Because we knew that the parameters suffix had to be
(16*hex(16)[2:].decode('hex')), we were able to brute force the key used to create the AES cipher.
This key was generated based on the time (as described above). Additionally, we got the information that the challenge is 2 days old, so our initial time guess was 2 days before the CTF started (00:00, 25mar2020) and we generated every key up to the competition started. After decrypting the string with all these keys we just had to check if the last 16 characters of any decoded string was equal to
(16*hex(16)[2:].decode('hex')). This had to be true for the flag so we collected all the candidates and looked for a possible solution amongst them.
The answer was
Our Python code to solve this challenge:
from Crypto.Cipher import AES import time from hashlib import md5 #The string given in the "encrypted" file b64="uzF9t5fs3BC5MfPGe346gXrDmTIGGAIXJS88mZntUWoMn5fKYCxcVLmNjqwwHc2sCO3eFGGXY3cswMnO7OZXOw==" b64dec = b64.decode('base64') #Since we base64 encode the string at the end of encryptor.py #setting the initial timestamp to 2020-03-25, 2 days before the event started timestamp = 1585094400 #since we're "searching" for this padding, creative name, ik searchPadding = 16 * hex(16)[2:].decode('hex') while(1): key = md5(str(int(timestamp))).digest() aes = AES.new(key, AES.MODE_ECB) inData = aes.decrypt(b64dec) padding = inData[-16:] #getting the last 16 characters of our decrypted string if padding == searchPadding: print("flag: " + str(inData) + " timestamp: " + str(timestamp)) timestamp+=1 if timestamp > 1585328400: #few hours after the start of the competition break