NoName Writeup

Written on March 29, 2020

NoName was a task in the VolgaCTF 2020 Qualifier

I have Noname; I am but two days old.

And included where two files, encryptor.py and encrypted.

encrypted contained a base64 encoded string(which we’ll come back to later), which was created with the included encryptor.py program:

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 1585531265.
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 1. 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 VolgaCTF{5om3tim3s_8rutf0rc3_i5_th3_345iest_w4y}.

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