Latest Tweets

Desclavando espinas 1/3: UAD360 SuperSafeWeb writeup (sin wasm2c o wasmdec)

El reto

Descargamos el archivo rev1.zip y obenemos 3 archivos: index.jsp, index.wasn y main.html. El archivo index.jsp es el envoltorio para poder ejecutar las llamadas a bajo nivel escritas en WASM del archivo index.wasm, y no tiene mayor interés. El archivo main.html es un simple formulario web con una caja de texto y un botón que nos insta a introducir una flag:

main.html muestra un simple formulario web.

Si miramos el código por debajo, vemos que hay una llamada de JavaScript a la función check_flag(). Si escribimos cualquier cosa, se hace una llamada a check_flag y, si la flag introducida no es la correcta, se nos devuelve el siguiente error:

Si la flag no es la correcta, mensaje de error.

Depurando con Chrome

La función check_flag() está implementada a bajo nivel en el archivo index.wasm. Para resolver el reto, utilizaremos las opciones de debugging de la consola web de Google Chrome y un poco de reversing. Con Google Chrome no se permite la apertura y ejecución de archivos WASM via file://, así que levantamos un pequeño servicio web con Python en el mismo directorio donde tengamos los 3 archivos descomprimidos:

python -m SimpleHTTPServer

Usando Google Chrome, abrimos la url: http://localhost:8000/main.html y nos aparece el formulario web. Abrimos las “Developer Tools” para poder depurar el código WASM. Antes que nada, tenemos que identificar la función check_flag() dentro de todo el código a bajo nivel para poder poner ahí algunos puntos de interrupción.

Localizando la función check_flag()

Descargamos y compilamos wabt . Convertimos el código binario a formato humano con:

wasm2wat index.wasm -o index.wat

Abrimos el archivo generado con el editor ASCII que más nos guste y buscamos la función check_flag():

(export “_check_flag” (func 19))

Vemos que la función check_flag, dentro del código WASM, está implementada como func 19. Ya sabemos dónde poner el primer punto de interrupción en el navegador, justo en la primera línea de la func 19:

Punto de interrupción en la función check_flag

Identificando los argumentos de check_flag

La función check_flag() recibe 2 argumentos de tipo integer 32 bits. Escribimos cualquier cosa en la caja de texto y pulsamos “Check flag!”. Automáticamente veremos claramente identificados en Chrome los dos valores de estos argumentos. El segundo parece ser la longitud de la cadena introducida; el primero podría ser la dirección dentro de la memoria donde se encuentra la cadena introducida. Eso se puede comprobar escribiendo diferentes cadenas con diferentes longitudes:

Argumentos de check_flag() para “hola”.

El primer argumento siempre tiene el mismo valor, y es la dirección dentro de la memoria dónde se almacena nuestra cadena. Podemos verlo navegando a dicha posición dentro de “globals/memory” en Chrome, tal y como se muestra a continuación (cada carácter ASCII se representa con su valor decimal, así “hola” pasa a ser: 104 111 108 97):

Apoyándonos en la depuración y el código del archivo WAT, podemos ir determinando lo que hace realmente la función, paso a paso.

Longitud de la flag

Los dos argumentos pasados a la función son local 0 y local 1. El siguiente código, líneas 18-21, cargan en el stack el primer argumento y lo asignan a la variable local 31, después carga al stack el segundo argumento y lo asigna a la variable local 42:

get_local 0;    en WASM, local 0 es #arg0.
set_local 31
get_local 1;   en WASM, local 1 es #arg1.
set_local 42

local42 tiene, por tanto, la longitud de la cadena apuntada por local31. La primera comparación tiene lugar poco después. Tras asignar el valor de la longitud a la variable local42, esta se vuelve a cargar en el stack y se asigna a la variable local53. Finalmente, se pone en la pila la constante 20 (i32.const 20) y se comparan ambos valores (i32.ne). Si es diferente, no se entra en el siguiente bloque y se retorna de la función check_flag con un 1 (el resultado de i32.ne, 1, se almacena en local64), lo que implica que la flag no es válida (líneas 23-33):

set_local 53
get_local 53;   ponemos en el stack la longitud de la cadena.
i32.const 20;   ponemos en el stack 20.
i32.ne;            comparamos los 2 valores del stack.
set_local 64:  el resultado lo guardamos en local64.
block
get_local 64
if
i32.const 0
set_local 20
else;           este bloque ya no se ejecuta; se sale retornando error.

La constante 20 es, evidentemente, la longitud de la flag esperada.

Repeticiones

Aunque WASM es francamente poco amigable de leer, es bastante básico. Si observamos lo que sucede tras la comparación de la longitud de la cadena, podemos ver claramente que hay varios bloques con instrucciones repetitivas. Por ejemplo, si la longitud es de 20 caracteres exactos, entonces en el código WASM se entra en el bloque del “else” del snippet de código anterior. Y ahí tenemos una estructura que se va repitiendo varias veces hasta el final de la función (con ligeros cambios, claro):

get_local 31
set_local 75
get_local 75
i32.load8_s offset=0 align=1
set_local 86
get_local 86
i32.const 24
i32.shl
i32.const 24
i32.shr_s
set_local 97
get_local 97
i32.const 119
i32.ne
set_local 2
get_local 2
if
i32.const 0
set_local 20
br 2
end

A priori puede parecer un código difícil de leer, pero no lo es en absoluto.  La primera línea pone en el stack la dirección de nuestra cadena. Asigna dicho valor a la variable local75, y luego la pone de nuevo en el stack. Y ahora viene la parte interesante: la instrucción i32.load8_s offset=0 align=1 lo que hace es poner en el stack el byte apuntado por la dirección que tenemos en el stack, que es precisamente el primer byte de nuestra cadena. En nuestro caso, y para el ejemplo: “12345678901234567890” cargará en el stack el valor decimal 49 (“1”). Después asigna dicho valor a la variable local86, vuelve a ponerla en el stack, y realiza 2 operaciones de rotación de bits que se anulan entre ellas, por lo que a efectos prácticos es como si no hiciese nada:

get_local 86;   el primer byte de nuestra cadena.
i32.const 24;   ponemos 24 en el stack.
i32.shl;           byte_de_la_cadena << 24. Resultado en stack.
i32.const 24;  ponemos 24 en el stack.
i32.shr_s;      el resultado de la operación anterior se vuelve a rotar 24 bits a la derecha.
set_local 97:  el resultado se guarda en local97.

Por lo tanto, la variable local97 tendrá, en realidad, el mismo valor original que tenía la variable local86, es decir, el primer byte de nuestra cadena. Rotar 24 bits a la izquierda y dicho resultado rotarlo 24 bits a la derecha de nuevo es equivalente a no hacer nada sobre el valor original. Sin duda, esto era para despistar.

Tras esta operación absurda, se hace una comparación con un valor constante, decimal, que es claramente un valor ASCII imprimible:

get_local 97;               local97 se pone en la pila, es nuestro byte.
i32.const 119;             se pone en el stack el valor 119, que es “w”.
i32.ne;                         se comparan los dos valores.

Si los valores coinciden, entonces se pasa al siguiente bloque que es una repetición casi idéntica del código ya estudiado, haciendo exactamente la misma rotación de bits pero esta vez con dos cambios importantes: el byte de la cadena que se usa, y el valor con el que se compara. En caso contrario, se ejecuta la instrucción br 2 y se retorna error:

set_local 2;      el resultado de la comparación se guarda en local2.
get_local 2;     se pone dicho valor en el stack.
if;                    si los valores no son iguales, entramos en este if.
i32.const 0
set_local 20
br 2;                saltamos y retornamos error.
end

Resto de bytes de la cadena

A estas alturas ya nos podemos imaginar que se van a ir comparando todos los bytes de nuestra cadena, desde el primero hasta el último, con diferentes valores constantes, todos ellos valores en decimal que se traducen en códigos ASCII imprimibles. Si coinciden todos, es decir, si la comparación tiene éxito, entonces esa será la única flag correcta. Para cada bloque, las únicas diferencias importantes respecto del bloque ya analizado para el primer byte serán:

get_local 31          ponemos en el stack la dirección de nuestra cadena.
set_local 11;         asignamos la dirección de la cadena en local11 (primer byte).
get_local 11;         la ponemos en el stack.
i32.const <idx>;    ponemos el indice (de 1 hasta 19) en el stack del byte que queremos usar.
i32.add;                 sumamos esto al OFFSET que tenemos ya en el stack.
set_local 12:         guardamos resultado en local12.
get_local 12;        ponemos el valor de local12 en el stack.
i32.load8_s offset=0 align=1;  con esto, cargaremos el byte apuntado por offset+indice en el stack.

Se usará en todos los bloques el valor de constante correspondiente para operar con el enésimo byte de nuestra cadena. Por ejemplo, el carácter en la posición 13 de nuestra cadena:

get_local 31
set_local 71
get_local 71
i32.const 13;     índice del carácter: 13.
i32.add;            sumamos esto al OFFSET que ya tenemos en el stack.
set_local 72
get_local 72
i32.load8_s offset=0 align=1;  en el stack ahora cargamos el byte de la posición 13.
set_local 73
get_local 73
i32.const 24
i32.shl
i32.const 24
i32.shr_s
set_local 74
get_local 74
i32.const 53;   el carácter 13 de la flag debe ser 53, es decir, “5”.
i32.ne
set_local 76
get_local 76
if
i32.const 0
set_local 20
br 2
end

Sabiendo esto, basta con revisar todos los bloques y los valores constantes con los que se compara para poder saber cuál es la flag correcta. Cuidado que no están totalmente consecutivos; por ejemplo: los dos últimos bloques comparan las posiciones 11 y 14 con el carácter “_” (95 en decimal).

La flag.

La flag del reto.