For those of you who don’t know, Flare-On is an annual “reverse engineering marathon” organized by Mandiant (formerly by FireEye). It runs for 6 weeks, and contains usually 10-12 tasks of increasing difficulty. This year I completed as 103 (solves board here). In this short series you will find my solutions of the tasks I enjoyed the most.
Unquestionably, the most interesting and complex challenge of this year was the 8th one.
You can find the package here: 08_backdoor.7z , password:
flare
This challenge is a PE written in .NET. Even at first sight we can see it is some atypical. It contains 74 sections. In addition to the standard sections like .text
, .rsrc
and .reloc
, there are sections that clearly contain some encrypted/obfuscated content. Their names look like some byte strings (that could be checksums or fragments of hashes).
As usually when encountering a .NET file, I opened it in dnSpy to have a look at the decompiled code.
The program contains multiple classes with a names starting with “FLARE”:
The Entry Point is in the class named Program
. Looking inside we can realize that the bytecode of most of the methods is obfuscated, and can’t be decompiled with dnSpy:
It looks very messy and intimidating, but we still have some methods that haven’t been obfuscated, so let’s start from those ones.
The function that is executed first, FLARE15.flare_74
, initializes some tables, that are going to be used further:
The next function to be executed, Program.flared_38
, can’t be decompiled. So I previewed the CIL code, to check if it makes any sense:
It doesn’t – we can see some instructions that are marked as UNKNOWN. So we can assume, that this function is here only to throw an exception, and the meaningful code is going to be in the exception handler. So, let’s take a look there.
The function flare_70
that is executed in the exception handler, follows the same logic. It calls a function flared_70
which contains invalid, nonsensical code, just to trigger an exception.
And then, in the exception handler, flare_71
is executed. It gets as parameters two of the global variables, that were initialized in the Main
, by the function FLARE15.flare_74
.
The first of those passed variables is a dictionary, and the other – an array of bytes.
Fortunately, this rabbit-hole doesn’t go deeper for now, and the function flare_71
contains a meaningful code:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
// Token: 0x060000BC RID: 188 RVA: 0x00013EB8 File Offset: 0x0001AEB8 | |
public static object flare_71(InvalidProgramException e, object[] args, Dictionary<uint, int> m, byte[] b) | |
{ | |
StackTrace stackTrace = new StackTrace(e); | |
int metadataToken = stackTrace.GetFrame(0).GetMethod().MetadataToken; | |
Module module = typeof(Program).Module; | |
MethodInfo methodInfo = (MethodInfo)module.ResolveMethod(metadataToken); | |
MethodBase methodBase = module.ResolveMethod(metadataToken); | |
ParameterInfo[] parameters = methodInfo.GetParameters(); | |
Type[] array = new Type[parameters.Length]; | |
SignatureHelper localVarSigHelper = SignatureHelper.GetLocalVarSigHelper(); | |
for (int i = 0; i < array.Length; i++) | |
{ | |
array[i] = parameters[i].ParameterType; | |
} | |
Type declaringType = methodBase.DeclaringType; | |
DynamicMethod dynamicMethod = new DynamicMethod("", methodInfo.ReturnType, array, declaringType, true); | |
DynamicILInfo dynamicILInfo = dynamicMethod.GetDynamicILInfo(); | |
MethodBody methodBody = methodInfo.GetMethodBody(); | |
foreach (LocalVariableInfo localVariableInfo in methodBody.LocalVariables) | |
{ | |
localVarSigHelper.AddArgument(localVariableInfo.LocalType); | |
} | |
byte[] signature = localVarSigHelper.GetSignature(); | |
dynamicILInfo.SetLocalSignature(signature); | |
foreach (KeyValuePair<uint, int> keyValuePair in m) | |
{ | |
int value = keyValuePair.Value; | |
uint key = keyValuePair.Key; | |
bool flag = value >= 1879048192 && value < 1879113727; | |
int tokenFor; | |
if (flag) | |
{ | |
tokenFor = dynamicILInfo.GetTokenFor(module.ResolveString(value)); | |
} | |
else | |
{ | |
MemberInfo memberInfo = declaringType.Module.ResolveMember(value, null, null); | |
bool flag2 = memberInfo.GetType().Name == "RtFieldInfo"; | |
if (flag2) | |
{ | |
tokenFor = dynamicILInfo.GetTokenFor(((FieldInfo)memberInfo).FieldHandle, ((TypeInfo)((FieldInfo)memberInfo).DeclaringType).TypeHandle); | |
} | |
else | |
{ | |
bool flag3 = memberInfo.GetType().Name == "RuntimeType"; | |
if (flag3) | |
{ | |
tokenFor = dynamicILInfo.GetTokenFor(((TypeInfo)memberInfo).TypeHandle); | |
} | |
else | |
{ | |
bool flag4 = memberInfo.Name == ".ctor" || memberInfo.Name == ".cctor"; | |
if (flag4) | |
{ | |
tokenFor = dynamicILInfo.GetTokenFor(((ConstructorInfo)memberInfo).MethodHandle, ((TypeInfo)((ConstructorInfo)memberInfo).DeclaringType).TypeHandle); | |
} | |
else | |
{ | |
tokenFor = dynamicILInfo.GetTokenFor(((MethodInfo)memberInfo).MethodHandle, ((TypeInfo)((MethodInfo)memberInfo).DeclaringType).TypeHandle); | |
} | |
} | |
} | |
} | |
b[(int)key] = (byte)tokenFor; | |
b[(int)(key + 1U)] = (byte)(tokenFor >> 8); | |
b[(int)(key + 2U)] = (byte)(tokenFor >> 16); | |
b[(int)(key + 3U)] = (byte)(tokenFor >> 24); | |
} | |
dynamicILInfo.SetCode(b, methodBody.MaxStackSize); | |
return dynamicMethod.Invoke(null, args); | |
} |
By analyzing the code we finally come to know what is happening here. The function that has thrown the exception, along with its prototype, is retrieved, as well as the parameters that were passed to it.
Then, a dynamic method is created, as a replacement, using the values passed as flare_71
arguments (FLARE15.wl_m
, FLARE15.wl_b
in the analyzed case). The last function parameter, containing the byte array, is in fact a bytecode of the new method.
Finally, the newly created dynamic function is called, with the same prototype and arguments as the function that thrown the exception that leaded to here:
Creation of the dynamic function:
So, if we manage to get the code that was about to be executed, and fill it in on the place of the nonsensical code, we could get the function decompiled, and the flow deobfuscated.
I found 7 functions total that were obfuscated in the same way:
flared_35
flared_47
flared_66
flared_67
flared_68
flared_69
flared_70
My first thought was to just dump the code before the execution, and fill it in at the offset where the original function was located. I tried to do it, and although the code that I got looked like a valid IL code, still something was clearly wrong. Some of the functions (i.e. flared_70
) decompiled correctly, but had fragments that were not making sense:
Other function wasn’t decompiling. When I looked at the bytecode preview, I noticed that some references inside are clearly invalid:
But why is it so, if I dumped exactly the same code that worked fine while dynamically executed? Well – there is a catch (thanks to Alex Skalozub for a hint on this!). Before the function can be executed, all the referenced tokens need to be rebased. This is the responsible fragment:
When the function was prepared to be executed dynamically, they were rebased to that dynamic token. To be able to fill it in, back to the place of the static function, we need to rebase them to the original, static function’s token. This modified version of the function does the job:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
public static byte[] flare_71(Dictionary<uint, int> m, byte[] b) | |
{ | |
foreach (KeyValuePair<uint, int> keyValuePair in m) | |
{ | |
int value = keyValuePair.Value; | |
uint key = keyValuePair.Key; | |
int tokenFor = value; | |
b[(int)key] = (byte)tokenFor; | |
b[(int)(key + 1U)] = (byte)(tokenFor >> 8); | |
b[(int)(key + 2U)] = (byte)(tokenFor >> 16); | |
b[(int)(key + 3U)] = (byte)(tokenFor >> 24); | |
} | |
return b; | |
} |
I implemented a simple decoder, basing on the original, decompiled code, plus the modified version of flare_71
. The decoder was initializing all the global variables, and then calling the function flare_71
with parameters appropriate for a particular function. After that the resut was saved into a file.
Example – decoded bytecode for the function flared_70
:
There were only 7 functions to be filled at this stage, so I decided to copy-paste the resulted bytecode manually. The file offset where the function starts can be found in dnSpy:
However, we need to take into consideration that that the function starts with a header, and then the bytecode follows. We can see this layout in dnSpy hexeditor:
So, in above function, the bytecode starts at the offset 0x1AE10, and this is where we can copy the decoded content. As we can see, the size of the decoded bytecode is exactly the same as the size of the nonsensical code that was used as the filler – that makes this whole operation possible.
The same method filled with the decoded body:
After pasting all the fragments we can see a big progress – all the 7 functions decompiled fine!
Yet – this is just a beginning, because there is another stage to be deobfuscated…
Now, after deobfuscating the function `flared_70` we can see what is happening there.
The function flare_66
that is called first, is responsible for calculating a SHA256 hash from a body of the obfuscated function which has thrown the exception:
Then, the function flared_69
takes this hash, and enumerate all the PE sections, searching for the section names exactly like the beginning of that hash. The body of this section is being read:
The function flared_47
(called by flare_46
) decodes the read section’s content:
And finally, the function flared_67
uses the decoded content and creates a dynamic function to be called, out of the supplied bytecode.
Full function snippet here.
It turns out that we need to decode it analogous to the previous layer.
This time the original token is first decoded:
So, this is the value that we need to use as a token for the static version of the function:
uint num = (uint)FLARE15.flared_68(b, j); num ^= 2727913149U; uint tokenFor = num; // use decoded num as a token b[j] = (byte)tokenFor; b[j + 1] = (byte)(tokenFor >> 8); b[j + 2] = (byte)(tokenFor >> 16); b[j + 3] = (byte)(tokenFor >> 24); j += 4; break;
This time, the number of the functions to be filled is much bigger than in the previous layer, making filling it by hand inefficient and unreasonable.
There are various ways to automate it.
For automating the decoding of the body of each function, I used .NET reflection. I loaded the challenge executable (with the stage 1 patched) from the disk, and retrieved the list of all included types. Then walked through that list, filtering out non-static types, and those with names not starting from flared_
(which was a prefix of every obfuscated function):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Assembly a = Assembly.LoadFrom(fileToPatch); | |
Module[] m = a.Modules.ToArray(); | |
if (m.Length == 0) return false; | |
Module module = m[0]; | |
Type[] tArray = module.FindTypes(Module.FilterTypeName, "*"); | |
int notFound = 0; | |
foreach (Type t in tArray) | |
{ | |
foreach (MethodInfo mi in t.GetMethods()) | |
{ | |
var metadataToken = mi.MetadataToken; | |
string name = mi.Name; | |
if (!mi.IsStatic) { continue; } | |
if (!name.StartsWith("flared_")) { continue; } | |
// Do the stuff | |
} | |
} |
This is how I got the list of methods to be deobfuscated. I could retrieve their deobfuscated bodies pretty easily, by applying the (slightly modified) original functions, that were discussed above: calculating the hash of the content, finding proper section, decoding it).
Still the remaining problem to be solved, was to automatically patch the executable with the decoded contents. Probably the most elegant solution here would be to use dnlib. What I did was more “hacky” but nevertheless it worked fine. I decided to make a lookup table of the file offsets where the functions were located. As we saw earlier, those offsets are given as a comments generated by dnSpy. So, I saved the full decompiled project from dnSpy, and then used the grep
to filter the lines with the file offsets. Post-processed the output a bit, in a simple text editor, and as a result I’ve got the following table: file_offsets.txt. Now this table needs to be read by the decoder, and parsed into a dictionary:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
static Dictionary<int, int> createMapOfTokens(string tokensFile) | |
{ | |
string tokenStr = "Token: "; | |
string offsetStr = "File Offset: "; | |
string sepStr = " RID:"; | |
var tokenToOffset = new Dictionary<int, int>(); | |
foreach (string line in System.IO.File.ReadLines(tokensFile)) | |
{ | |
int tokenStart = line.IndexOf(tokenStr); | |
int sep = line.IndexOf(sepStr); | |
int offsetStart = line.IndexOf(offsetStr); | |
int len = sep – (tokenStart + tokenStr.Length); | |
string tokenPart = line.Substring(tokenStart + tokenStr.Length, len); | |
string offsetPart = line.Substring(offsetStart + offsetStr.Length); | |
int tokenVal = Convert.ToInt32(tokenPart, 16); | |
int offsetVal = Convert.ToInt32(offsetPart, 16); | |
Console.WriteLine(System.String.Format(@"Adding: '{0}' '{1:X}'", tokenPart, offsetVal)); | |
tokenToOffset[tokenVal] = offsetVal; | |
} | |
return tokenToOffset; | |
}; |
That’s how we have the offset where each function starts. Yet, as we mentioned before, this offset is not exactly the offset where the patch is to be applied – there is still a header. And to make things more complicated, multiple different versions of header are possible, with different lengths.
Still, I could retrieve the original (obfuscated) function’s body with .NET reflection. So, as a workaround of the mentioned problem, I decided to just search where the obfuscated function’s body is located in the file, starting from the function’s offset.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
byte[] currentBody = methodBody.GetILAsByteArray(); | |
if (currentBody.Length != decChunk.Length) | |
{ | |
Console.WriteLine("Length mismatch: {0:X} {1}", metadataToken, mi.Name); | |
continue; | |
} | |
// offset where the method body starts (headers may have various sizes) | |
int bodyOffset = 0; | |
for (var i = offset; i < (offset + hdrSize + decChunk.Length); i++) | |
{ | |
//memcmp: | |
bool isOk = true; | |
for (var k = 0; k < decChunk.Length; k++) | |
{ | |
if (fileBuf[i + k] != currentBody[k]) | |
{ | |
isOk = false; | |
break; | |
} | |
} | |
if (isOk) | |
{ | |
bodyOffset = i; | |
break; | |
} | |
} | |
if (bodyOffset == 0) | |
{ | |
Console.WriteLine("Function body not found: {0:X} {1}", metadataToken, mi.Name); | |
continue; | |
} | |
// apply the patch on the file buffer: | |
Buffer.BlockCopy(decChunk, 0, fileBuf, bodyOffset, decChunk.Length) |
I dumped the patched file on the disk, and finally, the whole code decompiles!
I saved the decompiled dnSpy project, and it turns out, that after some trivial cleaning, it became possible to even compile it back to the binary. The sourcecode of my decompiled and cleaned version is available here:
Working on the code gives much more flexibility – allows to add logs, quickly rename the functions and variables, etc. So overall, the understanding of the whole logic is a lot easier.
One thing that was very helpful in the analysis, was noticing that the challenge is actually based on Saitama malware.
I’ve got Saitama Agent from Virus Total (79c7219ba38c5a1971a32b50e14d4a13).
Decompiling both applications, and comparing them side by side, allowed me very quickly to notice what parts are added by the challenge authors, and where the flag can be located. Additionally, in contrast to the FlareOn task, Saitama’s code is not obfuscated, and functions have meaningful names. So, following them, and renaming all the functions in the challenge to the same names as in Saitama, was an easy way to understand the whole functionality.
The main function of the Saitama Agent gives right away the hint that we are dealing with a state machine, and what functionality is it going to provide:
The same state machine, and analogous functions, we can find in the deobfuscated challenge executable:
There are already some writeups available detailing how Saitama’s state machine work, i.e. https://x-junior.github.io/malware%20analysis/2022/06/24/Apt34.html
Following the Saitama code, and renaming the matching functions, I produced the cleaned version of the challenge. It will be also helpful for further experiments and better understanding of inner workings of the app. The final version of the processed code (including modifications that are described further in this writeup), is given here:
Saitama is a RAT that executes various commands requested by the Command-and-Control (C2) server. The C2 communication is encoded as DNS requests/responses. Details about how they are encoded are described here and here.
The agent installed on the victim machine sends to the C2 some domain to be “resolved”. In reality the it is a keep alive token, showing that the agent is active and waiting for commands. Just like a normal DNS, the C2 responds with an IP address – however, those IPs are in reality commands, just wrapped in a custom format.
Our challenge works exactly the same – sends to the C2 requests to resolve generated domains, ending with flare-on.com
, and then parse the response.
The function responsible for executing the requested tasks: https://github.com/hasherezade/flareon2022/blob/main/task8/FlareOn.Backdoor_dobfuscated_cleaned/FlareOn.Backdoor/TaskClass.cs#L199 .
As we can see, tasks are identified by their IDs, given as ASCII strings.
The task ID is retrieved from the DNS response. First, the length of the next response (that will carry the command) is be retrieved, in form of an IP. The IP addresses that carry the size must start with a chunk with a value >= 128. (See the code here).
Then, in the next IP, the command itself is passed. The first chunk of the IP address defines the command type, as given in the enum. We will be using command type 43 (Static
), which means plaintext. Then, in the next chunks of the IP, follows the command ID in ASCII.
The output of the successfully executed command will be saved in a file named: flare.agent.recon.[unique_id]
. Example:
By processing the code, it was also easy to notice where the authors added their custom code. In the function analogous to Saitama’s DoTask
we can see some chunks being appended to an internal buffer on each command execution. Example:
bool flag27 = text == "17"; if (flag27) { TaskClass.AppendFlagKeyChunk(int.Parse(text), "2e4"); //$.(.p.i.n.g. .-.n. .1. .1.0...6.5...4.5...1.8. .|. .f.i.n.d.s.t.r. ./.i. .t.t.l.). .-.e.q. .$.n.u.l.l.;.$.(.p.i.n.g. .-.n. .1. .1.0...6.5...2.8...4.1. .|. .f.i.n.d.s.t.r. ./.i. .t.t.l.). .-.e.q. .$.n.u.l.l.;.$.(.p.i.n.g. .-.n. .1. .1.0...6.5...3.6...1.3. .|. .f.i.n.d.s.t.r. ./.i. .t.t.l.). .-.e.q. .$.n.u.l.l.;.$.(.p.i.n.g. .-.n. .1. .1.0...6.5...5.1...1.0. .|. .f.i.n.d.s.t.r. ./.i. .t.t.l.). .-.e.q. .$.n.u.l.l. text = Cmd.Powershell("JAAoAHAAaQBuAGcAIAAtAG4AIAAxACAAMQAwAC4ANgA1AC4ANAA1AC4AMQA4ACAAfAAgAGYAaQBuAGQAcwB0AHIAIAAvAGkAIAB0AHQAbAApACAALQBlAHEAIAAkAG4AdQBsAGwAOwAkACgAcABpAG4AZwAgAC0AbgAgADEAIAAxADAALgA2ADUALgAyADgALgA0ADEAIAB8ACAAZgBpAG4AZABzAHQAcgAgAC8AaQAgAHQAdABsACkAIAAtAGUAcQAgACQAbgB1AGwAbAA7ACQAKABwAGkAbgBnACAALQBuACAAMQAgADEAMAAuADYANQAuADMANgAuADEAMwAgAHwAIABmAGkAbgBkAHMAdAByACAALwBpACAAdAB0AGwAKQAgAC0AZQBxACAAJABuAHUAbABsADsAJAAoAHAAaQBuAGcAIAAtAG4AIAAxACAAMQAwAC4ANgA1AC4ANQAxAC4AMQAwACAAfAAgAGYAaQBuAGQAcwB0AHIAIAAvAGkAIAB0AHQAbAApACAALQBlAHEAIAAkAG4AdQBsAGwA"); TaskClass.CommandsAndMethods.AppendData(Encoding.ASCII.GetBytes(TaskClass.GetMethodNamesFromStack() + text));
We can see that on each chunk being appended to the buffer, some value from a hardcoded buffer Util.c
is being removed:
// Token: 0x06000097 RID: 151 RVA: 0x00004C6C File Offset: 0x0000BC6C public static void _AppendFlagKeyChunk(int i, string s) { bool flag = Util.c.Count != 0 && Util.c[0] == (i ^ 248); if (flag) { TaskClass.FlagSectionNameHash += s; Util.c.Remove(i ^ 248); } else { TaskClass._someFlag = false; } } // Token: 0x06000098 RID: 152 RVA: 0x00004CD0 File Offset: 0x0000BCD0 public static void AppendFlagKeyChunk(int i, string s) { try { TaskClass._AppendFlagKeyChunk(i, s); } catch (InvalidProgramException e) { Util.flare_70(e, new object[] { i, s }); } }
Util.c
is an observable collection, initialized with the following values:
Util.c = new ObservableCollection<int> { 250, 242, 240, 235, 243, 249, 247, 245, 238, 232, 253, 244, 237, 251, 234, 233, 236, 246, 241, 255, 252 };
When the collection gets emptied, the following function is executed:
// Token: 0x06000095 RID: 149 RVA: 0x00004B94 File Offset: 0x0000BB94 public static void _DecodeAndSaveFlag() { byte[] sectionContent = Util.FindSectionStartingWithHash(TaskClass.ReverseString(TaskClass.FlagSectionNameHash)); byte[] hash = TaskClass.CommandsAndMethods.GetHashAndReset(); byte[] flagContent = FLARE12.RC4(hash, sectionContent); string text = Path.GetTempFileName() + Encoding.UTF8.GetString(FLARE12.RC4(hash, new byte[] { 31, 29, 40, 72 })); using (FileStream fileStream = new FileStream(text, FileMode.Create, FileAccess.Write, FileShare.Read)) { fileStream.Write(flagContent, 0, flagContent.Length); } Process.Start(text); }
This function drops and executes some file, and we can guess at this point that this is where the flag is located.
So, by analyzing the above function, we know that:
Although in order to obtain the valid flag we need the original binary, still, the recompiled one will be very helpful for some experiments, testing assumptions, and figuring out the valid commands sequence.
My first assumption is that the elements in the observable collection Util.c
have to be removed in the same order as they are defined, so, they will give us the answer to the question in which order the commands should be run. So, by looping over the full list, and XOR-ing each value with the value 248
(as in the function referenced as _AppendFlagKeyChunk
) we obtain each command ID. Now we just have to encode those commands as IP addresses – as the Saitama communication protocol defines. This is the sequence works,the decoder that generates proper IPs sequence:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
static void decodeIndexes() | |
{ | |
byte[] indexes = { | |
250, | |
242, | |
240, | |
235, | |
243, | |
249, | |
247, | |
245, | |
238, | |
232, | |
253, | |
244, | |
237, | |
251, | |
234, | |
233, | |
236, | |
246, | |
241, | |
255, | |
252 | |
}; | |
List<string> resolved = new List<string>(); | |
for (var i = 0; i < indexes.Length; i++) | |
{ | |
var val = indexes[i] ^ 248; | |
//make IP | |
string str = val.ToString(); | |
byte[] a = Encoding.ASCII.GetBytes(str); | |
string lenIP = String.Format("199.0.0.{0}", str.Length + 1); | |
resolved.Add(lenIP); | |
string valIP = ""; | |
if (str.Length > 1) | |
{ | |
valIP = String.Format("43.{0}.{1}.0", a[0], a[1]); | |
} | |
else | |
{ | |
valIP = String.Format("43.{0}.0.0", a[0]); | |
} | |
resolved.Add(valIP); | |
} | |
for (var i = 0; i < resolved.Count; i++) | |
{ | |
//Console.WriteLine("DomainsList.Add(\"{0}\");", resolved[i]); | |
Console.WriteLine("{0}", resolved[i]); | |
} | |
} | |
static void Main(string[] args) | |
{ | |
decodeIndexes(); | |
} |
I obtained a list of domains, and modified the code of the recompiled crackme, in order to emulate the appropriate responses to the DNS requests.
The list:
public static void initDomainsList() { DomainsList = new List<string>(); DomainsList.Add("200.0.0.1"); // Init id -> 1 DomainsList.Add("199.0.0.2"); DomainsList.Add("43.50.0.0"); DomainsList.Add("199.0.0.3"); DomainsList.Add("43.49.48.0"); DomainsList.Add("199.0.0.2"); DomainsList.Add("43.56.0.0"); DomainsList.Add("199.0.0.3"); DomainsList.Add("43.49.57.0"); DomainsList.Add("199.0.0.3"); DomainsList.Add("43.49.49.0"); DomainsList.Add("199.0.0.2"); DomainsList.Add("43.49.0.0"); DomainsList.Add("199.0.0.3"); DomainsList.Add("43.49.53.0"); DomainsList.Add("199.0.0.3"); DomainsList.Add("43.49.51.0"); DomainsList.Add("199.0.0.3"); DomainsList.Add("43.50.50.0"); DomainsList.Add("199.0.0.3"); DomainsList.Add("43.49.54.0"); DomainsList.Add("199.0.0.2"); DomainsList.Add("43.53.0.0"); DomainsList.Add("199.0.0.3"); DomainsList.Add("43.49.50.0"); DomainsList.Add("199.0.0.3"); DomainsList.Add("43.50.49.0"); DomainsList.Add("199.0.0.2"); DomainsList.Add("43.51.0.0"); DomainsList.Add("199.0.0.3"); DomainsList.Add("43.49.56.0"); DomainsList.Add("199.0.0.3"); DomainsList.Add("43.49.55.0"); DomainsList.Add("199.0.0.3"); DomainsList.Add("43.50.48.0"); DomainsList.Add("199.0.0.3"); DomainsList.Add("43.49.52.0"); DomainsList.Add("199.0.0.2"); DomainsList.Add("43.57.0.0"); DomainsList.Add("199.0.0.2"); DomainsList.Add("43.55.0.0"); DomainsList.Add("199.0.0.2"); DomainsList.Add("43.52.0.0"); }
The modifications in the domain retrieving function, in order to fetch the domain from the list instead of making a DNS query:
// Token: 0x06000045 RID: 69 RVA: 0x00003820 File Offset: 0x0000A820 public static bool DnsQuery(out byte[] r) { bool result = true; r = null; try { //IPHostEntry iphostEntry = Dns.Resolve(FLARE05.A); //r = iphostEntry.AddressList[0].GetAddressBytes(); string domainStr = DomainsList[DomainIndex % DomainsList.Count]; DomainIndex++; IPAddress ip = IPAddress.Parse(domainStr); r = ip.GetAddressBytes(); Console.WriteLine("IP: {0}.{1}.{2}.{3}", r[0], r[1], r[2], r[3]); DnsClass._Try = 0; Config._IncrementCounterAndWriteToFile(); } catch { DnsClass._Try++; result = false; } return result; }
I also patched out some sleeps to speed up the execution, and added more logging. Then I run my recompiled application, to verify if this is really the correct sequence to reach the flag decoding function.
WARNING: mind the fact that before running the application, it is required to remove all the previous files generated by the challenge, such as flare.agent.id
etc, otherwise they will distort the sequence.
And it works! So it is confirmed that the list of the IPs is valid. Also, the composed string leads to a section in the original PE, so the previous assumptions were correct:
Now all we have to do is to feed the sequence of the DNS responses to the original app.
In order to obtain the flag, we will use the original application and feed into it the list of the resolved IPs.
At first I thought about using some fake DNS, but finally I decided to just make a hooking DLL (based on MS Detours) and inject it into the original app. This is my implementation:
https://github.com/hasherezade/flareon2022/blob/main/task8/hooking_dll/main.cpp
My app assume that there is a simple fake DNS running, giving a dummy response for any queried IP. So, I am just replacing the content of this response with the IP from the list. The cleaner solution would be to construct the full fake response from scratch, and make it independent from a dummy response, but I had Apate DNS already running on my machine, and it was faster.
I injected the DLL into the executable using dll_injector:
And now we can watch the IPs queried, and just wait for the flag to be dropped…
At the same time we can see the domains being listed by ApateDNS, where they first reach:
After a while, this beautiful animated GIF is dropped to the TEMP, and popped out:
So, the task is solved!