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 :)