Web Assembly CTF Write-up
Banner source: https://medium.com/trainingcenter/webassembly-a-jornada-o-que-%C3%A9-wasm-75e3f0f03124
I'm been trying to get into Web Assembly for a while, so when I found this CTF write-up by Chiam YJ I decided to give it a try.
The original Challenge
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>crypto</title>
</head>
<body>
</body>
<script type="text/javascript">
var bin = new Uint8Array([0,97,115,109,1,0,0,0,1,138,128,128,128,0,2,96,0,1,127,96,1,127,1,127,3,131,128,128,128,0,2,0,1,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,163,128,128,128,0,3,6,109,101,109,111,114,121,2,0,14,103,101,116,73,110,83,116,114,79,102,102,115,101,116,0,0,5,109,111,114,112,104,0,1,10,148,129,128,128,0,2,132,128,128,128,0,0,65,16,11,133,129,128,128,0,1,6,127,2,64,32,0,65,1,72,13,0,65,0,33,3,3,64,32,3,65,208,0,106,34,1,32,3,65,16,106,45,0,0,34,4,58,0,0,2,64,32,3,65,1,72,13,0,65,0,33,5,32,3,33,6,2,64,3,64,32,4,32,6,65,15,106,45,0,0,115,33,4,32,6,65,2,72,13,1,32,6,65,127,106,33,6,32,5,65,4,72,33,2,32,5,65,1,106,33,5,32,2,13,0,11,11,32,1,32,4,58,0,0,11,32,3,65,1,106,34,3,32,0,71,13,0,11,11,65,208,0,11]);
var m = new WebAssembly.Instance(new WebAssembly.Module(bin));
var offset = m.exports.getInStrOffset();
var flag = prompt("teh flag?");
var strBuf = new TextEncoder().encode(flag.slice(0, 64));
var inBuf = new Uint8Array(m.exports.memory.buffer, offset, strBuf.length);
for (let i = 0; i < strBuf.length; i++) {
inBuf[i] = strBuf[i];
}
var morph = m.exports.morph(strBuf.length);
var outBuf = new Uint8Array(m.exports.memory.buffer, morph, strBuf.length);
if (btoa(new TextDecoder().decode(outBuf)) === "dxB9BH8RVRMKG1NPI3UyOFRIJyJObAZdXkF8DUEJ") {
document.write("congratz!");
} else {
document.write("nope!");
}
</script>
</html>
Static Code Analysis
We can divide the code into major sections:
1. Compiling and Loading the binary Code
A WebAssembly.Instance
object is a stateful, executable instance of a WebAssembly.Module
that contains all the Exported WebAssembly functions.
In this case, we're creating a new WebAssembly.Instance
object (m
) that contains all the Exported WebAssembly functions that derive from the compilation of the bytes in the bin
byte array.
var bin = new Uint8Array([0,97,115,109,1,0,0,0,1,...]);
var m = new WebAssembly.Instance(new WebAssembly.Module(bin));)]
2. Encoding/Transforming the User Input
First, this script starts by converting the first 64 characters introduced by the user into bytes and then writes them to strBuf
.
var offset = m.exports.getInStrOffset();
var flag = prompt("teh flag?");
var strBuf = new TextEncoder().encode(flag.slice(0, 64));
When the Uint8Array
constructor "is called with a buffer, and optionally a byteOffset and a length argument, a new typed array view is created that views the specified ArrayBuffer. (...)" [Source].
In this case, the inBuf
variable represents a typed array that (most likely) views the entirety of the memory.buffer
(this depends on how the getInStrOffset
function works and we don't have access to it).
var inBuf = new Uint8Array(m.exports.memory.buffer, offset, strBuf.length);
Then, this new Uint8Array
is filled with the bytes from strBuf
.
for (let i = 0; i < strBuf.length; i++) {
inBuf[i] = strBuf[i];
}
Finally, the Uint8Array
constructor is called with morph
as the second parameter. We don't really know what morph
does but it seems that the inBuf
bytes are being transformed (somehow) and written to the outBuf
array.
var morph = m.exports.morph(strBuf.length);
var outBuf = new Uint8Array(m.exports.memory.buffer, morph, strBuf.length);
3. String comparison
Finally the bytes in the outBuf
are decoded back into ASCII, then encoded using Base64, and then compared with the hardcoded base64 key: if they're equal, success!
btoa(new TextDecoder().decode(outBuf)) === "dxB9BH8RVRMKG1NPI3UyOFRIJyJObAZdXkF8DUEJ"
1. Finding the Uint8Array that matches the flag
We start by reversing the last step in order to get the outBuf
that when encoded matches the hardcoded key:
var key = "dxB9BH8RVRMKG1NPI3UyOFRIJyJObAZdXkF8DUEJ"
var base64decoded = atob(key)
var textEncoded = new TextEncoder().encode(base64decoded)
console.log(textEncoded)
// Uint8Array(30) [119, 16, 125, 4, 127, 17, 85, 19, 10, 27, 83, 79, 35, 117, 50, 56, 84, 72, 39, 34, 78, 108, 6, 93, 94, 65, 124, 13, 65, 9]
2. Brute-forcing for the Solution
In order to get the flag, we're going to brute-force the solution by comparing the outBuf
that results from each char with the successful buffer found in the previous step, byte by byte.
var bin = new Uint8Array([0,97,115,109,1,0,0,0,1,...]);
var m = new WebAssembly.Instance(new WebAssembly.Module(bin));
var offset = m.exports.getInStrOffset();
var successOutBuf = [119, 16, 125, 4, 127, 17, 85, 19, 10, 27, 83, 79, 35, 117, 50, 56, 84, 72, 39, 34, 78, 108, 6, 93, 94, 65, 124, 13, 65, 9];
var alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\\"#$%&\\'()*+,-./:;<=>?@[\\\\]^_`{|}~";
var match = function(flag, pos) {
console.log("Test flag: " + flag)
var strBuf = new TextEncoder().encode(flag.slice(0, 64));
var inBuf = new Uint8Array(m.exports.memory.buffer, offset, strBuf.length);
for (var i = 0; i < strBuf.length; i++) {
inBuf[i] = strBuf[i];
}
var morph = m.exports.morph(strBuf.length);
var outBuf = new Uint8Array(m.exports.memory.buffer, morph, strBuf.length);
console.log("Comparing " + outBuf[pos] + " with " + successOutBuf[pos])
if (outBuf[pos] == successOutBuf[pos]) {
return true;
}
return false;
}
var pos = 0;
var flag = "";
var letter = 0;
var foundPos = false;
while (pos < 30) {
foundPos = false
for (var letter = 0; letter < alphabet.length; letter++) {
if (!foundPos) {
var tmpLetter = alphabet[letter];
var testFlag = flag + tmpLetter;
var foundPos = match(testFlag, pos);
if (foundPos) {
flag += tmpLetter;
console.log("Found match! Updated flag: " + flag);
pos++;
}
}
}
if (!foundPos) {
alert("Didn't find match!")
}
}
Hope you enjoyed it :)