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.amd64
- chall.arm64
- chall.js
- main.cc
- makedep.sh
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
- checkFlag
- _
- map
- fromCharCode
- charCodeAt
- Wrong flag!
- Right flag!
"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!
_ Likes