Back to blogs

Research Report

CripCrypts - Dropping an Pulsar RAT.

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.

Start1


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.

Start2

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

Start3

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.

Start4-Exports

Operational exports:

  • ohdjFSmM -> launch orchestration / copy / delegate execution
  • dFGsktoqNrFX -> 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:

  1. FindResourceA(hModule, 0x1D6, 0x28B)
  2. SizeofResource / LoadResource / LockResource
  3. VirtualAlloc and buffer copy
  4. custom decrypt call (sub_180001240(..., key_ptr, 43))
  5. CLR/OLE setup path (mscoree, oleaut32, ole32 resolved dynamically)

Start5-MainSub

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))

Start6-fakerender

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.

Start6-ExtractedRSRC-And-Its-A-NET-STUB

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
  • resource_28B_1D6_decrypted.exe
    • SHA256: 2F4E96020FC234A334D3819808B354C08522B4C79C3E054E4BC191DAF4345FF3
  • DefaultBuffer.bin
    • SHA256: AD70D33491E61D690EC2A9F6B645D1F66C4FFEDEF04353A31F72878F57E0F054
  • Pulsar.Client.v2.4.5.0.stage3.exe
    • SHA256: 3A528398AA38327ED63B711CB85D864E5FE0A18B71D0174EEDE068CDD4B32719

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 ❤️