
Author: Danyal Rana
SHA-256: 0ec8cd5306167f1c4a1480d0fc5f09099834435ec6ea308c48eabaa8eb2cd019
MD5: 06bac500056ea85c8e73e2677e97c902
The analyzed specimen initially presents as a heavily obfuscated JavaScript file, but the JavaScript is only the outermost layer of a considerably more sophisticated execution chain.
The script uses a custom character-code transformation to conceal strings, interacts with Windows Script Host through WScript and WScript.Shell, writes files through ADODB.Stream, establishes persistence using hidden Windows Scheduled Tasks, and launches a PowerShell stage with execution-policy bypass and hidden execution.
That PowerShell stage reconstructs a large Base64-encoded blob which is decrypted using AES-128-CBC and then decompressed with GZip, producing a 315,904-byte x64 .NET/Mono PE assembly.
The .NET component contains multiple layers of additional obfuscation and loader logic. It implements a custom RC4-like string transformation, manually walks PE structures to resolve exported functions, and avoids conventional P/Invoke imports by converting resolved addresses into delegates through Marshal.GetDelegateForFunctionPointer.
Recovered API names include:
AmsiScanStringVirtualProtectGetCurrentProcessFlushInstructionCacheThe resolved APIs are passed into a dedicated runtime patching routine. That routine identifies target function addresses, examines their initial bytes, changes memory protections with VirtualProtect, overwrites instructions byte-by-byte, calls FlushInstructionCache, and subsequently restores the original memory protection.
The .NET assembly also contains a 299,520-byte embedded static blob. Rather than processing it directly, the loader extracts every fifth byte into a 59,904-byte buffer, applies another cryptographic transformation, and validates the resulting data as a PE by checking for the MZ signature. The resulting object is then handled through .NET reflection.
The analysis therefore reveals a multi-stage architecture combining script-based persistence, encrypted staging, custom cryptography, manual API resolution, runtime code modification, and embedded PE reconstruction.
The final collection modules, C2 configuration, and complete purpose of the recovered secondary PE were not conclusively established during the current analysis pass and require further reversing.
The original specimen was a roughly 494 KB UTF-16LE JavaScript file with CRLF line endings.
After normalization to UTF-8, the script contained approximately 388 KB of JavaScript source and 2,470 lines.
The outer layer makes extensive use of generated variable names and a custom string decoder:
The transformation subtracts 1248 from every character code, reconstructing the underlying strings at runtime.
This initially defeated straightforward searches for common JavaScript execution and networking primitives such as eval, Function, fetch, XMLHttpRequest, WebSocket, child_process, and powershell.
Once the custom decoder was reversed, the outer layer became considerably more transparent.
The script uses Windows Script Host objects rather than operating as conventional browser JavaScript.
Recovered strings included:
The presence of ADODB.Stream is particularly relevant because the loader uses stream operations such as:
This provides the specimen with a convenient mechanism for reconstructing and writing additional content to disk.
The script also expands %TEMP% and constructs temporary-path based output.
The outer script contains functionality for creating persistence through the Windows Task Scheduler.
Recovered strings include:
alongside XML defining a hidden logon-triggered task.
The generated task configuration contains, among other attributes:
The loader also contains task naming fragments resembling:
The use of hidden Scheduled Tasks allows the payload to be launched at user logon while attempting to blend the persistence mechanism into normal Windows task activity.
A second task-generation path uses a time-based trigger and similarly registers a hidden task.
The JavaScript reconstructs a PowerShell script in %TEMP% and launches:
with:
The resulting execution chain is therefore:
After the PowerShell stage completes its work, the temporary script is explicitly removed using:
This provides rudimentary cleanup of the generated PowerShell material.
The decoded PowerShell stage invokes:
The two Base64 fragments are each 68,832 characters and must be concatenated before decoding.
Together they produce:
The ciphertext has a length divisible by the 16-byte AES block size, confirming that the two fragments form a single encrypted stream.
The PowerShell code explicitly constructs:
with:
and the following hardcoded parameters:
The decrypted output begins with:
confirming a GZip stream.
The complete transformation is therefore:
The 51,624-byte single-fragment decoding initially failed AES block alignment, which initially appeared anomalous. The source was subsequently determined to contain two adjacent parseConfig fragments, resolving the discrepancy.
The GZip stage decompresses to:
file identifies the result as:
The assembly contains:
and extensive obfuscated metadata.
Raw strings reveal numerous compiler-generated structures and renamed values such as:
The assembly contains only a small number of PE sections, including:
and extensive static data embedded into the assembly.
The .NET assembly contains a particularly large embedded static array:
mapped through:
and instantiated at runtime as:
The loader’s static constructor materializes this data using:
The same constructor initializes numerous smaller arrays used for obfuscated strings and cryptographic material.
The static data layout includes a 32-byte field used as a cryptographic seed as well as multiple small arrays ranging from approximately 6 to 26 bytes.
The loader’s string obfuscation uses a custom stream-cipher implementation.
A 32-byte embedded seed is recovered from:
with the following value:
The loader XORs every byte with:
producing:
The resulting data is hashed using SHA-256:
This digest is then used as the key material for a custom RC4-like key scheduling and stream-generation routine.
Rather than calling an obvious RC4 implementation, the assembly constructs its own 256-byte permutation state, performs key scheduling manually, and uses the resulting state to transform embedded byte arrays.
The custom string decryption recovered four important API names.
AmsiScanStringVirtualProtectGetCurrentProcessFlushInstructionCacheThe loader does not simply import these through ordinary P/Invoke declarations.
Instead, it manually walks loaded modules, parses PE structures, resolves exported function addresses, and invokes:
to convert raw addresses into callable managed delegates.
This allows the sample to avoid exposing the native API relationships through conventional static import declarations.
A dedicated resolver routine performs operations including:
The implementation walks PE structures from module base addresses and reconstructs exported function addresses manually.
Rather than simply asking the CLR to resolve APIs, the malware essentially implements its own lightweight export-resolution mechanism.
The resulting function addresses are then associated with specific delegate signatures.
This technique also allows the same loader to keep the native API names encrypted until runtime.
This is one of the most significant behaviors uncovered during the analysis.
The resolved functions are passed to a dedicated routine responsible for modifying code at runtime.
The patch routine:
VirtualProtect to change the target memory protection.GetCurrentProcess.FlushInstructionCache against the modified address/range.Conceptually:
This is materially different from a loader simply allocating a new executable buffer.
The code is modifying already-loaded executable memory.
The use of FlushInstructionCache immediately after the instruction writes is particularly notable because it is part of the code-modification sequence rather than merely being present elsewhere in the assembly.
AmsiScanString is one of the functions resolved by the loader’s native export mechanism and passed into the patching routine.
The combination of:
with a routine that:
is strongly indicative of AMSI-related runtime patching/evasion.
The analysis did not yet recover and formally document the final patch bytes themselves, so the exact modification applied to the AMSI function should not be overstated.
What is established is that the loader has the capability and the target selection necessary to perform runtime modification of resolved native functions, including AmsiScanString.
The 299,520-byte v4576 blob is not consumed directly.
The loader first clones it:
then calculates:
and creates a new byte array of that size.
The extraction loop increments the source index by 5 bytes per iteration, effectively selecting:
from the original embedded data.
This produces:
The extracted buffer then enters another custom cryptographic transformation.
The loader clones the same 32-byte seed and applies a different byte transformation before SHA-256 processing.
A separate static byte value is initialized as:
rather than the 0x07 constant used for the earlier string-decryption stage.
The sequence is again:
This demonstrates that the loader uses the same general cryptographic architecture multiple times with different constants and input buffers.
After the second-stage transformation, the resulting buffer is explicitly tested for:
which corresponds to the MZ PE signature.
The loader therefore expects the transformed data to contain a Windows executable.
Once the signature check succeeds, the loader switches to .NET reflection primitives, including:
This indicates that the recovered PE is intended to be dynamically handled through managed reflection rather than simply being written to disk and launched as a conventional executable.
The exact final execution path and functionality of this reconstructed PE remain subjects for continued analysis.
The complete chain recovered so far can be summarized as:
| Component | Finding |
|---|---|
| Initial format | UTF-16LE JavaScript |
| Outer obfuscation | Character code −1248 |
| Script engine | Windows Script Host |
| File staging | ADODB.Stream |
| Persistence | Hidden Scheduled Tasks |
| PowerShell | Hidden, no profile, execution-policy bypass, non-interactive |
| Encryption | AES-128-CBC |
| Padding | PKCS#7 |
| Compression | GZip |
| Recovered .NET payload | 315,904 bytes |
| Architecture | x64 PE32+ |
| Managed runtime | Mono/.NET |
| Embedded blob | 299,520 bytes |
| Extraction | Every fifth byte → 59,904 bytes |
| String crypto | Custom RC4-like implementation |
| Key derivation | XOR + SHA-256 |
| Native resolution | Manual PE export traversal |
| Delegate creation | GetDelegateForFunctionPointer |
| AMSI API | AmsiScanString |
| Memory manipulation | VirtualProtect + byte-wise writes |
| Instruction synchronization | FlushInstructionCache |
| Process handle | GetCurrentProcess |
| Payload validation | MZ PE signature |
| Managed loading | Reflection |
This specimen demonstrates a considerably more deliberate implementation than a simple script-based malware loader.
The JavaScript layer acts primarily as an initial execution and staging mechanism. The interesting functionality begins after the script hands execution to PowerShell and eventually loads the .NET component.
The .NET assembly combines several anti-analysis and loader techniques:
The most notable behavior is the runtime patching mechanism. The loader does not simply allocate a new executable buffer and transfer control to it. Instead, it identifies target functions already resident in memory, validates their initial instructions, temporarily changes their memory protections, overwrites instructions, flushes the instruction cache, and restores the page protections afterward.
That makes FlushInstructionCache particularly interesting in this sample. Its presence is not incidental; it forms part of a coherent native-code modification workflow.
The analysis has established the loader architecture, cryptographic stages, API-resolution mechanism, runtime patching behavior, and embedded PE reconstruction.
Several important questions remain:
What exact instructions are written to the AMSI target?
The patch descriptor has been located, but the final patch semantics should be fully reconstructed before documenting the exact AMSI modification.
What is the final embedded PE?
The loader reconstructs and validates a PE from the 299 KB blob, but its full behavior still needs to be reversed.
What information is collected?
The current stage of analysis has not yet provided enough evidence to make a definitive claim about credential, browser, wallet, messaging, or filesystem theft.
Where is the C2 infrastructure?
No final C2 endpoint has been conclusively established from the stages analyzed so far.
What began as an obfuscated JavaScript file turned out to be a multi-stage loader architecture spanning:
WSH → PowerShell → AES/GZip → .NET → custom cryptography → manual API resolution → runtime code patching → embedded PE reconstruction.
The sample is particularly interesting because the authors did considerably more than hide strings or pack a payload. The loader actively manipulates its runtime environment, resolves native functions manually, targets AMSI-related functionality, modifies executable memory, synchronizes instruction execution, and reconstructs another PE from a deliberately interleaved embedded blob.
There is still another layer waiting at the end of this chain.
Further reversing confirmed that the loader’s runtime patching routine explicitly targets AmsiScanBuffer and AmsiScanString. The routine performs a byte-oriented scan of the resolved function address rather than decoding instructions. It treats 0xE8 as a 5-byte patch target, 0xFF 0x15 as a 6-byte patch target, replaces matching regions with 0x90 NOP instructions, and terminates scanning when it encounters 0xC3 or 0xC2.
Testing the routine against a clean x64 amsi.dll build produced two concrete patches within AmsiScanBuffer: the 5-byte relative CALL at 0x1800025A5 and the 6-byte indirect CALL at 0x18000260D. Both would be replaced entirely with NOP instructions.
AmsiScanString, however, exposes an implementation weakness. The routine encounters 0xC3 at offset +0x2F, but that byte is not a RET; it is part of the 49 3B C3 encoding for cmp rax, r11. The scanner therefore terminates before reaching the actual E8 call to AmsiScanBuffer at 0x180002682. This demonstrates that the patcher relies on raw byte signatures rather than instruction-aware disassembly and can be sensitive to the exact target DLL build.
The recovered .NET execution framework contains a manual x64 PE mapping routine that searches the payload export table for a custom hype_map_entry export, creates and maps a section with NtCreateSection/NtMapViewOfSection, applies x64 relocations, resolves imports, registers the exception table, executes TLS callbacks, flushes the instruction cache, and creates a thread at the calculated execution address. If direct thread execution fails, the loader falls back to a trampoline/fiber-based execution path using StompModuleTrampoline, ConvertThreadToFiber, CreateFiber, and SwitchToFiber
The manual PE loader contains a fallback mechanism that resolves kernel32!ExitThread, loads one or more DLLs specified by internal _stompDlls/_stompExports arrays, and overwrites the selected exported function with a 0x28-byte dynamically constructed trampoline. The generated stub embeds both the mapped payload entry address and the resolved ExitThread address. The target export is temporarily made writable with VirtualProtect, overwritten with Marshal.Copy, and followed by FlushInstructionCache. The resulting trampoline is then used through a fiber-based execution path when direct thread creation fails.
The manual loader contains four hardcoded DLL/export pairs winmm!PlaySoundW, cabinet!FCICreate, dbghelp!MiniDumpWriteDump, and version!GetFileVersionInfoW. It loads each candidate DLL, resolves the selected export, constructs a 0x28-byte x64 trampoline containing the mapped payload entry address and kernel32!ExitThread, temporarily changes the export’s memory protection to writable/executable, overwrites the export, restores its previous protection, and flushes the instruction cache. The resulting function address is then supplied as the start routine for a fiber when direct payload-thread creation fails.