This year, I happened to finally have a chance to be in a good position to play Flare-On CTF, a yearly CTF published by FireEye. This year’s edition offered 12 reverse-engineering challenges to solve in 6 weeks.
This post is mostly a dump of the notes taken during all the challenges. Link to challenges and scripts are also given.
For quick jump:
All the challenges are in the ZIP file that you can download here.
My complete arsenal was (in no particular order):
- Modern-IE Windows VM
- IDA Pro
- CFF Explorer
- AIP Monitor
- SysInternals Suite
- Binary Ninja
- GDB + GEF
- Python modules:
- Interactive Delphi Reconstructor
And a lot of C and Python snippets…
By checking the HTML source code, we see:
Classic ROT-13, can be decoded by:
is a small PE that reads
what a buffer from stdin and chain-xor it in reverse (with an IV set to
function at 0x00401000) and then compared to an
encoded_key located at
It’s a classic simple XOR encoding challenge, the script IgniteMe.py was used to decode it :
greek_to_me is a PE file that will
start by binding and listen tcp/2222, and receive 4 bytes from the socket. This
value read will be used to decode the instructions at 0x40107c to 0x4010ee:
Being lazy, I’ve reconstructed this C script from IDA decompiler which allowed me to perform simply a brutefore locally:
With those keys, we can re-run the binary by sending those value (properly encoded) to the socket on tcp/2222:
which will show as a response:
But by setting WinDBG to break at 0x040107c and by passing the correct decoding key when prompted, a whole new code shows up:
Revealing the key to this level.
This challenge was very fun at the beginning, but the last part really sucked:
notepad.exe is a small PE that by all
appearance spawns Windows classic
notepad. I was fooled for a bit at first by
the instruction to this challenge, I expected a malware or something hostile,
but it is nothing of the sort. Disassembling the
start in IDA shows a bunch of
So I created the folder
flareon2016challenge and spawned
clearly showing that
notepad is looking for something in this
we discover that the loop at 0x10140B0 performs
classic file lookup in directory,
and calling the function at 0x1014E20 when a file is found. That’s where stuff
notepad maps the file in memory, checks if it started with
MZ, gets the
value at offset 0x3c, then jump to
the offset and checks if the mmaped memory at this offset is equal to
looks like it is searching for one or more valid PE executables in the
flareon2016challenge folder. It does a few extra checks (is it Intel machine
in PE header, etc.) and if everything passes, calls 0x010146C0.
This function will take the timestamps from
of the current program (
notepad.exe) and the PE file mapped to memory. If
those 2 values are the ones expected, then 2 functions are called successively:
- Function @ 0x1014350 which will format the timestamp of the mapped file and
- Function @ 0x1014BAC which will open a file
flareon2016challengefolder and write 8 bytes from some offset in the mapped file into it.
Or in horrible pseudo-code:
So now we know how the decoding key is built, but we don’t know which PE to use. This guessing game made me lose too much time. The hint was to use 2016 PE files from last year’s FlareOn challenge.
In the many folders of
the FlareOn3 archive
(pass: flare), we could find several PE files whose timestamps match perfectly
with the ones we are looking for. All we need now is drop those files in the
flareon2016challenge directory, and tweak
notepad.exe to update its
timestamp. After 4 executions we get the
key.bin file properly filled:
And after updating
notepad to the last PE timestamp, we get:
The binary starts by initializing the PRNG with the current timestamp, then allocated a 0x240 in the heap, and starts populating it randomly. It then enters a loop of game, where the player (us) have 0x64 attempts to win the game.
Inside the loop, the function
play() (at 0x4038d6) is called and will print the game grid
and display whether your shot was hit or miss. The coordinates themselves are
read from the function
enter_coor() (at 0x40377d).
So if we want to win, we need to
- disable the randomness of the game board
- determine which values are being compared when we set coordonates
To disable the randomness, I simply used
LD_PRELOAD variable against a
homemade shared library that will override calls to
rand() to a
With randomness out of the way, our board game with the position of all the ships will be the same at every runtime.
draw_grid() called with a pointer to the game board as
parameter. By reading it, the function knows how to print a cell (empty, full)
and therefore knows the configuration of the board.
This is a bitmask representing the position of the board: to make easier I wrote a Python function to convert this value into a list of position on the board:
We get 2 things: one, we have all the positions for the ennemi boats; two, the disposition of the boats on the board forms an ASCII letter (here ‘F’).
By advancing through all the levels, we can collect more letters:
- 0x0008087808087800 → “f”
- 0x008888f888888800 → “h”
- 0x7e8181f10101817e → “g”
- 0xf090909090000000 → “u”
- 0x0000f8102040f800 → “z”
- 0x0000000905070907 → “r”
- 0x7010701070000000 → “e”
- 0x0006090808083e00 → “j”
- 0x1028444444000000 → “v”
- 0x0c1212120c000000 → “o”
Reaching the final level and entering the valid positions of boats gets a message:
By simply applying this formula, we find the result to be
which when in uppercase ROT13-ed gives
BUTWHEREISTHERUM. Give this password as
input, and after a bit of computation time obtain the key to finish the level:
payload.dll is a PE32+ DLL x86-64. The
DLL doesn’t sweat much info out of the box, so I decide to use both dynamic and
static analysis. Although the static part is perfectly handled by IDA, I wanted
the dynamic analysis to be custom so I had to make a small loader for this
Since the notation is stdecl, the arguments are passed to registers in the following order: rcx, rdx, r8, r9
With this simple library loader, I have an accurate way of invoking any location withing the DLL and display runtime information directly inside WinDBG.
IDA quickly pointed me to the function at offset 0x5A50 - which I’ve called
Func3(). The loop at 0x180005B05 is a simple
strcmp() like loop comparing
arg1 (that we control) to a value from the DLL.
When WinDBG break at this location, we can get the value of the value our argument is compared to:
Using the loader, we can now invoke this function easily:
Which when compiled and executed triggers to display the following MessageBox:
We get one letter of the key! Good start, but how could we get more? And why do we get the 26th character? To know that we must understand the function 0x180005D30:
This function gets a pointer to the Export Directory table then calls the function 0x180004710:
Or better in pseudo-code
Since FlareOn goes from September 2017 to October 2017, the possible return
values are 24 if executed in September, or 25 if in October. We know why we
key now, but we don’t know where the passphrase comes from. This is
done in the function 0x180005C40 that will do the decoding of a part of
at index given by the return of function 0x180004710.
So to get the keys, we must decode all sections in
The following passphrases are collected:
And then force calling the
Func3() function with the specific password:
That will print out successively the key parts via successive
which translated gives
zsud.exe is a PE32 binary. Running
binwalk against it immediately shows 2 things:
- this binary is C# compiled
- it embeds a DLL
flareon.dll, can be easily extracted with a simple
dd command, and
shows some strings like “
soooooo_sorry_zis_is_not_ze_flag”, but not really
interesting (yet). Debugging the binary with
dnSpy gives a whole new view as to what
it’s doing: the function
Smth() receives a Base64 encoded string, which once decoded is
AES decrypted with the key “
soooooo_sorry_zis_is_not_ze_flag”. The result is a
Powershell script that is being invoked, and that is another maze game, entirely
written in Powershell. The script can be downloaded here.
The game is an escape room, so it would make sense that the flag will be given to us if we escape! And since it’s a maze, we need to find the proper directions, which comes into 2 parts.
First part of the directions
Getting the first part of the directions is relatively simple.
so it is possible to bruteforce the first directions by generating HTTP requests
and analysing the output:
And we start getting the beginning of the path:
Second part of the directions
By following the directions found above, we end up in the “infinite maze of
(confirmed by the PowerShell script). The
cubicles are linked through random connections to one another. To find the way,
we must be able to predict the
line 431 we
see that if we transfer the key (located in the desk drawer), the script will
trigger a call to
srand(42). The implementation of
msvcrt::rand() is an
known algorithm that goes along the lines of
Which now makes the path predictable, and we get the final directions:
If we now follow the entire directions found above
ewwwdundundunsuneunsewdunsewsewsewsewdun, we get the final message
RIGHT_PATH!@66696e646b6576696e6d616e6469610d0a, so the complete answer to the
But still no flag. The hex-encoded block right nexto
RIGHT_PATH says to:
By going back to the Powershell script using Powershell ISE, we notice that the
only place Kevin is mentioned is in the function
Invoke-Say(). We then seek the function
Invoke-Say() and force the
if branch to be taken by setting the
variable to not None, and the
$key to the path we found:
Then execute only this portion of code to see:
Which unhexlified gives the flag:
This really fun challenge offers an Android APK
flair.apk. The static analysis was
exclusively done with JADX and I used the awesome GenyMotion + JDB for the dynamic analysis.
This app presents itself as a traditional Android app,
You can get the final flag by solving the 4 mini challenges:
1. Micheal 2. Brian 3. Milton 4. Printer
JADX, we can reach easily the method
simply solve com.flare_on.flair.Michael.checkPassword():
Which trivially gives us the first answer:
jdb, it is possible to break at any location inside a running Android
app. JADX shows that when the validation button is clicked on, the method
com.flare_on.flair.Brian.teraljdknh() is called and checked for success. This
function is a simple
memcmp()-like function, so we can break on it and dump
We get the answer:
Milton class, we can see that the input field is not enabled unless the
rating is equal to 4 (i.e. give 4 stars).
onClick event will call the method
breop(<given_password>). That method
will compare our input with the result of the call to the function
nbsadf() does nothing but call
So let’s break on that with jdb:
intr holds our answer:
A rich man is nothing but a poor man with
money. Once decoded, we see that
Stapler.poserw() is nothing more than a SHA1
So the answer is
The check in the
Printer class takes the same principles than the ones covered
Milton. After deobfuscation, we can see that the check is also performed
So use jdb to break and dump the values
And we get:
The challenge is in a text file
remorse.ino.hex. This format
is frequently used for sharing encoded firmwares, and so the
module provides a useful script to convert it back to binary
hex2bin.py). From the string inside the firmware, we learn that this firmware
is meant to be
a Arduino Uno board. This
board embeds an Atmel AVR 8bit CPU, running at 16MHz. Easily
enough, Google points us to the datasheet of the processor.
Being totally new to AVR, I stop the challenge at that point for long enough to
read a good part of the datasheet, which proved to be extremely useful for the
rest of this exercise.
With a much better understanding of AVR, I setup a SimAVR environment and also
simduino, which allows me to connect a GDB to it, and debug the runtime:
Simduino will open a /dev/pts that can be used for UART (so we can use tools
minicom to debug it).
The firmware seems to be expecting a new PIN configuration: luckily I came accross this information in the datasheet (“35. Register Summary”).
After trying to manipulate the PINB and PINC (resp. at offset 0x23 and 0x26) without success, I saw that a change of value in PIND (offset 0x29) immediately provoked a response from the firmware:
Since the possible values are limited to 1 byte (8bit), and being lazy I wrote a GDB script to bruteforce all the values
And then I use
xdotool to programmatically send the right xkeysyms commands to
the GDB terminal:
Went for a coffee, and when back saw the pleasant screen:
This challenge was a good reminder that reading the documentation first kept me from spending probably hours of not understanding how the CPU was getting input/output data from the PIN or what the ABI was doing. So more than ever, RTFM!
Another guessing game type of challenge. The challenge comes as a PHP script
shell.php. It was solvable in 3 different steps:
Step 1: get the key length
This script is a mess so the cleaned version was pushed here.
This challenge is not about cracking the MD5 hash given, but reversing the way
$block is manipulated with the XOR operation. We don’t know the
$param, including its length. However, we do know that after L4 the
strlen($param) will be in [32..64]. Additionally, we know after this line that
every byte of
$param is in the hexadecimal namespace (“0123456789abcdef”). And
finally, because of the call
line 15, we know that the block once de-XOR-ed will have all bytes in
Now the guessing game starts: we must guess at the same time the length and the key. So the idea is in pseudo-code
This gives us a good iteration pattern, allowing us to narrow down all possible
values and find the possible length for the key, as done in
Unanimously, we find that if the length of
$param is 64 bytes, we have at
least one candidate that ensures that we can de-xor
$block and get ASCII back
for each byte of the key.
$param = md5($param) . substr(MD5(strrev($param)), 0, strlen($param));
strlen($param) == 64, it means that our key
o_o is 32 byte long, which
way too huge to bruteforce. Consequently we must unxor the block by another way,
without knowing the key.
Step 2: unxor all the blocks!
The Step1 allowed us to get the key length along with a list of potential candidates for each position ([0, 63]). This 2nd step directly extends the earlier one by trying to bruteforce chunk by chunk.
This will be the main idea:
I used Python’s
itertools.product to generate all the candidate blocks, and
little by little recovered the value for
After a few iteration, it appears that the encoded block contains not just pure PHP but also HTML, which allowed me to perfect the condition for finding a valid candidate
After many iterations, we get the value for
Entering the correct value for
$param found in step 2 allow us to discover the
decoded script passed
And back to square 1, we have 3 new base64-encoded blocks to decode. Depending
on the value given in the
$_POST['t'] (can be ‘c’, ‘s’ or ‘w’), will split the
key every 3 character, starting from index 0, 1, or 2 (respectively).
I took a huge assumption here, which was that
$key would be the flag to end the
challenge. Therefore, even though we don’t know its length (yet), we know that
it ends with
So for this step, I used the same technique than step2 but split the key every 3 characters and see if the block of byte was successfully decoded.
Just like step1 this approach gives us 2 possible length for the flag prefix
@flare-on.com): 8 or 9 bytes.
So there again, semi-manual bruteforce:
We quickly notice that the output has some HTML in it, so we can discard candidates with invalid HTML patterns. For example:
Only code with key=AAAAAAAA4froc makes most sense so it must be it. So we’ll
assume this is how the key ends, and bruteforce the byte before, and so on, and
so forth. Reiterating this for all bytes, we get the first subkey to be
And reiterating the exact same thing for the 2nd and 3rd base64-encoded block and we get all the subkeys:
This challenge was out of space! And so fun! It comes as a PE32 file
The most notable string (http://bitly.com/98K8eH) from the PE points us nostalgically to Rick Astley timeless masterpiece, “Never Gonna Give You Up”.
Many other strings appear, but are weirdly aligned to one DWORD per character:
covfefe.exe is very simple, and only asks for finding a correct
password. The PE itself only:
- randomly chooses an integer in [0, 9[ and store in 0x0403008+0x110*4
- starts the VM itself at 0x0403008, and jumps to it
The VM is an array of
logique_addr_in_pe = 0x0403008 + relative_addr_in_vm*4
The execution of the virtual machine starts at
pc_start = vm + 0x463. And each
instruction is executed in the same way:
Since the code is super easy, I decided to recreate the C source code from it. So first, I used WinDBG to dump the VM location:
And used this to create a C script that would run the VM as well. The reason for that is that now I can set breakpoint and analyse the VM more precisely. I also used Binary Ninja to write a new custom architecture. The reason for that being that it greatly helped tracking down operations at the bytecode level of the VM.
We know that we must provide a good password to validate the task. So there must be a comparison that fails as soon as a wrong character is entered. Those new tools were of great help to identify the culprit: the comparison instruction is done in the block at 0xde6.
Now that we know that, all I need was to use the C script to “set a breakpoint” at 0xde9 and see what value was expected.
Knowing this, creating the bruteforce script (cov.py) was the next immediate step:
And finally recover the key to this level =
This level alone could have been an entire CTF. It came as 2 files:
coolprogram.exe is a Borland compiled PE file that is nothing more than a
stager to download and execute the real payload. Using API Monitor, we can trace
that it attempts to connect to FQDN
maybe.suspicious.to, checking also that
the domain name doesn’t point to the localhost
The behavior seems consistant with the first TCP stream of the PCAP. However, the data received seems encoded/encrypted:
IDR and IDA helped identify the “real main” function to be at 0x04103DC, which performs sequentially the following operations:
- unxor the URL from memory: the URL is located at 0x04102B4 and xor-ed with 0x73
- perform the HTTP GET request to get the
- decode the buffer, recovering a valid PE file,
secondstage.exeby hollowing the default HTTP browser
Instead of decoding manually the encoded response from the C2 server, we can be
lazy by recovering
secondstage.exe breaking at 0x4104C1:
Initial analysis secondstage
Thanks to CFF Explorer, one can easily edit
secondstage.exe PE header to
deactivate the randomization of the code by
and rebuild the header.
secondstage analysis starts at 0x405220 by initializing a bunch a stuff,
including loading all dynamically loaded functions into an array of points,
ensuring a bit of obfuscation during static analysis, since all function calls
will be performed by indirect calls. Then if the executable is run on
client-side, initiates the connection to the C2 server:
Every time a packet is received the function 0x0402C50 is called for parsing the
new message, and sending the answer back. The C2 is still behind the FQDN
maybe.suspicious.to which in the PCAP file is associated to the IP address
Reversing the communication protocol
A big part of this challenge consisted in understanding the protocol, because once entirely assimilated, every piece of code would fall into place.
An initial glimpse into the second TCP stream of the PCAP reveils already many valuable information regarding the protocol:
- it is a non-standard (i.e. custom) binary protocol
- it is (for most part) non encrypted
- some parts of the header can be instantly recognized (magic=’2017’, the size of the header, size of the data, etc.)
- it transmits some PE code (presence of strings like “text”, “rdata”, “reloc”, “kernel32.dll”, names of methods, etc.)
The function 0x403210 reveals a whole deal regarding the protocol: when a new packet is received, the function ensures that its length is at least 0x24 bytes, and that the first 4 bytes are equal to “2017”. This will be the aspect of the first 0x24 bytes of header:
What the hell are those modules? What is their magic number?
To understand that, I wrote a “replayer” that would spoof the C2 IP address, and
replay all the packets to the instance of
secondstage. After a few packets,
!address showed that some new memory areas were allocated in the address
space, all with
PAGE_EXECUTE_READWRITE permission, all starting with
LM.... Searching for the constant 0x4d4c (‘LM’ in little endian), IDA spotted
004053CE cmp edx, 4D4Ch, which happens to be followed by a
PAGE_EXECUTE_READWRITE (0x40) set for
permission, then a
LoadLibraryA. This must be it, so we can now use WinDBG to dump all those modules:
8 modules were found. Each of them can be convert back to a valid PE format by replacing “LM\x00\x00” with “MZ\x00\x00”, and “NOP\x00” with “PE\x00\x00”. Finally the entrypoint must be xored with the value 0xABCDABCD.
Reversing the “Loadable Modules”
All those modifications give us 8 DLL that are sent by the C2 and loaded in
secondstage, with the following names in them
Using Diaphora to bin-diff those DLL showed that they are 99% similar, except for a handful of functions. So naturally I focused reversing only those functions.
In all DLLs (and even
secondstage), one function could always be found doing
Which appears to be the function called when a packet is received, and that the “magic” field matched to the DLL. Symetrically, another function could be found, but this one to build a response packet from this module. Reversing all those modules could be summarized in the table below:
|secondstage.exe||51298F741667D7ED2941950106F50545||Handles basic packets handling, loads modules, sends MessageBox messages, stop process, etc.||*|
|6.dll||BA0504FCC08F9121D16FD3FED1710E60||Base64 (with custom alphabet) implementation||COMP|
|f.dll||F47C51070FA8698064B65B3B6E7D30C6||didn’t see the need for reversing||?|
3 types of plugin actions can be found (as detailed by 0x04025DF):
CMD: send and receive command to the client (get OS information, execute command in terminal, etc.)
CRPT: cryptographic operation
COMP: compression operation
And here is where the header field
DataSize2 (at header+0x10) comes in handy:
actions triggered by crypto or compression modules can produce an output whose
length is different from the original
header.DataSize. So the field
DataSize2 indicates the size of the output after the cryptographic or
compression operation has been done. Although some crypto operations were used, the key (and IV when needed) could
always be found in the message header.
Chaining modules together allows to create some pretty complex
output (for example
Base64( zlib_deflate( XTEA(data) ) ) ), that would be
absolutely impossible to reverse correctly, solely with the static analysis of
the PCAP file. So if we want to reconstruct the data, we must write a parser at some point to
parse the data of the PCAP (the final version of the parser can be found here).
Reconstructing the screen capture
m.dll captures the desktop as a bitmap and send the raw data back to the C2
(uses the same function as
MSDN example). But
because it is a pure bitmap, there is no information of the dimensions of the
image. In addition, the image is split in several packets, some of them are sent
in plaintext, like this
Whereas others are compressed and/or encrypted by the different algorithms mentioned above. However, they are all sent sequentially. Once all the fragments extracted by the parser, they were merged into a raw file. Thanks to a good tip by alex_k_polyakov , I used the website RawPixels.net, and when setting a resolution of 1420x720, the following capture showed up:
After all those efforts, finally a good lead on the challenge to find.
More Loadable Modules !!
Continuing the replay of packets showed something very interesting:
secondstage.exe was sending commands to a child process
to reach a host whose NetBIOS name is
larryjohnson-pc, and if found, would run
drop 2 files in
srv2.exe. Finally it would execute
pse.exe is nothing more
so the command would push and execute
srv2.exe as the user
all went well,
secondstage.exe then attempts to load a new Loadable Module,
p.dll, whose magic is 77D6CE92347337AEB14510807EE9D7BE. This DLL will be used
to proxy the packets from/to the C2 directly to
addition, the C2 then sends a few new Loadable Modules to the running
|b.dll||2965E4A19B6E9D9473F5F54DFEF93533||Blowfish implementation (CBC Mode)||CRPT|
|d.dll||46C5525904F473ACE7BB8CB58B29968A||DES implementation (CBC Mode)||CRPT|
|c.dll||9B1F6EC7D9B42BF7758A094A2186986B||Camellia implementation (ECB Mode)||CRPT|
Smart parsing of the PCAP
It is altogether 15 Loadable Modules that are needed to be implemented for decompression or decryption. In some cases, the implementation of the algorithm was not standard (for example RC4), so I had to rewrite from scratch according to the reversed DLL solely. Particularly the ApLib module was a pain to use properly.
But it was critical that our implementation strictly stick to the one from the module. So a lot (a LOOOOOT) of testing was required all the time, as even a one byte mistake could make the content of a packet unreadable for the upper layer, leading to not be able to decrypt files later on…
But after some long hours perfecting the decrypting script, the result pays off directly, and all traffic is now in plaintext, revealing some crispy information:
2 new files can be found from the extract:
cf.exea C# compiled file
- a 561972 byte file beginning with the pattern
cf.exe doesn’t show much mystery: it takes 2 parameters, a path to file, and a
base64 encoded key. And it will AES encrypt the file with the given key.
As seen in the capture above, we were capable of decrypting the packet that holds the command used for encrypting the file.
So we can build a decryptor in few lines of Python
We’ve got the real challenge!
And to conclude, unzip
lab10.zip with the password from the screenshot:
infectedinfectedinfectedinfectedinfected919. This will drop a file in
GoChallenge/build/challenge10, which is a Go challenge in ELF. But when we
execute it, we see a well deserve reward:
Thank you to FireEye for those fun challenges… and congratulations to all the winners (especially those who managed to finish in under a week, massive props)!! I hope those writeups don’t make those challenges look trivial, they weren’t (only ~130 over more than a thousand participants completed the 12 challenges). IMHO, some challenges (like the end of challenge 4 or 10) involved too much guessing, which can be very (VERY) frustrating.
But all in all, it was a fun experience… And thank you for whomever prepared challenge 12, it was huge in all the possible meanings, and it must certainly have required a serious patience to build!
See you next year for Flare-On 5!
Share this post: