HackIt! 2014 _ Level 1

24 de julio, a alguna hora de la noche cercana a las 22:00. Mis compañeros de DL me indican que marcan ha dado comienzo al HackIt de este año. Esta vez no podré acudir a la presentación y me tendré que aguantar hasta el viernes 25 a eso de las 18:00, así que estaré un día ayudando como pueda a través del móvil :-O Cuando el 25 llego por fin a mi puesto, abro el portátil y me sumerjo en el reto… del que no me despegaré hasta el domingo 27 a las 14:00 de la tarde… o para ser más exactos, a las 14:01, como podréis comprobar en los siguientes posts.

A decir verdad, los retos comenzaron mucho antes… en la lista de correo del HackIt, donde, como todos los años marcan nos pidió colaboración en la elaboración de las pruebas. Llevaba dándole vueltas a la idea de crear un nivel donde hiciera falta explotar la vulnerabilidad HeartBleed, y este año, a diferencia de otros, conseguí pasar de la idea a la realidad 🙂 Como de costumbre, marcan hizo que esa realidad fuera aún más enrevesada (¿a quién demonios se le ocurrió si no, desplegar el level en una máquina con arquitectura Big Endian?).

Coincidimos con la gente de amn3s1a, w0pr, NavarParty y TimoSoft y nos «peleamos» por el segundo y tercer puesto (el primero, it goes without saying, está reservado para w0pr, juegan en otra liga). También estuvo Willix Team, aunque no pudimos hablar con ellos pues no les vimos cerca de la Raspberry Pi (enseguida sabréis la razón de que aparezca aquí este dispositivo).

En fin, buenos recuerdos, muchas cosas que aprendimos por el camino y muchas más cosas que apunté para profundizar y aprender hasta el HackIt de 2015 😉 En esta serie de posts, siguiendo la recomendación del propio admin de la prueba, pasaré a comentar nuestra forma de superar algunos de los niveles. No todos… la prueba de reversing en ARM se nos atragantó (aquí agradecería que la gente de W0pr – o tal vez NavarParty, no recuerdo bien si consiguieron superarla – nos iluminara con un write-up); y con respecto a la última, la «marcanada del año» (sic), llegamos sobre la campana (un minuto más allá) y no pudimos ni intentarlo. También se agradecería un write-up, especialmente a @abeaumont, del que sabemos que estuvo muuuuchas horas pegado a la pantalla hasta solucionarlo 😉

El primer nivel, Lienzo Digital, empieza diciendo que podrás superarlo rápidamente, a no ser que uses Internet Explorer. Analizando el código fuente, nos encontramos con un script en JS, que tras pasarlo por un beautifier, nos dice algo como:

$(document).ready(function() {
    var a = new Image();
    a.addEventListener("load", function() {
        $("#password").keyup(function(b) {
            var c = $("#password").val();
            if (27 != c.length) {
                $("#password").css({
                    "background-color": "#f88"
                });
                return;
            }
            var d = 0;
            var e = document.createElement("canvas");
            e.width = 9;
            e.height = 1;
            var f = e.getContext("2d");
            f.globalCompositeOperation = "difference";
            var g = f.createImageData(9, 1);
            for (var h = 0; h < 36; h++) g.data[h] = 255;
            for (var h = 0; h < c.length; h++) g.data[h + Math.floor(h / 3)] = Math.min(c.charCodeAt(h), 255);
            f.putImageData(g, 0, 0);
            for (var h = 0; h < 9; h++) f.drawImage(a, 0, -h);
            var g = f.getImageData(0, 0, 9, 1);
            for (var h = 0; h < 27; h++) d |= g.data[h + Math.floor(h / 3)];
            $("#password").css({
                "background-color": d ? "#f88" : "#8f8"
            });
        });
    });
    a.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAIAAABv85FHAAABB0lEQVR4nAH8AAP/APXX7L74zhsb6Bni6gjRu84PCwPnFRrD/iQJEADp+fop4w7/ClXcP1SiE/cmvMOp+MHm/T7ppegA1O7CJP/IGQvj+xTHyPU3uRbJ6gA34E3f4dNfAOsR5vkP0TXLIO7E3uYm1Auzux4bx+fsD9bN9QD3IbM6yxDkNLjgussk5SUKCxXK1dMIHKPVMf8A2REc1MHrOtrT962///YfuB++E8fdDzAQoanoAOKODsDtBa31xuj6IizvxQYV3AIv4+wkxgy1vgAJFqLRFxD7HQCh7MwE3Aq0qwzg6yr69d28AtUBnV1+0/HzLiLX19o25jfnBP/hIDkMDBUX3YAUC0mGddt48B4AAAAASUVORK5CYII=";
});

Lo primero que vemos es que la longitud del pass debe ser 27 (en otro caso, el css que se aplica es de color rojizo #f88). Lo siguiente es que el level crea un elemento canvas (lienzo digital…) con el que empieza a jugar. Dentro del canvas (‘f’) tenemos representado un array de 9×1 pixels en una variable de nombre ‘g’. Cada pixel, a su vez, viene representado en RGBA (por tanto, con 4 bytes por pixel, necesitamos 36 bytes para gestionar dicho array). Por otro lado, en la variable de nombre ‘a’ tenemos una extraña imagen usando un Data URI Scheme (RFC 2397). Si copias y pegas esa imagen como una URL normal en el navegador, podrás visualizarla. La idea para pasar este nivel es que con los 27 caracteres del password (su código ASCII) rellenamos los datos del array g. ¿Por qué 27 y no 36? Porque en el bucle vemos que cada 3 bytes, nos saltamos uno (g.data[h + Math.floor(h/3)] = código ascii).

A continuación hay tres líneas que vuelcan el valor del array g (construido con los valores ASCII del pass, recuerda) en el canvas ‘f’ y calculan una diferencia de ‘f’ con ‘a’ (el f.globalCompositeOperation = ‘difference’ entra en juego ahora), dejando el resultado en ‘g’.

El último «meneo» aplica un OR a los datos de ‘g’ y deja el resultado en ‘d’.

d = 0;
for (var h = 0; h < 27; h++) d |= g.data[h + Math.floor(h / 3)];

El objetivo es conseguir satisfacer que ‘d’ valga 0 al salir del bucle. Si lo conseguimos (para ello, todos los datos de g.data deben ser 0), el valor del background del campo pass será verde: («background-color»: d ? «#f88» : «#8f8»), y por tanto, sabremos que el password es correcto.

Abramos la consola de Chrome y comencemos a jugar. Pegamos el siguiente trozo de código:

var a = new Image();
a.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAIAAABv85FHAAABB0lEQVR4nAH8AAP/APXX7L74zhsb6Bni6gjRu84PCwPnFRrD/iQJEADp+fop4w7/ClXcP1SiE/cmvMOp+MHm/T7ppegA1O7CJP/IGQvj+xTHyPU3uRbJ6gA34E3f4dNfAOsR5vkP0TXLIO7E3uYm1Auzux4bx+fsD9bN9QD3IbM6yxDkNLjgussk5SUKCxXK1dMIHKPVMf8A2REc1MHrOtrT962///YfuB++E8fdDzAQoanoAOKODsDtBa31xuj6IizvxQYV3AIv4+wkxgy1vgAJFqLRFxD7HQCh7MwE3Aq0qwzg6yr69d28AtUBnV1+0/HzLiLX19o25jfnBP/hIDkMDBUX3YAUC0mGddt48B4AAAAASUVORK5CYII=";
var d = 0;
var e = document.createElement("canvas");
e.width = 9;
e.height = 1;
var f = e.getContext("2d");
f.globalCompositeOperation = "difference";
var g = f.createImageData(9, 1);
for (var h = 0; h < 36; h++) g.data[h] = 255;

Inspeccionando un poco el entorno, vemos lo siguiente:

// creamos un pass de 27 'a's
> var c = Array(27).join("a")
> g.data
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,255, 255, 255, 255, 255, 255, 255, 255, 255]
> for (var h = 0; h < c.length; h++) g.data[h + Math.floor(h / 3)] = Math.min(c.charCodeAt(h), 255);
> f.putImageData(g, 0, 0);
> f.getImageData(0,0,9,1).data
[97, 97, 97, 255, 97, 97, 97, 255, 97, 97, 97, 255, 97, 97, 97, 255, 97, 97, 97, 255, 97, 97, 97, 255, 97, 97, 97, 255, 97, 97, 97, 255, 97, 97, 255, 255]

¿Y qué datos tenemos en la imagen misteriosa?

var b = new Image()
var myCanvas = document.createElement("canvas")
var ctx = myCanvas.getContext("2d")
 b.onload = function(){ ctx.drawImage(b,0,0); } ; b.src = a.src;
 // primera fila de la imagen misteriosa
 
 ctx.getImageData(0,0,9,1).data
[245, 215, 236, 255, 190, 248, 206, 255, 27, 27, 232, 255, 25, 226, 234, 255, 8, 209, 187, 255, 206, 15, 11, 255, 3, 231, 21, 255, 26, 195, 254, 255, 36, 9, 16, 255]
 // segunda fila de la imagen misteriosa
 ctx.getImageData(0,1,9,1).data
[233, 249, 250, 255, 41, 227, 14, 255, 255, 10, 85, 255, 220, 63, 84, 255, 162, 19, 247, 255, 38, 188, 195, 255, 169, 248, 193, 255, 230, 253, 62, 255, 233, 165, 232, 255]

OK! Ahora sólo nos queda entender esto:

for (var h = 0; h < 9; h++) f.drawImage(a, 0, -h);

La primera iteración del bucle es sencilla: f.drawImage(a,0,0) . Lo que estamos haciendo es copiar en el canvas (desde la posición 0,0 del canvas) los bytes del array ‘a’. Pero como hemos definido que el globalCompositeOperation del canvas es «difference», realmente lo que hacemos no es machacar lo que ya hubiera en el canvas (la ristra de 97, 97, 97…) sino que se calcula la diferencia. Es decir, estamos calculando esta operación: abs( [97, 97, 97, 255, …, 97, 97, 255, 255] – [245, 215, 236, 255, …, 36, 9, 16, 255]), obteniendo como resultado: [148, 118, 139, 255, …, 61, 88, 239, 255].

El modo difference es uno de los posibles modos «composite» del canvas HTML5. Podéis investigar al respecto en este ejemplo de CodePen.io.

Este es un ejemplo de composite «normal» (fíjate en las zonas de solapamiento en los colores magenta, cyan y amarillo)

normal

y este otro un ejemplo de composite «difference».

difference

La segunda iteración del bucle tiene una pequeña complejidad: f.drawImage(a,0,-1) . ¿Qué es ese -1? Que la imagen misteriosa se solapa sobre el canvas, pero saltando una fila, es decir, estaremos haciendo:
abs ([148, 118, 139, 255, …, 61, 88, 239, 255] – [233, 249, 250, 255, …, 233, 165, 232, 255]) obteniendo [85, 131, 111, 255, …, 172, 77, 7, 255]

Y seguimos igual para f.drawImage(a,0,-2) … f.drawImage(a,0,-8). Recordemos que en la última iteración debemos obtener [0,0,0,255,0,0,0,255,…,0,0,0,255]. Así que, deshaciendo el entuerto tenemos que para la primera letra del pass hay que resolver:

|||||||245 (pos 0,0 de la imagen) – Letra del pass| – 233 (pos 0,1 de la imagen)| – …. – pos(0,8 de la imagen)| = 0

Es decir:

Math.abs(Math.abs(Math.abs(Math.abs(Math.abs(Math.abs(Math.abs(Math.abs(Math.abs(x – 245) – 233) – 212) – 235) – 247) – 217) – 226) – 9) – 157) = 0

Para la segunda letra del pass, de forma equivalente, hay que resolver:

|||||||215 (pos 1,0 de la imagen) – Letra del pass| – 249 (pos 1,1 de la imagen)| – …. – pos(1,8 de la imagen)| = 0

etc.

Resolviendo la ecuación para todas las letras del pass:

// preparar la imagen en el contexto del canvas
var b = new Image()
var myCanvas = document.createElement("canvas")
var ctx = myCanvas.getContext("2d")
b.onload = function(){ ctx.drawImage(b,0,0); } ; 
b.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAIAAABv85FHAAABB0lEQVR4nAH8AAP/APXX7L74zhsb6Bni6gjRu84PCwPnFRrD/iQJEADp+fop4w7/ClXcP1SiE/cmvMOp+MHm/T7ppegA1O7CJP/IGQvj+xTHyPU3uRbJ6gA34E3f4dNfAOsR5vkP0TXLIO7E3uYm1Auzux4bx+fsD9bN9QD3IbM6yxDkNLjgussk5SUKCxXK1dMIHKPVMf8A2REc1MHrOtrT962///YfuB++E8fdDzAQoanoAOKODsDtBa31xuj6IizvxQYV3AIv4+wkxgy1vgAJFqLRFxD7HQCh7MwE3Aq0qwzg6yr69d28AtUBnV1+0/HzLiLX19o25jfnBP/hIDkMDBUX3YAUC0mGddt48B4AAAAASUVORK5CYII=";
 
// resolver la ecuación
pass = ''
for (ind = 0; ind < 36; ind++) {
  for (letra=32;letra<127;letra++) {
   c = letra; 
   for(var i=0; i < 9; i++) {
       c = Math.abs(ctx.getImageData(0,i,9,1).data[ind] - c)
   } 
   if(c == 0)
      pass += String.fromCharCode(letra);
   }
}
console.log(pass)

Obtenemos lo que buscábamos 😉

Parchear Ubuntu para evitar el bug OpenSSL Heartbleed

Ayer a la tarde se levantó una buena polvareda con el bug Heartbleed de OpenSSL. Básicamente, cualquier usuario podría acceder a una zona de 64Kb de memoria del servidor, cercana a la zona donde se gestionan las operaciones con SSLv3. Esto podría llegar a permitir acceder a información privilegiada, como por ejemplo la clave privada usada para el cifrado. Aunque hay dudas técnicas sobre este aspecto específico (acceso a las claves privadas), los investigadores que descubrieron el bug así lo afirman (como indica el primer link). Conclusión: estás tardando en actualizar OpenSSL en tu servidor 🙂

sudo apt-get update
sudo apt-get install -y libssl1.0.0 openssl
openssl version -a # y confirma que es la nueva versión ("built on" >= 2014-04-07)
sudo lsof -n | grep ssl | grep DEL  # reload de todos los servicios que usen versión vieja

Repite el último paso hasta que no queden servicios usando la versión vieja de openssl.

Gracias a coderanger por los tip.

Update (11/04/2014): como dije, hay serias dudas de que el acceso a las claves privadas sea viable. De hecho, uno de las primeras webs donde se expuso el bug lanza ahora un reto: han puesto un servidor HTTPS vulnerable en marcha y retan a la comunidad a extraer la clave privada usando el bug Heartbleed.

Update (13/04/2014): pues han tardado unas pocas horas en extraer la clave privada. Vaya, vaya…

HackIt! 2013. Level 8. RPN (y II)

Warning: si no has intentando entender el intérprete RPN antes, ni te molestes en leer este post, porque te sonará a chino. Lo dejo explicado aquí para aquellos que se hayan pegado con este reto y no hayan obtenido la solución o estén totalmente atascados. Al resto de los mortales les puede explotar la cabeza (brainfuck!) si intentan comprender una mínima parte de todo lo que diga a continuación 🙂 Avisados quedáis.

Vamos a por ello, por partes. Para entender la primera sección del programa, le pondremos puntos de ruptura (bp=break point) allá donde creamos conveniente y ejecutaremos con:

./RPN -fprograma.txt

Ahora veremos que si ejecutamos ésto:

"aaaaaaaaaaaaaaaaa" 0Oo.oO0_ _ir]2;l[l6UmIvz3]S
bp 0ask ¿? -1neg [ 1 + @@ @ .] @@ 
}:-( 17k + [ @@ @ ¿? + 2 / .¿? 1 - @@ @ .]
¿? _d 0.6990432739 + - =>> 1zero 0one =>o) 

El programa se para al llegar a la instrucción bp, mostrándonos el contenido de la pila.

$ ./RPN -fretocado.txt 
Bpoint alcanzado
stack:97.0000000000 97.0000000000 97.0000000000 97.0000000000 97.0000000000 97.0000000000 97.0000000000 97.0000000000 97.0000000000 97.0000000000 97.0000000000 97.0000000000 97.0000000000 97.0000000000 97.0000000000 97.0000000000 97.0000000000 0.0000000000 105.0000000000 114.0000000000 93.0000000000 50.0000000000 59.0000000000 108.0000000000 91.0000000000 108.0000000000 54.0000000000 85.0000000000 109.0000000000 73.0000000000 118.0000000000 122.0000000000 51.0000000000 93.0000000000 83.0000000000 | 
37: 0ask |

Los 97 del principio son el código ASCII de la letra «a». Suelo repetir esa letra en este tipo de pruebas en el que quiero ver cómo «evoluciona» el código con la entrada que le paso. Luego viene un 0, que corresponde al comando: 0Oo.oO0_ del churro de código que nos pasan. Es decir, parece que el intérprete, cuando ve una ristra de números y letras, sin espacios, interpreta los primeros dígitos como un número, hasta encontrarse con el primer carácter no numérico. En este caso, 0Oo.oO0_ = 0.
El 105, 114, 93…83 es el código ASCII del literal: _ir]2;l[l6UmIvz3]S (sin contar el _ que marca comienzo de literal).

El 0ask, se convertirá en un 0 en la pila. Lo podremos ver pulsando INTRO (ejecutar paso a paso). La siguiente instrucción, ¿?, parece que hace un simple pop (aunque luego veremos que no, que lo que hace es traer de memoria lo último que hayamos memorizado con el comando .¿? . Parece que si no hubiéramos memorizado nada es cuando hace un simple pop…luego lo veremos mejor). -1neg, empila un -1. Y llegamos a la definición de bucle, que comienza por [ y termina con ]
Las instrucciones del bucle se ejecutarán en cada ciclo, hasta que se cumpla la condición de salida. ¿Cuál es dicha condición? Si al llegar al cierre del bucle «]», la cima de la pila tiene un 0, saldremos, si no, seguiremos ciclando.

Así que empezamos con un -1 en la pila y nos metemos en el bucle: [ 1 + @@ @ .]
Lo que hace es empilar el 1, y luego sumar (-1 + 1 = 0). A continuación, @@ duplica la cima (tendremos 0 0) Luego llega una instrucción que nos trajo de cabeza hasta que conseguimos entenderla: @. Esta instrucción hace algo como lo siguiente:

offset = pop();
push(pila[offset])

Así que en la primera vuelta, hacemos un push(pila[0]). Como en la posición 0 de la pila tenemos nuestra primera letra del posible pass, estamos haciendo push(97).

Como 97 != 0, seguimos ciclando (.] cicla haciendo antes pop). En la siguiente vuelta tenemos 0 + 1 = 1. Duplicamos el 1 con @@ (1 1) y la sentencia @ empila el segundo carácter del pass (push(pila[1])). Seguiremos haciendo ésto hasta recorrer todos los caracteres del posible password. El siguiente carácter es un 0 (el que venía de 0Oo.oO0_). Es decir, al abandonar el bucle tendremos en la cima de la pila la longitud del posible pass (tras las pruebas supimos que era 17 la longitud de dicho pass). Lo duplicamos con @@ (17 17) y luego llega el comando }:-( que desempila y guarda en memoria ese valor (17) . A continuación 17k = empilar un 17 (17 17). «+» sumar los dos valores de la cima de la pila. nos quedamos con 34. Luego, el autor del level nos volvió locos con esto: [ @@ @ ¿? + 2 / .¿? 1 – @@ @ .]
Realmente lo que está haciendo (con ese @) es capturar la posición 34 (que quedaba tras la suma de 17 y 17) y traerlo a la pila. ¿Qué hay en esa posición? Es el último carácter del literal que empilamos al principio: _ir]2;l[l6UmIvz3]S , es decir, la S (ASCII:83). Lo suma con lo que hubiera en memoria (el famoso comando ¿? Inicialmente parece que tenemos un 0 -> 83 + 0 = 83. A continuación lo divide entre 2 y lo memoriza con el comando .¿?, sacándolo de la pila (tenemos en memoria un 41.5. En la cima de la pila un 34). Restamos 1 (tenemos 33) y volvemos a repetir el proceso: push(pila[offset]) (con offset = 33 obtenemos el carácter «]» (ASCII:93) (del literal _ir]2;l[l6UmIvz3]S). Le sumamos el contenido de la memoria (el 41.5), dividimos entre 2 y guardamos el resultado, memorizándolo (67.25) y sacándolo de la pila. Seguimos así hasta la condición de salida (hasta alcanzar el 0 de 0Oo.oO0_ .

¿Qué tiene que dar? Pues si la longitud del posible pass es 17, al haberlo duplicado tenemos un 34 (lo que nos permite recorrer el literal _ir]2;l[l6UmIvz3]S hacia atrás, operando como he explicado y dando como resultado el valor 100.6990432739. Lo tendremos en memoria y lo recuperaremos con «¿?». Luego el código empila «_d» (un 100 en ASCII), al que le suma «0.6990432739» (tenemos 100.6990432739) y resta ambos valores. Lo dicho, si la longitud del pass era 17, ahora estaremos restando 100.6990432739 – 100.6990432739 lo que dará 0. Si así fuera, saltaremos a la etiqueta «>». Esto fue otro quebradero de cabeza. Las sentencias «=>XXXX» son JUMPS condicionales. Si el valor de la cima de la pila es 0, salta a XXXX. Si no, sigue el flujo en la siguiente instrucción. En nuestro caso es 0, por lo que saltaremos (salto =>> a la etiqueta :> ) Una etiqueta comienza con «:», de ahí que saltemos a :>. De aquí, :> , empilamos 17, restamos a la cima – (nos quedamos con 0) y saltamos a «=>>>» (es decir, a :>>)

Aquí no me extenderé más, pero éste trozo de código:

:>>
0 .;) [ ;) @ sin 10 ;) 7con + ** @@ 1ocho % - @@ +>gen neg :gen 79O % 48$ + ;) }:-( 1Oo. ++ @ -

Realmente es el quid de la cuestión. Analiza cada carácter del posible pass y le aplica esta fórmula:

abs[sin(x) * 10 * (offset + 7)] % 79 + 48

Donde X es la letra del pass que estamos analizando cada vez (offset es la distancia hasta esa letra. offset 0 para la primera, offset 1 para la segunda, etc.). Cada letra X del pass, tras pasar por esa fórmula, tiene que dar los valores:

105, 114, 93, 50…. 83, es decir, los valores ASCII del famoso literal del principio
(ir]2;l[l6UmIvz3]S)

Basta con resolver esa ecuación para cada X y fin de la prueba… bueno, casí, porque por ejemplo, para la primera letra del pass, abs[sin(x) ….] = 105 no tiene una única solución. De hecho, hay 3 posibles soluciones. Lo mismo ocurre para otras cuantas letras (fijaros en la foto que publiqué de la primera parte de la solución de este nivel). Lo que nos dejó probando varias soluciones hasta alcanzar la buena. Cuando la lees te das cuenta de que tiene sentido al leerla en hAx0r, pero … no fuimos los únicos, @navarparty también se divirtió probando un rato 🙂

Aunque parezca mentira, nos gustó este puzle mental 🙂 pero metimos horas por un tubo para resolverlo. Algún año estaría bien que los autores de los retos del año anterior nos contaran cómo demonios se les ocurrió, así como el proceso de creación que llevaron a cabo hasta obtener este tipo de levels de artesanía pura.

HackIt!2013. Level 8. Reverse Polish Notation (RPN)

IMG_20130727_033810Real Pesky Numbers (RPN), así se subtitula el reto 8. La noche del sábado conseguimos terminar esta prueba – la imagen de la izquierda, con parte de la solución, es una foto realizada ese mismo día  – tras varias horas intentando descifrar cómo demonios funcionaba la aplicación que escondía el mensaje…. o más bien, cómo funcionaba el intérprete RPN proporcionado ante este galimatías de código que lo acompaña:

"your password here" 0Oo.oO0_ _ir]2;l[l6UmIvz3]S
0ask ¿? -1neg [ 1 + @@ @ .] @@ 
}:-( 17k + [ @@ @ ¿? + 2 / .¿? 1 - @@ @ .]
¿? _d 0.6990432739 + - =>> 1zero 0one =>o) 
:> 17 - =>>>
1the 0p0pE 0g0ne 12crAzy
9 _fmnqV [ @@ =><= 1 + # 
:<= ] =>o) 
:>>
0 .;) [ ;) @ sin 10 ;) 7con + ** @@ 1ocho % - @@ +>gen neg 
:gen 79O % 48$ + ;) }:-( 1Oo. ++ @ - =>asin
1one 0zero @@ 14five 11two _ebC [ @@ =><) 1infinity - # 
:<) ] =>o) 
:asin ;) 1zero + .;) ;) 17l - .]
0zero @@ 15RPN 12skill _vjikT [ @@ =>oP 2NaN - # 
:oP ]
:o)

El intérprete en cuestión estaba alojado en una aplicación para MacOSX (archivo .dmg). Suerte que este año llevamos uno 😉 Para el lector que no disponga del sistema de la manzana, el autor del reto ( thEpOpE , gracias!) nos ha proporcionado el mismo intérprete RPN compilado para Linux. ¿Te atreves a deshacer el ovillo del código anterior?

HackIt!2013. Level 7. Old School Spectrum (y II)

Fuse_pokedAnalicemos el código. La línea 15 define la función módulo (no existente en el Basic del Spectrum). La línea 20 declara e inicializa ciertas variables que luego veremos. La línea 30 pide al usuario un password (Code). La línea 40 comprueba que la longitud del pass introducido sea al menos igual a 6 (las letras de «HackIt»). Si fuera menor, concatena el pass hasta que se cumpla la condición. En la línea 50 comienza la chicha. Iteraremos 6 veces. En cada iteración, la línea 55 del programa cambiará el contenido de la línea 60, sobre-escribiendo en memoria 3 posiciones (como si fuera el código de un virus polimórfico). ¿Cómo sabemos qué es lo que escriben esos POKE y dónde? Poniendo un STOP justo al comienzo de la línea 70 (ver figura de la izquierda).

10 REM PoliMorph
15 DEF FN m(a,b) = a- (INT (a/b)*b) : REM a Mod b
20 LET pos1=00053: LET pos2=00069: LET pos3=24000: LET x1=181: LET x2=42: LET x3=45
30 LET n$="HackIt": INPUT "Code:";c$: LET x$=""
40 IF LEN c$< LEN n$ THEN LET c$=c$+c$: GOTO 40 
50 FOR g=1 TO LEN n$ 
55 POKE pos1,x1: POKE pos2,x2: POKE pos3,x3 
60 LET cod = INT (98*ATN (g/ LEN n$)) + CODE c$(g)/0 + g 
70 LET x1=x1+1: IF x1>183 THEN LET x1=181
80 LET x2=x2+1: IF x2>47 THEN LET x2=42
90 LET x3=x3+2: IF x3>45 THEN LET x3=43
110 LET x$=x$ + CHR$ (32 + FN m(cod, 95))
120 NEXT g: PRINT "Decoded:";x$
130 IF n$ <> x$ THEN PRINT "Wrooonnggg!": FOR g=pos1-11 TO pos3+2: POKE g,48+ INT (RND*10): NEXT g: STOP
140 PRINT "Right!, here goes the rest..."

Vemos que tras la ejecución, la línea 60 ha cambiado:

Antes:
60 LET cod = INT (98*ATN (g/ LEN n$)) + CODE c$(g)/0 + g
Ahora, tras los POKE(pos_memoria, valor):
60 LET cod = INT (98*ASN (g/ LEN n$)) + CODE c$(g)*0 - g

¿Por qué? Las posiciones de memoria las calculó pacientemente el autor de la prueba (thEpOpE). ¿Y el valor? Podemos ayudarnos de esta tabla de mnemónicosPor ejemplo, el primer POKE(00053, 181) cambia la posición 00053 (donde teníamos un ATN -arcotangente-) por el operador ASN -arcoseno-, dado que al código 181 le corresponde el operador ASN según la tabla indicada. La división por 0 (ilegal), ha cambiado a multiplicación por 0 (legal) con POKE(00069, 42). El «+ g» ha cambiado a «- g» con el POKE(24000, 45). El 45 corresponde al operador «-«. Lo que no tengo claro es cómo demonios la dirección 24000 corresponde a esa sección del código (seguro que thEpOpE nos ilumina en los comentarios 🙂

Los valores 181, 42 y 45 corresponden a las variables x1, x2 y x3 definidas en la línea 20. En cada vuelta del bucle, x1, x2 y x3 cambiarán, por lo que los POKE modificarán la línea 60 en cada vuelta. La tabla enlazada nos ayudará a conocer qué hace en cada vuelta. Esa línea 60 va modificando cada letra de nuestro password (dejándola en la variable cod). En la línea 110 se calcula el carácter asociado al código ASCII de 32 + cod módulo 95. Esto nos dará un carácter imprimible entre ASCII(32) y ASCII (32+94). Ese carácter, en cada vuelta, debe coincidir con las letras del string «HackIt». ¡Un level muy trabajado!.

Algunos comentarios finales. Me ha gustado el efecto de bombardear la memoria y cambiar el código en caso de introducir un pass incorrecto (línea 130). También nos dejó ojipláticos al comienzo la división por cero de la línea 60. Cuando vimos los POKE de la 55 supusimos que corregiría ese bug intencionado. También nos costó encontrar un emulador que cargara el código en BASIC. No sé si fue un efecto buscado o no…

Finalmente, el level tenía un bug no intencionado. La clave final aparece en los strings del binario… nos dimos cuenta tras pasar horas y horas con la prueba, encontrar la solución de forma ortodoxa y recordar que ese string lo habíamos visto antes en algún sitio O:-)

Y con esto, llegamos al level 8, donde nos espera un interesante intérprete RPN del mismo autor…