Reversing .NET Applications: CCCamp19 CTF CampRE Challenge

August 25, 2019
ctf dotnet reverse-engineering

Finally a nice .NET CTF challenge - time to pull out dnSpy :)

The provided ZIP includes a CampRE.dll file which, according to the challenge description, is a .NET Core application. Time to boot a Windows VM and install the .NET Core runtime environment.

After decompiling the dll, this source code can be inspected:

private static void Main(string[] args)
{
    byte[] sourceArray = File.ReadAllBytes(Assembly.GetAssembly(typeof(Program)).Location);
    for (int i = 0; i < 1333337; i++)
    {
        MD5 md = MD5.Create();
        byte[] bytes = Encoding.ASCII.GetBytes("i=" + i.ToString());
        byte[] array = md.ComputeHash(bytes);
        MD5 md2 = MD5.Create();
        byte[] bytes2 = Encoding.ASCII.GetBytes("i1337=" + i.ToString());
        byte[] sourceArray2 = md2.ComputeHash(bytes2);
        byte[] destinationArray = new byte[32];
        Array.Copy(array, 0, destinationArray, 0, 16);
        Array.Copy(sourceArray2, 0, destinationArray, 16, 16);
        byte[] array2 = new byte[16];
        MD5 md3 = MD5.Create();
        byte[] array3 = new byte[10];
        Array.Copy(sourceArray, 4432, array3, 0, 10);
        byte[] sourceArray3 = md3.ComputeHash(array3);
        Array.Copy(sourceArray3, 0, array2, 0, 16);
        try
        {
            global::Aes aes = new global::Aes(array, array2);
            byte[] rawAssembly = aes.DecryptFromBase64StringAsByte(Program.toDecrypt);
            MethodInfo entryPoint = Assembly.Load(rawAssembly).EntryPoint;
            entryPoint.Invoke(null, new object[]
            {
                new string[] { "NOT_A_BADBOY" }
            });
        }
        catch (Exception ex) {}
    }
}

It seems that the application brute-forces the AES key for an embedded Base64 string in memory and tries to execute it as a binary afterwards. The first thing I did was modifying the dll with dnSpy in order to dump the decrypted binary to a local file:

byte[] decrypted = new global::Aes(array, array2).DecryptFromBase64StringAsByte(Program.toDecrypt);
// try to read some assembly info
Console.WriteLine(Assembly.Load(decrypted).FullName);
File.WriteAllBytes("Foo.dll", decrypted);

Obviously this didn’t work out of the box. It seems that the application reads some bytes from its own binary file in the beginning:

byte[] sourceArray = File.ReadAllBytes(Assembly.GetAssembly(typeof(Program)).Location);
[...]
Array.Copy(sourceArray, 4432, array3, 0, 10);

After applying the changes above, these bytes seem to be wrong since the offset changed. My second modification to the application fixes this my making the routine read the untouched file instead of its own binary:

byte[] sourceArray = File.ReadAllBytes("original.dll");

After calling the modified binary with dotnet CampRE.dll, a file called Foo.dll has been written. Now it’s time to decompile it:

private static void Main(string[] args)
{
    bool flag = args[0] != "BADBOY";
    if (flag) { Environment.Exit(-2); }

    StackTrace stackTrace = new StackTrace();
    StackFrame[] frames = stackTrace.GetFrames();
    bool flag2 = frames.Length == 1;
    if (flag2) { Environment.Exit(-1); }
    int metadataToken = frames[1].GetMethod().MetadataToken;
    byte[] array = new byte[32];
    byte[] array2 = new byte[16];
    for (int i = 0; i < 8; i++)
    {
        byte[] bytes = BitConverter.GetBytes(metadataToken);
        Console.WriteLine(BitConverter.ToString(bytes));
        Array.Copy(bytes, 0, array, i * 4, 4);
    }
    for (int j = 0; j < 4; j++)
    {
        byte[] bytes2 = BitConverter.GetBytes(metadataToken);
        Console.WriteLine(BitConverter.ToString(bytes2));
        Array.Copy(bytes2, 0, array2, j * 4, 4);
    }
    Aes aes = new Aes(array, array2);
    byte[] bytes3 = aes.DecryptFromBase64String("fQjMpHu5cK122YUeJHX8T5wzTKbVhVZcNcCvSGSedV8=");
    Console.WriteLine("Good job. Flag: " + Encoding.ASCII.GetString(bytes3));
}

It decrypts another Base64 string using a computed AES key. This key results from calling frames[1].GetMethod().MetadataToken and doing some magic stuff with it afterwards. The important thing to notice is that it’s using frames[1], which doesn’t exist when executing this second stage as a standalone binary. This stack frame must result from CampRE.dll calling this embedded second stage.

I decided to patch out the BADBOY check above and construct a new loader for the second stage:

private static void Main(string[] args)
{
    try
    {
        Assembly.Load(File.ReadAllBytes("Foo.dll")).EntryPoint.Invoke(null, new object[]
        {
            new string[]
            {
                "NOT_A_BADBOY"
            }
        });
    }
    catch (Exception) {}
}

This mimics CampRE.dll decrypting and calling the embedded second stage, while the MetadataToken from which the AES key is computed remains intact since the call stack is the same.

After re-compiling and running this, the flag is being printed:

yay

Peace out, thanks to ALLES CTF and 0x4d5a for this challenge!

In-Process Fuzzing With Frida

October 24, 2019
frida exploiting fuzzing reverse-engineering

Dynamic Instrumentation: Frida And r2frida For Noobs

September 13, 2019
radare2 r2 frida r2frida ctf reverse-engineering

r2con 2019 CTF Writeups

September 2, 2019
r2 radare2 ctf reverse-engineering