Hack the Box Business 2020 - Ghost

This challenge was from the Hack the Box Business CTF and was a malware analysis kind. When reversing, I tend not to focuss too deeply on what’s is really going on and only look towards what can be useful. Renaming functions and variables, deobfuscation and doing a good work is not something that matters during a CTF; we only need the flag. Thus, why reverse engineer when you don’t have too? The less we reverse, the better it is.

Quick look

We are given 3 files; a pcap, a PE file and an encrypted PDF. We will need to reverse engineer the PE file, decrypt the traffic and then decrypt the PDF. Let’s start off by doing a string command on the PE file. The pcap contains only TCP communications and it is also encrypted.

[...]
golang.org/x/sys/windows/registry.OpenKey
golang.org/x/sys/windows/registry.Key.getValue
golang.org/x/sys/windows/registry.Key.GetStringValue
golang.org/x/sys/windows/registry.Key.setValue
golang.org/x/sys/windows/registry.Key.setStringValue
golang.org/x/sys/windows/registry.regSetValueEx
golang.org/x/sys/windows/registry.init
main.main
main.rzSF37vf8khc8Vye
main.g7Srk53hUCFt86bQ
main.QJ4rg98Fn233nn4s
main.DrERWCNyzs9az2eW
main.JJ23FehAEuwgD2Qv
main.KyRf6LTuBVzUfacr
main.ZCxF5YJQ7tsSP3Zn
main.TsPDE4w9RXwcc4rm
main.Tf5QX6MLXHpCESbu
main.s3jaCsNTu4J8cqth
main.mUBSP5wQrvrhp5FC
main.mjDE8mmD57D2pk6L

So, we will need to deal with a binary wrritten in Golang, and that’s probably something we don’t want. Launching IDA, we can find all the main.* functions. We can quickly open them all and check wether they worth a closer look. We can find two functions calling crypto_aes_NewCipher and crypto_cipher_newGCMWithNonceAndTagSize. There’s a hardcoded key, 12345612345612345612345612345612 and we can try to decrypt what’s inside the pcap.

AES CFB

from Cryptodome.Cipher import AES
import binascii

key = "12345612345612345612345612345612"
ciphers = ["b00347dee4399de165d65fde63efcc30786c4190186a13442e8a8bc6f2272819701ca86c",
		"d738b57fa4e3271a0acddf5279dc596e0f9673092a2fb9c1f00ef81111dfb4377624acc3f47b889997a8b43bb8af",
		"ee28ef0ddf01ff82d7436465e1b1b5df843b7679e44253a0bd6a7afeb8c53a309774"]

for cipher in ciphers:
	data = binascii.unhexlify(cipher)
	nonce, tag = data[:12], data[-16:]
	cipher = AES.new(key, AES.MODE_GCM, nonce)
	cleartext = cipher.decrypt_and_verify(data[12:-16], tag)
	print(cleartext)

.

$ python2 decrypt_pcap.py 
yOaw1fs6
megacorp\rashley

WS02

We can bot decrypt what was sent to the Command and Control (CnC) server (hexadecimal communications in red) and what was sent by it (in blue). There’s a big chunk of blue and when decrypted is actually another PE file, beginning with 0xa2bb3d872360.

TCP traffic

Encryptor

This decrypted PE file is written in .dotnet, and therefore easier to analyze, although the other binary wasn’t a problem so far.

At first glance, there’s a function called s2T4rbNt4JPA6DeM that encrypts things and write them into files. The function x3bMQH27hJEt2sz8 is the only one calling s2T4rbNt4JPA6DeM, and it passes filenames as a parameter and the encryption key. The encrypted key is generate like so: Guid.NewGuid().ToString();. When decrypting the network traffic, we found a Guid, so we will use this one. Strings used through the binary are encrypted via a xor key, in the function nVw6FkXDzmwHwVWZ and this xor key comes from arg[0]. Having no idea what the xor key can be, we can look what’s calling this function. We will find this oone.

private static void whJa4rDSBHveMmfj(string value)
{
	string str = umj3VnYEF8fY3bkw.nVw6FkXDzmwHwVWZ("CRg7LjVkJgAjdi1rBhg5PBw=", true);
	string str2 = umj3VnYEF8fY3bkw.nVw6FkXDzmwHwVWZ(
			"HQARER5QEiAUby5WOj8FCiFVBmAhXUY0Dy8TPRUdJAwlA0pyJCENHgVJLwUYXQdQLj4/CSpSBlk=",
					true);
	Registry.SetValue(str + str2, umj3VnYEF8fY3bkw.nVw6FkXDzmwHwVWZ("FCMaFh5CODcI", true), value);
}

The function Registry.SetValue will modify the registry. It needs, as parameters, the name/value pair. Looking on MSDN (https://docs.microsoft.com/en-us/dotnet/api/microsoft.win32.registry.setvalue?view=net-5.0), we find this:

Valid root names include HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, HKEY_CLASSES_ROOT, HKEY_USERS, HKEY_PERFORMANCE_DATA, HKEY_CURRENT_CONFIG, and HKEY_DYN_DATA.

Now knowing both the encrypted and the clear texts, we can find the encryption key, or at least, a part of it.

cleartext = "HKEY_CURRENT_USER"
cipher = str("CRg7LjVkJgAjdi1rBhg5PBw=".decode('base64'))
cipher += str("HQARER5QEiAUby5WOj8FCiFVBmAhXUY0Dy8TPRUdJAwlA0pyJCENHgVJLwUYXQdQLj4/CSpSBlk=".decode('base64'))

key = ""
for i in range(0, len(cleartext)):
    for j in range(0, 255):
        c = ord(cipher[i]) ^ j
        if c == ord(cleartext[i]):
            key += chr(j)
print("key:", key)

ct = ""
for i in range(0, len(cipher)):
    c = ord(cipher[i]) ^ ord(key[i % len(key)])
    ct += chr(c)
print(buf)

.

$ python2 dec_srings.py
('key:', "AS~wj'sRq3c?YMjyN")
HKEY_CURRENT_USER\Software\MicrosoUV7aG

We successfully decrypted a part of the string and we can guess some more: HKEY_CURRENT_USER\Software\Microsoft. Adding this part to our cleartext lead to more decryption.

$ python2 dec_strings.py
('key:', "AS~wj'sRq3c?YMjyNAS~wj'sRq3c?YMjyN3r")
HKEY_CURRENT_USER\Software\Microsoft!r#1^(\AL&~hi3<ersion\WindowsUpdat

Looking up at the Windows registry, we can guess the remaining missing text: HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\WindowsUpdate. Running again, we recover the full encryption key, AS~wj'sRq3c?YMjyNAS~wj'sRq3c?YMjyN3r<v4(PXaVhV~@m>$AS~wj'sRq3c?YMjyN3r<. We now have everything needed to decrypt anything that was encrypted by the .dotnet binary.

The encryption function looks like this.

private static void s2T4rbNt4JPA6DeM(string yr9Zavw65pKUDrxZ, string PFEFqXuurH3zqxNp)
{
	byte[] array = umj3VnYEF8fY3bkw.GWW79N5ekj2aMunb();
	FileStream fileStream = new FileStream(
					yr9Zavw65pKUDrxZ + umj3VnYEF8fY3bkw.nVw6FkXDzmwHwVWZ("bzQWGBlT", true),
						FileMode.Create);
	byte[] bytes = Encoding.UTF8.GetBytes(PFEFqXuurH3zqxNp);
	RijndaelManaged rijndaelManaged = new RijndaelManaged();
	rijndaelManaged.KeySize = 256;
	rijndaelManaged.BlockSize = 128;
	rijndaelManaged.Padding = PaddingMode.PKCS7;
	Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(bytes, array, 50000);
	rijndaelManaged.Key = rfc2898DeriveBytes.GetBytes(rijndaelManaged.KeySize / 8);
	rijndaelManaged.IV = rfc2898DeriveBytes.GetBytes(rijndaelManaged.BlockSize / 8);
	rijndaelManaged.Mode = CipherMode.CFB;
	fileStream.Write(array, 0, array.Length);
	CryptoStream cryptoStream = new CryptoStream(fileStream,
						rijndaelManaged.CreateEncryptor(),
						CryptoStreamMode.Write);
	FileStream fileStream2 = new FileStream(yr9Zavw65pKUDrxZ, FileMode.Open);
	byte[] array2 = new byte[1024];
	try
	{
		int count;
		while ((count = fileStream2.Read(array2, 0, array2.Length)) > 0)
		{
			cryptoStream.Write(array2, 0, count);
		}
		fileStream2.Close();
	}
	catch (Exception)
	{
		return;
	}
	finally
	{
		cryptoStream.Close();
		fileStream.Close();
	}
	File.Delete(yr9Zavw65pKUDrxZ);
}

To recap, we have the xor key to generate the encryption key from the Guid, the IV is added to the beginning of every encrypted files. The remaining part is to create the decryption function and this can be done by changing rijndaelManaged.CreateEncryptor() to rijndaelManaged.CreateDecryptor(). Our final decryption function will look like this.

public static string nVw6FkXDzmwHwVWZ(string FjzAXMdCE8cSHttD, bool cSHsdasdqwttD = true)
{
	byte[] PFEFqXuurH3zqxNp = Encoding.UTF8.GetBytes("AS~wj'sRq3c?YMjyN3r<v4(P`X`aVhV~@m>$AS~wj'sRq3c?YMjyN3r<")

	byte[] array;
	if (cSHsdasdqwttD)
	{
		array = Convert.FromBase64String(FjzAXMdCE8cSHttD);
	}
	else
	{
		array = Encoding.UTF8.GetBytes(FjzAXMdCE8cSHttD);
	}
	Console.WriteLine(array.Length);
	StringBuilder stringBuilder = new StringBuilder();
	for (int i = 0; i < array.Length; i++)
	{
		stringBuilder.Append((char)(array[i] ^ PFEFqXuurH3zqxNp[i % PFEFqXuurH3zqxNp.Length]));
	}
	return stringBuilder.ToString();
}

public static void s2T4rbNt4JPA6DeM()
{
	// first 32 bytes taken from Confidential.pdf.ghost
	byte[] array = { 0x9d, 0xa4, 0xef, 0x14, 0xa1, 0xfc, 0xa3, 0xfa, 0xcb, 0x31, 0x70, 0xd9,
	 		0x43, 0xf4, 0x62, 0x7f, 0xa5, 0xb2, 0xbd, 0xb6, 0x44, 0x69, 0x5d, 0x65,
			0xfe, 0x57, 0xa8, 0xb5, 0x90, 0xf1, 0x08, 0x5a };
	FileStream fileStream = new FileStream("confidential.pdf.ghost", FileMode.Create);
	byte[] bytes = Encoding.UTF8.GetBytes(nVw6FkXDzmwHwVWZ("d31dd518-8614-4162-beae-7a5a2ad86cc6", false));
	RijndaelManaged rijndaelManaged = new RijndaelManaged();
	rijndaelManaged.KeySize = 256;
	rijndaelManaged.BlockSize = 128;
	rijndaelManaged.Padding = PaddingMode.PKCS7;
	Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(bytes, array, 50000);
	rijndaelManaged.Key = rfc2898DeriveBytes.GetBytes(rijndaelManaged.KeySize / 8);
	rijndaelManaged.IV = rfc2898DeriveBytes.GetBytes(rijndaelManaged.BlockSize / 8);
	rijndaelManaged.Mode = CipherMode.CFB;
	fileStream.Write(array, 0, array.Length);
	CryptoStream cryptoStream = new CryptoStream(fileStream,
					rijndaelManaged.CreateDecryptor(),
					CryptoStreamMode.Write);
	FileStream fileStream2 = new FileStream("confidential.pdf", FileMode.Open);
	byte[] array2 = new byte[1024];
	try
	{
		int count;
		while ((count = fileStream2.Read(array2, 0, array2.Length)) > 0)
		{
			cryptoStream.Write(array2, 0, count);
		}
		fileStream2.Close();
	}
	catch (Exception)
	{
		return;
	}
	finally
	{
		cryptoStream.Close();
		fileStream.Close();
	}
}

Flag

Wrap up

As seen in this write-up, we didn’t reverse engineer that much; we spent only a few minutes on the Golang binary. This binary was reading commands to execute through a socket, and there’s was a switch case based on what was sent by the CnC (see the function main_rzSF37vf8khc8Vye). It could launch a process, write to the Windows registry, execute in-memory assemblies, etc. Was this relevent to solve the challenge? Not at all!