Note: This blog post is not about exploiting WebAssembly itself. It provides sandboxed execution and was never designed to prevent memory corruptions within the application itself. We will be going through exploitation of a basic buffer overflow in an example application compiled to WebAssembly. Triggering a buffer overflow inside WebAssembly does not escape the sandbox and it does not corrupt memory outside the sandbox, the only affected memory is the linear memory of the WebAssembly module.
Here we'll demonstrate a simple buffer overflow exploit in code intentionally written to be exploitable for the purpose of this blog post. Real world applications may require more or less work depending on circumstances.
// void Base64::Decode(const std::string& input, char* out);
#include "base64.hpp"
#include <cstdio>
#include <string>
char* CallbackFunc_1()
{
static char example_data[] = "some public data";
return example_data;
}
char* CallbackFunc_2()
{
static char example_data[] = "computed private data";
return example_data;
}
struct ExampleStruct
{
using DataProcessCallback = char*(*)();
char name[32];
DataProcessCallback cb;
ExampleStruct(int type, char const* encoded_name)
{
if (type == 0)
cb = CallbackFunc_1;
else
cb = CallbackFunc_2;
std::memset(name, 0, sizeof(name));
Base64::Decode(encoded_name, name);
}
};
int main(int argc, char* argv[])
{
if (argc != 3)
{
std::printf("Usage: %s <type> <name>\n", argv[0]);
return 1;
}
int type = std::stoi(argv[1]);
ExampleStruct e(type, argv[2]);
std::printf("Hello %s: %s\n", e.name, e.cb());
return 0;
}
The code is intentionally kept simple and contains a bug for the purpose of the example. The application takes two arguments: an integer and a base64 encoded string. ExampleStruct is then initialized using these arguments: based on the integer a callback function is initialized and base64 string is decoded into the name field. Further in the code, we print the name and output of the callback.
The above code is structured this way simply to generate a functionality in the simplest possible way, but let's pretend that integer is not console input and we cannot actually provide it through command line arguments and that we are currently limited to value 0, which will give us output of the first callback function ("some public data").
$ mkdir out
$ emcc -O2 example.cpp -o out/example.js
$ echo -n 'John Doe' | base64
Sm9obiBEb2U=
$ node out/example.js 0 Sm9obiBEb2U=
Hello John Doe: some public data
$ node out/example.js 1 Sm9obiBEb2U=
Hello John Doe: computed private data
The structure has a char[] name field of hardcoded size 32. The Base64::Decode() function takes a char* pointer for output buffer. There are no bound checks, base64 input is decoded into the buffer with a...
]]>