Undo 5/9 -- BSidesLisbon 2017
Description:
We are given files snip.php, readme.txt, and readme.txt.fsociety. The goal is to decript the message in readme.txt.fsociety
knowing that it uses AES with a randomly generated key.
flag format: flag{...}
Solution:
(by L0rdC0mm4nd3r)
The given snip.php
file is fairly small and simple to analyse.
function destroy_file($fp,$len) {
$random = "";
for ($i = 0;$i < $len;$i++)
$random.= chr(mt_rand(0, 255));
fseek($fp, 0);
fwrite($fp, substr($random, 0, $len));
fclose($fp);
}
function gen_aes_key() {
$key = "";
for ($i = 0;$i < 16;$i++)
$key.= chr(mt_rand(0, 255));
return $key;
}
$crypted = fopen($file . ".fsociety", "w");
$fp = fopen($file, "r+");
$clear = fread($fp, 2048);
// destroy original file
destroy_file($fp,strlen($clear));
// generate unique key
$key = gen_aes_key();
$aes = new Crypt_AES(CRYPT_AES_MODE_ECB);
$aes->setKeyLength(128);
$aes->setKey($key);
// create encrypted file
$clear = $aes->encrypt($clear);
fwrite($crypted,$clear,strlen($clear));
-
The program opens file
readme.txt
to read andreadme.txt.fsociety
to write. -
It erases the
readme.txt
file that contains the original message using functiondestroy_file
that basically writes a random char over each position of the original message. -
It generates a 16-byte AES key.
-
It ciphers the original message with the generated key, and writes the output in
readme.txt.fsociety
.
So there is no padding, no oracle, AES is impossible to brute-force… What should we do? The question that immediately came to my mind was why do we have the readme.txt
file? Is there any known vulnerability on mt_rand()?
Search for mt_rand()
Immediately got
Caution This function does not generate cryptographically secure values, and should not be used for cryptographic purposes.
I immediately knew that I was on the right track. After a bit of searching I knew that it is possible to find out the used seed and a few tools for that can be found in the internet mt_rand attacks
.
After looking at a few of them I found php_mt_seed
that receives a sequence of random generated numbers with mt_rand()
and returns all possible seeds. Longer inputs narrow down the number of outcomes.
The attack was the following
- Read
readme.txt
to get the firstn
generated random numbers.
input_to_derandomizer = ''
with open('readme.txt', 'r') as f:
x = f.readline()
for char in x:
input_to_derandomizer += str(ord(char)) + ' ' + str(ord(char)) + ' ' + '0 255 '
- Give these numbers to
php_mt_seed
to get all possible seeds. There was just one –844114388
./php_mt_seed 123 123 0 255 54 54 0 255 14 14 0 255 233 233 0 255 249 249 0 255 185 185 0 255 28 28 0 255 254 254 0 255 208 208 0 255 187 187 0 255 208 208 0 255 230 230 0 255 19 19 0 255 17 17 0 255 88 88 0 255 40 40 0 255 252 252 0 255 254 254 0 255 132 132 0 255 166 166 0 255 116 116 0 255 83 83 0 255 3 3 0 255 246 246 0 255 133 133 0 255 182 182 0 255 226 226 0 255 112 112 0 255 118 118 0 255 195 195 0 255 65 65 0 255 248 248 0 255 174 174 0 255 196 196 0 255 156 156 0 255 165 165 0 255
-
Fix the seed on
destroy_file
function withmt_srand(844114388)
and generate a new filereadme.txt.rand
. Ifreadme.txt.rand
andreadme.txt
are equal than we are on the right path. -
Generate 16 more random bytes using
gen_aes_key()
to get the used AES key. -
Use this key to decrypt
readme.txt.fsociety
.
BLOCK_SIZE=16
def encrypt(message, passphrase):
#IV = Random.new().read(BLOCK_SIZE)
aes = AES.new(passphrase, AES.MODE_ECB)
return aes.encrypt(message)
def decrypt(encrypted, passphrase):
#IV = Random.new().read(BLOCK_SIZE)
aes = AES.new(passphrase, AES.MODE_ECB)
return aes.decrypt(encrypted)
message=''
with open('readme.txt.fsociety', 'rb') as f:
message = f.read(1000)
with open('readme.txt.key', 'rb') as f:
key = f.readline().strip()
print len(message), repr(message)
print repr(decrypt(message, key))
- Done.
'flag{the_darkarmy_is_now_on_to_you}\n\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c'
Since we were in a CTF everything that works is suitable to solve the challenge so I used a mix of python and php and whatever worked out.