This page has been machine-translated from the original page.
I participated in DiceCTF 2022.
I was working on the Rev challenges, but I got stuck on the very first problem and had to retire.
This post is a writeup / review of the challenge.
flagle (Rev)
The challenge was a website with five input forms, where you enter the correct five characters of the Flag for each slot to verify them.
I managed to identify four of the five slots by reading the WASM by brute force, but the fourth slot involved analyzing obfuscated JavaScript code, and I gave up there.
Here I will work through the full solution, referencing another participant’s writeup.
Decompiling the WASM
I was reading the WASM directly using the browser’s debug tools, but it turns out there is a decompiler available.
Reference: wabt/decompiler.md at main · WebAssembly/wabt
The WASM sections I needed were already solved, but I built wabt and tried decompiling anyway.
wasm-decompile flag-checker.wasm -o decompile.txtThe decompiled output came out quite clean.
(It feels silly that I was reading it raw in the browser…)
export memory memory(initial: 256, max: 256);
global g_a:int = 5244000;
global g_b:int = 0;
global g_c:int = 0;
export table indirect_function_table:funcref(min: 1, max: 1);
data d_a(offset: 1024) = "dice{\00";
data d_b(offset: 1030) = "";
import function env_validate_4(a:int):int;
export function wasm_call_ctors() {
emscripten_stack_init()
}
export function a():int {
return 1684628325
}
export function streq(a:ubyte_ptr, b:ubyte_ptr):int {
var c:int;
return loop L_a {
c = a[0];
if (c) goto B_b;
if (b[0]) goto B_b;
return 1;
label B_b:
if (c == b[0]) goto B_c;
return 0;
label B_c:
b = b + 1;
a = a + 1;
continue L_a;
}
}
export function validate_1(a:int):int {
return streq(a, 1024)
}
function validate(a:int, b:int, c:int, d:int, e:int):int {
var f:int = g_a - 16;
f[15]:byte = a;
f[14]:byte = b;
f[13]:byte = c;
f[12]:byte = d;
d = f[14]:ubyte;
f[14]:byte = f[13]:ubyte;
f[13]:byte = d;
d = f[13]:ubyte;
f[13]:byte = f[12]:ubyte;
f[12]:byte = d;
d = f[13]:ubyte;
f[13]:byte = f[15]:ubyte;
f[15]:byte = d;
d = f[13]:ubyte;
f[13]:byte = f[12]:ubyte;
f[12]:byte = d;
d = f[15]:ubyte;
f[15]:byte = f[14]:ubyte;
f[14]:byte = d;
d = 0;
if (f[15]:ubyte != 51) goto B_a;
if (f[14]:ubyte != 108) goto B_a;
if (f[13]:ubyte != 33) goto B_a;
d = e == 68 & f[12]:ubyte == 70;
label B_a:
return d;
}
export function validate_3(a:int, b:int, c:int, d:int, e:int):int {
var f:int = 0;
if (b * a != 4800) goto B_a;
if (c + a != 178) goto B_a;
if (c + b != 126) goto B_a;
if (d * c != 9126) goto B_a;
if (d - e != 62) goto B_a;
f = c * 4800 - e * d == 367965;
label B_a:
return f;
}
export function validate_5(a:int, b:int, c:int, d:int, e:int):int {
var f:int = g_a - 16;
f[15]:byte = a;
f[14]:byte = b;
f[13]:byte = c;
f[12]:byte = d;
f[15]:byte = f[15]:ubyte + 12;
f[14]:byte = f[14]:ubyte + 4;
f[13]:byte = f[13]:ubyte + 6;
f[12]:byte = f[12]:ubyte + 2;
d = 0;
if (f[15]:ubyte != 121) goto B_a;
if (f[14]:ubyte != 68) goto B_a;
if (f[13]:ubyte != 126) goto B_a;
d = e == 77 & f[12]:ubyte == 35;
label B_a:
return d;
}
export function validate_6(a:int, b:int, c:int, d:int, e:int):int {
var f:int = 0;
if ((b + 2933) * (a + 1763) != 5483743) goto B_a;
f = e == 125 & (d + 1546) * (c + 3913) == 6431119;
label B_a:
return f;
}
export function guess(a:int, b:int):int {
var c:int = g_a - 16;
g_a = c;
var d:int = 2;
if (f_k(b) != 5) goto B_a;
if (eqz(streq(b, 1024))) goto B_b;
d = a != 1;
goto B_a;
label B_b:
var e:int = b[4]:ubyte;
d = b[3]:ubyte;
var f:int = b[2]:ubyte;
var g:int = b[1]:ubyte;
c[11]:byte = b[0]:ubyte;
c[10]:byte = g;
c[9]:byte = f;
c[8]:byte = d;
d = c[10]:ubyte;
c[10]:byte = c[9]:ubyte;
c[9]:byte = d;
d = c[9]:ubyte;
c[9]:byte = c[8]:ubyte;
c[8]:byte = d;
d = c[9]:ubyte;
c[9]:byte = c[11]:ubyte;
c[11]:byte = d;
d = c[9]:ubyte;
c[9]:byte = c[8]:ubyte;
c[8]:byte = d;
d = c[11]:ubyte;
c[11]:byte = c[10]:ubyte;
c[10]:byte = d;
if (c[11]:ubyte != 51) goto B_c;
if (c[10]:ubyte != 108) goto B_c;
if (c[9]:ubyte != 33) goto B_c;
d = c[8]:ubyte;
if ((e & 255) != 68) goto B_c;
if ((d & 255) != 70) goto B_c;
d = a != 2;
goto B_a;
label B_c:
f = b[1]:byte;
if (f * (d = b[0]:byte) != 4800) goto B_d;
g = b[2]:byte;
if (g + d != 178) goto B_d;
if (g + f != 126) goto B_d;
if (g * (d = b[3]:byte) != 9126) goto B_d;
if (d - (f = b[4]:byte) != 62) goto B_d;
if (g * 4800 - f * d != 367965) goto B_d;
d = a != 3;
goto B_a;
label B_d:
if (eqz(env_validate_4(b))) goto B_e;
d = a != 4;
goto B_a;
label B_e:
var h:int = b[4]:ubyte;
g = b[3]:byte;
e = b[2]:byte;
f = b[1]:byte;
c[15]:byte = (b = b[0]:byte);
c[14]:byte = f;
c[13]:byte = e;
c[12]:byte = g;
c[15]:byte = c[15]:ubyte + 12;
c[14]:byte = c[14]:ubyte + 4;
c[13]:byte = c[13]:ubyte + 6;
c[12]:byte = c[12]:ubyte + 2;
if (c[15]:ubyte != 121) goto B_f;
if (c[14]:ubyte != 68) goto B_f;
if (c[13]:ubyte != 126) goto B_f;
d = c[12]:ubyte;
if ((h & 255) != 77) goto B_f;
if ((d & 255) != 35) goto B_f;
d = a != 5;
goto B_a;
label B_f:
d = 2;
if ((f + 2933) * (b + 1763) != 5483743) goto B_a;
if ((h & 255) != 125) goto B_a;
if ((g + 1546) * (e + 3913) != 6431119) goto B_a;
d = a != 6;
label B_a:
g_a = c + 16;
return d;
}The function that receives user input from the web page and performs the check is guess.
Reading through it is relatively straightforward and the Flag falls out fairly directly — I’ll skip a detailed explanation.
Had to solve a system of linear equations for slot 3. First time doing that in a while!
Reading the JavaScript
The fourth slot (which I couldn’t solve during the contest) was validated not by WASM but by the following JavaScript:
This is quite obfuscated code.
function c(b) {
var e = {
'HLPDd': function (g, h) {
return g === h;
},
'tIDVT': function (g, h) {
return g(h);
},
'QIMdf': function (g, h) {
return g - h;
},
'FIzyt': 'int',
'oRXGA': function (g, h) {
return g << h;
},
'AMINk': function (g, h) {
return g & h;
}
}
, f = current_guess;
try {
let g = e['HLPDd'](btoa(e['tIDVT'](intArrayToString, window[b](b[e['QIMdf'](f, 0x26f4 + 0x1014 + -0x3707 * 0x1)], e['FIzyt'])()['toString'](e['oRXGA'](e['AMINk'](f, -0x1a3 * -0x15 + 0x82e * -0x1 + -0x1a2d), 0x124d + -0x1aca + 0x87f))['match'](/.{2}/g)['map'](h => parseInt(h, f * f)))), 'ZGljZQ==') ? -0x1 * 0x1d45 + 0x2110 + -0x3ca : -0x9 * 0x295 + -0x15 * -0x3 + 0x36 * 0x6d;
} catch {
return 0x1b3c + -0xc9 * 0x2f + -0x19 * -0x63;
}
}After cleaning up the code, I got to the point of understanding that you need to find the 5-character string b such that when substituted into window[b], the result Base64-encodes to "dice". Unfortunately I couldn’t find it.
Looking at the writeup, the approach was to enumerate all 5-character properties of the window object and brute-force them:
Object.getOwnPropertyNames(window).filter(x=> {if (x.length == 5 && x[3] == 'a') console.log(x)})I had the same general idea, but I was manually checking properties listed in Window - Web API | MDN. That approach was fundamentally wrong — I needed to enumerate the actual runtime properties of the window object.
Reference: Object.getOwnPropertyNames() - JavaScript | MDN
Wrap-up
That was a brief writeup of the challenge.
All the CTFs I’ve participated in since the new year have been high-difficulty, and I haven’t been solving much. Need more practice.