case origin
This case started from a https://tria.ge MSI sample: Endpoint_Agent_Setup_v5.1.75.msi.
I treated it as a staged container immediately, not an endpoint installer, because the file profile and inner object layout were more consistent with a delivery wrapper than a normal software package.

stage 0: MSI container triage (no execution)
First move was container inspection (7-Zip style open) to avoid runtime side effects while preserving chain integrity.

Inside MSI structures, the key pivot is the Binary table object. That object (Binary.rxztvUnN) becomes the first executable payload node.

This is the exact point where Win32 installer semantics end and staged malware semantics begin.
stage 1: native stage map (export-level control flow)
After resolving Binary.rxztvUnN into the native DLL stage, export-level triage shows a classic split: compatibility noise exports and two real operational exports.

Operational exports:
ohdjFSmM-> launch orchestration / copy / delegate executiondFGsktoqNrFX-> resource extract + decrypt + CLR bridge
Low value exports (GetCacheSize, GetRuntimeVersion, RegisterCallbacks, etc.) are mostly decoys/scaffolding.
This pattern is common in crypter chains because it slows shallow triage and makes static “named export” heuristics less useful.
stage 2: dFGsktoqNrFX (native unpack boundary)
This function is the real native unpack hub. The call trace aligns with a resource decrypt loader pattern:
FindResourceA(hModule, 0x1D6, 0x28B)SizeofResource/LoadResource/LockResourceVirtualAllocand buffer copy- custom decrypt call (
sub_180001240(..., key_ptr, 43)) - CLR/OLE setup path (
mscoree,oleaut32,ole32resolved dynamically)

This is the first hard boundary where encrypted bytes become executable managed-stage material.
why am i telling u about it?
If you are doing static reconstruction, this is the most important native function in the file. If you are doing dynamic instrumentation, this is also the breakpoint hub for grabbing decrypted stage bytes before handoff.
stage 3: ohdjFSmM (delegated launcher semantics)
ohdjFSmM handles continuity and delegated execution. In plain terms:
- creates mutex
- resolves
%LOCALAPPDATA% - copies DLL to
wpfrender_<hex>.dll - launches
rundll32.exe "<copy>",dFGsktoqNrFX - schedules delayed cleanup (
MoveFileEx(..., 4))

This is not the decrypt core, but it is operationally important because it pushes execution into a controlled launch path and separates trigger context from unpack context.
stage 4: recovered managed bridge (resource_28B_1D6_decrypted.exe)
After native-layer resource decrypt, output is a .NET bridge stage. Metadata/resources view shows embedded DefaultBuffer.bin.

This bridge is where second-layer decryption happens before reflective in-memory load of the final inner assembly.
stage 5: AMSI handling details
The bridge is not just a passive decryptor. IL flow shows explicit AMSI interaction:
- resolves
amsi.dll - resolves symbols (
AmsiScanBuffer,AmsiScanString,AmsiOpenSession,AmsiInitialize) - creates delegate stubs (
FakeScanBuf,FakeScanStr,FakeOpenSes) - patches/replaces function pointer targets in memory
- uses
Marshal.Write*around AMSI context fields post-init
stage 6: bridge crypto pipeline for DefaultBuffer.bin
DefaultBuffer.bin is encrypted stage data, not a final PE. The bridge applies two transforms:
transform A: repeating XOR
ConvertModule(byte[] data, byte[] key) applies key-cycled XOR across full blob.
transform B: PBKDF2 + AES-CBC
ConvertValidator(...) uses Rfc2898DeriveBytes(password, salt, iter) and derives 32-byte key + 16-byte IV.
ParseStack(...) then runs AES decrypt with CBC and PKCS7 semantics.
Output validates as PE (MZ) and becomes the inner Pulsar client stage.
How did i recover IL/metadata method - used to recover keys
I did not brute force anything. I recovered constants by tracing IL and static arrays.
where the XOR key comes from
In IL, InitializeQueue() reads DefaultBuffer.bin, then calls ConvertModule(data, key). The key comes from RuntimeHelpers.InitializeArray(...) seeded by a <PrivateImplementationDetails> field of size 63. Dumping that field gives the exact repeating XOR key bytes.
where salt/password/iterations come from
In ConfigureFeature() I tracked:
ldstr-> password literal- array init from
<PrivateImplementationDetails>(16 bytes) -> salt ldc.i4 2026-> PBKDF2 iteration count- call into
ConvertValidator(...)-> derives key/IV - call into
ParseStack(...)-> AES-CBC decrypt final stage bytes
This is deterministic because each value is directly wired into method calls in IL.
why this matters for RE
When obfuscation mangles names, IL opcodes and argument flow remain authoritative. Token-to-field mapping beats name-based reversing every time in this kind of bridge stage.
script notes (Stage1.py, Stage2.py)
I split scripts per boundary so each stage can be validated independently.
Stage1.py
This script crosses native boundary 1. It reads Binary-rxztvUnN.dll, extracts resource 0x28B/0x1D6, applies the native RC4-like decrypt routine, and writes decrypted output. This confirms native stage decryption is reproducible without detonation.
Stage2.py
This script crosses managed boundary 2. It takes DefaultBuffer.bin.raw.bin, applies repeating XOR, then PBKDF2(HMAC-SHA1) derivation, then AES-CBC decrypt + PKCS7 unpad. MZ output confirms successful reconstruction of inner stage.
recovered artifacts
Binary-rxztvUnN.dll- SHA256:
CFCA24613E0C4DEA0C615B9BEB92D1B7CF8229E6B40091D054CFDA631FDA0E54
- SHA256:
resource_28B_1D6_decrypted.exe- SHA256:
2F4E96020FC234A334D3819808B354C08522B4C79C3E054E4BC191DAF4345FF3
- SHA256:
DefaultBuffer.bin- SHA256:
AD70D33491E61D690EC2A9F6B645D1F66C4FFEDEF04353A31F72878F57E0F054
- SHA256:
Pulsar.Client.v2.4.5.0.stage3.exe- SHA256:
3A528398AA38327ED63B711CB85D864E5FE0A18B71D0174EEDE068CDD4B32719
- SHA256:
family attribution
Inner-stage namespaces and behavior map cleanly to Pulsar family. I have Pulsar.* references, client metadata (Client, Version=2.4.5.0), encrypted config model, certificate material handling, and RSA/SHA-256 signature verification gate.
little info
I traced RuntimeHelpers.InitializeArray calls back to <PrivateImplementationDetails> fields and dumped those exact byte arrays. This is how I recovered the XOR key and salt cleanly. I took password and iteration count from IL opcodes (ldstr, ldc.i4) at the call site before key derivation.
After rebuilding each transform offline, I validated every stage with structure checks and hashes before moving to the next stage. That prevented drift and made the chain deterministic from MSI input to final Pulsar client recovery.
sample package note
This report and related stage artifacts are intended to be bundled as:
- archive name:
CripCrypts.rar - password:
INFECTED
Use controlled lab handling only.
disclaimer
Defensive malware analysis and reverse-engineering context only.
Made by Zypherion Technologies Team ❤️