Introduction
After some time away (lots of distractions) from the initial analysis of the infection chain, explained in the previous blog post, I decided to continue the investigation.
In that post, I briefly suggested that the malware might be part of the Heracles infostealer family. However, that assumption was superficial and primarily based on VirusTotal labels. Some vendors had also tagged it as “PureLog,” but at the time, I had no idea what that referred to, so I blindly ignored it.
As I dug deeper into the next stage, what I initially thought was the final payload, I discovered that it was actually another step in the infection chain. Eventually, I identified it as a loader called PureCrypter.
There are several detailed analyses of this malware. The reports I found most useful are listed below:
To summarize, PureCrypter is a .NET-based malware whose primary function is to download and execute additional payloads. This type of functionality classifies it as a loader or dropper, a critical component in the modern malware ecosystem.
PureCrypter is distributed as part of a Malware-as-a-Service (MaaS) offering by a threat actor known as PureCoder. The service includes various malware tools from the “Pure” family, such as PureMiner, PureLogs, and PureClipper, alongside PureCrypter itself. Customers can subscribe to the service with either monthly or lifetime licenses and are given the ability to customize their PureCrypter builds by selecting options for code injection methods, anti-analysis techniques, and persistence strategies.
Interestingly, I discovered the most recent article on this list, from eSentire, while wrapping up this blog post. It turns out I was analyzing more or less the same sample (or malware version) described in their report, which helped validate many of my findings. It also guided me in renaming some variables to better reflect the original ones and improve overall readability.
In this article, I’ll take a deeper look into the loader itself and correct some of the (bad) assumptions I made in the previous post.
Pure Crypter Outer Layer
This section focuses on analyzing the file 5.exe
, which was extracted in the previous post: Analyzing the Infection Chain of a Stealer Malware.
Initial Analysis
The first step is to identify the file:
Detect It Easy reveals it’s a 32-bit .NET binary, heavily obfuscated, and contains packed (encrypted) resources.
The file includes obfuscated class and method names, and two different embedded resources.
Automatic Deobfuscation
In order to understand more what it really does, I used Net Reactor Slayer to help me deobfuscate it.
The resulting structure is slightly more readable in dnSpy, though still partially obfuscated.
The manifest tries to impersonate Macrium Rescue Media Builder. It is a legitimate utility used to restore disk images, repair Windows boot issues, or recover from ransomware attacks wtf.
No relevant strings were found in the binary, only some general imports, the module name (Upsiugyll.exe
), and some cryptographic class references. This strongly suggests the file decrypts and loads an embedded payload.
Upsiugyll
Upsiugyll.exe
Txompdygpzu.Properties.Resources.resources
System.Security.Cryptography
CryptoStream
CryptoStreamMode
ICryptoTransform
SymmetricAlgorithm
FromBase64String
CreateDelegate
GenerateKey
set_Key
set_IV CreateDecryptor
The binary contains two embedded resources:
- One is very small (359 bytes), with apparently random data.
- The other is ~1 MB with high entropy (
8.0
), most likely encrypted.
Main Method Analysis
The program’s entry point is the Main method, which immediately calls another method.
String (de)obfuscation
The strings of the binary are obfuscated, and use the Class4.smethod_0(279212158)
function to decrypt them during runtime.
While Karsten Hahn’s (MalwareAnalysisForHedgehogs) excellent video explains how to reverse this kind of obfuscation, it didn’t work in my case.
Example output:
decoded 279212158 : X0X
decoded 279212426 : X0X
decoded 279212474 : X0X
decoded 279212449 : X0X
decoded 279212220 : X0X ...
Analyze Decryption Methods
Looking further, it’s quite easy to discover the real functionality.
The key function is GClass1.smethod_0
, which takes two parameters: a Type
and a string
.
According to .NET documentation, it uses CreateDelegate
to invoke a method dynamically:
typeof(Action)
is used to encapsulate a method with no parameters and no return value.type_0
is the class instance.string_0
is the name of the method to be called.
In our case, GClass1.smethod_0(Class4.smethod_0(279212158))
will be the new class instance, and Class4.smethod_0(279212426)
the method to call.
All signs point to this method being responsible for decrypting the large encrypted resource.
This function also acts as a wrapper around a deeper decryption method:
The decryption logic becomes obvious when looking at the method body:
It takes three parameters:
- Encrypted Data
- Decryption Key
- Initialization Vector (IV)
With this knowledge, we just have to place some breakpoints and execute the malware to capture the decrypted payload.
Extract the Encrypted Resource
Dynamic Analysis
Using dnSpy, I began dynamic analysis by setting breakpoints inside the decryption routine. I first identified the AES key, which is retrieved from a separate method:
Continuing execution through the decryption function, I captured the IV and the decrypted data:
Key:
- 5488863CFB8BAFDDD9194AD722F0D263DFF9A80051D7739DE22E24316BF42C46
- VIiGPPuLr93ZGUrXIvDSY9/5qABR13Od4i4kMWv0LEY=
IV:
- 9F01D9E9F6881F70A4A59268514BE120
- nwHZ6faIH3CkpZJoUUvhIA==
The decrypted data (held in the array
variable) is another binary, which I saved as 6.dll
.
At the end of the main method, we can see the malware creating a delegate for a new method call using the decrypted assembly (“MkGDQLeJ4nATPJctZX.NGCpBwA62Zg4TyqkUP”) and entry method (“IBdGkVOwc”).
Static Analysis With Openssl
To verify the dynamic results, I attempted to decrypt the resource statically using OpenSSL. I saved the encrypted resource as Jobotp
, and used the known key and IV:
$ openssl enc -d -aes-256-cbc -in Jobotp -out Jobotp_dec -A -K 5488863CFB8BAFDDD9194AD722F0D263DFF9A80051D7739DE22E24316BF42C46 -iv 9F01D9E9F6881F70A4A59268514BE120
$ sha256sum Jobotp* 6.dll
b988bafe5ed6c8249f8f930be0b5c825cc76d62b23ec635d6e2ee03170415cb2 Jobotp
1edfa0f52db64ab0ee4ec893e49967eea811da7aec2c5f865c5653d2b6cfc643 Jobotp_dec 1edfa0f52db64ab0ee4ec893e49967eea811da7aec2c5f865c5653d2b6cfc643 6.dll
The hashes of the decrypted file (Jobotp_dec
) and the dynamically extracted 6.dll
are the same, great :)
Pure Crypter Inner Layer
The next stage of the malware, saved as 6.dll
, is another obfuscated .NET binary.
Initial Analysis
Once again, this is a 32-bit .NET executable. Packed, encrypted, and obfuscated.
The entropy shows packed content:
The binary is deeply obfuscated, and although the entry point (“IBdGkVOwc”) was extracted in the previous stage, it doesn’t seem to do anything meaningful on its own.
Embedded resources of 6.dll
:
- 0x000526B3:
a8sd8xXjW1fQDg5lt1.TtAThnlX8QQWlpiIxY
(328269 bytes) - 0x0004EC48:
eL6CJJmBevreXd7yBF.JaNMZ3LqTZJqXYVa7s
(14951 bytes) - 0x000A2904:
NjeipF3nBvRK43tZ0T.kF2eOl1lrd1DxwkVBP
(256 bytes) - 0x000A2A08:
OgrEIKkQH20BVumdDM.6xsIgkhl6Yf897SD0n
(337408 bytes) - 0x000F500C:
tgZ33CHmTLqfrCcG2b.8Z63dnKmBDPRIoEiDg
(1116 bytes) - 0x0004D894:
xVi40WxNWgcbs5XrtX.XflwYC6A8MigIQwC9W
(5040 bytes)
Automatic Deofbuscation
I ran .NET Reactor Slayer again to simplify the analysis process.
While the obfuscation is slightly reduced and the code is easier to navigate, it still contains over a hundred classes and even more methods.
The resources have been reduced to only three:
- 0x0001BAF0:
eL6CJJmBevreXd7yBF.JaNMZ3LqTZJqXYVa7s
(14951 bytes) - 0x0001F55C:
NjeipF3nBvRK43tZ0T.kF2eOl1lrd1DxwkVBP
(256 bytes) - 0x0001F660:
OgrEIKkQH20BVumdDM.6xsIgkhl6Yf897SD0n
(337408 bytes)
String Deofbuscation
This sample uses a similar string encryption technique as seen in the previous layer. Here, strings are decrypted using Class47.smethod_0()
.
I tested this by decrypting a single string, and it worked! Then I used Karsten’s Python script to automatically extract more strings:
.exe .\string_decrypter.py "C:\Users\user\Desktop\6_Slayed.dll" 0x0600020a python
This time, the script worked perfectly. Shoutout to Karsten Hahn <3
With the decrypted strings in place, many functions became much clearer. For example, in Class3
, several dynamically loaded system functions are now visible:
After renaming the methods and variables, the code starts to make more sense:
By analyzing these functions and their usage across the binary, I was able to identify what appears to be the main logic of this payload. After some time and a lot of manual renaming and tracing, I began to understand the inner workings.
I renamed all the function names and parameters myself. Some names might not be exact or even misleading, but I picked ones that made the most sense to me during the analysis.
Main Function and Malware Configuration
The main function begins by loading an embedded resource, which is then decrypted and decompressed. The resulting data is deserialized into objects, which are mapped to internal classes.
By debugging the malware and placing a breakpoint after this stage, I was able to extract the configuration data (distributed across three classes), and rename the associated fields to improve readability and make more sense of it. This process took several iterations.
All these configuration options are used later in the code, enabling or disabling specific functionalities. Many of these options have been previously observed in the samples analyzed by Romain Dumont from Zscaler (2022), and they are also present in the lastest eSentire report (2025).
Additionally, leaked images of PureCrypter’s builder helped me to confirm, rename, and identify some features in the binary.
PureCrypter Builder v2 (2022):
PureCrypter Builder v4 (2025):
Resources
There are three embedded resources after slayering the binary. One of them, OgrEIKkQH20BVumdDM.6xsIgkhl6Yf897SD0n
, is quite large (337408 bytes). By looking at the entropy, it revealed a constant value of 8.0
, strongly indicating that this resource is encrypted.
I tried to manually extract its contents, but nothing worked. I kept thinking it might hide another part of the malware, like a payload, or some extra piece of the loader. That idea stuck with me the whole time and became the reason I kept going deeper into the analysis.
I had already found that the malware’s configuration is in some place of the resources, but the line that gets the object ("Qjbdw"
) doesn’t directly match any of the visible resources in the binary.
internal static byte[] \uE004 {
get {
object @object = Resources.\uE002.GetObject("Qjbdw", Resources.cultureInfo_0);
return (byte[])@object;
}
}
[EditorBrowsable(EditorBrowsableState.Advanced)]
internal static ResourceManager \uE002 {
get {
if (Resources.resourceManager_0 == null) {
= new ResourceManager("Ygksfxaml.Properties.Resources", typeof(Resources).Assembly);
ResourceManager resourceManager .resourceManager_0 = resourceManager;
Resources}
return Resources.resourceManager_0;
}
}
This discrepancy is explained by a helper method within Class68, which I manually renamed to load_resource_OgrEIKkQH20BVumdDM()
. This function is called in a lot of different places of the code.
Resource Loader
Class68 is a central piece of the resource unpacking logic:
- Extracts the embedded resource
OgrEIKkQH20BVumdDM.6xsIgkhl6Yf897SD0n
usingAssembly.GetManifestResourceStream()
- Unpacks the data
- Loads the data into memory as an
Assembly
object - Adds the in-memory assembly in
AppDomain.CurrentDomain.ResourceResolve
, in order to dynamically resolve dependencies.
This mechanism acts as a dependency resolver, and all the dependencies are embedded in a single compressed resource.
Class49 also decrypts resources and adds them to AppDomain.CurrentDomain.ResourceResolve
.
Embedded Resource Registry
Class45 reinforces this hypothesis. It contains a dictionary mapping different assembly dependencies.
This list includes multiple resource DLLs, commonly bundled via tools like Costura
. These DLLs are not directly visible in the unpacked binary because they’re nested inside the larger OgrEIKkQH20BVumdDM.6xsIgkhl6Yf897SD0n
resource.
Pure Crypter Features
With the resource topic clarified, we can continue with the main function analysis.
The main function calls 17 methods, each corresponding to a distinct feature of the malware. These features are enabled or disabled based on the configuration parameters, extracted just before.
In the analyzed sample, many of these features were disabled. However, it was still possible to identify their intended behavior.
The PureCrypter malware includes several mechanisms for executing additional payloads:
-
Executes a PowerShell command
-
-
Drops a payload to the system’s temporary directory and executes it.
-
-
Loads and executes a payload either embedded within the binary or downloaded from a remote URL.
Supports multiple injection techniques:
- Reflection - Process Hollowing - Shellcode
-
-
Executes shellcode directly from an embedded resource.
-
Only run_input_payload
was enabled in my sample.
1. ensure_single_instance
This function is called at the very start, and ensures the malware runs only once per system. It creates a Mutex using a name from the configuration.
If the mutex already exists, it calls a function to restore the network and exits.
2. antidebug
If is_debug()
returns true
, the malware exits early after restoring the network.
The is_debug
function performs several anti-analysis checks:
CheckRemoteDebuggerPresent
- Checks if
SbieDll.dll
orcuckoomon.dll
are loaded (Sandboxie and Cuckoo Sandbox detection) - Number of processors >
2
- Parent process is not
cmd
- Detects virtual environments via WMI (
"VMware|VIRTUAL|A M I|Xen"
or"Microsoft|VMWare|Virtual"
) - Checks monitor size
- Confirms system is 64-bit
- Username is not
"john"
,"anna"
or ones containing"xxxxxxxx"
3. ipconfig_release
Disconnects the network using ipconfig /release
4. sleep_x_seconds
Implements a delay before proceeding. Wasting my time.
5. bypass_AMSI
Bypasses the AMSI using in-memory patching.
The obfuscation layer simply strips “Hzhmoojfp” from strings to reveal amsi.dll
and AmsiScanBuffer
.
private static string smethod_1(string string_0)
{
return string_0.Replace("Hzhmoojfp", null);
}
The function:
- Resolves
AmsiScanBuffer
viaGetProcAddress
- Deobfuscates the patch
- Sets memory to
PAGE_EXECUTE_READWRITE
- Patches the function using
Marshal.Copy
- Restores the original memory protection
Patch forces AmsiScanBuffer
to always return 0x80070057
(E_INVALIDARG
), making AMSI scans fail.
- 64-bit patch:
"uFcAB4D"
0x00000000 b857000780 mov eax, 0x80070057
- 32-bit patch:
"uFcAB4DCGAA="
0x00000000 b857000780 mov eax, 0x80070057
0x00000005 c21800 ret 0x18
This technique is well-documented in Rasta Mouse’s article.
6. bypass_ETW
Disables Event Tracing for Windows (ETW) by patching EtwEventWrite
.
This technique is detailed in The Red Team Vade Mecum. It prevents ETW from logging telemetry data, evading detection by Windows Defender and EDR tools.
The function patches EtwEventWrite
similarly to the AMSI bypass:
- 64-bit patch:
"whQA"
0x00000000 c3 ret
- 32-bit patch:
"ww=="
0x00000000 c21400 ret 0x14
7. DLL_unhooker
Implements API unhooking for ntdll.dll
and kernel32.dll
on Windows 10+ systems.
The core logic is in remove_security_hooks
, which performs the following steps:
- Loads the target DLL
- Gets information about the loaded module via
GetModuleInformation
- Locates the original file from the system directory
- Opens the clean DLL file from disk and creates a file mapping of it
- Maps the file into memory to access its original contents
- Changes memory protection to allow writing
- Copies the clean, original code from the file mapping over the potentially hooked code in memory
- Restores the original memory protection
This is a technique used to blind security tools that use API hooking to monitor malware behavior. By “unhooking” these DLLs, the malware restores the original, unmodified functions, effectively bypassing the security monitoring.
8. defender_exclusion
If the defender_bypass
option is enabled in the configuration, the malware attempts to add its own executable and the persistence file to Windows Defender’s exclusion list.
The process:
- Constructs PowerShell commands to exclude:
- The malware file path
- The persistence file path
- Encodes the commands in Base64
- Enters an infinite loop attempting to execute the commands with elevated privileges and a hidden window
9. run_powershell
Executes arbitrary PowerShell commands defined in the configuration.
- If
runas
is set in the config, it attempts to execute with elevated privileges - Uses
ProcessStartInfo
withCreateNoWindow
andWindowStyle.Hidden
to remain stealthy - Keeps retrying until the command runs successfully (if elevated execution is required)
10. run_dropper
Drops and executes an embedded payload to the system’s temporary directory.
Execution flow:
- Checks if the dropper feature is enabled and if persistence is already installed
- Builds the file path in the system’s temp directory (
Path.GetTempPath()
) - If
one_time
is false or the file doesn’t already exist:- The payload is reversed and GZip decompressed
- Saved to disk
- Executed using
Process.Start()
This is a traditional dropper approach.
11. show_fake_message
Displays a fake message box to deceive the user.
12. setup_persistence
This function checks the configuration to determine if persistence should be installed, and chooses one of three available methods:
Registry Run key
Adds a value to
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
pointing to the malware executable path.VBS Startup Script
Drops a
.vbs
file into the Startup folder which silently executes the malware..WriteAllText(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Startup), Path.GetFileNameWithoutExtension(fileInfo.FullName)) + ".vbs", "CreateObject(\"WScript.Shell\").Run \"\"\"" + fileInfo.FullName + "\"\"\""); File
Scheduled Task
Creates a Windows scheduled task that:
- Runs repeatedly every 2–4 minutes
- Is not disabled by battery usage settings
- Has no time limit and is not hidden
- Executes the malware persistence path
In all cases, the malware first ensures the target persistence directory exists and copies itself to the configured location.
Anti-upload
If anti_upload
option is enabled in the configuration, and no input_payload.destination_path
is defined, the malware appends 260MB-300MB of random bytes to its copied file. This bloats the file to avoid automatic submission to antivirus vendor clouds, which often restrict large samples.
13. anti_file_delete
If enabled, the malware opens its own file with restrictive FILE_SHARE_READ
permissions, then uses DuplicateHandle
to pass the handle to explorer.exe
.
This prevents the file from being deleted, as explorer.exe
holds an open handle with exclusive access.
14. run_input_payload
This is the main loader. It first attempts to enter debug mode if running as admin, then retrieves the payload and executes it using one of the configured injection methods.
If load_from_disk
is false, the loader fetches the payload from a remote URL. Otherwise, it decrypts and decompresses the local embedded payload.
If the payload is embedded, it is decrypted and decompressed using the same method and encryption keys as the configuration, meaning it is indeed double-packed.
- AES Key:
/aafR1VtIS+XL4NVuj3yhOxi1YHh8mOLQ9CIjDHJPwM=
- AES IV:
EnIC8TwBlRAospftIvUaCA==
The loader supports three execution methods:
execute_reflection
Loads and executes the payload as a .NET assembly in memory.
execute_process_hollowing
Starts a new process (
InstallUtil.exe
,RegAsm.exe
,MSBuild.exe
,aspnet_compiler.exe
, orAppLauch.exe
, based on the information from the leaked builder), then deallocates its original memory and injects the malicious payload in its place.execute_shellcode
Allocates memory in another process (or the same one if target doesn’t exist), writes shellcode, and spawns a thread to run it.
15. run_loader
This function executes an additional payload embedded in the configuration. It works similarly to the run_dropper
function (reversing and decompressing the payload) but instead of writing it to disk, it injects and runs it directly using the previously described execute_shellcode
method.
16. self_deletion
As part of its cleanup routine, and if enabled in the configuration, the malware attempts to delete its own executable using a PowerShell command.
17. ipconfig_renew
As a final step, the malware tries to reconnect the network using ipconfig /renew
Then, it terminates its own process:
.GetCurrentProcess().Kill(); Process
Next Stage Payload
After all this looong analysis, let’s continue with my actual sample.
By looking at the configuration, many of the explained options were disabled. The only relevant information was the payload, of only 0x22 bytes.
hxxps://usa[.]trendys[.]cloud/code.bin
Sadly, at the time of the analysis, the domain was no longer alive :(
But that didn’t stop me there. I checked urlscan and bingo! There was one hit, and it even captured the downloaded file.
SHA256: dffdda5092b49622eb45606a32a23015c08e81df9fc4a8ba75bc1f6c32fa1b8e
MIME: Audio file with ID3 version 2.4.0
Size: 7 MB (6975602 bytes, 100% done) Downloaded from: https://usa[.]trendys[.]cloud/
A 7MB audio file? That sounded familiar…
The hash matched the one analyzed in my previous blog post, confirming it’s from the same campaign. However, it wasn’t the code.bin
file I was after.
Luckily (or not), I found it on VirusTotal:
According to VirusTotal, the downloaded file is a Rhadamanthys stealer.
But this post is already long enough. Let’s save that analysis for another time, if so.
Conclusion
I wanted to be absolutely sure the loader didn’t contain any hidden payloads, so I went all in on a full deep dive.
In the end, my particular sample wasn’t all that interesting, but the journey was well worth it. I learned a ton about this loader and .NET internals along the way.
If you made it this far, I hope you learned something too :)