All Articles

DiceCTF 2022 Writeup - flagle (Rev)

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.

2022/02/dice1.jpg

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.txt

The 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.