Hi there! This year is my first time taking part in Flare-On CTF. This year’s Flare-On was held from 1st October 2022 to 12th November 2022. There are a total of 11 challenges in this year’s Flare-On. Do download and reverse engineer the binary as my write-up doesn’t include screenshots the whole reverse engineering process. Otherwise, this post will get too messy. You can get all the files for the Flare-On 9 CTF here. The “Personal Files” folder will contain my own files for each challenge to help me obtain the flag. Therefore, if you want to give each challenge a try, just ignore the “Personal Files” folder. Let’s get started!
Summary of 2022 Flare-On 9 challenges:
- Flaredle
- Web challenge that requires us to read a PHP source code to determine what to input to get the flag.
- Pixel Poker
- A graphical Windows-based application challenge where we need to reverse engineer the windows procedure functions for a specific pixel’s coordinate to click to obtain the flag.
- Magic 8 Ball
- A graphical Windows-based application challenge that uses the SDL library where we have to reverse engineer the directional keys sequence to press and the string input required to get the flag.
- darn_mice
- A Windows console-based application challenge where we have to input the correct value as console argument. The value will be encoded before written as a function’s content and gets executed. Thus, we have to make sure the value after encoded is valid for function execution.
- T8
- A C++ malware application where we will have to pass a number of conditions. Object Oriented Programming knowledge is required for this challenge.
- à la mode
- A C# application mixed with C hidden functions in the program. We are required to find the hidden functions and obtain the flag.
- anode
- EXE file compiled from JavaScript using Nexe. We are required to input the flag. Therefore, a script has to be created to reverse the math operations to obtain the flag based on the final encoded value stored in the program.
- Backdoor
- A .NET packed Saitama-like application using dynamic methods to hide methods from users and decompilers. Unpacking is required before reverse engineer the program to find out the correct commands to send it to obtain the flag.
- Encryptor
- A ransomware program was used to encrypt the file containing the flag. We are required to figure out there are two layers of encryption which are RSA and chacha20. RSA was used to encrypt chacha20’s key, counter, and nonce’s values. We will have to figure out there is a misconfiguration in the RSA implementation to decrypt chacha20’s key, counter, and nonce’s values.
- Nur geträumt
- A 90s macOS’s binary challenge which requires us to setup mini vmac as part of the challenge. This challenge also requires us to discover the img filename must not contain unicode characters. Once loaded, we have to input the correct password to obtain the flag. Input will be encoded with a value to get the flag thus we have to figure out the encoding algorithm and the song whose lyrics is part of the password.
- The challenge that shall not be named.
- EXE file compiled from Python using PyInstaller. Pyarmor is used to obfuscate the code. We can either use resource hacker to dump memory or use module hijacking/injection to obtain the flag.
1. Flaredle

A total of four files were provided which contain the source code of the website. You may download them here.
1.1 Analyze and obtain the flag

We can see that it is a wordle-like website. Taking a quick look at the source code, what we are interested in is script.js and words.js. The script.js contains the logic that checks if it matches the correct word from a list of words in words.js.
Looking at script.js, I found an interesting function which does check if we got the right word.


Seems like the correct word is the substring of the flag. We will need to see what sets the rightGuessString
variable. CTRL+F allows us to find that the 57th index of the WORDS
variable from words.js is the flag.

A quick search allows us to find the string and giving it a try on the website determined that we have found the flag.


2. Pixel Poker

In this challenge, two files are provided which you can obtain here. The link also includes the IDA Pro 7.7 database of the binary I have reverse-engineered.
2.1 Overview
readme.txt
Welcome to PixelPoker ^_^, the pixel game that's sweeping the nation! Your goal is simple: find the correct pixel and click it Good luck!

Based on the readme.txt, we have to click on the correct pixels to obtain the flag.
2.2 Reversing with IDA Pro
Dumping the binary into IDA, we can see that it is a 32-bit program and it uses Microsoft Visual C++ (VC++) development environment. In wWinMain()
, there is nothing interesting and is just generating the basic GUI of the program. What we are interested in is a Windows Procedure function inside register_mainclass_window (note that the screenshots contain user-defined functions I renamed).


While running with a debugger, I noticed that modify_pixel()
at line 80 is always called while line 59 isn’t. This makes me curious why isn’t line 59 being executed and what conditions are stopping it. Besides that, I was able to identify that modify_pixel()
modifies of the pixel based on the coordinates given by reversing that function.

2.3 Found the condition checker and obtain the flag
Since we know that modify_pixel()
modifies pixel based on the coordinates given in the function argument, I noticed that line 59’s modify_pixel()
take in incrementing coordinates X and Y. This is most likely the pixel’s coordinate we need to select so that the pixels will be modified to turn into a flag.

dword_412004
and dword_412008
values are static hence I was able to obtain them via IDA. However, max_x_coordinate
and max_y_coordinate
cannot be seen. Hence the easy way was to run it runtime on IDA and check its values. I also noted those two variables’ values are always fixed.

Clicking on the pixel at coordinate 95,313, I was able to obtain the flag.

3. Magic 8 Ball

In this challenge, the main binary and its libraries are provided which you can obtain here. The link also includes the IDA Pro 7.7 database of the binary I have reverse-engineered.
3.1 Inputs
There are a lot of functions that are pretty confusing, but after some reversing and running with a debugger, the most important function, checker, is located at the address 0x4024E0.

Looking at the checker function, we will see the arrow direction’s letter is appended to a string, arrow_sequence_string
, based on the arrow direction we pressed. This was discovered by testing with a debugger. But you can match the object variables from the event_handler()
if you want.

3.2 Conditions and obtaining the flag
Scrolling down the checker function, we will see lots of conditions and another function call that decodes the encoded flag. Using the debugger, I was able to identify that to pass the first condition, we must press the ENTER key. The subsequent conditions are based on the sequence of arrows we pressed which the string, arrow_sequence_string
, was constructed based on what I just talked about.

If we managed to clear 9 of the arrow key sequences, there is still another condition to check the condition of the input string. Thus, I used a debugger to check what is the string that we have to input to match.


We can now obtain the flag by pressing the arrows in sequence order (L, L, U, R, U, L, D, U, L), typing in “gimme flag pls?” and then pressing enter will give us the flag.

4. darn_mice

In this challenge, a binary is provided which you can obtain here. The link also includes the IDA Pro 7.7 database of the binary I have reverse-engineered as well as a Python script to compute the required string to pass the condition.
4.1 Number of arguments condition
Dumping the binary into IDA Pro, we can immediately see there must only be 1 argument given in the terminal.

4.2 Final conditions and the flag
Looking at process_arg1()
, there are two conditions we have to pass. The 1st condition is not to supply a value of 0 as it will pre-terminate the FOR loop. The 2nd condition is each element + each char in our input will be used as opcode to be executed in v2()
. The first condition is easy to fulfill. The 2nd condition we have to give some thought to it. As it is a function call that only accepts 1 opcode, NOP (0x90) is a bad idea. The best is 0xC3 (retn) so that v2()
can execute without any issue and return back to process_arg1()
.

Hence, I created a Python script, cal.py, to help me compute the string I need to input so that the computed value will always be 0xC3 (retn).
v5 = [ord('P'), ord('^'), ord('^'), 0xA3, 0x4F, 0x5B, 0x51, 0x5E, 0x5E, 0x97,
0xA3, 0x80, 0x90, 0xA3, 0x80, 0x90, 0xA3, 0x80, 0x90, 0xA3, 0x80, 0x90,
0xA3, 0x80, 0x90, 0xA3, 0x80, 0x90, 0xA3, 0x80, 0x90, 0xA2, 0xA3, 0x6B,
0x7F]
arg1 = ""
for num in v5:
arg1 += chr(0xC3 - num)
print(arg1)
Based on the output string, I can guarantee it is correct.

Inputting the string into the binary, I obtained the flag.

5. T8

In this challenge, a binary and a PCAP file are provided which you can obtain here. The link also includes the IDA Pro 7.7 database of the binary I have reverse-engineered.
5.1 Passing the date condition
At the start of the program in _main()
, I noticed that the program will only continue execution based on the result of some_math_calculation()
that is date dependent.

Looking at the PCAP file, our PC’s date should set to 14 June 2022 since the PCAP file showed that that day the program worked with network traffic discovered.

You can also brute force it by modifying the decompiled code of some_math_calculation()
. Set the variables to your current day of the week and month then using the decompiled code in Python, , it will brute force the year to use so that some_math_calculation()
returns 0xF.
# Decompiled version (modified) of some_math_calculation() @ 0x00404570
import math
year = 2000
while True:
day = 4
month = 10
year_final = year - 1
if month > 2:
year_final = year
v4 = month + 12
if month > 2:
v4 = month
systemtime_12 = (int(year_final//100//4 + day + int((year_final + 4716) * 365.25) - int(int(v4 + 1) * -30.6001) - year_final//100 + 2) - 1524.5 - 2451549.5) / 29.53
v5 = math.floor(systemtime_12)
result = int(round((systemtime_12 - v5) * 29.53))
# print((systemtime_12 - v5) * 29.53)
# print(result)
if result == 0xF:
print("Year: " + str(year))
break
else:
year += 1
5.2 Discover encryption/decryption key and set them
After passing the date condition, we will notice that the program creates a CClientSock object where vtable etc are initialized. These vtable functions are later called throughout the program.
Going down the _main()
, we will see that random_value_via_time
‘s value will be converted into string and concatenate with “FO”.


This “FO<random_value_via_time>” string will be MD5-hashed in CClientSock::to_hash_n_store_in_CClientSock_obj()
and store in the CClientSock_obj object’s +0x2C offset variable. The MD5-hash value will be used as an symmetric encryption/decryption key for the HTTP POST request value and response’s value.
At point 1 below, the string “ahoy” will be the string/command send to the C2 server, flare-on.com, where it will be encrypted with the MD5-hash key then base64 encoded before sending to the C2 server in CClientSock::send_receive_http()
. At point 2 below after receiving the HTTP response, the response will be base64 decoded, decrypted with the same MD5-hash key, and a final decryption is done to obtain a plain-text string which is the first half of the flag. All these will be done in CClientSock::translate_http_response()
. The actual encryption/decryption is done by CClientSock::encrypt_decrypt_wrapper()
which is called by these two functions.

Therefore, random_value_via_time
plays a huge role in generating the encryption MD5-hash key. Since it is MD5-hash, we will not be able to reverse back to find the original “FO<random_value_via_time>” value. Besides that, random_value_via_time
is randomly generated thus it is hard to brute force.

So how do we figure out? Actually, if we reverse engineered CClientSock::send_receive_http()
, we will see that random_value_via_time
is placed in the User-Agent!

Therefore, random_value_via_time
should be equals to 11950.

As a result, we can run the binary in the debugger, set breakpoint and modify the value of random_value_via_time
before it is used to create the MD5-hash key and also set a breakpoint before CClientSock::translate_http_response()
before processing of HTTP response so that we can modify the location that is suppose to store the base64 response from the C2 server.
bp 404830 g ew 450870 2EAE bp 4049B2 g dd edi+44 eza <address> "TdQdBRa1nxGU06dbB27E7SQ7TJ2+cd7zstLXRQcLbmh2nTvDm1p5IfT/Cu0JxShk6tHQBRWwPlo9zA1dISfslkLgGDs41WK12ibWIflqLE4Yq3OYIEnLNjwVHrjL2U4Lu3ms+HQc4nfMWXPgcOHb4fhokk93/AJd5GTuC5z+4YsmgRh1Z90yinLBKB+fmGUyagT6gon/KHmJdvAOQ8nAnl8K/0XG+8zYQbZRwgY6tHvvpfyn9OXCyuct5/cOi8KWgALvVHQWafrp8qB/JtT+t5zmnezQlp3zPL4sj2CJfcUTK5copbZCyHexVD4jJN+LezJEtrDXP1DJNg=="

5.3 Get the flag
At point 1, translate_http_response()
will base64 decode the HTTP response, decrypt it with the MD5-hash key, and further decode it into a proper string which is the first half of the flag, “i_s33_you_m00n”. At point 2, the next few blocks of instructions will concatenate the strings “i_s33_you_m00n”, “@”, “flare-on.com” together to form the full flag.

Hence we can set the breakpoint anywhere that has the fully concatenated string.

bp 404AAD g dd esi du <address>

You might notice that the program does not end there as there is get_shellcode_and_run()
which will do another HTTP request/response to get the encrypted base64 shellcode and execute it later. We don’t need to make it work
5.4 Summary
Summary of the binary from start to getting the flag:
- Initialize program.
- Get current date and time.
- Get random value
random_value_via_time
.
- Concatenate “FO” + str(
random_value_via_time
), MD5 hash it and use it as symmetric encryption/decryption key. - Encrypt “ahoy” command with encryption/decryption key and base64 encode it.
- Place
random_value_via_time
in user-agent and send HTTP POST request with the encoded command. - Receive HTTP response, base64 decode it, decrypt it with the encryption/decryption key, decode it to plain text flag.
- Concatenate parts of the flag into a full flag.
I would recommend you to RE the binary yourself and try to match with my IDA database for better understand as I left out a lot of my RE analysis steps and thought process of this challenge. Otherwise, my post will be too lengthy and messy.
6. à la mode

In this challenge, a .NET DLL and a README text file are provided which you can obtain here. The link also includes the IDA Pro 7.7 database of the binary I have reverse-engineered as well as the binary that helps me to execute the .NET DLL file.
6.1 Possible hidden functions
As it is .NET DLL, I dumped the binary in dnSpy to analyze it. Based on the results, there is only one visible function that tries to get the flag via a namedpipe. If we looked at the read me, we know that this is the only file available. Therefore, there could be hidden functions in the binary that can’t be seen by .NET decompilation. This is because more functions are needed to even create the namedpipe and wait for an incoming connection to get the flag.

6.2 Discovered non-referenced code
Loading the .NET DLL in IDA and switching to text view, we can see that there is a non-referenced function/code where the addresses are in red. This is likely to be the hidden functions entry point.

6.3 Create a .NET file loader
To run with a debugger to speed up our reverse engineering, we will need to create an executable that will load HowDoesThisWork.dll. Build it as x86 binary in release mode.
#include <Windows.h>
#include <stdio.h>
typedef char * (*GetFlag)(const char * flag);
int main()
{
HMODULE managedDLL = LoadLibraryA("HowDoesThisWork");
}
Once built, copy the executable to where HowDoesThisWork.dll is located before running the executable with a debugger such as WinDBG. Set break on HowDoesThisWork.dll load.
sxe ld:HowDoesThisWork.dll g

Once we checked that HowDoesThisWork.dll is successfully loaded, we can then break on HowDoesThisWork!DllEntryPoint.


Once the program reached HowDoesThisWork!DllEntryPoint, we change the EIP register to anywhere in HowDoesThisWork.dll to help us with our RE.
6.4 Figured out how functions were hidden
While looking through the hidden functions, I became interested in this function that seems to process a lot of weird strings. Using a debugger for faster analysis, I discovered that these weird strings are actually encoded strings of functions used by the namedpipe server.

I also noticed that the Windows shellcode way is used to obtain the address of these functions via names by searching for kernel32.dll’s address first before matching the function names with kernel32.dll’s Name Pointer Table (AddressOfNames). You can read more from my Windows shellcode source code here.

6.5 Found create namedpipe function and flag function
If we dereferenced from decode_function_names_and_get_functions_addresses()
, we will see that create_flag_pipe()
is executed next which handles the creation of namedpipe and sending the RPC client the flag.


There are many things going on in create_flag_pipe()
. We just need to look at decode_n_get_flag()
and get the decoded flag by modifying the EIP register to avoid unnecessary complications of other conditions to fulfill.
Looking at decode_n_get_flag()
, we can see that an encryption key will be generated that is used for the decryption of the encrypted password and encrypted flag that is stored statically. Therefore if we modify EIP to get the flag easily, get_decryption_key()
must be executed too before we can get the flag.

6.6 Getting the flag
Since we know where is the flag, we can use WinDBG to help us get the flag. Restart the debugging if you haven’t and break at HowDoesThisWork!DllEntryPoint (follow section 6.3’s instructions). Next, we will need to execute decode_function_names_and_get_functions_addresses()
to set all the global variables with the “import” functions’ address. The easiest way is to set EIP at the orange arrow location and a breakpoint at the pink arrow. We must also set the Zero Flag to 1 so that JNZ will be false.

r eip=10001168 r zf=1 bp 1000116F g

We are now ready to get the decryption key and get the flag. Set the EIP register to the address at the orange arrow in decode_n_get_flag()
. Set breakpoints on both pink arrows’ addresses. Once reached the first breakpoint at the first pink arrow’s location, we have to set the Zero Flag to 1 so that JZ will be true. This helps us to ignore the need to make [ebp+password_from_client] have “MyV0ic3!” which is troublesome. Finally, we can read the flag once the breakpoint hits the 2nd pink arrow.

r eip=10001000 bp 10001041 bp 10001079 g r zf=1 g da esi

7. anode

In this challenge, an executable binary is provided which can be found here. The link also includes the IDA Pro 7.7 database of the binary I have reverse-engineered, extracted JavaScript, Python script to help with formating data, “databases” of text files, and the reversing JavaScript file for me to get the flag.
7.1 Nexe compiled program and extract original source code
Below shows an example of running the program which prompts us for a flag.

If we set the breakpoint at node!main
and breakpoint on access on node!ReadConsoleW
in WinDbg, we will find the location of where is the ReadConsoleW()
that is getting our input for “Enter flag: “. Before doing so, it will be easier for us to set breakpoints by generating PDB using FakePDB on IDA.
p node!main g ba r4 node!ReadConsoleW g k

Looking at the function that gets our input, I noticed interesting strings as shown in the orange arrow. A quick google for “.nexe” allows me to find out that anode.exe is actually compiled from a Javascript file by nexe.

Actually, nexe stores the script by appending it to the end of the compiled binary as shown in this discussion. I decided to do a test by compiling a simple testprint.js file I made on my own with nexe and it indeed appends to the end of the binary.

Thus, we can obtain the original script of anode.exe by opening it with a hex editor and copying the whole Javascript content.

7.2 V8 randomness isn’t so random
I came across this video which shows the older version of V8 isn’t so random. I decided to try it out and indeed, it isn’t so random. You can execute my modified anode.exe, now known as showrand.exe, which shows that the random numbers are always the same when running the binary again. Therefore, the random numbers generated aren’t random.

NOTE: When modifying the JavaScript code at the end of the binary, we must not change the size. For redundant code, we will just convert them into spaces. As for the green box, I noticed that the binary has been modified by the creator such that if (1n)
always return FALSE instead of TRUE.

7.3 Getting states sequence, TRUE/False, & last Math.random()
I generated the list of states/switch-cases the program will go to so that we can reverse the flow later. Besides that, I also have to replace all of the strings as shown in the red box below with console.log()
to know which IF-ELSE branch the program goes to as we know that the binary is modified such that if (1n)
results in FALSE instead of TRUE.

Replaced the red boxes with this in 010 hex editor:
console.log("True");}else{ b[
Remember to replace all occurrences. I did it in 010 hex editor. I included “b[” otherwise the program will find an occurrence of the same string in another location of the binary which we do not intend to replace.

I also replaced console.log('Congrats!');
with console.log(Math.random());
to find what is the last Math.random()
value so that I later can reverse the list in table.txt when reversing the target’s value to get the required input value.

Finally, statesorder.txt is generated which shows which switch-cases the binary will go to and for that switch-case, did it take the TRUE branch or not.

Based on the result, the last Math.random()
is actually 0.8142892562902004. Thus anything after that, we can delete them.

Now we need to add “False” to those states. You can run this Python script, insert_false.py, to insert “False” in those states that do not have “True under them. Remember to remove extra stuff from states_and_truefalse.txt such as "Enter flag: "
and "0.3777655420365358"
.
filename = "states_and_truefalse.txt"
with open(filename) as fd:
content = fd.readlines()
new_content = list()
i = 0
while i < len(content):
try:
new_content.append(content[i])
if content[i+1] == "True\n":
new_content.append(content[i+1])
# Skip "True" in the next line
i += 1
else:
new_content.append("False\n")
i += 1
except IndexError:
# Means we are at the last case's state which we don't need to add True/False for it anyway
break
# print(new_content) # For checking and debugging the output
with open("new_" + filename, "w") as fd:
fd.writelines(new_content)
The result will look like this:

7.4 Build a reversing script
Since we know the end result, we will have to reverse the end result, target
, into the intended input which is the flag. There are a lot of considerations when reversing the process. Firstly, all equations that hash “+=” must become “-=” and vice versa.

We will need to get the list of states + the condition as well as the list of random numbers from the text files in reverse order.

Next, we will need to convert the first case that was executed its continue statement into break because since our script will be in reverse, the first case will now become the last case.

We also have to remove the state assignment in each case since we no longer need it. Besides that, we have to increment the index to traverse the list of random numbers whenever anode.js has a line of code that calls Math.random()
. Remember to put the increment code, ++rand_values_index;
, at the bottom of the switch-case because we have to reverse the process so Math.random(
) get called at the start of the switch-case will now be called later.

We can now replace the variable b
with the target’s value. The state_index
will increment by 2 in each loop as in-between each state in the list if the True/False string.

Finally, executing the script with node will give us the flag.

8. Backdoor

For this challenge, a DOTNET binary has been provided to us which can be obtained from this link. The link also includes my script to unpack the binary, the unpacked binary, the set-up file, and a Python script to help us get the C2 commands condition sequence.
8.1 Obfuscation via DynamicMethod
At the start when load the binary on dnSpy, we will noticed that a number of functions seems to have error when decompiled as shown below.

This is a trick to make people wonder if the binary is packed causing decompilation issue. Using tools like de4dot or dumping memory from WinDbg during runtime will not work. This is because the error in MSIL instructions for those functions is intended. Looking at for example in Main()
, Program.flared_38(args)
contains opcodes that cannot be decompiled due to invalid opcodes. This will actually cause InvalidProgramException which is actually intended by the author of the challenge.

Let’s look at the exception handler function, FLARE15.flare_70()
. This function will call FLARE15.flared_70()
which also has issues that will throw InvalidProgramException before calling the interesting function, FLARE15.flare_71()
.

This interesting function actually uses values from variables such as FLARE15.wl_m and FLARE15.wl_b
set during execution of FLARE15.flare_74()
at the start of Main()
, and used them to create a dynamic method and execute it. FLARE15.wl_m equals to metadata tokens. FLARE15.wl_b
equals to bytes for IL instructions. To understand metadata tokens etc as shown in the function below, you can check out this CLI documentation.


Note that dynamic methods cannot use the default methods’ metadata tokens as it must use a token valid in the scope of the current DynamicILInfo as quoted from this MSDN page. Strangely, this is not part of the CLI documentation.
To create instances of other types, call methods, and so on, the MSIL you generate must include tokens for those entities. The DynamicILInfo class provides several overloads of the GetTokenFor method, which return tokens valid in the scope of the current DynamicILInfo. For example, if you need to call an overload of the Console.WriteLine method, you can obtain a RuntimeMethodHandle for that overload and pass it to the GetTokenFor method to obtain a token to embed in your MSIL.
Incase you are wondering how did I know that wl_m
is a dictionary of metatokens, check out the documentation of Module.ResolveString(Int32).
8.2 Modifying binary
To view that dynamic method’s opcodes in decompiled form, I had to modify the binary. Based on FLARE15.flare_70()
that calls FLARE15.flare_71() to handle the exception, we can see that {e,a}
is set as argument for the dynamic method. This is the same as FLARE15.flared_70()
‘s parameters. Thus, we should be overwriting this method.

I created a python script, replace_instructions.py, to replace methods’ instructions in the binary to increase the process of modifying the binary than doing it manually. As we know that the binary has many methods that has issues, I set a breakpoint at the start of FLARE15.flare_71()
and check the call stack whenever the breakpoint triggers so I will know what methods are there to be replaced.
Below shows that the next method with issue is FLARE15.flare_66()
. We can then look at the variable of metadata tokens and bytes used as well as the method we can overwrite. Repeat these steps until the binary has been fully modified.

My script, replace_instructions.py, as already done all those. Just have to use the correct metadata token variables, code’s bytes variables, and the methods’ raw addresses to overwrite.

8.3 More obfuscation
Once the binary has been modified, we can analyze the “dynamic methods”. I set a breakpoint and noticed that a metadata token of Program.flared_38()
is generated along with its supposed bytes passed to FLARE15.flare_67()
-> FLARE15.flared_67()
that creates a dynamic method for it. Just a reminder that Program.flared_38()
was the 2nd function we saw that was called in Program.Main()
which has invalid bytes.

Therefore, we can see that stage 1 deobfuscation for some methods is done by FLARE15.flare_71()
and the stage 2 deobfuscation for other methods such as Program.flared_38()
is by FLARE15.flared_67()
.
Analyzing FLARE15.flared_67()
, we can see that this time round, the metadata tokens are obfuscated and will be deobfuscated before using for dynamicILInfo.GetTokenFor()
.

There are more switch cases below that will modify variable j but I won’t show them here.
With this knowledge, we can create the functions in replace_instructions.py to decrypt the metadata tokens and modify the code’s bytes that are for Program.flared_38()
. (You can obtain the code’s bytes during the debugger breakpoint by right-clicking on b’s value -> Show in Memory Window -> Memory 1. Then copy the code’s bytes from the hex editor view in dnSpy.)

Execute replace_instructions.py and Program.flared_38()
‘s code will now be shown on dnSpy.

However, many methods are still obfuscated such as FLARE13.flare_47()
which we have to deobfuscate. Similarly, since FLARE15.flared_67()
code is now available, it will now be used for dynamic method creation for FLARE13.flare_47()
, etc.

Therefore, similar to how we deobfuscated Program.flared_38()
, repeat these steps for the remaining obfuscated functions by setting a breakpoint, obtaining the code’s bytes, and adding them to replace_instructions.py.
There will be a period where the breakpoint seems to show the same metadata tokens and code’s bytes despite keep on pressing CONTINUE. That is when we have to execute replace_instructions.py to modify the functions and then repeat the step of breakpoint again. Even then, there will still be many methods not deobfuscated until they are executed depending on the flow of the program. Thus, we will need to deobfuscate while we reverse engineer along the way.
Another better way is to set two breakpoints at FLARE15.flared_70()
where we manually set the metadata tokens of functions we want to deobfuscate before getting its bytes.


Note that most methods are using the FAT format for their method headers. Only two methods, in the binary use the TINY header. Therefore, the offsets will be different as methods using the TINY header has their code on index 1.

You can directly use my Python code, replace_instructions.py, to deobfuscate FlareOn.Backdoor.exe. I renamed my deobfuscated binary as FlareOn.Backdoor_renamed.exe.
8.4 Reversing the binary
If you reverse the binary, you will notice that this binary is almost the same as the malware, Saitama, as both use DNS tunneling and uses the same way to set ID for the infected PC, as well as communicating with the “C2” server for commands to execute. Do check out that well-written article by MalwareBytes before proceeding.
8.4.1 Setting ID and command size
If you read the article by MalwareBytes already, you will understand that similarly, if Agent ID is not set in the flare.agent.id file, its first DNS query will use to set an Agent ID for the current PC based on the last byte of the IP address. The next DNS query will be used to obtain the size of the incoming command based on the IP’s last 3 bytes.

If you read the article already, you will know the subdomain is based on the counter value. Therefore, I set my counter to 0 while ID to -. Below shows my content of flare.agent.id file.
- 0
We can then set a breakpoint before the DNS query instruction is executed so that we can obtain the root domain that the binary will do a DNS query.

Since the ID doesn’t matter, I modified the windows c:\Windows\System32\Drivers\etc\hosts file and set it to allow the binary to set the ID as 1.
0.0.0.1 i1wivemoaaa.flare-on.com # agent-id=-, counter=0
Save the hosts file and we can just continue execution in dnSpy and wait for the breakpoint to hit again to know what is the next root domain. Below shows my hosts file where I set the IP to return a size of 2 for the incoming command. Note that the first byte of the IP that sets the incoming command size must be 128 or above which is a condition checked by the binary.
0.0.0.1 i1wivemoaaa.flare-on.com # agent-id=-, counter=0 128.0.0.2 eaam.flare-on.com # agent-id=1, counter=1. Command_size=1

8.4.2 Sending the correct commands
If we randomly set an IP for the 3rd query in the hosts file to set the command, we will notice that the program will go to FLARE05.flare_20()
. Below shows the main control flow in Program.flared_38()
where it initially gets the IP and size in point 1, and gets the command from the “C2” server in point 2, before handling the command in point 3.

If we traverse FLARE05.flare_20()
, we will see that the main method that handles the command is FLARE14.flared_56()
. We will see that similar to Saitama malware, the 1st byte of the IP will instead determine the task. However, the last 3 bytes for this malware should be equal to the string of the integer value, for example “19”.

If we look at FLARE14.flare_56()
which leads to FLARE14.flared_55()
, we will see that for example 19 and “146” will be used for some kind of decoding, and “146” add to the variable FLARE14.sh
. Looking at this method, we can understand that FLARE15.c
contains a collection of encoded commands (numbers) that checks the sequence of commands being executed.

Knowing the values of FLARE15.c
below, we just need to reverse the value of i
in “i^248” to know the sequence of commands to execute.

Therefore, I crafted get_command_order.py to help me obtain the IP addresses based on the commands to execute.
c_command_order = [250, 242, 240, 235, 243, 249, 247, 245, 238, 232, 253, 244, 237, 251, 234, 233, 236, 246, 241, 255, 252]
def get_ip(command: int) -> str:
ip = ["43", ".", "0", ".", "0", ".", "0"]
for index, letter in enumerate(str(command)):
# Only write to index 2 and onwards and only at the bytes locations
ip[(index+2)+(index*1)] = str(ord(letter))
return "".join(ip)
for command_encoded in c_command_order:
command = command_encoded ^ 248
print(str(command) + "(" + get_ip(command) + ")", end=", ")
# New line
print("")

Therefore, we can repeat the set of having a breakpoint at the DNS query instruction and get the root domains of the next counter while we set the IP for the root domain in the hosts file. Below shows my final hosts file.
# Copyright (c) 1993-2009 Microsoft Corp. # # This is a sample HOSTS file used by Microsoft TCP/IP for Windows. # # This file contains the mappings of IP addresses to host names. Each # entry should be kept on an individual line. The IP address should # be placed in the first column followed by the corresponding host name. # The IP address and the host name should be separated by at least one # space. # # Additionally, comments (such as these) may be inserted on individual # lines or following the machine name denoted by a '#' symbol. # # For example: # # 102.54.94.97 rhino.acme.com # source server # 38.25.63.10 x.acme.com # x client host 0.0.0.1 i1wivemoaaa.flare-on.com # agent-id=-, counter=0 128.0.0.2 eaam.flare-on.com # agent-id=1, counter=1. Command_size=1 43.50.0.0 f9aaaaas.flare-on.com # agent-id=1, counter=2. Command="2" 128.0.0.3 11wwwwwur928wx02jvna0giudjlxaaj.flare-on.com # agent-id=1, counter=3. Command_size=2 43.49.48.0 n7ww9wwwkgaal.flare-on.com # agent-id=1, counter=4. Command="10" 128.0.0.2 uu999995arhi2n8b20a49bc41rdjaa6.flare-on.com # agent-id=1, counter=5. Command_size=1 43.56.0.0 a400v000a1aaz.flare-on.com # agent-id=1, counter=6. Command="8" 128.0.0.3 ll111119bqbtz4iq26njviwk2w1oaac.flare-on.com # agent-id=1, counter=7. Command_size=2 43.49.57.0 js11811101aai.flare-on.com # agent-id=1, counter=8. Command="19" 128.0.0.3 ddkkkkkxpyqx7srzoqyaix5d8swkaa2.flare-on.com # agent-id=1, counter=9. Command_size=2 43.49.49.0 sdffafffsuaa0.flare-on.com # agent-id=1, counter=10. Command="11" 128.0.0.2 ffjjjjj96uy9rxo35yuql9mfkxajaad.flare-on.com # agent-id=1, counter=11. Command_size=1 43.49.0.0 igpp3pppioaab.flare-on.com # agent-id=1, counter=12. Command="1" 128.0.0.3 hhwwwwwp5xikwebih8ajr74pcnqeaat.flare-on.com # agent-id=1, counter=13. Command_size=2 43.49.53.0 5m11n111wuaa3.flare-on.com # agent-id=1, counter=14. Command="15" 128.0.0.3 99mmmmms3q5wwkwfbq52n0mmaa5.flare-on.com # agent-id=1, counter=15. Command_size=2 43.49.51.0 fpnnnaag.flare-on.com # agent-id=1, counter=16. Command="13" 128.0.0.3 55pppppn3zonel7bhozg0ns5vlqpaau.flare-on.com # agent-id=1, counter=17. Command_size=2 43.50.50.0 0ywwdwww0paah.flare-on.com # agent-id=1, counter=18. Command="22" 128.0.0.3 22nnnnnpxc9dba03wcmo8u3ngyx1aaw.flare-on.com # agent-id=1, counter=19. Command_size=2 43.49.54.0 0l99k999h2aa7.flare-on.com # agent-id=1, counter=20. Command="16" 128.0.0.2 44jjjjjbg6y77w7nq6yavsjjaan.flare-on.com # agent-id=1, counter=21. Command_size=1 43.53.0.0 dj333aa1.flare-on.com # agent-id=1, counter=22. Command="5" 128.0.0.3 mmpppppbsc2oif5qz6juil48axopaaf.flare-on.com # agent-id=1, counter=23. Command_size=2 43.49.50.0 1roo0ooob9aaq.flare-on.com # agent-id=1, counter=24. Command="12" 128.0.0.3 44uuuuu59srooxongsrja7uuaav.flare-on.com # agent-id=1, counter=25. Command_size=2 43.50.49.0 r7333aax.flare-on.com # agent-id=1, counter=26. Command="21" 128.0.0.2 tt11111ms9d363fy4idz8w7zodfjaa4.flare-on.com # agent-id=1, counter=27. Command_size=1 43.51.0.0 z6ffufff96aak.flare-on.com # agent-id=1, counter=28. Command="3" 128.0.0.3 qqfffff8z1p6obhej933felx6187aa8.flare-on.com # agent-id=1, counter=29. Command_size=2 43.49.56.0 ahvvcvvv18aay.flare-on.com # agent-id=1, counter=30. Command="18" 128.0.0.3 44000005yg1eeaejzg1qkc00aa9.flare-on.com # agent-id=1, counter=31. Command_size=2 43.49.55.0 4ftttaar.flare-on.com # agent-id=1, counter=32. Command="17" 128.0.0.3 kkaaaaa3dug11514lugmwvaaaap.flare-on.com # agent-id=1, counter=33. Command_size=2 43.50.48.0 86vvvaao.flare-on.com # agent-id=1, counter=34. Command="20" 128.0.0.3 bb33333l6xv7pnfxzkvslaae.flare-on.com # agent-id=1, counter=35. Command_size=2 43.49.52.0 ftrrrama.flare-on.com # agent-id=1, counter=36. Command="14" 128.0.0.2 99hhhhhxbf122d2w6f1r45hhamm.flare-on.com # agent-id=1, counter=37. Command_size=1 43.57.0.0 swrrrams.flare-on.com # agent-id=1, counter=38. Command="9" 128.0.0.2 88bbbbbdu9u0s4x92rv6oxzk2zdbamj.flare-on.com # agent-id=1, counter=39. Command_size=1 43.55.0.0 8iww3www6raml.flare-on.com # agent-id=1, counter=40. Command="7" 128.0.0.2 vv222228iqi9r1pqdjhntplydj17am6.flare-on.com # agent-id=1, counter=41. Command_size=1 43.52.0.0 deggcgggvoamz.flare-on.com # agent-id=1, counter=42. Command="4" 128.0.0.2 nneeeeecf4wa2ub4sjwmcamc.flare-on.com # agent-id=1, counter=43. Command_size=1 # localhost name resolution is handled within DNS itself. # 127.0.0.1 localhost # ::1 localhost
If we check out where is FLARE14.sh being used, we will see that the method, FLARE14.flared_54()
will be executed whenever FLARE15.c
changes. A file will be created and executed.

8.5 Obtaining the flag
We will have to run with the original binary then it will work. Make sure your hosts file has set all the root domains and IP addresses to respond, as well as the correct values in your flare.agent.id file. Running the original binary will take a while before paint.exe is popped with an image of the flag.

9. encryptor

For this challenge a ransomware binary developed in C and an encrypted file is given to us which can be obtained from this link. The linked also includes my IDA database of the reverse engineered ransomware, a testing file to be encrypted, as well as Python scripts to help with reverse engineering or getting the flag.
9.1 Flag is encrypted by binary
Running the binary, we can see that it is actually a encryption/randomware binary to encrypt files. Besides that, the file given to us has the extension “Encrypted”.

9.2 Encryption requirement
If we look at main()
that is located @ 0x403BF0, we will see that a file will only be encrypted if it has the extension “.EncryptMe”.

9.3 RSA encryption discovery
While looking at encryption()
@ 0x4022A3 that prints content to the encrypted file, I noticed that a variable contains 0x10001 which is a common value used as the e
value in RSA encryption. Check out this RSA encryption explanation by Brilliant which is helpful to me.

Running with a debugger where I created a dummy file to encrypt, I noticed that variable n seems to be random. Thus, I decided to investigate it by checking out how is it created. Looking at generate_public_keys()
@ 0x4021D0, I noticed that the random number in the two array variables seen in the debugger seems to be random and is a prime number. They are most likely p and q used to generate n (n=p*q).
Below shows the math steps for R public and private key generation:
- n = p*q
- ϕ(pq)=(p−1)(q−1)
- de≡1(modϕ(n)). dd is the private key
- To get the value of d, we have to use modular multiplicative inverse.
- c≡me(mod n)
- c is the encrypted value.
- e is usually 0x10001
9.3.1 Multiplication function
To prove that variable n
is really generated based on the multiplication of the random two large prime numbers, I used a debugger to check the value of the variable n
after executing the multiplication function.

Only using the last 4 bytes of values to multiply, we can see that the end result on the calculator matches the value of variable n
. Thus, we can conclude that the function indeed does multiplication.

9.3.2 Minus 1 function
Going to the next function, we can see it is just minus 1 for (p-1)
and (q-1)
. After subtraction, we can see multiplication done for ϕ(pq)=(p−1)(q−1)
.

9.3.3 Modular multiplicative inverse function and misconfiguration

Based on the RSA steps, the next step should be the generation of the decryption key, d
variable. Therefore, I created a script that does modular multiplicative inversion to obtain the d
variable.
# Ref: https://stackoverflow.com/questions/4798654/modular-multiplicative-inverse-function-in-python
def egcd(a, b):
if a == 0:
return (b, 0, 1)
else:
g, y, x = egcd(b % a, a)
return (g, x - (b // a) * y, y)
def modinv(a, m):
g, x, y = egcd(a, m)
if g != 1:
raise Exception('modular inverse does not exist')
else:
return x % m
# Value of e used by the randomware
a = 0x10001
# ϕ(n). Change this
m = 0xcdfe7076154d4e876f3187d3cc97da8181a5daafc06c0ea90a6b574e1e8adadb57e82fd92254866b7f0736d78d5c01b3f92735c0a8415dbd17440ceaf86491e70318ec9fdc3dc487c73d84dc131d9ac54f68495c12e44cfd2dd7706313e5207c1a21e09ed231cd91b34170b51bd1611bb0b9173e3d73d0c52955beeb35383244
print(hex(modinv(a, m)))

The above screenshot shows that the value matches which proves that it is a modular multiplicative inverse function.
Note that the generation of the decryption key is written back to the e
variable (e_will_become_d
). This is actually a misconfiguration. As a result, e_will_become_d
will be used as e for encryption of the random value (list_of_random_numbers
) in encryption(
) @ 0x4022A3.

9.3.4 Decryption to get list_of_random_numbers
Since D(E(m)) = (me)d mod n
is the formula to decrypt the encrypted value and d
has been swapped with e
due to misconfiguration, the decryption key is just 0x10001 due to the swap becoming D(E(m)) = (md)e mod n
for decryption. Thus, we can get the encrypted_value
and n
from SuspiciousFile.txt.Encrypted.

Therefore, I coded a script below to get the value of list_of_random_numbers.
# Convert large hex number to list of numbers like what we see in hexeditors
def get_list_of_num(value: int):
list_of_num = list()
while value != 0:
list_of_num.append(value % 0xFF)
value = value >> 8
# Get list_of_random_numbers's value
encrypted_value = 0x5a04e95cd0e9bf0c8cdda2cbb0f50e7db8c89af791b4e88fd657237c1be4e6599bc4c80fd81bdb007e43743020a245d5f87df1c23c4d129b659f90ece2a5c22df1b60273741bf3694dd809d2c485030afdc6268431b2287c597239a8e922eb31174efcae47ea47104bc901cea0abb2cc9ef974d974f135ab1f4899946428184c
n = 0xdc425c720400e05a92eeb68d0313c84a978cbcf47474cbd9635eb353af864ea46221546a0f4d09aaa0885113e31db53b565c169c3606a241b569912a9bf95c91afbc04528431fdcee6044781fbc8629b06f99a11b99c05836e47638bbd07a232c658129aeb094ddaf4c3ad34563ee926a87123bc669f71eb6097e77c188b9bc9
d = 0x10001
# decrypted_value = (encrypted_value ** d) % n
decrypted_value = 0x958f924dfe4033c80ffc490200000000989b32381e5715b4a89a87b150a5d528c943a775e7a2240542fc392aa197b001
print("Decrypted value: " + hex(decrypted_value))
9.4 Chacha20 encryption
In chacha20_encryption()
@ 0x4020F0 where encryption is done to the content of a targeted file such as the original flag file, we can see there is a string “expand 32-byte k”. This is used in chacha20 algorithm as the first 16 bytes as shown in this article. We can also see that the decrypted value which is a list of random values is actually 32 bytes key + 4 bytes counter + 12 bytes nonce. This list of random values will be appended to “expand 32-byte k” to generate a 512bits key as shown in the chacha20 article before XORing wit the file’s content.

9.5 Get flag

Knowing that it is chacha20 encryption and we have the key+counter+nonce value (48 bytes in total), we can decrypt it using a Python script, get_flag.py.
# RSA decryption: https://www.mandiant.com/resources/blog/cryptography-blackmatter-ransomware
# Chacha20 Algorithm: https://xilinx.github.io/Vitis_Libraries/security/2019.2/guide_L1/internals/chacha20.html
# Chacha20 Python library: https://pycryptodome.readthedocs.io/en/latest/src/cipher/chacha20.html
# py -m pip install pycryptodome
from Crypto.Cipher import ChaCha20
from base64 import b64decode
# Convert large hex number to list of numbers like what we see in hexeditors
def get_list_of_num(value: int):
list_of_num = list()
while value != 0:
list_of_num.append(value % 0xFF)
value = value >> 8
# Get list_of_random_numbers's value
encrypted_value = 0x5a04e95cd0e9bf0c8cdda2cbb0f50e7db8c89af791b4e88fd657237c1be4e6599bc4c80fd81bdb007e43743020a245d5f87df1c23c4d129b659f90ece2a5c22df1b60273741bf3694dd809d2c485030afdc6268431b2287c597239a8e922eb31174efcae47ea47104bc901cea0abb2cc9ef974d974f135ab1f4899946428184c
n = 0xdc425c720400e05a92eeb68d0313c84a978cbcf47474cbd9635eb353af864ea46221546a0f4d09aaa0885113e31db53b565c169c3606a241b569912a9bf95c91afbc04528431fdcee6044781fbc8629b06f99a11b99c05836e47638bbd07a232c658129aeb094ddaf4c3ad34563ee926a87123bc669f71eb6097e77c188b9bc9
d = 0x10001
decrypted_value = (encrypted_value ** d) % n
print("Decrypted value: " + hex(decrypted_value))
decrypted_value = decrypted_value.to_bytes(48, "little") # 48 bytes as 32bytes=key, 4bytes=counter, 12bytes=nonce
# key = [ord(char) for char in "expand 32-byte k"].extend(get_list_of_num(decrypted_value))
encrypted_flag = b64decode("f4r6Y2WcXvaeucPcE+iyMTqP422UhjQhRitv6K0wjSp56Op7ZgnY0FgCPZcUa/KqYIUGSE2XDnHqggY1ukv8UY8G5K1pK+YlWw==")
key = decrypted_value[:32]
counter = decrypted_value[32:36]
print("Counter: " + str(int.from_bytes(counter, "little"))) # Just to check if counter has any values
nonce = decrypted_value[36:]
cipher = ChaCha20.new(key=key, nonce=nonce)
plaintext_flag = cipher.decrypt(encrypted_flag)
print(plaintext_flag.decode())

10. Nur geträumt

For this challenge, a macOS img file and a README text file was given to us which you can obtain it via this link. This link also includes the emulate to load the img file, the ROM for the emulator, the set-up image for the emulator so you don’t have to re-setup the emulator, and a Python script to help with getting the flag.
10.1 Set up
As the challenge already give us some guidance on what to do with the challenge’s Macintosh disk image, I followed the exact same steps of using a mini vmac emulator which allows easy drag and drop of the image compared to other Mac emulators like Basilisk II. You can follow the guide here on how to set up mini vmac. As the guide does not provide the link to download the ROM, you can get it here. You can also use my emulator and configured disk image if you prefer to avoid the setup process.
10.2 Unicode not allowed in the image name
If we try to drag and drop Nur geträumt.img
, there will be an error. This is because Unicode characters are not allowed in the olden days system. Therefore, we have to change the name of the file that does not have a Unicode character. I changed it to le_imageeeee.img
. Once dragged and dropped, you should see the image in the emulator as shown below.

10.3 Hints on flag structure + content
If we double-click on the image and execute the Nur geträumt binary, we can see that we have to input something which will give us the flag. If we input nothing and press Try, it will give us some encoded unchanged flag value as shown below.

Quit the program and run Super ResEdit 2.1.3. “Nur geträumt” to open the file in Super ResEdit 2.1.3. Click OK when prompted that the file is a locked volume and that changes will not be saved. You should see a list of features on Super ResEdit 2.1.3 as shown below.

Go to TEXT > double-click the row. We will see the author of the challenge as a message for us. Scroll to the bottom for hints for the next few steps. In fact, this hint is telling us how will the flag look like.

10.4 Decoding algorithm
If we look at the code’s module, we can see that decoding of the flag somewhat involves CRC16-CCITT. However, the assembly instruction is quite complex. Thus, I used another way to figure out the decoding algorithm. But first, let’s go to FLAG. We can see a 1983 song called “99 Luftballons”. Next, open a Hex Editor.

We should see the content is actually the encoded flag which has the same value as what we saw earlier when we pressed Try with an empty input. You can actually open Nur geträumt.img
in a hex editor in your PC and search for those bytes to copy and paste it elsewhere.

Now that we know the encoded flag’s bytes, we can test the changes of the decoded flag based on our input. By spamming lots of “a”, I realized that it is just XOR operations between our input and the encoded flag. Besides that, the encoded flag actually starts from offset 0x1 and ends at offset 0x2D.

Example: 0x61 (a) XOR 0xC = 0x6D (m) 0x61 (a) XOR 0x4 = 0x65 (e)
10.5 Get flag
Since we now know the flag offsets and we know that it is just XOR operation, we can try to get the 2nd half of the required input to get “@flare-on.com”. I created a Python script, get_2ndhalf_input.py, to automate the process.
encrypted_flag = [0x0C, 0x00, 0x1D, 0x1A, 0x7F, 0x17, 0x1C, 0x4E, 0x02, 0x11, 0x28, 0x08, 0x10, 0x48, 0x05, 0x00, 0x00, 0x1A, 0x7F, 0x2A, 0xF6, 0x17, 0x44, 0x32, 0x0F, 0xFC, 0x1A, 0x60, 0x2C, 0x08, 0x10, 0x1C, 0x60, 0x02, 0x19, 0x41, 0x17, 0x11, 0x5A, 0x0E, 0x1D, 0x0E, 0x39, 0xA, 0x4]
flag_2nd_half = "@flare-on.com"
index = 0
for value in encrypted_flag[-len(flag_2nd_half):]:
print(chr(ord(flag_2nd_half[index]) ^ value), end="")
index += 1
print("")

Since we now know the content to input which is in German, we can search for 99 Luftballons’s lyrics and immediately find a match to the first sentence of the lyrics.

Even if we input “Hast du etwas Zei” as the input in the binary, there are still missing 28 characters needed since the length of the encoded flag is 45. Remember earlier on I mentioned that the author of the challenge gave us a hint of the flag’s content + structure. It says that we should as the program if it has some time for us and it might sing a song for us. Therefore, we can copy the first line with a question mark and prepend to “Hast du etwas Zei”.

The above shows that the flag is the 2nd line of the lyrics. Note that the flag value is showing ‘É’ instead of ‘i’ is because I cannot input ‘ü’. Thus, I used ‘u’.
Flag: Dann_singe_ich_ein_Lied_fur_dich@flare-on.com
11. The challenge that shall not be named.

For this challenge, a Python-based EXE binary is given to us which can be obtained via this link. The link also contains the extracted PYC files and “malicious” module files to help us obtain the flag.
11.2 The dynamic way
While the previous challenges hide the flags pretty well, this challenge allows us to obtain the flag easily via dynamic analysis. We can use Process Hacker, execute 11.exe and do a memory dump on the child process which allows us to obtain the flag.


11.3 The troublesome way
The troubling way requires us to do some unpacking and interaction with the program to obtain the flag.
11.3.1 EXE is compiled from Python
When loading the binary on IDA and looking at the Strings sub-view, I noticed that PyInstaller is used to compile Python script(s) into this EXE binary.

11.3.2 Extract Py script and decompile
Since we know that PyInstaller is used to compile the Python scripts into an EXE, we can extract .pyc files from the binary using pyinstxtractor. After extracting, we can use uncompyle6 to decompile from .pyc to .py.
py pyinstxtractor.py 11.exe uncompyle6.exe -o . 11.pyc
Remember to change the path above if you need to. There are also many other .pyc files but 11.pyc is the only important file.

Opening up 11.py, we can see Super Mode pyarmor is used to obfuscate the Python script(s).

11.3.3 Module injection/hijacking & get the flag
When I tried to execute 11.pyc, we can see there is a missing module error as it is implemented by pyarmor.

We can create _crypt.py with the code as shown below and placed it in the same directory as 11.pyc:
import inspect
for frameinfo in inspect.stack():
print(frameinfo.frame)

Based on the frames, we are interested in the frame at offset 0x0000012AFF2CC230. Hence, we can note down that frame. I also decided to check out names other than arguments and function locals and found something interesting.
frame = inspect.stack()[12]
frame = frame.frame
c = frame.f_code
print(c.co_names)
We can see that there seems to be base64 encoding, ARC4 encryption, and traffic over the network via request and post.

I created crypt.py but there seems to be nothing printed out from word and salt.
import inspect
for frameinfo in inspect.stack():
print(frameinfo.frame)
def crypt(word, salt) -> None:
print(word)
print(salt)

As the requests module is missing, I created an empty requests.py in the same directory as 11.pyc. The program then complains that there is no ARC4 attribute.

I append the following code to crypt.py and the ARC4 encryption key is printed for us.
def ARC4(anything):
print(anything)

As NoneType object is called by 11.pyc, this means that either ARC4()
or crypt()
should return an object. I created a class to test and it turns out ARC4()
should return an object.
def ARC4(anything):
print(anything)
return GG()
class GG:
def __init__(self) -> None:
pass
def encrypt(self, anything):
print(anything)

I hope these tabs have been helpful to you. Feel free to leave any comments below. You may also send me some tips if you like my work and want to see more of such content. Funds will mostly be used for my boba milk tea addiction. The link is here. 🙂