Post

Flare-On 2024: meme maker 3000

Introduction

This JavaScript reversing challenge was part of the yearly Flare-On 2024 CTF by Mandiant. The challenge contained a basic .html file to combine memes, together with a lot of obfuscated JavaScript code. Reversing the JavaScript code resulted in being able to retrieve the flag.

Initial behaviour

Opening mememaker3000.html shows the following website:

website

It’s a very basic website to make different combinations out of text and pictures, which results in new memes.

JavaScript

Understanding the code

The JavaScript code is heavily obfuscated, combined with 2 different arrays that have a lot of text elements (I will cut this out since it makes loading the page very slow).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
const a0p = a0b;
(function (a, b) {
  const o = a0b, c = a();
  while (true) {
    try {
      const d = parseInt(o(55277)) / 1 * (parseInt(o(14365)) / 2) + -parseInt(o(68223)) / 3 * (-parseInt(o(90066)) / 4) + parseInt(o(76024)) / 5 + -parseInt(o(73788)) / 6 + parseInt(o(58137)) / 7 * (parseInt(o(59039)) / 8) + -parseInt(o(97668)) / 9 + parseInt(o(26726)) / 10 * (-parseInt(o(11835)) / 11);
      if (d === b) break; else c.push(c.shift());
    } catch (e) {
      c.push(c.shift());
    }
  }
}(a0a, 356255));
const a0c = [a0p(85135) + a0p(70879) + a0p(97482) + ...];
function a0a() {
  const u = ["QhRsJn", "aTAs75", "FW9AQA", ...];
    a0a = function () {
    return u;
  };
  return a0a();
}
function a0f() {
  const q = a0p;
  document[q(52569) + "mentBy" + "Id"]("caption1")[q(3926)] = true, document[q(52569) + "mentBy" + "Id"](q(84859) + "n2")[q(3926)] = true, document[q(52569) + q(73335) + "Id"]("caption3").hidden = true;
  const a = document[q(52569) + q(73335) + "Id"]("meme-template");
  var b = a[q(15263)][q(95627)](".")[0];
  a0d[b][q(8136) + "h"](function (c, d) {
    const r = q;
    var e = document["getEle" + r(73335) + "Id"](r(84859) + "n" + (d + 1));
    e[r(3926)] = false, e.style[r(17269)] = a0d[b][d][0], e.style[r(88249)] = a0d[b][d][1], e[r(69466) + r(75179)] = a0c[Math[r(16279)](Math[r(28352)]() * (a0c[r(87117)] - 1))];
  });
}
a0f();
function a0b(a, b) {
  const c = a0a();
  return a0b = function (d, e) {
    d = d - 475;
    let f = c[d];
    return f;
  }, a0b(a, b);
}
const a0g = document[a0p(52569) + a0p(73335) + "Id"](a0p(7063) + a0p(61697)), a0h = document[a0p(52569) + a0p(73335) + "Id"](a0p(69287) + a0p(50870) + "er"), a0i = document[a0p(52569) + "mentBy" + "Id"](a0p(64291)), a0j = document[a0p(52569) + "mentBy" + "Id"](a0p(67415) + a0p(95610) + "e");
a0g[a0p(98091)] = a0e[a0j.value], a0j[a0p(51076) + a0p(95090) + "ener"](a0p(18165), () => {
  const s = a0p;
  a0g[s(98091)] = a0e[a0j[s(15263)]], a0g[s(2589)] = a0j[s(15263)], a0f();
}), a0i[a0p(51076) + "ntList" + "ener"]("click", () => {
  a0f();
});
function a0k() {
  const t = a0p, a = a0g[t(2589)].split("/")[t(2024)]();
  if (a !== Object[t(22981)](a0e)[5]) return;
  const b = a0l.textContent, c = a0m[t(69466) + t(75179)], d = a0n.textContent;
  if (a0c[t(77091) + "f"](b) == 14 && a0c[t(77091) + "f"](c) == a0c[t(87117)] - 1 && a0c[t(77091) + "f"](d) == 22) {
    var e = (new Date)[t(67914) + "e"]();
    while ((new Date)[t(67914) + "e"]() < e + 3e3) {}
    var f = d[3] + "h" + a[10] + b[2] + a[3] + c[5] + c[c[t(87117)] - 1] + "5" + a[3] + "4" + a[3] + c[2] + c[4] + c[3] + "3" + d[2] + a[3] + "j4" + a0c[1][2] + d[4] + "5" + c[2] + d[5] + "1" + c[11] + "7" + a0c[21][1] + b[t(89657) + "e"](" ", "-") + a[11] + a0c[4][t(39554) + t(91499)](12, 15);
    f = f[t(82940) + t(35943)](), alert(atob(t(85547) + t(19490) + "YXRpb2" + t(94350) + t(43672) + t(91799) + t(68036)) + f);
  }
}
const a0l = document[a0p(52569) + a0p(73335) + "Id"]("caption1"), a0m = document[a0p(52569) + a0p(73335) + "Id"](a0p(84859) + "n2"), a0n = document.getElementById(a0p(84859) + "n3");
a0l["addEve" + a0p(95090) + "ener"]("keyup", () => {
  a0k();
}), a0m[a0p(51076) + a0p(95090) + a0p(97839)](a0p(46837), () => {
  a0k();
}), a0n[a0p(51076) + a0p(95090) + a0p(97839)](a0p(46837), () => {
  a0k();
});

Even though the code is pretty hard to read, function a0k() looks interesting:

If a certain value doesnt match a, the function exits.

1
if (a !== Object[t(22981)](a0e)[5]) return;

We recognize three values are being retrieved and checked for a certain value.

1
2
const b = a0l.textContent, c = a0m[t(69466) + t(75179)], d = a0n.textContent;
if (a0c[t(77091) + "f"](b) == 14 && a0c[t(77091) + "f"](c) == a0c[t(87117)] - 1 && a0c[t(77091) + "f"](d) == 22)

By just checking what a0c is, we get a big clue on what this function really does:

a0c

Its the array of possible texts used for the memes.

When cleaning up the code, this is the result:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function ShowFlag() { 
    const t = a0p, 
    a = "boy_friend0.jpg"; 
    
    //Must be boy_friend0.jpg
    if (a !== Object["keys"](a0e)[5]) 
        return; 
    
    // Retrieve the HTML meme texts and it must be the following 3: 
    const b = "FLARE On", 
    c = "Security Expert", 
    d = "Malware"; 
                    
    if (a0c["indexOf"](b) == 14 && a0c["indexOf"](c) == a0c["length"] - 1 && a0c["indexOf"](d) == 22) { 
        var e = new Date()["getTime"](); 
        
    // There is code which basically makes you wait 50 minutes if you assemble the correct picture & text manually which I left out


    // Assemble the flag
    var f = d[3] + 'h' + a[0xa] + b[0x2] + a[0x3] + c[0x5] + c[c["length"] - 0x1] + '5' + a[0x3] + '4' + a[0x3] + c[0x2] + c[0x4] + c[0x3] + '3' + d[0x2] + a[0x3] + 'j4' + a0c[0x1][0x2] + d[0x4] + '5' + c[0x2] + d[0x5] + '1' + c[0xb] + '7' + a0c[0x15][0x1] + b["replace"]('\x20', '-') + a[0xb] + a0c[0x4]["substring"](0xc, 0xf); 
    
    // Alert the flag
    f = f["toLowerCase"](), alert(atob("Q29uZ3JhdHVsYXRpb25zISBIZXJlIHlvdSBnbzog") + f); }

 }

Assembling the flag

I tried choosing the values from the drop down, but that didn’t seem to work. My guess is that it updates whenever you type in the fields because it’s hooked to the onKeyup event. If you set it through JavaScript, you probably don’t trigger that event, so the “d” variable won’t get updated. So I set the values via the console:

console

Next, we can simply assemble the flag by copying the code from the challenge and pasting it into our console:

assemble

Extra

There is also a rabbit hole included in this specific challenge. Within the challenge, you can find a base64 string which you can turn into an executable:

data

You can reverse and debug this executable (which I won’t go into since it’s not relevant to the challenge) which I thought was the solution at first :(

fake

But shortly after debugging further, you get disappointed unfortunately:

fake

This post is licensed under CC BY 4.0 by the author.