Simon Says

Aspiring Polymath

KalmarCTF 2025 - FlagSecurityEngine

First published: 11th March 2025

Last updated: 27th April 2025

This post is a writeup of a reversing challenge from a CTF competition called KalmarCTF, that took place 7th-9th March 2025.

The Challenge

The prompt for this challenge was:

The Kalmar FlagSecurityEngine™'s usage of the loadall() function will surely protect the flag from reverse engineering, right?

The challenge zip includes these files:

chall.js is a JavaScript file that looks like this:

loadall is not a standard JavaScript function, so we'll take a look at the other files. main.cc imports quickjs, an open-source JavaScript engine, defines a couple of extra functions, and then runs the provided JavaScript file.

Those extra functions are called print and loadall. print does exactly what you'd expect. loadall loads raw bytecode in quickjs and then executes it. Fun.

We can run the program using ./chall.amd64 ./chall.js (depending on your architecture of course) and it runs and prints ("Wrong flag!")

Analysis

We can take a look for strings in the bytecode and see these near the beginning:

"use strip" is a directive similar to "use strict" that is specific to quickjs, and is not interesting or relevant.

checkFlag must be the name of a function that is defined somewhere in the bytecode

Even after solving this challenge, I am not sure what the underscore is or whether it is really being used as a string

map is of course Array.prototype.map, then we have String.fromCharCode and String.prototype.charCodeAt

print is an extra function defined in main.cc

"Wrong flag!" is what we saw, and hopefully "Right flag!" is something we will see soon.

Monkeypatching to figure out the program structure

One of the best/worst features of JavaScript is monkeypatching. We can do this to take a quick look at where the named built-in functions are being called:

We do the same for Array.prototype.map:

And for String.prototype.charCodeAt:

I also monkeypatched every function on Array.prototype and String.prototype, and some other functions like eval, but the only function that I found being called was Array.prototype.join, which is called on the output of map. We know this because we can get the map call to return a monkeypatched object instead of monkeypatching Array.prototype.join.

We can squeeze a little bit of extra information out of this program by overwriting [Symbol.toPrimitive]. This is called during type coercion with a single argument that is either "number", "string" or "default". The "default" hint is used for addition (because both numbers and strings can be added) and for loose equality checks.

Then we can get fromCharCode, charCodeAt, map, and join to return values wrapped in this function.

Putting all of this together, we can reconstruct that the function looks something like this:

Digging into arithmetic

Now it's time to look more closely at some of those values, and how our input affects them.

With a bit of trial and error, it looks like the call to fromCharCode that comes immediately after the first flag.charCodeAt(0) has that XORed with 9.

The second one is harder to figure out, because it depends on both of the first two characters. Let's introduce some notation. Let c[i] be the value passed to String.fromCharCode in the call after flag.charCodeAt(i). Let f[i] be the flag.charCodeAt(i)

After some trial and error I am convinced that:

c[0] = f[0] ^ 9

c[1] = (f[0]&97) ^ f[1] ^ 83

At this point, I give up on this manual method.

Stop being clever and copy

We can see the values [98,57,35,34,42,41,104,79,18,...] in the bytecode, interspersed with 191, whatever that means:

We see in our logs that the first calls to String.fromCharCode are [98,56,33,33,46,44...] which looks like arr[i]^i

The logs from the run are:

doing fromCharCode: 98 b
doing fromCharCode: 56 8
doing fromCharCode: 33 !
doing fromCharCode: 33 !
doing fromCharCode: 46 .
doing fromCharCode: 44 ,
doing fromCharCode: 110 n
doing fromCharCode: 72 H
doing fromCharCode: 26 
doing fromCharCode: 21 
doing fromCharCode: 23 
doing fromCharCode: 64 @
... truncated
doing fromCharCode: 57 9
doing charCodeAt: kalmar{flag} 0 107
doing fromCharCode: 98 b
doing charCodeAt: kalmar{flag} 0 107
doing charCodeAt: kalmar{flag} 1 97
doing fromCharCode: 56 8
doing charCodeAt: kalmar{flag} 1 97
doing charCodeAt: kalmar{flag} 2 108
doing fromCharCode: 33 !
doing charCodeAt: kalmar{flag} 2 108
doing charCodeAt: kalmar{flag} 3 109
doing fromCharCode: 33 !
doing charCodeAt: kalmar{flag} 3 109
doing charCodeAt: kalmar{flag} 4 97
doing fromCharCode: 46 .
doing charCodeAt: kalmar{flag} 4 97
doing charCodeAt: kalmar{flag} 5 114
doing fromCharCode: 44 ,
doing charCodeAt: kalmar{flag} 5 114
doing charCodeAt: kalmar{flag} 6 123
doing fromCharCode: 110 n
doing charCodeAt: kalmar{flag} 6 123
doing charCodeAt: kalmar{flag} 7 102
doing fromCharCode: 96 `
doing charCodeAt: kalmar{flag} 7 102
doing charCodeAt: kalmar{flag} 8 108
doing fromCharCode: 49 1
doing charCodeAt: kalmar{flag} 8 108
doing charCodeAt: kalmar{flag} 9 97
doing fromCharCode: 97 a
doing charCodeAt: kalmar{flag} 9 97
doing charCodeAt: kalmar{flag} 10 103
doing fromCharCode: 46 .
doing charCodeAt: kalmar{flag} 10 103
doing charCodeAt: kalmar{flag} 11 125
doing fromCharCode: 103 g
doing charCodeAt: kalmar{flag} 11 125
Wrong flag!

Let's compare those sequences:

During map 98 56 33 33 46 44 110 72 26 21 23 64
Among flag.charCodeAt 98 56 33 33 46 44 110 96 49 97 46 103

The first 7 are identical. We know that the first 7 characters of the flag are "kalmar{". Coincidence?

We can use the following set of monkeypatches to capture the values called during the computation involving the argument of checkFlag:

Then we can look at the output array to guess what the flag is:

This prints the flag: kalmar{NOW_ThA7-y0U_kn0W-HOW-Qu1CKj5-W0rKs-CaN_yOu_PWN_it_4$_WelL}. It also prints the "Right flag!" message. Hooray!