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:
Peace out, thanks to ALLES CTF and 0x4d5a for this challenge!