Latest Tweets

Defeating an ELF32 binary with absolutely no leaks without using the ret2_dlresolve technique

 The binary

I was presented with an ELF32 binary with the following protections:

ch77 protections

Disassembling the binary with r2, I quickly recognized a classic stack overflow by abusing the call to read:

There’s a buffer overflow in the read function.

read() would read up to 0x64 bytes into the buffer pointed to by *buf, clearly overwriting EBP and RET. Apart from this more than obvious stack overflow, this binary did not have any interesting functions to analyse. As you can imagine, there was no feasible way to leak data, and we had to assume that ASLR on the remote server was enabled. With no puts, write, printf or alike calls within the binary, we were supposed to use the ret2_dlresolve technique. This, of course, would be the only technique giving us a 100% chance of success, no matter which version of the GNU/Libc6 the remote server was running, and without the need of leaking some data from the program (which, in turn, was not an obvious thing to accomplish).

So I read about this technique and wrote the exploit. But then I sort of though: what if I can leak some data anyways? Wouldn’t that be a good way of improving my knowledge about powning, even if my new exploit was obscenely a lot less reliable? I mean, why not?


Let’s suppose that the binary we were provided with was running on the very same system where it was originally built. I started by reading the data put into the .comment section of the binary by gcc itself:

readelf -p .comment ch77

String dump of section ‘.comment’:
[ 0] GCC: (Ubuntu 4.8.5-4ubuntu8) 4.8.5

A quick search on Google gave me that that version of the compiler shipped with Ubuntu Xenial Xerus (16.04), and the GNU/Libc6 version for this GNU/Distro was 2.23. So I downloaded this version of the library by using the libc database. Well, at this point I knew that in order to leak data, I needed to replace the call to read with a call to, maybe, write. But of course you cannot do that by just overwriting the .got.plt entry for read() with the OFFSET of the write() function. Then I though: but of course you can, as long as that offset fits within one byte! So I checked both OFFSETS:

[*] Libc6 read offset: 0xd4350
[*] Libc6 write offset: 0xd43c0

These OFFSETS where obtained from the  libc6-i386 package on Xenial Xerus; when it came to the libc6:i386 package (after adding the i386 architecture with dpkg –add-architecture i386), I got the following offsets:

[*] Libc6 read offset: 0xd5b00
[*] Libc6 write offset: 0xd5b70

I tried this on a Debian Jessie LTS with libc6 version 2.19 as well and I got the following OFFSETS:

[*] Libc6 read offset: 0xc8810
[*] Libc6 write offset: 0xc8880

So yes; by overwriting the LSB of the .got.plt entry for read() with the right byte offset of write(), I would have a call to write() any time I forced the binary to execute call read@plt. Thanks to the first read call, I had a write-what-where primitive, so overwriting the LSB was a piece of cake:

ROP to overwrite LSB of .got.plt for read() with the LSB of write’s OFFSET.

After that, the leak was easy too: because now read() was in fact a call to write(), all I had to do was to call read@plt yet again, forging write() arguments so that I could get write’s libc6 address leaked into stdout:

ROP to leak to stdout the address of write.

By running the exploit I had so far, my leak was working fine and I could get all the interesting addresses (system(), the string “/bin/sh”) and so on. But I had a new problem now: because the call to read had been overwritten with write(), now I had no way of inputting more data to the program!

Leaking write’s address right off the binary’s memory.

Restoring read()

The total number of bytes read by the call to read() was 0x64. I needed 20 bytes of padding in order to reach EBP and then RET. Which means I was left with a total of 0x64 -0x18 = 0x4c bytes (76 in decimal) for my ROP chain. Because I had already wasted 0x28 bytes to overwrite read@got.plt and then call write to leak its own address to stdout, I was in fact left with 0x24 bytes of a ROP chain. My idea was, of course, to yet again overwrite LSB of read@got.plt but this time with its original value. This would restore the read() function and so I would be able to input a classic ROP-shell attack to spawn a shell and powned the program. But, how?

I started looking for ROP gadgets within the binary. I sort of though that a good way of restoring the original byte would be to XOR it. So I looked for gadgets that would allow me to XOR something with an address of my control. I found these:

ROP gadgets to XOR a value on memory.

The second ROP gadget (skipping the first instruction, push cs), seemed like a good choice. I needed to make sure that EBP held the value read@got.plt-0xe, and CL the right byte (key) used for the XOR operation in order to restore the original LSB of read(). Which byte that should be? Easy; I computed a XOR key like this:

xor_byte_cl = libc6_read_byte ^ libc6_write_byte

I kept looking for ROP gadgets that would allow me to write the XOR key to CL, or ECX, to no avail. Putting the address read@got.plt-0xe into EBP was easy; a pop ebp; ret gadget would do it. I had that ROP, so I focused all my attention to get the XOR key into CL. After a while, I realized the following truth: if you do not have the proper ROP gadget that suits you, go abuse a legitimate call to a function that will do all the dirty job for you. Internally, the parameters passed to a libc6 function by the stack (on 32 bits) will eventually ended up into registers when making a system call (int 0x80). So for the call to write I had the following:

Before making a system call, the parameters are passed into registers. ECX will hold the buffer to output from.

The third parameter to write, i.e., the total number of bytes to write to, would be put into the EDX register. The buffer where to read data from would be put into the ECX register. The file descriptor to output data to would be put into the EBX register and finally EAX will hold the system call id, 0x4, for write. What I found out by debugging my exploit was that the value put into ECX would hold right after calling write. So by forging a new ROP chain after the leak that would make a new call to write with the second parameter set to a valid address within the program’s memory ending with the XOR key, I would have the XOR key in the CL register.

I wrote this ROP chain:

rop_cl_90 = p32(binary_read_plt)
rop_cl_90 += p32(clean_3_args_stack)
rop_cl_90 += p32(0x1) + p32(patch_address) + p32(0x4)

The offset patch_address was computed this way:

patch_address = 0x804a000 + xor_byte_cl

The only problem with this ROP chain was that I was left with a total of 12 bytes. I still needed to put read@got.plt-0xe into EBP by popping off the stack its value. That meant 8 more bytes (4 for the ROP gadget pop ebp; ret) and 4 more bytes for the actual address I wanted: 0x8049ffe. Then, I needed 4 more bytes to call the ROP gadget that would XOR the LSB of read@got.plt. So it was obvious that I could indeed restore read with those 12 left-over bytes of ROP magic, but from that point on I would not be able to call main() again to get another chance of inputting my ROP-shell! Stuck again.

Let’s clean 4 registers instead of 3

I needed to save 4 bytes and that would be all. Instead of using a ROP gadget that would pop 3 registers, I needed one that would pop 4 registers (the three write parameters already on the stack plus a new one, the address I wanted into EBP). For this to work, of course, EBP should be the last register in the ROP gadget sequence of pops. I found that ROP gadget:

0x080484b8: pop ebx; pop esi; pop edi; pop ebp; ret;

So I rewrote my original ROP like this:

The final ROP chain to restore read’s LSB.

I crafted the third parameter of write to be that of a valid memory address within the program (as a matter of fact, the .bss symbol workaround ;-)) because this value would be popped into EDI, and then the ROP gadget I was about to use to XOR the LSB of .got.plt had a second instruction that might segfault the program:

The second instruction could segfault the program if EDI+0xe was pointing to a non-valid memory address.

At the same time, this was a large value. Because it was used first as the third parameter of write, that meant write will read up to 0x804a040 (workaround address within the .bss segment) bytes of data. As long as this was not segfaulting, I did not care because I could just discard any junk coming from the program.

So the last part of my first ROP attack would be to put the desired value of EBP into the stack, call the ROP gadget that would XOR the key with the LSB of read@got.plt and then call back to main in order to get a second chance of inputting my ROP-shell:

The last part of my ROP attack to restore read().

Trying it out

Once my first ROP attack was written, I tried it out by sending my exploit to the program, waiting for the second call to read and then sending it the LSB of the write function. After that, the program should be sending me 4 bytes back, the address of libc6’s write:

exploit = “A”*padding + p32(0xdeadbeef) + rop_overwrite + rop_write + rop_cl_90 + rop_restore_read
leaked_write = int(r.recv(4)[::-1].encode(“hex”),16)

After this first attack, performed in a single shot, the program was back to main and thus waiting for me to input more data. This time, my ROP-shell that was finally populated with the right addresses for system() and the string “/bin/sh”:

Sending my ROP-shell and gaining a remote shell.

Once my exploit was completed, I ran it against Xenial Xerus and Debian Jessie LTS with success:

Running my exploit and gaining a shell without ret2_dlresolve.

You can get the binary and my exploit and try it out yourselves. Of course, this exploit is not reliable when you do not know the libc6 version of the remote server. Besides, it only works with libc6 versions where the offset between write and read are less than 0xff bytes. The intended way was to use ret2_dlresolve, which is 100% reliable for this case. But anyway, I think it’s been interesting to pull this off and I tried harder, I think. No doubt there may be some other ROP gadgets or some other possible combinations so that I could do the same thing with even less bytes, I’m sure. So feel free to let me know!

Desclavando espinas 2/3: UAD360 go4fun writeup

El reto

Revisamos la información básica sobre el binario:

file go4fun.uu
go4fun.uu: ELF 32-bit MSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked, not stripped

Estamos ante un binario ELF (Linux), con arquitectura MIPS de 32 bits. Además, es Big Endian (MSB).

Ejecutando el binario con arm_now

Durante la UAD360, no conocía el proyecto arm_now. @julianjm me lo chivó (luego también lo haría @oreos_ES). Basta con instalar arm_now y luego levantar una instancia emulada con qemu de una máquina MIPS32 Big Endian:

arm_now start mips32 –sync

El binario nos pide una clave, luego parece estar haciendo algún tipo de comprobación y finalmente acaba sin ningún mensaje de error:

Ejecutando el binario MIPS32


Ejecutamos r2 y lo primero que nos sorprende es la cantidad de funciones que tiene este binario. Se ve claramente que estamos ante un binario escrito en golang, compilado para MIPS32. Leemos un poco sobre binarios Go para saber dónde comenzar nuestra búsqueda. La primera función que realmente nos interesa es sym.main.main, que a su vez llama a sym.main.check_key. Pedimos a r2 que nos muestre el callgraph desde sym.main.check_key:

El callgraph generado nos muestra claramente el flujo del programa con una llamada a Sleep (seguramente el “Please Wait….”) y una llamada a Decrypt. Luego, el binario desencripta probablemente la flag, que estará encriptada y almacenada en el propio binario, a partir de la key que se nos pide.

La function callgraph para check_key

En la función sym.main.main encontramos la flag cifrada:

La flag cifrada.

Recordemos que esto es Go, y las cadenas no terminan con \x00. Si observamos claramente la imagen, en realidad la flag cifrada está compuesta de los siguientes bytes:


Si miramos el código de la función Decrypt, vemos claras llamadas a las funciones en Go crypto_aes.NewCipher y crypto_cipher.NewGCM:

[0x0010050c]> s sym.main.decrypt
[0x000ff758]> pdf
Do you want to print 747 lines? (y/N) 
[0x000ff758]> pdf~jal
| |: 0x000ff770 0c01b2cc jal sym.runtime.morestack_noctxt
| 0x000ff798 0c03fd8a jal sym.main.createHash
| 0x000ff7b8 0c016a6b jal sym.runtime.stringtoslicebyte ; sym.runtime.rawstringtmp+0xc4
| 0x000ff7d8 0c0222e1 jal sym.crypto_aes.NewCipher
| | 0x000ff800 0c01d9fa jal sym.crypto_cipher.NewGCM

Así que ya sabemos lo que hace el código a groso modo. A partir de nuestra entrada, hace algunas comprobaciones sobre la misma. Si la key introducida es válida, entonces genera un hash en MD5 de la clave (llamada a createHash) y la utiliza como clave AES en modo GCM (llamadas a sym.crypto_aes.NewCipher y sym.crypto_cipher.NewGCM) . Con esto descifra la flag y entonces se hace una llamada a show_flag que la imprime por pantalla.

Leyendo un poco sobre Go y AES en modo GCM, vemos que se suele concatenar el nonce delante del texto cifrado. Dentro del binario tenemos claramente la flag cifrada, pero el nonce no lo vemos por ninguna parte. Al final, con ayuda de r2, vemos claramente que hay una llamada a gcm.NonceSize() justo aquí:

0x000ff828 8c230010 lw v1, 0x10(at)
| || 0x000ff82c afa20004 sw v0, (var_4h)
| || 0x000ff830 0060f809 jalr v1; gcm.NonceSize()

Así que la flag cifrada tiene seguramente el nonce concatenado al principio. La siguiente llamada es la que ejecuta el descifrado:

0x000ff8c0 0080f809 jalr a0

La documentación sobre AES en modo GCM indica que el número de bytes para el nonce es de 12 (0xc en hexadecimal). Así pues, consideramos los 12 primeros bytes de la flag como el nonce, y el resto como la flag cifrada:

nonce =7fd4000ff0ae3bdd14ff3bd7

flag  = 0aa12ce69636366c77c1a7298ae9f5aeab69a0f4739f4f069561c44b2d6597bfc4834236f1756290ae

La única manera de descifrar la flag es, pues, con la key válida de AES. Por si acaso, revisando el código del binario en MIPS, nos encontramos con que la función createHash, que entendemos calcula el MD5 de la clave AES (aparentemente, la que introducimos como entrada), tiene un código que llama la atención.

La falsa clave AES

Si observamos el código con r2 de la función createHash, vemos un snippet de código que acaba generando 32 bytes hexadecimales (que podría perfectamente ser una clave AES válida):


Las operaciones lui cargan inmediatos en el registro v0, y las operaciones ORI hacen el or con el mismo registro y otro inmediato, almacenando el resultado en el mismo registro v0. Tras cada operación, se guardan 4 bytes en at + offset. Tras el código mostrado en la siguiente captura, $at contendrá los 32 bytes del falso hash:

Este código genera un hash falso de 32 bytes.

Como ya sabemos que el código descifra la flag, cifrada con AES en modo GCM; tenemos la flag y el nonce, sabemos que la clave será un hash md5. Con todo esto, probamos el hash generado con las operaciones OR con un código Go típico para descifrar. El resultado no puede ser más desolador:

Nope, @oreos_ES nos ha jodido bien …

Por desgracia, no hay más remedio que enfrentarse a la lógica de la función check_key … A base de mucho café, claro.

Sobre check_key, retdec y otros quebrantos

Mis conocimientos de MIPS32 son bastante nulos, aunque han mejorado tras este reto :). Usando r2, el código de comprobación de la clave se me hizo bastante infumable, así que tiré de retdec para intentar obtener una representación a más alto nivel de la función. Tras instalar retdec, de-compilé la función check_key: -a mips -e big –select-functions main.check_key go4fun.uu

El archivo de código C resultante mejoró considerablemente, aunque seguía siendo algo duro de roer. Más bien tedioso en su lectura e interpretación, aunque a simple vista ya se podía ver que se estaba usando una variable como base y diferentes constantes numéricas como OFFSET para realizar ciertas comprobaciones sobre lo que parecían ser las diferentes posiciones de nuestra clave. Por ejemplo:

Primera comprobación de las posiciones key[15] y key[4]

En el código anterior, v2 contendrá el valor entero de la posición 15 de la clave, o sea, key[15]. v6, en cambio, contendrá el valor entero de la posición 4 de la clave, es decir, key[4]. La primera comprobación es, si substituimos v6 por key[15] y v2 por key[4], la siguiente:

key[15] + key[4] != 12

Evidentemente, si esto se cumple se retorna y no se comprueban el resto de posiciones de la clave, con lo que no es válida y no se procede a descifrar la flag. Por lo tanto, nuestra clave debe satisfacer todo lo contrario:

key[15] + key[4] == 12

Si seguimos mirando el resto del código, podemos ver que esta realizando más comprobaciones con diferentes posiciones de la clave. Esto cada vez más parece un sistema de ecuaciones dónde diferentes operaciones sobre diferentes posiciones de la clave deben dar un valor concreto para seguir con la enésima comprobación. Si cualquiera de estas operaciones no da el resultado correspondiente, se retorna y no se llama a decrypt. Tras armarme de paciencia, llegué a identificar hasta 24 ecuaciones con diferentes posiciones de la clave. El archivo en código C generado con retdec y analizado y documentado lo podéis ver aquí.

Todas las ecuaciones se podían identificar con relativa facilidad gracias a retdec, aunque en algunos casos era algo más obtuso. También es cierto que he simplificado algunas, porque por ejemplo esto:

((v12 > 9 ? -1 : v12) – (v13 > 9 ? -1 : v13) ^ 8) == 0

es totalmente equivalente a esto:

((v12 > 9 ? -1 : v12) – (v13 > 9 ? -1 : v13) ) == 8

Entra z3 (gracias @julianjm)

Estas son todas las ecuaciones (24) que identifiqué leyendo el código de-compilado de la función check_key (os recomiendo mirar el archivo check_key.c):

int(key[4]) + int(key[15]) == 12
int(key[1]) * int(key[18]) == 15
int(key[15]) / int(key[9]) ^ 1 == 0 => int(key[15]) / int(key[9]) == 1
int(key[17]) – int(key[0]) ^ 8 == 0 => int(key[17]) – int(key[0]) == 8
int(key[5]) – int(key[17]) == -1
int(key[15]) – int(key[1]) == 6
int(key[10]) * int(key[1]) == 24
int(key[13]) + int(key[8]) == 11
int(key[8]) * int(key[18]) == 15
int(key[11]) * int(key[4]) == 18
int(key[8]) + int(key[9]) == 12
int(key[12]) – int(key[19]) == -4
int(key[9]) % int(key[17]) == 1
int(key[16]) * int(key[14]) == 2
int(key[7]) – int(key[4]) == 1
int(key[6]) + int(key[0]) == 0
int(key[2]) – int(key[16]) == -1
int(key[4]) – int(key[6]) == -3
int(key[0]) % int(key[5] == 0
int(key[11]) * int(key[5]) == 42
int(key[10]) % int(key[15]) ^ 8 == 0 => int(key[10]) % int(key[15]) == 8
int(key[11]) / int(key[3]) == 2
int(key[14]) – int(key[13]) == -7
int(key[19]) + int(key[18] == 12

Necesitaba algo que resolviera todas estas ecuaciones. Entonces fue cuando @julianjm me dijo que existía el z3 solver. Así que lo instalé, y escribí el solver a partir de las ecuaciones anteriores. El archivo del solver en z3 lo podéis descargar aquí. Para resolver estas ecuaciones, las introduje una a una y ejecuté el solver:

[s[8] = 3,
s[4] = 3,
s[19] = 7,
s[17] = 8,
s[16] = 2,
s[2] = 1,
s[9] = 9,
s[1] = 3,
s[3] = 3,
s[15] = 9,
s[11] = 6,
s[10] = 8,
s[12] = 3,
s[18] = 5,
s[0] = 0,
s[14] = 1,
s[7] = 4,
s[6] = 0,
s[5] = 7,
s[13] = 8]

El resultado me daba el valor numérico de cada posición de la clave. Por lo tanto, re-ordenando las soluciones se obtenía la clave:


Tras mi análisis de lo que hacía el código, era evidente que había que calcular el hash MD5 de la clave y usarla como clave de 32 byres AES de descifrado en mi programa escrito en go “decrypt.go”. El resultado:

The flag is mine!

Reto superado, eso sí, cerca de 72 horas más tarde de lo esperado …

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

El reto

Descargamos el archivo 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 ( 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, 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.;            comparamos los 2 valores del stack.
set_local 64:  el resultado lo guardamos en local64.
get_local 64
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.


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.const 24
set_local 97
get_local 97
i32.const 119
set_local 2
get_local 2
i32.const 0
set_local 20
br 2

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”.;                         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.

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.const 24
set_local 74
get_local 74
i32.const 53;   el carácter 13 de la flag debe ser 53, es decir, “5”.
set_local 76
get_local 76
i32.const 0
set_local 20
br 2

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.