The Loader From Hell: JavaScript, .NET, Custom Crypto, and Runtime Code Patching

Author: Danyal Rana

SHA-256: 0ec8cd5306167f1c4a1480d0fc5f09099834435ec6ea308c48eabaa8eb2cd019

MD5: 06bac500056ea85c8e73e2677e97c902

Executive Summary

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:

  • AmsiScanString
  • VirtualProtect
  • GetCurrentProcess
  • FlushInstructionCache

The 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.

1. Initial JavaScript Layer

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:

var PU=“length”;
var v9hz=“cha”+“rCodeAt”;
var NkTg=“from”+“CharCode”;
 
var zWQK=function(s){
var ZLT=“”,K2,VF,wBc=1248;
for(K2=0;K2<s[PU];K2++){
VF=s[v9hz](K2)–wBc;
ZLT+=String[NkTg](VF)
}
return ZLT
}

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.

2. Windows Script Host and File Creation

The script uses Windows Script Host objects rather than operating as conventional browser JavaScript.

Recovered strings included:

WScript
WScript.Shell
WScript.Network
ADODB.Stream
GetObject
new:

The presence of ADODB.Stream is particularly relevant because the loader uses stream operations such as:

Open
WriteText
SaveToFile
Close
 

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.

3. Scheduled Task Persistence

The outer script contains functionality for creating persistence through the Windows Task Scheduler.

Recovered strings include:

Schedule.Service
Connect
GetFolder
RegisterTask

alongside XML defining a hidden logon-triggered task.

The generated task configuration contains, among other attributes:

<LogonTrigger>
<Enabled>true</Enabled>
<UserId>…</UserId>
</LogonTrigger>
 
<Settings>
<Enabled>true</Enabled>
<Hidden>true</Hidden>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
</Settings>
 

The loader also contains task naming fragments resembling:

MicrosoftEdgeUpdateTaskCo…
PhotoEngine
PhotoStudio.

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.

4. PowerShell Execution

The JavaScript reconstructs a PowerShell script in %TEMP% and launches:

 
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
 

with:

-WindowStyle Hidden
-NoP
-EP Bypass
-NonInteractive
-File
 

The resulting execution chain is therefore:

JavaScript
↓
WScript
↓
write PowerShell stage
↓
PowerShell
├── hidden window
├── no profile
├── execution policy bypass
└── non-interactive execution
 

After the PowerShell stage completes its work, the temporary script is explicitly removed using:

 
 
Remove-Item ‘<path>’,‘<path>’ –Force –EA 0
 

This provides rudimentary cleanup of the generated PowerShell material.

5. Encrypted PowerShell Payload

The decoded PowerShell stage invokes:

parseConfig ‘<blob1>’ + parseConfig ‘<blob2>’

The two Base64 fragments are each 68,832 characters and must be concatenated before decoding.

Together they produce:

Base64 length: 137,664 characters
Ciphertext: 103,248 bytes
 

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:

System.Security.Cryptography.AesManaged
 

with:

Mode = CBC
Padding = PKCS7
 

and the following hardcoded parameters:

AES key

84e8872ef28616c25c195cfcd1269dfa

IV

1a63c7ec80aea70c21278679afc6c487
 

The decrypted output begins with:

1f 8b 08

confirming a GZip stream.

The complete transformation is therefore:

Base64 fragments
↓
concatenate
↓
AES-128-CBC
↓
PKCS#7
↓
GZip
↓
315,904-byte PE
 

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.

6. Recovered .NET Payload

The GZip stage decompresses to:

315,904 bytes
 

file identifies the result as:

PE32+ executable (GUI) x86-64 Mono/.Net assembly, for MS Windows
 

The assembly contains:

.NET Framework 4.0-era metadata
.NET 4.8 targeting information

and extensive obfuscated metadata.

Raw strings reveal numerous compiler-generated structures and renamed values such as:

__StaticArrayInitTypeSize=299520
__StaticArrayInitTypeSize=32
…
v8573.exe

The assembly contains only a small number of PE sections, including:

.text
.rsrc

and extensive static data embedded into the assembly.

7. Embedded Static Data

The .NET assembly contains a particularly large embedded static array:

299,520 bytes

mapped through:

v7507

and instantiated at runtime as:

v4576

The loader’s static constructor materializes this data using:

RuntimeHelpers::InitializeArray

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.

8. Custom RC4-Like String Encryption

The loader’s string obfuscation uses a custom stream-cipher implementation.

A 32-byte embedded seed is recovered from:

v7941

with the following value:

9c50867c1de2922f6804c7d78e1d658fe267d562aaff883e87c6ba8464b71407

The loader XORs every byte with:

0x07

producing:

9b57817b1ae595286f03c0d0891a6288e560d265adf88f3980c1bd8363b01300

The resulting data is hashed using SHA-256:

38d6085ec037ad4875c01e92acea43ed580f815e55a0efde79595756eb7e7f98

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.

9. Dynamically Resolved Native APIs

The custom string decryption recovered four important API names.

AmsiScanString

 
AmsiScanString
 

VirtualProtect

 
VirtualProtect
 

GetCurrentProcess

 
GetCurrentProcess
 

FlushInstructionCache

 
FlushInstructionCache
 

The 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:

 
Marshal.GetDelegateForFunctionPointer
 

to convert raw addresses into callable managed delegates.

This allows the sample to avoid exposing the native API relationships through conventional static import declarations.

10. Manual PE Export Resolution

A dedicated resolver routine performs operations including:

 
 
Process.GetCurrentProcess()
Process.Modules
ProcessModule.BaseAddress
Marshal.ReadInt16()
Marshal.ReadInt32()
Marshal.PtrToStringAnsi()
Marshal.GetDelegateForFunctionPointer()
 

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.

11. Runtime Code Patching

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:

  1. Resolves a target function.
  2. Reads the first byte(s) from the target address.
  3. Compares those bytes against hardcoded signatures.
  4. Determines an appropriate patch length.
  5. Calls VirtualProtect to change the target memory protection.
  6. Overwrites the target instructions byte-by-byte.
  7. Calls GetCurrentProcess.
  8. Calls FlushInstructionCache against the modified address/range.
  9. Restores the original memory protection.

Conceptually:

 
Resolve target
↓
Inspect function bytes
↓
Match known signature
↓
VirtualProtect(…)
↓
Overwrite instructions
↓
GetCurrentProcess()
↓
FlushInstructionCache(…)
↓
Restore protection
 

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.

12. AMSI-Related Runtime Patching

AmsiScanString is one of the functions resolved by the loader’s native export mechanism and passed into the patching routine.

The combination of:

AmsiScanString
VirtualProtect
GetCurrentProcess
FlushInstructionCache
 

with a routine that:

reads target instructions
→ changes memory protections
→ overwrites bytes
→ flushes the instruction cache
→ restores protection
 

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.

13. Embedded Payload Reconstruction

The 299,520-byte v4576 blob is not consumed directly.

The loader first clones it:

v4576 → Clone()

then calculates:

299,520 / 5 = 59,904

and creates a new byte array of that size.

The extraction loop increments the source index by 5 bytes per iteration, effectively selecting:

byte 0
byte 5
byte 10
byte 15
…

from the original embedded data.

This produces:

59,904-byte intermediate buffer

The extracted buffer then enters another custom cryptographic transformation.

14. Second Cryptographic Stage

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:

0x0A

rather than the 0x07 constant used for the earlier string-decryption stage.

The sequence is again:

 
embedded key material
↓
XOR transformation
↓
SHA-256
↓
256-byte permutation
↓
custom stream transformation
 

This demonstrates that the loader uses the same general cryptographic architecture multiple times with different constants and input buffers.

15. PE Validation and Reflection

After the second-stage transformation, the resulting buffer is explicitly tested for:

0x4D 0x5A

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:

 
Type.GetType()
Type.GetMethod()
MethodBase.Invoke()
PropertyInfo.GetGetMethod()
MethodInfo.GetParameters()
 

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.

16. Overall Execution Chain

The complete chain recovered so far can be summarized as:

 
Obfuscated JavaScript
│
▼
Custom -1248 string decoder
│
▼
Windows Script Host
│
├── WScript.Shell
├── ADODB.Stream
└── Scheduled Task persistence
│
▼
Hidden PowerShell
│
├── -NoP
├── -EP Bypass
├── -NonInteractive
└── -WindowStyle Hidden
│
▼
Base64 payload fragments
│
▼
AES-128-CBC
│
▼
GZip
│
▼
315 KB .NET PE
│
┌─────────────┴─────────────┐
│ │
▼ ▼
Custom string crypto Manual PE export resolver
│ │
│ ├── AmsiScanString
│ ├── VirtualProtect
│ ├── GetCurrentProcess
│ └── FlushInstructionCache
│
▼
Obfuscated API names
│
▼
Runtime patch engine
│
▼
Embedded 299 KB blob
│
▼
Every 5th byte extracted
│
▼
Custom crypto stage
│
▼
`MZ` check
│
▼
Reflection-based PE handling
 

17. Key Technical Findings

ComponentFinding
Initial formatUTF-16LE JavaScript
Outer obfuscationCharacter code −1248
Script engineWindows Script Host
File stagingADODB.Stream
PersistenceHidden Scheduled Tasks
PowerShellHidden, no profile, execution-policy bypass, non-interactive
EncryptionAES-128-CBC
PaddingPKCS#7
CompressionGZip
Recovered .NET payload315,904 bytes
Architecturex64 PE32+
Managed runtimeMono/.NET
Embedded blob299,520 bytes
ExtractionEvery fifth byte → 59,904 bytes
String cryptoCustom RC4-like implementation
Key derivationXOR + SHA-256
Native resolutionManual PE export traversal
Delegate creationGetDelegateForFunctionPointer
AMSI APIAmsiScanString
Memory manipulationVirtualProtect + byte-wise writes
Instruction synchronizationFlushInstructionCache
Process handleGetCurrentProcess
Payload validationMZ PE signature
Managed loadingReflection

18. Indicators and Cryptographic Artifacts

AES key

 
84e8872ef28616c25c195cfcd1269dfa
 

AES IV

 
1a63c7ec80aea70c21278679afc6c487
 

32-byte embedded seed

 
9c50867c1de2922f6804c7d78e1d658fe267d562aaff883e87c6ba8464b71407
 

Derived RC4-like key

 
38d6085ec037ad4875c01e92acea43ed580f815e55a0efde79595756eb7e7f98
 

Recovered native APIs

AmsiScanString
VirtualProtect
GetCurrentProcess
FlushInstructionCache
 

19. Assessment

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:

  • delayed string revelation;
  • custom cryptographic transformations;
  • manual PE export resolution;
  • dynamically constructed native delegates;
  • avoidance of obvious P/Invoke relationships;
  • runtime executable-memory modification;
  • AMSI-related targeting;
  • embedded payload fragmentation/deinterleaving;
  • secondary cryptographic processing;
  • reflection-based loading of a recovered PE.

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.

20. Remaining Questions

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.

Conclusion

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.

Updated Findings

Instruction-Level AMSI Patching

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.

Custom mapped execution path:

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

Trampoline-based execution fallback:

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.

Export trampoline stomping:

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.