Flare-On 9 writeup: 08 - backdoor
This year, I attempted Flare-On for the first time and solved all 11 challenges. Of these, challenge 8 was by far the hardest, taking me much longer to solve than any of the others. I started this challenge with literally no knowledge of .NET, so I learned a lot by the end.
Overview
Challenge description:
I’m such a backdoor, decompile me why don’t you…
Looking at the program in dnSpy, we immediately notice several things:
- We have 74 functions with a name beginning with
flare
and 70 with a name beginning withflared
. - None of the functions beginning with
flared
can be decompiled, and all throw an exception when called. - Nearly every function beginning with
flare
attempts to call one of theflared
functions in atry
block, seemingly throwing an exception on purpose. The stack trace of that exception is passed in as an argument to the function called in thecatch
block. - Most of the functions have little to no actual code in the body of the method, so it isn’t immediately clear how anything is actually being called.
- The executable has a large number of sections, most of which have names consisting of 8 hexadecimal digits.
Since this program was supposedly a backdoor, I first tried running it using REMnux’s fakedns and inetsim packages to intercept any network traffic the program might produce. I found that the program made several DNS requests to seemingly random subdomains of flare-on.com, but it didn’t seem to do anything after that.
Looking at the strings in the program, I found several base64 strings that decoded to what appeared to be PowerShell commands, such as
$(ping -n 1 10.65.45.3 | findstr /i ttl) -eq $null;$(ping -n 1 10.65.4.52 | findstr /i ttl) -eq $null;$(ping -n 1 10.65.31.155 | findstr /i ttl) -eq $null;$(ping -n 1 flare-on.com | findstr /i ttl) -eq $null
ping -n 1 10.65.45.18 | findstr /i ttl) -eq $null;$(ping -n 1 10.65.28.41 | findstr /i ttl) -eq $null;$(ping -n 1 10.65.36.13 | findstr /i ttl) -eq $null;$(ping -n 1 10.65.51.10 | findstr /i ttl) -eq $null
nslookup flare-on.com | findstr /i Address;nslookup webmail.flare-on.com | findstr /i Address
$(ping -n 1 10.65.4.50 | findstr /i ttl) -eq $null;$(ping -n 1 10.65.4.51 | findstr /i ttl) -eq $null;$(ping -n 1 10.65.65.65 | findstr /i ttl) -eq $null;$(ping -n 1 10.65.53.53 | findstr /i ttl) -eq $null;$(ping -n 1 10.65.21.200 | findstr /i ttl) -eq $null
Get-NetTCPConnection | Where-Object {$_.State -eq "Established"} | Select-Object "LocalAddress", "LocalPort", "RemoteAddress", "RemotePort"
[System.Environment]::OSVersion.VersionString
However, these commands weren’t being run: I wasn’t capturing pings to any of the addresses mentioned in the commands. Clearly, I would need to send the program some kind of signal in order to get it to do anything.
From here, I’ll be splitting this writeup into two main parts. The first will focus on how I figured out what methods were being called and how I reversed the obfuscation protecting most of the program. The second will focus on how I figured out the logic the program was following, which eventually allowed me to reconstruct the program’s C2 server and obtain the flag.
Part 1: Disassembly and Deobfuscation
Dynamic Methods
After taking another look at dnSpy, I found out how most of the program’s functions were being called. In the flare_71
method, we can see calls to dynamicILInfo.SetCode
and dynamicMethod.Invoke
, revealing that a dynamic method is being constructed from an array of bytecode. Since dynamic methods are generated at runtime, dnSpy can’t decompile them, which means we’ll have to find another way to figure out what’s being called. The flare_71
method takes two arguments: m
and b
. I found that b
corresponded to the actual bytecode of the dynamic method being called, and m
was a dictionary containing all of the metadata tokens used in the dynamic method and the locations where they were used. (For those who haven’t worked with .NET before, metadata tokens are basically references to functions, classes, methods, and fields.)
The flare_74
function initialized several arrays (gs_b
, gh_b
, cl_b
, rt_b
, wl_b
, d_b
, pe_b
) that were then passed to flare_71
. For the actual disassembly, I used the Python package dncil. The bytecode arrays were missing headers, but I managed to reconstruct them by modifying the sample headers that appeared in dncil’s test cases. Once we have that, getting the disassembly is easy.
from dncil.cil.body import reader
def print_il(header, il_code):
body = reader.read_method_body_from_bytes(header + il_code)
for i in body.instructions: print(i)
And here’s an example of the output:
000C 00 nop
000D 73 02 00 00 06 newobj token(0x06000002)
0012 25 dup
0013 1f 58 ldc.i4.s 88
0015 16 ldc.i4.0
0016 6f 03 00 00 06 callvirt token(0x06000003)
...
I still didn’t know which metadata tokens corresponded to which function calls, so it was difficult to tell what most of these dynamic methods were doing. However, after running the program in dnSpy and examining the arguments passed to each dynamic method, I was able to get a rough idea as to what was happening:
gh
returns a string representation of a hexadecimal number, with the first 8 digits being equal to the name of a section from the executable.gs
takes in the string obtained fromgh
and passes it tope
, which retrieves the executable section with the name matching the string. The executable section contains obfuscated bytecode.d
takes in the obfuscated bytecode and somehow deobfuscates it.- The now-deobfuscated bytecode is passed to
cl
andwl
, which eventually results in a dynamic method being called.
Deobfuscation
I decided to take a look at the dynamic method d
, as it was deobfuscating the sections of the executable. The method takes two arguments: the code to be deobfuscated, and a key to be used in the deobfuscation. I recreated the deobfuscation algorithm with the following script:
def decrypt_section(to_decrypt):
#initialize key arr
key = [0x12, 0x78, 0xab, 0xdf]
key_arr = [0] * 256
for i in range(256):
key_arr[i] = key[i % 4]
#initialize index arr
index_arr = [i for i in range(256)]
#first swap
offset = 0
for i in range(256):
offset += key_arr[i] + index_arr[i]
offset &= 0xff
tmp = index_arr[i]
index_arr[i] = index_arr[offset]
index_arr[offset] = tmp
#swap again and add
offset = 0
b = b''
for j in range(1, len(to_decrypt)):
i = j % 256
offset += index_arr[i]
offset &= 0xff
tmp = index_arr[i]
index_arr[i] = index_arr[offset]
index_arr[offset] = tmp
xor_val = (index_arr[(index_arr[i] + index_arr[offset]) & 0xff])
b += ((xor_val ^ to_decrypt[j-1]).to_bytes(1, 'big'))
return b
The same key of [0x12, 0x78, 0xab, 0xdf]
was used for every section, so I extracted each section of the executable using pedump and deobfuscated each one. However, when I attempted to disassemble them, there was another problem: none of the metadata tokens used in the disassembly were actually valid, with values such as 0xA698A6A0
. Clearly, there was another level of obfuscation protecting the tokens.
Based on the values I was seeing in the debugger, the most likely location for the deobfuscation was cl
. Looking at cl
, there was an XOR instruction that stood out:
0BD6 08 ldloc.2
0BD7 20 bd a6 98 a2 ldc.i4 -1567054147
0BDC 61 xor
0BDD 0c stloc.2
Converting the constant to an unsigned hexadecimal value, we obtain 0xA298A6BD
. This was very close to the invalid token values, so I XORed each of the tokens with this constant. Sure enough, the resulting token values were all valid.
Some of the functions still contained invalid instructions after disassembly, so there was clearly another level of obfuscation being used here. However, there were few enough of these functions that I was able to deal with the issue by setting breakpoints and dumping the deobfuscated code from dnSpy’s debugger right before the dynamic method was invoked.
Retreiving the Token Values
The disassembly now contained the correct tokens, but dncil didn’t have a way of showing me which functions and fields these tokens corresponded to. I ended up dealing with this problem in a very hacky way - basically, I wrote another script to add comments to the disassembly with the correct function names.
Looking back at dnSpy, I found that the values of the metadata tokens were stored in the Tables
stream of the executable (except for tokens corresponding to strings, which are stored in a separate table for some reason).
I then copied the tokens and their values into a text file.
RID Token Offset Flags Name Signature Info
1 0x04000001 0x0001B91C 0x36 0x8B6 0x24 <>9
2 0x04000002 0x0001B922 0x16 0x10D 0x28 <>9__6_0
3 0x04000003 0x0001B928 0x16 0x2066 0x39 min_alive_delay
4 0x04000004 0x0001B92E 0x16 0x2076 0x39 max_alive_delay
5 0x04000005 0x0001B934 0x16 0x20A6 0x39 min_comm_delay
...
We can see that each token has a human-readable name for the function or object that it corresponds to. After modifying the disassembly to contain these names, we finally have something we can analyze:
000C 00 nop
000D 73 02 00 00 06 newobj token(0x06000002) .ctor
0012 25 dup
0013 1f 58 ldc.i4.s 88
0015 16 ldc.i4.0
0016 6f 03 00 00 06 callvirt token(0x06000003) Add
...
Control Flow
It still wasn’t clear which dynamic methods were being called in which order. Somehow, each of the flare
functions figured out which section of the executable to retrieve and which dynamic method to call.
I was never able to figure out the details of how the control flow worked, so it didn’t end up being useful for my analysis. However, if you’re curious, my best guess is that it works something like this:
- Each
flare
function calls one of theflared
functions, which is guaranteed to throw an exception. The stack trace of that exception is then passed as an argument to the function called in thecatch
block. - Several substrings of the stack trace are concatenated together with some of the bytecode from the function that threw an exception. Then, the SHA256 hash of the result is taken.
- Then, the string representation of the hash value is taken. This is the string that is passed as an argument to the
gs
dynamic method, which takes the first 8 characters and retrieves the section of the executable with a matching name. This section contains the obfuscated bytecode of the method that will be called next.
Part 2: Program Logic and C2 Emulation
First Observations
When I first traced through the program with dnSpy’s debugger, it didn’t seem to do much. The program generated a seemingly random string of four characters, then made a DNS request using that string as a subdomain of flare-on.com
. The resulting IP address was stored, then passed as an argument to several functions. If a file called flare.agent.id
didn’t already exist, it would be created, and the last octet of the IP address would be written to the file along with a random number.
Afterward, the program would sleep for a long period of time, then eventually repeat the same process again. I guessed that there was probably some kind of check being performed on the IP address returned by the DNS request, and that I was probably failing this check.
To figure out what checks were being made, I would need to figure out how the IP address was being stored. I got stuck on this for a while, but I eventually found it by looking at the static fields.
As far as I know, there’s no way to show static fields in the Locals window. However, when you set a breakpoint in dnSpy, you can add a logging message that prints the values of different variables, and those messages can display static fields.
Once I figured out how to view static fields, I found that the FLARE05
enum was being used to store information about the DNS requests. A
was storing the subdomain of flare-on.com
, B
was storing the number of requests that had been made so far, and C
was storing the IP address. I didn’t know what the other members were for yet, but I’ll get to that later.
Tracing through with the debugger to find when FLARE05.C
was referenced, I found a check in section f9a758d3
that took the first octet as an argument. It seemed to be checking whether the value was greater than 128.
I had initially been using the IP 10.0.0.1
for fakedns, so I had been failing this check. I modified the first octet to be greater than 128, and I finally started seeing some variation in the DNS requests:
_Send, _Receive, and _DoTask
Before, every DNS request sent by the program was for a subdomain that was 4 characters long. Now, the program was sending requests of varying lengths. Though the characters used in the requests were at least partially randomly generated, the lengths of the subdomains seemed to correlate with different behaviors.
This difference can be observed in the parameter passed to flare_31
. One of the arguments fo flare_31
is a function, and a different function is passed for each subdomain length.
# of characters in subdomain | Function called |
---|---|
4 | _Alive |
8 | _Receive |
31 | _Send |
12 | _SendAndReceive |
I found that when _Receive or _SendAndReceive was called, the program created a text file called flare.agent.recon
. It appeared that the program was trying and failing to run a command:
But what commands was the program capable of running? At first, none of my disassembly appeared to contain any code responsible for handling this, but I eventually found that the program was calling a function called _DoTask
in section 4ea4cf8d
of the executable.
I initially had trouble finding _DoTask
, as it didn’t seem to appear anywhere in dnSpy’s decompilation, nor did it correspond to any of the obfuscated sections of the executable. In the end, I found it by calculating its location in memory.
The offset of the method can be calculated using the formula
(relative virtual address of method) - (virtual address of section) + (pointer to raw data of section).
In this case, the RVA of _DoTask is 0x5058, the virtual address of the .text section is 0x2000, and the pointer to raw data of the .text section is 0x9000. So the address we need is 0x5058 - 0x2000 + 0x9000 is 0xc058.
A Closer Look at _DoTask
To understand what _DoTask does, we first need to look at section 4ea4cf8d
, where its arguments are constructed. The ListData
parameter used here is a list of arrays, which each array storing the octets of an IP address.
Normally, this list only contains one array corresponding to a single IP address, so the value being stored to taskType
is the first octet:
003C 7e 97 a7 98 a6 ldsfld token(0xA698A797) 0x0400012A ListData
0041 16 ldc.i4.0
0042 6f 24 a6 98 a8 callvirt token(0xA898A624) 0x0A000099 get_Item
0047 0d stloc.3
0048 08 ldloc.2
0049 09 ldloc.3
004A 16 ldc.i4.0
004B 91 ldelem.u1
004C 7d 96 a7 98 a6 stfld token(0xA698A796) 0x0400012B taskType
Each of the remaining octets of the IP address is then converted to a character in a string. This string is stored to the field cmd
before _DoTask is called.
0145 11 04 ldloc.s local(0x0004)
0147 6f 2e a6 98 a8 callvirt token(0xA898A62E) 0x0A000093 GetString
014C 7d 90 a7 98 a6 stfld token(0xA698A790) 0x0400012D cmd
0151 11 0b ldloc.s local(0x000B)
0153 fe 06 1d a6 98 a4 ldftn token(0xA498A61D) 0x060000A0 <_DoTask>b__0
Knowing this, we can figure out what _DoTask does. First, it compares taskType
to 43:
001A 7b 2b 01 00 04 ldfld token(0x0400012B) 0x0400012B taskType
001F 1f 2b ldc.i4.s 43
0021 fe 01 ceq
If it is 43, cmd
is then compared to a string. Each string comparison follows the same format:
0093 72 3d 03 00 70 ldstr string token(0x7000033D) 0x7000033D
0098 28 4a 00 00 0a call token(0x0A00004A) 0x0A00004A op_Equality
009D 13 09 stloc.s local(0x0009)
009F 11 09 ldloc.s local(0x0009)
00A1 2c 3e brfalse.s 225
00A3 00 nop
00A4 06 ldloc.0
00A5 28 3e 00 00 0a call token(0x0A00003E) 0x0A00003E Parse
00AA 72 43 03 00 70 ldstr string token(0x70000343) 0x70000343
00AF 28 98 00 00 06 call token(0x06000098) 0x06000098 flare_56
00B4 00 nop
00B5 72 4b 03 00 70 ldstr string token(0x7000034B) 0x7000034B
00BA 28 09 00 00 06 call token(0x06000009) 0x06000009 flare_04
There are 21 possibilities for cmd
: the string representations of the numbers 1 through 22, excluding 6. If cmd
matches one of these values, one of the base64-encoded PowerShell commands is passed to flare_04
, which then runs it. If taskType
is 43 and cmd
doesn’t match one of these values, the program simply tries to call it as an argument to the command line, followed by && exit
. If taskType
is not 43, then no command is passed to the command line and && exit
is run by itself, resulting in the && was unexpected at this time
message we were seeing earlier.
Once the PowerShell command is run, its output is written to a flare.agent.recon
file. As the name “recon” suggests, most of these commands seem to be intended to collect data from the victim. For example, this command lists information about what is installed on the device:
I also noticed that a second string token was being loaded. Each command has a different string associated with it, with seemingly random values such as 3c9974
and 8e6
. Every time a command is run, the corresponding string is concatentated to the static field sh
. This doesn’t seem to directly have anything to do with the commands being run, so we can guess that it has to do with the flag.
At this point, we finally have some idea of what we have to do: we need to run a sequence of commands in a particular order, which will construct the correct value of sh
, which will probably allow us to obtain the flag.
Choosing IP Addresses
At first, it wasn’t clear how to generate a cmd
string that matched one of the command numbers. It definitely seemed like the characters of the string were based on the octets of the IP address, but in order for the comparison to work, I also needed to generate a string of the correct length. How was the length being decided?
The key to figuring this out was the fact that _DoTask was only called every other DNS request, which meant that the IP addresses from the remaining requests were being used for something else. It turned out that the last three octets of these IP addresses were being used to decide how many bytes of data would be stored from the next IP address.
Knowing this, we can construct a sequence of IP addresses to run any of the commands. For example, if we respond with an address of the form *.0.0.3
, then the first three octets of the next address will be saved. If the next address is 43.49.48.0
, then the task type of 43 will be saved, along with the bytes 49 and 48, which are the characters ‘1’ and ‘0’. The string comparison will succeed, and command 10 will be run.
Finding the Flag
The last step is to figure out the order of commands to run. I could see in the debugger that a string was being concatenated to sh
after each command, so I searched for the function that was doing this, which turned out to be section 33d51cd2
. I could also see that something was being XORed with 248 and compared to a value in FLARE15.c
:
0019 7e 81 a7 98 a6 ldsfld token(0xA698A781) 0x0400013C c
001E 16 ldc.i4.0
001F 6f 2b a6 98 a8 callvirt token(0xA898A62B) 0x0A000096 get_Item
0024 02 ldarg.0
0025 20 f8 00 00 00 ldc.i4 248
002A 61 xor
002B fe 01 ceq
I XORed each value in FLARE15.c
with 248, which gave me [2,10,8,19,11,1,15,13,22,16,5,12,21,3,18,17,20,14,9,7,4]
.
Since each of the command numbers appeared exactly once in this sequence, it was obvious that this was the command order I was looking for. To run these commands, I responded to the program with the following list of IP addresses:
['143.0.0.2',
'43.50.0.0',
'143.0.0.3',
'43.49.48.0',
'143.0.0.2',
'43.56.0.0',
'143.0.0.3',
'43.49.57.0',
'143.0.0.3',
'43.49.49.0',
'143.0.0.2',
'43.49.0.0',
'143.0.0.3',
'43.49.53.0',
'143.0.0.3',
'43.49.51.0',
'143.0.0.3',
'43.50.50.0',
'143.0.0.3',
'43.49.54.0',
'143.0.0.2',
'43.53.0.0',
'143.0.0.3',
'43.49.50.0',
'143.0.0.3',
'43.50.49.0',
'143.0.0.2',
'43.51.0.0',
'143.0.0.3',
'43.49.56.0',
'143.0.0.3',
'43.49.55.0',
'143.0.0.3',
'43.50.48.0',
'143.0.0.3',
'43.49.52.0',
'143.0.0.2',
'43.57.0.0',
'143.0.0.2',
'43.55.0.0',
'143.0.0.2',
'43.52.0.0']
Once this was done, the program opened an image.
This gets us our flag: W3_4re_Kn0wn_f0r_b31ng_Dyn4m1c@flare-on.com
.
Conclusion
Looking at the official solutions of this challenge, I now see that there’s a lot I could have done better. I was only able to disassemble the code, but the intended solution actually decompiled it by patching the file at the correct offsets. When I started working on this challenge, I had no idea how .NET executables were structured, so I didn’t know how to patch it in this way. I eventually ended up with a pretty good understanding of how to calculate the locations of different functions and values, but by then I was most of the way through the challenge! I guess I’ll know for next time.
While I definitely did this challenge the hard way, it was incredibly interesting and I learned a lot. The idea of using IP addresses to encode commands was very clever, and I could easily see real malware using similar strategies to go unnoticed. I’m definitely going to attempt Flare-On 10 next year, and now that I have a better idea of what to expect, maybe I’ll be able to finish a little faster.