martes, 14 de noviembre de 2017

Undo Five/Nine (Crypto 300, Lisbon CTF)

La semana pasada tuve la oportunidad de participar en la parte presencial de CTF de las Bsides de Lisboa. Hice equipo con algunos compañeros e intentamos resolver algunas de las pruebas.

Uno de los retos en los que estuve trabajando fue "Crypto 300: Undo Five/Nine". No anoté el enunciado del reto, pero basicamente te proporcionaban un trozo de código PHP "snip.php" y otros dos ficheros: "readme.txt" y "readme.txt.fsociety".

Un vistazo rápido a "snip.php" nos ayuda a entender como los otros dos ficheros son utilizados o generados:

$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));

Como podemos ver, este código PHP lee un texto de "readme.txt" y posteriormente destruye el fichero de algún modo. Seguidamente una clave de cifrado es generada y el texto es cifrado usando AES-128 en modo ECB. El texto cifrado es guardado en el fichero "readme.txt.fsociety".

Por lo que parece, deberíamos ser capaces de recuperar el texto original de algún modo. Como la clave no es almacenada, es evidente que nos encontramos con algún tipo de debilidad en la generación de la clave. Echemos un vistazo:

function gen_aes_key() {
 $key = "";
 for ($i = 0;$i < 16;$i++)
   $key.= chr(mt_rand(0, 255));
 return $key;
}

Bueno, esto encaja con lo que pensabamos. La función "mt_rand" genera un valor aleatorio empleando el generador de números "Mersenne Twister". Esta función, tal y como su documentación avisa, no es criptograficamente segura. Después de "googlear" un poco, encontré más información sobre esta deficiencia, de donde se puede leer la siguiente información:

"Common misuses of mt_rand() include generation of anti-CSRF tokens, custom session tokens (not relying on PHP's builtin sessions support, which uses a different PRNG yet was also vulnerable until recently), password reset tokens, passwords, database backup filenames, etc. If one of these items is exposed and another is generated later without the web application or server reseeding the PRNG, then an attack is possible where the seed is cracked from the item generated earlier and is then used to infer the unknown item generated later."

Por lo que dice, según parece debería haber al menos otra llamada a "mt_rand" y deberíamos ser capaces de averiguar su resultado para ser capaces de explotar esta vulnerabilidad. Peguémosle un vistazo al código que define como se destruye el fichero original:

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);
}

¡Bingo! El fichero es sobreescrito con valores "aleatorios" generados con la misma función, lo cual quiere decir que si somos capaces de obtener la semilla, podríamos regenerar toda la secuencia de números a partir de esta, y obtener la clave de cifrado.

¡Vamos a ello! Estuve leyendo la documentación del seed cracker, pero no resultó tan fácil como inicialmente pensé. Esta herramienta tiene varios modos de operación y no resulta obvio elegir los parámetros que necesitas. Finalmente, entendí que debía construir el comando de la siguiente forma:

./php_mt_seed [first_num] [first_num] 0 255 [second_num] [second_num] 0 255 ...

Pero como tenía unos cuantos bytes (los del fichero "readme.txt"), decidí generar los parámetros empleando algunas lineas de código PHP:

$fp = fopen("readme.txt", "r+");
$clear = str_split( fread($fp, 2048) );
foreach ($clear as $v) {
    echo ord($v) . ' ' . ord($v) . " 0 255 ";
}

Este código generó los parámetros que necesitaba, con los que lancé el seed cracker. Pasados un minuto o dos, obtuve la respuesta: "844114388". Ahora necesitamos generar toda la secuencia a partir de esta semilla. Para ello podemos usar de nuevo unas pocas lineas de código PHP:

mt_srand(844114388);
for ($i = 0;$i < 64;$i++)
    echo chr(mt_rand(0, 255));

Cuando generamos esta secuencia, podemos observar que los primeros valores coinciden con los valores contenidos en "readme.txt". Después de esos bytes podemos observar la clave de cifrado (16 bytes).

$ php gen.php  | xxd
00000000: 7b36 0ee9 f9b9 1cfe d0bb d0e6 1311 5828  {6............X(
00000010: fcfe 84a6 7453 03f6 85b6 e270 76c3 41f8  ....tS.....pv.A.
00000020: aec4 9ca5 f658 dda4 20f2 1c9f 5d14 b5b1  .....X.. ...]...
00000030: beb5 1669 3135 31f9 30bc 9438 d0ac d0d6  ...i151.0..8....

Ya solo nos queda descifrar el fichero cifrado:

$ openssl enc -aes-128-ecb -d -K "f658dda420f21c9f5d14b5b1beb51669" -in readme.txt.fsociety
flag{the_darkarmy_is_now_on_to_you}

¡Bang! ¡Conseguido! Desafortunadamente no fui capaz de puntuar con el flag ¿Por qué? Por dos motivos. El primero porque, por alguna razón, tenía en la cabeza que "snip.php" estaba generando enteros y luego truncandolos a byte, así que estuve cerca de una hora intentando entender el código y modificarlo para ajustarlo a esta circunstancia. El segundo porque el código que estaba escribiendo contra-reloj para descifrar el mensaje me dio un "syntax error" 10 segundos antes de que se acabara el tiempo del CTF, así que ya no pude arreglarlo a tiempo. En cualquier caso, me lo pasé muy bien en el CTF :)

No hay comentarios :