Flare-on 9¶
My endeavours into playing Flare-on for the first time. Write-up goes up to challeng 8 - Backdoor and covers the process from description to flag.
Introduction¶
The best and worst events in life are often the ones you didn't plan for or went into unprepared for. This about sums up my first experience with Flare-on and actual reverse engineering in general.
Going into this years Flare-on I didn't have any actual reversing knowledge, having touched Ghidra maybe a handful of times and having toyed with some basic binaries for OSCP's buffer overflow. Now, with Flare-on behind me, I'm looking forward to next year's Flare-on and finishing it in its entirety, having achieved exponentially more than I could have ever dreamed during this year's edition!
First and foremost, a huge thanks to Yassir a.k.a. @kladblokje88, and an unnamed friend which will go by his username mryoranimo with whom I participated in these challenges with. mryoranimo is a lot more experienced in reversing than me and helped explain things when my peanut brain couldn't comprehend something. I probably wouldn't have made it this far without you, so a big thank you for that <3.
With the life story covered, let's dig into this years Flare-on!
Challenge 1: Flaredle¶

Upon clicking the play live link the website shows a Wordle clone as the challenge description hinted at. Unlike the official Wordle, this clone asks for a lot more characters for each guess. This is probably where the "is too hard to beat without cheating" part of the challenge description comes into play:

After entering some guesses it appears to act just like the original Wordle game, but due the increased word length it will much more difficult to brute-force shared letter combinations among English words. Onto the source code we go instead.
Looking in script.js
the following section of code checks whether we have entered the desired word:
Checking the definition of rightGuessString
shows the following:
With CORRECT_GUESS
defined as:
Checking the definition of WORDS
shows that the words.js
file also provides this:
Thus, checking the words.js
file at line the 57th entry shows the correct and single word containing "flare":
export const WORDS = ['acetylphenylhydrazine',
[snip]
'establishmentarianism',
'flareonisallaboutcats',
'gastroenterocolostomy',
[snip]
'zygomaticoauricularis',
]
Entering the word flareonisallaboutcats
shows that this is the correct answer and returns the flag:

Challenge 2: Pixel Poker¶

This challenge consisted of two files: an executable named PixelPoker.exe
and a text file named readme.txt
with the following in it:
Quote
Welcome to PixelPoker ^_^, the pixel game that's sweeping the nation!
Your goal is simple: find the correct pixel and click it
Good luck!
Opening the provided PixelPoker.exe
shows an application window with a random array of colours with the challenge being to click the correct one. Clicking around to see what happens the following pop-up got displayed, informing that one needs to try again:

Seeing as the last challenge took inspiration from the popular Wordle game, perhaps this took inspiration from elsewhere too 😉.
Jokes aside this challenge proved much simpler than initially thought, not requiring tools such as IDA, Ghidra, et al to solve. This "easier than expected" outcome was something that continued throughout Flare-on as I started understanding the hints in the challenge name.
Opening the PixelPoker.exe
binary with 7-zip showed a series of directories, including one containing two bitmaps:

mryoranimo found some code within the decompiled source in Ghidra doing XOR operations on something we did not quite get just yet. Later, Yassir compared the two images on diffchecker.com and noticed the flag at the bottom of the image:

The almost illegible text at the bottom reads [email protected]
, the flag for this challenge.
Challenge 3: Magic 8 Ball¶

Executing the challenge binary Magic8Ball.exe
shows a blue application window, within it a Magic 8 Ball one can ask questions:

Running the Mandiant FLOSS tool over the binary gives back an interesting strings spelling out gimme flag pls
:

Messing around a bit in IDA, trying to figure out how it even all worked, I found a series of if-else statements maybe checking for letters such as L
, R
, U
, and D
:

Ghidra decompiled this function at address 0x4024e0
to the following C++ code:
Putting the entire chain into order shows the following pattern: L L U R U L D U L
.
mryoranimo and I tried to enter the pattern of arrow-keys in combination with "gimme flag pls?" to no avail. The next day doing it all again it somehow worked this time, ain't that fun 🫠.
Doing the shuffle with the pattern, entering the phrase gimme flag pls?
, and pressing enter ~~finally~~ returned the flag:

Challenge 4: darn_mice¶

Executing the binary without any arguments does nothing and just exists the program afterwards it seems:

Entering a random input as argument returns an output stating "On your plate, you see four olives. You leave the room, and a mouse EATS one!"

However, giving an input of 36 characters or greater will show a different input: "On your plate, you see four olives. No, nevermind"

I did notice some strange behaviour when solely entering the letter s
. The binary returned the following new strings nibble...
, When you return, you only: s
followed by a random string of characters:

This "strange behaviour" that Yassir and I had discovered when entering the letter s
turned out to be part of getting to the flag for this challenge. What we did not yet fully grasp was the why for why this specific character returned this gibberish of characters. What Yassir and I had manually brute-forced was the first of 36 bytes that needed to be looped through to get to the challenge flag. This array of 36 bytes is located at offset 0x40100
and is used in a function which takes the value at the current offset, adds our input to it, and then executes it as if it were a valid instruction. The decompiled and annotated code produced by Ghidra can are down below:
Note
I thought I had made a screenshot of the cleaned up function, but apparently I don't have my Ghidra project nor any screenshots 🥲
The significance of the letter s
in this function now became clear to us. The letter S is equivalent to 0xC3
or 195, which is the same as the OP code RET
or return:
Opcode | Mnemonic | Description |
---|---|---|
C3 | RET | Near return to calling procedure |
CB | RET | Far return to calling procedure |
Using the documentation under c9x.me the RET
operation is described as the following:
Quote
Transfers program control to a return address located on the top of the stack. The address is usually placed on the stack by a CALL instruction, and the return is made to the instruction that follows the CALL instruction.
The RET
OP code doesn't cause any exceptions or errors as it returns the program flow to the calling procedure. Different OP codes such as ADD
or INC
change the state and would cause an exception, thus exiting the program. Now, with the behaviour of the character s
clarified the challenge description "If it crashes its user error" and the binary output "when you return you only:" now made sense to me. To ensure the program can loop through all 36 bytes in its array, we need to have it execute the RET
instruction for each of the 26 steps.
To achieve the above, I used a Cyberchef recipe that took a string of C3s and subtracted that with each item in the array to get the required value for each entry. The Cyberchef recipe and it's output can be seen down below:

With this recipe the magic string we need to enter to have everything return C3
/ RET
is: see three, C3 C3 C3 C3 C3 C3 C3! XD
. Entering this into the challenge binary spits out a fitting flag of [email protected]
:

Challenge 5: T8¶

First off, ouch. Second, this challenge started off strong with a binary programed to sleep for 43.200 seconds before it would continue:


As can be seen by my comment, the sleep syscall here didn't do much for the program itself, and can thus be patched out by changing the OP code to a JMP
from a JZ
instead:

However, with this patched sleep I wasn't able to get much further. Looking at the code following this showed a series of uStack
variable containing some hex with 0x00
between them. These turned out to be a wchar_t[16]
array as Windows uses UTF-16, rather than 8 making any UTF-8 string a series of characters with 0x00
to pad the length to the next character.
Doing this type change changed the data from the following to a neat uStack72
array which I later renamed to possible_domain
as it contained flareon.com
in the name:
Original uStack
array:

uStack
array changed to wchar_t[16]
and renamed to possible_domain
:
![Flare-on 9 T8 ustack array after changing types to wchar_t[16] in Ghidra decompile](../../../../images/flareon9/flareon9-t8-ustack-renamed-new-type.webp)
How I managed to get flareon.com
from this was a hint below this array that did an XOR operation over these values.
The straight_up_eight
value is decremented by 2 and uses it as its array index to do an XOR 17 operation over it. Doing this operation in Cyberchef with the string 0x7c007e0072003f
showed a string of .com
:

This is where I/we got stuck for a while, messing around with the binary and the PCAPs it would create. It seemed that the PCAP showed random numbers for each POST request. We tracked this random number generator back to the function FUN_00421ff4
:

Looking up those two hex values as their decimal counter parts (214013 & 2531011 respectively) returned a hit by Wikipedia for a linear congruential generator with the following table of parameters in use:

Patching this function to always return the value from the PCAP (11950) makes this function look the following:

Yassir wrote the following Python server to emulate the server and how it responded in the PCAP:
Running the binary now shows a pop-up saying "You're a machine!!!" in the form of a Fatal Application Exit:

And this is where we then got stuck again for a long time, trying to figure out what the binary did with the base 64 string sent by the server.
Turns out, we hade solved the challenge at this point an we hadn't figured that out just yet. The flag got stored somewhere in the application memory when it ran and triggered the exception, all you had to do was look for it:

Challenge 6: à la mode¶

At first glance this was a pure .NET challenge for which you had to perhaps develop some of your own code as the section that did something with the flag was missing:

And the empty flag section:

The other section of this challenge, and how I solved it, was dissecting the binary inside Ghidra and figuring out what it did during its runtime.
First, the function named dllmain_dispatch
at 0x100016e4
makes a call to the function FUN_10001163
:

FUN10001163
makes a call to renamed function FUN_100012f1
-> FUN_all_available_functions_list
if there are two parameters:

This FUN_100012f1
contains a list of definitions and calls to similar functions that, at first, appear obfuscated or transformed into something illegible:

When checking any of the FUN_100014ae
functions it becomes clear that the input is processed with XOR 17:

We can now decode all these weird char strings with XOR 17 in Cyberchef to get the original name for these functions:

Doing this for all the names changes FUN_100014ae
to look the following:

The following hours I spent renaming variables in various functions according to Microsoft documentation to match their originals.
In the end, the function and sub-functions of value were stored at FUN_1000100
renamed as FUN_processes_buffer
containing the following code:

The first named function FUN_init_substitution_table
or FUN_100011ef
contained the following code that sets the substitution table for the crypto algorithm:

And a FUN_decrypt
function that took the contents of the buffer and decrypted them, using the state reached by the substitution table:

There were now two ways of solving this, either patching enough of the binary to run it and somehow get the password out of it. Or, option two: rewriting the functions this binary did to decode the buffer and the return us the contents. I opted for number two since it seemed easier to me at the time. Little did I know, that attempting to reconstruct working C++ code from a decompiled source sucks and is much more difficult than I first thought.
In the end, with some help from mryoranimo explaining compiler errors, I used the following C++ code to solve this challenge, emulating its actions and not having to check for a password at all:
C++ code to solve challenge without password checks | |
---|---|
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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
|
Then compiling this with cl /EHsc main.cpp
and running it showed both the password, and the flag:

I would later find out that the challenge name is a reference to mixed mode assembly, a programming concept I hadn't come across until then.
Challenge 7: anode¶

This challenge did my head in for a while. Cryptography is a field that fascinates me, but I don't grasp a lot of the math in it, let alone enough to break or reverse that done by it to get back to the original state/input.
At first there was a big binary that took a while for Ghidra to process; its size was 54 MB. Capital M for Mega.

When running the binary it asked for a flag, and would return try again
when the flag wasn't valid, I assumed at the time.

Running FLOSS
over the binary returned an error that there were too many lines for FLOSS
to process; a good indicator of what was to come later down the line. Within the output of FLOSS
a section of JavaScript handling the flag you entered into it came to my attention:
With the above JavaScript we can now deduct that the flag needs to be 44 characters in length to pass the initial check, but we also need to patch it to prevent the binary from exiting and skipping its actions.
mryoranimo and his galaxy-brain spent that evening reading the Nexe compiler documentation and creating a Python script via which we would could replace the user code section of the anode.exe
binary with whatever JavaScript code we wanted to.
Usage:
Python script:
Another thing that we concluded from the JavaScript is that the PRNG used in this binary is probably seeded, and will thus always produce the same results with the same inputs. we concluded this from the JavaScript because it contains statements for the math either being "too correct" or "math.random() is too random".
mryoranimo explained that it would now be possible to revert the mutations done by the binary if we'd get all the mutations that it did in order, and then walked those back. To get all the states the binary went through we changed the JavaScript case code from executing anything to printing it to STDOUT instead.
Going from this:
To now this:
To do this for all 1024 states I used some Regex magic ^(\s+)(b[\d+].*;)$
and substituting it with $1state_transition_string += "$2";
. For the remaining substitutions a find-and-replace for Math.floor(Math.random() * 256
with " + Math.floor(Math.random() * 256) + "
would suffice. Now running the binary in a terminal returned the following:
LARE 10/15/2022 7:38:41 PM
PS C:\Users\IEUser\Documents\FLAREON9\07_anode\Code > ..\binary\anode-show-states.exe
Enter flag: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
b[29] -= b[37] + b[23] + b[22] + b[24] + b[26] + b[10] + 7;b[29] &= 0xFF;b[39] += b[34] + b[2] + b[1] + b[43] + b[20] + b[9] + 79;b[39] &= 0xFF;b[19] ^= (b[26] + b[0] + b[40] + b[37] + b[23] + b[32] + 255) & 0xFF;b[28] ^= (b[1] + b[23] + b[37] + b[31] + b[43] + b[42] + 245) & 0xFF;b[39] += b[42] + b[10] + b[3] + b[41] + b[14] + b[26] + 177;b[39] &= 0xFF;b[9] -= b[20] + b[19] + b[22] + b[5] + b[32] + b[35] + 151;b[9] &= 0xFF;b[14] -= b[4] + b[5] + b[31] + b[15] + b[36] + b[40] + 67;b[14] &= 0xFF;b[33] += b[25] + b[12] + b[14] + b[34] + b[4] + b[36] + 185;b[33] &= 0xFF;
[snip]
b[22] += b[16] + b[18] + b[7] + b[23] + b[1] + b[27] + 50;b[22] &= 0xFF;b[39] += b[18] + b[16] + b[8] + b[19] + b[5] + b[23] + 36;b[39] &= 0xFF;
Try again.
Leaving us with about 1700 odd lines of JavaScript we need to reverse the order of, that looked a little like this:
Reversing the document itself was easy with the tac
command. Reversing the math done in the document took a little while longer but we got there in the end.
Say we have the following mutation that happens at the end of the chain:
The math done here can be reversed/undone by doing the math in reverse order:
b[39] = ((211 + 106 + 66 + 68 + 102 + 38 + 36) & 255)
b[39] += 627 & 255
212 = (x + 627) & 255
x = 112 ( 627 & 255 = 100) ((100 +112) & 255 = 212)
To then do this for all the 1024 or so cases can be done with the following two regex patterns:
# flip - with +
search: ^(b\[\d+\]) -= (.*);$
replace: $1 = ($1 + ($2)) & 255;
# flip + with -
search: ^(b\[\d+\]) += (.*);$
replace: $1 = ($1 - ($2)) & 255;
Which creates something a little like this:
b[39] = (b[39] - (b[18] + b[16] + b[8] + b[19] + b[5] + b[23] + 36)) & 255;
b[22] = (b[22] - (b[16] + b[18] + b[7] + b[23] + b[1] + b[27] + 50)) & 255;
b[34] = (b[34] - (b[35] + b[40] + b[13] + b[41] + b[23] + b[25] + 14)) & 255;
b[21] = (b[21] - (b[39] + b[6] + b[0] + b[33] + b[8] + b[40] + 179)) & 255;
b[11] = (b[11] + (b[32] + b[8] + b[9] + b[34] + b[39] + b[19] + 185)) & 255;
b[19] ^= (b[0] + b[35] + b[14] + b[30] + b[21] + b[33] + 213) & 0xff;
b[40] = (b[40] - (b[13] + b[3] + b[43] + b[31] + b[22] + b[25] + 49)) & 255;
b[17] ^= (b[41] + b[14] + b[43] + b[6] + b[7] + b[28] + 196) & 0xff;
Putting this into a JavaScript program looked the following:
Running this returned the flag:

And to make sure, entering this flag into the challenge binary returned congrats
:

Challenge 8: Backdoor¶
From my time spent on Twitter lurking at what others had to mention about the challenges, it seemed that Challenge 8 was the hardest one of the bunch. This turned out correct as neither Yassir, mryoranimo nor I managed to solve it.
I didn't actually look at this challenge a whole lot, as at the time when we got to it I lost a bit of motivation to work on only Flare-on day-in-and-out.

Conclusions¶
All in all. I made it way further than I could have ever dreamed of going into flare-on pretty much blind and without any prior experience. Battling my way through the 7 challenges that I managed to complete felt rewarding once getting the hints in the challenge name or description and understanding what I had been looking at.With this positive experience behind me, I am looking forward to what next year's Flare-on has in store for me.