This is part 3 of the Flare-On 5 CTF writeup series.
5 - Web 2.0
The future of the internet is here, again. This next challenge will showcase some the exciting new technologies paving the information super-highway for the next generation.
Similar to the fourth, the fifth challenge is also based on Web technologies - HTML, Javascript and ... Web Assembly (WASM). Web Assembly is quite new and its usage is fairly limited but increasing nevertheless. As its name suggests, WASM allows to execute assembly code on the browser, but it doesn't mean traditional assembly languages like x86, ARM and MIPS we're familiar with. Instead, WASM defines an entirely new binary format with its own instruction set to be run on a stack-based virtual machine in the browser. WASM comes with the promise that it will provide near-native performance as a desktop app running on the user's system.
Let's go back to the challenge. We've three files index.html, main.js and test.wasm. Before WASM can be executed on the browser there are some tasks that must be done beforehand like setting up an interface so that the WASM code can interact with the JS code, setting up arguments for function calls, converting JS values such that WASM can operate on that etc. All of this is done by main.js.
It reads a URL parameter q
and converts it to a Uint8Array
of bytes. It allocates two 0x200
bytes memory block within the WASM instance. One of them is filled with an array of byte values declared at the start. The other is filled with the bytes of the URL parameter q
. Next, it calls a function named Match
exported from the WASM code with the proper arguments. It's evident the URL parameter q
is the flag. If the function returns 1, our flag is correct.
Analyzing the WASM code in JEB decompiler
JEB decompiler from PNF Software can analyze WASM binaries. The demo version is good enough at least for a basic analysis.
Apart from Match
, JEB identified nine other functions from f1
to f9
. Besides, there's another function writev_c
. Let's have a look at Match
.
The signature specifies it takes four i32
parameters and returns an i32
value as output. We already had found out this information by analyzing the JS code. Unlike regular native executables, the WASM binary format also describes the signature and number of local variables of each and every function. For example, Match
has 24 locals inclusive of the four parameters. Have a look at this document to know in-depth about the WASM format.
WASM is a stack-based virtual machine like CPython. There are no registers. All operations must be done by pushing and popping values off the stack. For example, adding two numbers is achieved by pushing the values; the i32.add
instruction pops off the topmost two values, adds them and pushes the result back on the stack.
For experimenting with Web Assembly, try out WasmExplorer and WebAssembly Studio.
Match
calls f9
and based on its return value either returns 1 or 0. So essentially the check is performed in f9
.
Using the WebAssembly Binary Toolkit
The WebAssembly Binary Toolkit (WABT) provides several useful tools to work on WebAssembly. Among them is wasm2c which aims to convert a wasm binary to equivalent C source with a header. It is not a true decompiler but rather converts the WASM instructions to equivalent C code.
After running wasm2c
on test.wasm, the C code for Match
looks like.
static u32 Match(u32 p0, u32 p1, u32 p2, u32 p3) {
u32 l0 = 0, l1 = 0, l2 = 0, l3 = 0, l4 = 0, l5 = 0, l6 = 0, l7 = 0,
l8 = 0, l9 = 0, l10 = 0, l11 = 0, l12 = 0, l13 = 0, l14 = 0, l15 = 0,
l16 = 0, l17 = 0, l18 = 0, l19 = 0, l20 = 0, l21 = 0, l22 = 0, l23 = 0;
FUNC_PROLOGUE;
u32 i0, i1, i2, i3, i4;
i0 = g0;
l0 = i0;
i0 = 32u;
l1 = i0;
....
snip
....
i0 = l2;
i1 = p1;
i32_store((&memory), (u64)(i0 + 20), i1);
i0 = l2;
i1 = p2;
i32_store((&memory), (u64)(i0 + 16), i1);
....
snip
....
i0 = i32_load8_u((&memory), (u64)(i0 + 11));
l18 = i0;
i0 = 1u;
l19 = i0;
i0 = l18;
i1 = l19;
i0 &= i1;
l20 = i0;
i0 = l2;
i1 = l20;
i32_store((&memory), (u64)(i0 + 28), i1);
....
snip
....
i0 += i1;
l23 = i0;
i0 = l23;
g0 = i0;
i0 = l21;
goto Bfunc;
Bfunc:;
FUNC_EPILOGUE;
return i0;
}
All assignment and arithmetic operations have been converted to their C equivalent. Additionally, wasm2c
introduced some helper functions like i32_load
and i32_store
since they no C equivalent. However, the code is still far away from being fit for analysis. It would be nice if we can optimize the code.
We don't want to optimize by hand. That's the work of a compiler. However, we need to provide a main
function before we can run gcc
.
Compiling with we get an x86 ELF. Decompiling the file using HexRays in IDA, f9
looks like
This is a lot better than what we obtained with wasm2c. If we analyze the WASM code of f9
we can see it there's a CALL_INDIRECT
instruction. This translates to the following C snippet.
Immediately below there's a comparison between a character of our input to the value calculated by the above indirect function call.
This means if set a breakpoint at this location we may be able to retrieve the flag character by character. However, we need to do this in the WASM code and not in the ELF as the decompiled wasm2c code may not have the exact behaviour as the WASM binary.
Debugging WASM in Firefox
Firefox offers the ability to debug WASM code. First, we need to set up a local web server which can be done using $ python -m SimpleHTTPServer
or something like https://github.com/m3ng9i/ran.
With Firefox and the web server setup let's identify the proper location in the WASM code which implements the comparison of the correct byte with that of the flag.
The CALL_INDIRECT
instruction is located at B7F
.
The comparison instruction must be located after this. Indeed, a few lines below we do find an i32.eq
instruction which performs a sign agnostic equality check.
Let's set a breakpoint here and rerun with a known flag as input like http://127.0.0.1:8080/?q=abcdefghijklmnopqrst
.
When the breakpoint hits we can see the instruction is essentially comparing the values of var82
and var83
which are loaded on the stack just before. var83
contains 97 (a)
the first character of our input. This is being compared to 119 (w)
. This suggests the first character of the flag is w
. Proceeding in the same fashion we can retrieve the entire flag character by character.
FLAG: wasm_rulez_js_droolz@flare-on.com
Continue to the next part: Flare-on 5 CTF Write-up (Part 4)