Reversing .NET Applications: CCCamp19 CTF CampRE Challenge

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!

37C3 CTF: ezrop

ctf reversing exploitation rop radare2 r2

BinaryGolf 2023: Building A GameBoy-Bash Polyglot

binary ctf

ShhPlunk: Muting the Splunk Forwarder

reverse-engineering c++ linux