Skip to main content
Return to TrendAI 保安網誌
Cyber crime

Inside SHADOW-WATER-084: A Steganographic Loader-as-a-Service Delivering Remcos, LXBASE, and More

TrendAI™ Research tracked three campaigns that ship completely different decoy applications and unrelated payloads, all riding one shared toolkit. This analysis covers the full chain, from the pixel data that hides the first stage, through a flexible shared loader to deliver multiple payloads, revealing how adversaries are standardizing their delivery mechanisms.

MalwareCyber threatsCyber crimePhishing & BECGeneral marketsInvestigations

Key Takeaways

  • TrendAI™ Research examined three samples, revealing that they served a single operation and used a shared flexible toolkit. In this operation, executable code is stored as raw pixel data. The decoy applications carry their next stage inside ordinary bitmap resources. Every bit of every color channel is payload, which is not what classic steganography does. We reproduced the extraction outside a live infection and recovered the loader binaries exactly.
  • Only the decoy ever touches disk. Stage 1 and Stage 2 of the campaign are reconstructed in memory and loaded reflectively. The final payload is decrypted in memory and injected. A file scanner sees one benign-looking .NET application.
  • The loader is the product and the malware is the customer choice. All three Stage 2 loaders, which we track as GraftLoader, share one 43-field configuration format, one payload cipher, one persistence template, and a leftover build artifact that is byte-identical across independently built samples.
  • We recovered both encryption routines and every key. Each stage transition and each embedded payload can now be decrypted statically. Payload extraction becomes a desk exercise.
  • Multiple payloads ride the same pipeline: an unidentified packed stub, Remcos RAT 7.2.5 Pro, and LXBASE, a bespoke .NET RAT that defeats Chrome App-Bound Encryption using hardcoded vtable offsets for Chrome, Chrome Beta, Brave, Edge, and Avast Secure Browser.

One toolkit behind three unrelated campaigns

The three samples that opened this investigation had almost nothing in common on the surface. One is a spirit level utility with a physics tutorial. Another is a turn-based card game with real move validation and score tracking. The last is a cellular automaton simulator. Different genres, different sizes, different resource layouts, different authorship metadata. Import hashing is no help. Two of the three decoys import only the Common Language Runtime (CLR) entry point, so they share an imphash with every managed binary ever compiled, and the third has no import directory at all.

They are the same operation. We track the cluster behind that toolkit as SHADOW-WATER-84, and the campaign activity as Operation LoremDrop, after a block of Lorem ipsum padding that turns out to be the single most distinctive artifact in the operation. These decoy applications execute just enough to deceive users who interacts with them for thirty seconds.

Underneath, however, each one carries the identical four-stage chain shown in Figure 1. The malicious branch executes during form initialization, before any of the application logic that makes the decoy convincing exists. Heuristics that key on a decoy running benign code and only later revealing itself have nothing to work with at that point, because the first stage is already resident before the user sees a window. The delay in this chain comes later, inside the loader, which we return to in the next section.

Figure 1. The SHADOW-WATER-84 delivery chain across the three analyzed campaigns. Stage 0 is the only component written to disk. Every stage left of the payload column is functionally identical across the three campaigns. Only the rightmost column changes.
Figure 1. The SHADOW-WATER-84 delivery chain across the three analyzed campaigns. Stage 0 is the only component written to disk. Every stage left of the payload column is functionally identical across the three campaigns. Only the rightmost column changes.

Stage 0: The decoy application

Every campaign begins with a .NET WinForms executable, a genuine application, delivered inside an archive. Its resources include the icons, layout data, and images a real WinForms build produces. Among them sits one bitmap that is not an image at all.

The decoy does three things during form initialization. First, it reads that bitmap pixel by pixel, treats the color values as file bytes, and reconstructs a .NET assembly in memory. Second, it splits one hardcoded string on a delimiter to recover the arguments the next stage will need. Third, it loads the reconstructed assembly reflectively and instantiates its first exported type, passing those arguments in. Nothing is written to disk, and the bitmap is never rendered.

Figure 2. Decoy handoff code from all three campaigns. While the steps are the same in each, the reflection API, the delimiter, the resource name, and the byte count are chosen per build.
Figure 2. Decoy handoff code from all three campaigns. While the steps are the same in each, the reflection API, the delimiter, the resource name, and the byte count are chosen per build.

Figure 2 also shows how little the builds share at the source level. Campaign 1 loads through AppDomain.CurrentDomain.Load and invokes a method whose name is hidden as a base64 string. Campaign 3 reaches the same place through the Visual Basic runtime helper CallByName. In short, it’s three different implementations of the same technique.

The handoff string

Each decoy pulls out a resource name and an exclusive OR (XOR) key for the Stage 2 loader and the decoy's own resource namespace, which are then passed into the Stage 1 loader.

Campaign 1 (BubbleLevel)
  ~hex12~BubbleLevel~hex12~4343756D~hex12~6B6777~hex12~
  4343756D -> “CCu”   (Stage 2 bitmap resource)
  6B6777   -> "kgw"    (Stage 2 key string)
 
Campaign 2 (CardGame)
()()48667954()()716B65()()";
  48667954 -> "HfyT"   (Stage 2 bitmap resource) 
  716B65   -> "qke"    (Stage 2 key string) 
 
Campaign 3 (CellularAutomaton)
  &&&&&&&&&&&&&&&50414472&&&&&616C71&&&&&&&&&&
  50414472 -> "PADr"   (Stage 2 bitmap resource)
  616C71   -> "alq"    (Stage 2 key string)

The carrier bitmaps

The technique is usually filed under steganography. However, this is steganography in its crudest form. Academic and hobbyist steganography perturbs only the low-order bit of each channel so that the image still looks like a photograph and still passes natural image statistics. SHADOW-WATER-84 does nothing of the kind. Every bit of every channel is payload, with no regard for visual plausibility.

The result renders as structured noise, shown in Figure 3: long runs of identical color blocks where the underlying portable executable (PE) has section alignment padding, and chaotic churn where it has code and strings. The figure shows the actual embedded image resources. None of the images is a photograph with a hidden low-bit channel. Every bit of every pixel is executable code. The carriers do not need to survive an analyst looking at them. They need to be valid bitmap objects so that a resource enumeration pass reports an image and moves on.

Figure 3. All six carrier bitmaps (two per campaign) rendered as images. Entropy is measured over the red, green, and blue channels in every case.
Figure 3. All six carrier bitmaps (two per campaign) rendered as images. Entropy is measured over the red, green, and blue channels in every case.

How the pixels become bytes

The decoy and the Stage 1 loader both convert pixels into bytes, and they do it differently. Figure 4 sets the two routines side by side. The upper half is the one we are focusing on in this section. We show these two routines together to highlight how differently they serialize pixels.

Figure 4. The two extraction routines. The decoy emits three channels per pixel in column-major order with no encryption; Stage 1 crops, emits four channels, reads a length prefix, and decrypts.
Figure 4. The two extraction routines. The decoy emits three channels per pixel in column-major order with no encryption; Stage 1 crops, emits four channels, reads a length prefix, and decrypts.

The decoy walks the bitmap in column-major order, outer loop over x and inner loop over y, and appends the red, green, and blue channels of each pixel in that order. The alpha channel is ignored. The number of bytes to keep is a hardcoded parameter passed alongside several decoy arguments that the routine never uses, which makes the call read like ordinary application code.

We reproduced this against the extracted bitmap resources. The output begins with a complete and correctly formed IMAGE_DOS_HEADER, and truncating to the hardcoded byte count reproduces the Stage 1 loader with a matching MD5 in all three campaigns.

Campaign 1  Pun  95 x 94   -> 26,790 bytes emitted, first 26,624 kept
            MD5 332b4cd85d1b878f5c5bf709b5984644  (SystemSafe.dll)
 
Campaign 2  CTR  124 x 123 -> 45,756 bytes emitted, first 45,568 kept
            MD5 53c39f839fd2efff0cea32307cecdd1c  (SystemPuzzle.dll)
 
Campaign 3  wck  178 x 178 -> 95,052 bytes emitted, first 94,720 kept
            MD5 f3f10a350a46d35893c6583dd78e9d16  (SystemPuzzle.dll)

Stage 1 loader

The Stage 1 loaders are small .NET DLLs. Campaign 1 ships SystemSafe.dll and Campaigns 2 and 3 both ship a file named SystemPuzzle.dll. All three are name-obfuscated, and notably under three different schemes: Campaign 1 uses Unicode confusables for method names, Campaign 2 uses short alphanumeric tokens, and Campaign 3 uses dictionary words together with control flow flattening that inflates it to 380 methods against 55 and 64 for the other two.

The Campaign 3 build is additionally wrapped in a commercial protector, which our tooling identifies as .NET Reactor 6.x with anti-debug enabled. That version and setting come from the tool rather than from anything recoverable in the binary, so we treat the version as an attribution rather than a measurement. Underneath all of it the logic is the same in all three.

The routine takes three constructor arguments from the decoy: two hex-encoded secrets and the decoy root namespace. Figure 5 shows the whole handoff in a single method. Before it does anything with those arguments, it sleeps.

Figure 5. The Campaign 2 Stage 1 loader
Figure 5. The Campaign 2 Stage 1 loader

The sleep value is 19,004 milliseconds. Not a clean 19,000, not a randomized value drawn from a range.

After the sleep, Stage 1 performs the second pixel extraction, loads the resulting assembly reflectively, invokes its entry point, and calls Environment.Exit(0). At that point, the decoy process is gone and its replacement is running.

The second carrier and how Stage 1 reads it

Stage 1 reaches back into the decoy resource bundle rather than its own, using the namespace and hex-encoded resource name it received. The lower half of Figure 4 covers this routine. The second carrier is larger and is handled differently in four ways:

  • It is cropped to a square. The crop is derived by subtracting two hardcoded constants, 177 and 225, from the carrier width and height, respectively. The same two constants appear in all three Stage 1 loaders, and in all three carriers the two subtractions happen to yield the same number.
  • It uses four channels per pixel rather than three, serialized in “blue, green, red,” alpha order. That is the byte order produced by taking the integer form of a pixel color and writing it out little-endian, which is the natural way to implement this in .NET.
  • The first pixel is not payload. Its four channels are a little-endian length prefix giving the exact size of the assembly to follow.
  • The remaining bytes are decrypted with a keystream built from the hex-decoded key string and a mask taken from the extracted data itself.

Reproducing that path recovers all three Stage 2 loaders exactly:

Campaign 1  CCum  662 x 710 PNG, cropped to 485 x 485
            length prefix 01 4C 0E 00  =    936,961 bytes
            MD5 5689f586213c88b9cd9ebb3cf7b9709b  (WinTune.dll)
 
Campaign 2  HfyT  710 x 758 PNG, cropped to 533 x 533
            length prefix 01 44 11 00  =  1,131,521 bytes
            MD5 aebb74ad24253b9cd5fa22787fab6af8  (System Optimizer Ultimate.dll)
 
Campaign 3  PADr  685 x 733 PNG, cropped to 508 x 508
            length prefix 01 AC 0F 00  =  1,027,073 bytes
            MD5 3d10420f13575318d1d5379ff05edb2e  (Edge Optimizer.dll)

Stage 2: GraftLoader

We track this component as GraftLoader. It is a configurable .NET loader that decrypts an embedded payload, optionally elevates, optionally persists, and then either hollows a host process or loads the payload in process. All three builds are protected, and every string of interest is individually encrypted, so a strings pass returns nothing useful. Everything below is read from the deobfuscated builds.

Figure 6 shows the flow. GraftLoader’s behavior is entirely configuration-driven: Every branch below is gated by one slot of a 43-field, pipe-delimited configuration string parsed by the static constructor at load time.

Figure 6. GraftLoader execution flow. Each branch is gated by a slot in the configuration string.
Figure 6. GraftLoader execution flow. Each branch is gated by a slot in the configuration string.

One configuration string, 42 slots

Every capability is controlled by a single hardcoded string split on a double pipe. The static constructor shown in Figure 7 parses it once and assigns each slot to a named field, alongside the payload resource name, the decryption key, the watchdog mutex, and the persistence filename. All three builds use the same format and the same slot-to-feature mapping. Only the values differ.

Figure 7. The Campaign 2 GraftLoader static constructor, showing the configuration string, the payload resource name, the decryption key, the mutex, and the dynamically resolved process-hollowing API set
Figure 7. The Campaign 2 GraftLoader static constructor, showing the configuration string, the payload resource name, the decryption key, the mutex, and the dynamically resolved process-hollowing API set

We show the three configuration strings recovered from the samples below. Each splits into exactly 43 fields. The last of these fields is the empty artifact of the trailing separator, so that there are 42 real slots.

Campaign 1  WinTune.dll
3||1||0||1||0||||||0||1||1||0||||||||||||||0||0||0||0||0||0||0||0||4.0||2||
17752||1||0||||||0||0||0||||0||auto||0||.exe||0||0||
 
Campaign 2  System Optimizer Ultimate.dll
0||1||1||0||0||||||1||1||1||0||||||||||||||0||0||0||0||0||0||0||0||4.0||0||
11126||1||0||||||0||0||1||3||3||auto||0||.com||1||1||
 
Campaign 3  Edge Optimizer.dll
0||1||0||1||0||||||0||0||0||0||||||||||||||0||0||0||0||0||0||0||0||4.0||2||
17387||1||0||||||0||0||0||||0||auto||0||.pif||0||0||

Mapping those slots took longer than anything else in this analysis. Seventeen of the 42 resolve cleanly to a named field with a code path behind it, and those are the ones in Table 1.

Slot Controls Campaign 1 Campaign 2 Campaign 3
0 Delivery mode 3 (RegSvcs.exe) 0 (self-hollow) 0 (self-hollow)
1 Persistence 1 (enabled) 1 (enabled) 1 (enabled)
4 Download and execute 0 (dormant) 0 (dormant) 0 (dormant)
5, 6 Download URL and filename empty empty empty
9 Defender exclusion 1 (enabled) 1 (enabled) 0 (disabled)
25 Hosted CLR version 4.0 4.0 4.0
28 Watchdog 1 (enabled) 1 (enabled) 1 (enabled)
29 Fake dialog 0 (dormant) 0 (dormant) 0 (dormant)
30 to 33 Dialog caption, text, buttons, icon empty, empty, 0, 0 empty, empty, 0, 0 empty, empty, 0, 0
34 Anti-sandbox sleep 0 (disabled) 1 (enabled) 0 (disabled)
35 Sleep duration in seconds Empty 3 Empty
39 Extension string .exe .com .pif
40 CMSTP UAC bypass 0 (disabled) 1 (enabled) 0 (disabled)
Table 1. List of configuration slots whose purpose we confirmed. The rest are present in every build and carry values we could not tie to any code path.

Several others carry values that are stable across all three builds, including a 4.0 that is almost certainly a hosted runtime version. A few more are present in every build, namely three five-digit numbers, that were never tied to anything we could confirm. We have left them out rather than guess.

The differences that do show up are the kind a build-time generator produces: identical feature set across the three loaders, different switches thrown. Figure 8 shows the order they are evaluated in.

Figure 8. The GraftLoader entry point, wherein the order of operations is fixed and only the branches taken vary
Figure 8. The GraftLoader entry point, wherein the order of operations is fixed and only the branches taken vary

The CMSTP UAC bypass

Only Campaign 2 elevates. When slot 40 is set and the process is not already running as an administrator, GraftLoader goes through cmstp.exe, the Microsoft-signed Connection Manager Profile Installer. The bypass itself is well documented. What matters here is the INF file it writes, which is specific enough to hunt on.

The loader builds a setup information file (INF) in the Windows temp directory and launches the installer against it with the auto install switch. The INF declares a RunPreSetupCommands section, which the installer executes at elevation before the installation routine completes. The profile identifies itself as a service named CorpVPN, and the first command it runs is a taskkill against cmstp.exe itself, which suppresses the installer window once the pre-setup command has fired.

The confirmation prompt is handled without the user. The loader sleeps for 5 seconds, locates the window titled CorpVPN by name, and posts a key down message carrying the return virtual key code. The dialog accepts itself. Figure 9 shows the routine and the template.

Figure 9. The CMSTP bypass and the INF template it writes. The lower block is the decrypted template string in full.
Figure 9. The CMSTP bypass and the INF template it writes. The lower block is the decrypted template string in full.

The INF template is the most distinctive artifact in the operation, and it is where Operation LoremDrop gets its name. Beyond the CorpVPN service name, the file carries a padding section whose purpose is written into the file as a comment. A block of text explains that the comments exist to increase the file size and do not affect functionality, and three numbered comment blocks of Lorem ipsum follow it.

Only Campaign 2 enables the branch, so within these three samples, the template appears once, and one sample proves nothing. The byte-for-byte match comes from a second loader in an adjacent case in the same investigation. Two separately built loaders delivering different payloads, carrying the same Lorem ipsum block character for character, is hard to read as anything but a shared builder emitting a static template.

[version]
Signature=$chicago$
AdvancedINF=2.5
 
[DefaultInstall]
CustomDestination=CustInstDestSectionAllUsers
RunPreSetupCommands=RunPreSetupCommandsSection
 
[RunPreSetupCommandsSection]
<attacker command line>
taskkill /IM cmstp.exe /F
 
[CustInstDestSectionAllUsers]
49000,49001=AllUSer_LDIDSection, 7
 
[AllUSer_LDIDSection]
“HKLM”, “SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\CMMGR32.EXE”,
“ProfileInstallPath”, "%UnexpectedError%", ""
 
[Strings]
ServiceName="CorpVPN"
ShortSvcName="CorpVPN"
 
; ============================================
; Padding Section - Extra Comments for File Size
; ============================================
; This section contains additional comments to increase file size
; These comments do not affect the functionality of the INF file
; Comment block 1
; Lorem ipsum dolor sit amet, consectetur adipiscing elit.
; Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
; ...

The watchdog thread for process monitoring and restart

All three builds run a watchdog. After the sleep and the elevation branch, GraftLoader opens a named mutex. If it already exists, the new instance exits, which keeps one copy per host. Otherwise, it creates the mutex and spawns a background thread.

That thread is a two-second polling loop, shown in Figure 10. It enumerates running processes looking for a specific identifier, and if that process is gone, it relaunches from a stored path and exits the current instance. Killing the injected process does not end the infection; the watchdog restarts it from the persistence copy. The mutex name is build-specific and encrypted, so it is a per-campaign indicator.

Figure 10. Mutex initialization and the watchdog polling loop
Figure 10. Mutex initialization and the watchdog polling loop

The defender exclusion

Campaigns 1 and 2 safelist themselves before doing anything else. Campaign 3 does not, which is the only defensive-evasion difference we found between the builds. The loader shells out to a hidden PowerShell window running Add-MpPreference -ExclusionPath against the running decoy path, which is the same path it later copies for persistence, so the exclusion lands before the copy is made.

Two dormant capabilities

Two capabilities ship in every build with their switches off. The download and execute branch in Figure 11 reads a URL and a filename from the configuration, writes the download into the temp directory, and runs it, with the whole call inside a silent catch so a failure never interrupts the primary payload. The fake dialog branch in Figure 12 shows a message box built entirely from configuration: caption, text, button style, icon. The intended use is a plausible error or license prompt while the payload runs behind it.

Figure 11. The secondary payload download branch, present but disabled in every build we analyzed
Figure 11. The secondary payload download branch, present but disabled in every build we analyzed

Both are off in all three builds with their argument slots empty. The builder ships them; this operator left them alone.

Figure 12. The configurable decoy dialog. Caption, text, buttons, and icon all come from configuration slots.
Figure 12. The configurable decoy dialog. Caption, text, buttons, and icon all come from configuration slots.

Persistence and dropped launcher

When persistence is enabled, GraftLoader builds a destination path in the roaming application data directory using a build-specific name. Figure 13 shows the sequence. If that file does not already exist, the loader locks down the destination’s access control list (ACL) and copies the running decoy executable in. It then sets the copy to hidden, system, read-only, and not content indexed. A separate ACL is applied at this point as well, permitting read and execute but denying delete, write, permission changes, and ownership changes. The whole lockdown sits inside a silent catch, because the chain does not depend on it succeeding. It depends on the file existing.

Figure 13. The persistence branch: Lock down the destination, copy, set attributes, and then build the launcher.
Figure 13. The persistence branch: Lock down the destination, copy, set attributes, and then build the launcher.

The ordering matters for cleanup. The copy and the lockdown sit behind that existence check and run once. The launcher script and the registry value sit outside it and are rewritten on every execution. Remove the Run value without removing the copy in the roaming directory and it comes back the next time the payload runs.

The registry entry is written last. Its data is not a path to the copied executable but a full PowerShell command line that launches a script the loader drops into the temp directory under a random name. Run values that hold an entire powershell.exe invocation are rare in a healthy environment.

The dropped script

The launcher script does exactly one thing. It starts the hidden copy in the roaming directory. Everything else in the file exists to make that one line hard to find. Different obfuscation techniques are stacked, and these same ones appear in the scripts dropped by all three independently built loaders. Figure 14 puts one script next to its decoded equivalent.

Figure 14. A dropped launcher as captured, next to its decoded equivalent. 15 lines resolve to a single Start-Process call.
Figure 14. A dropped launcher as captured, next to its decoded equivalent. 15 lines resolve to a single Start-Process call.

The techniques we observed are listed here:

  • Character code reconstruction: Short strings such as cmdlet parameter names are built by summing individual character casts. The file path uses a decimal integer array piped through a character cast and joined. Neither form is base64 or hex, so scanners looking for encoded blobs find nothing, and a plaintext search for the payload filename fails.
  • Indirect invocation: Even the cmdlet is not named. Start-Process is assembled into a variable and invoked through the call operator, so a search for the literal cmdlet name also fails.
  • Nondeterministic junk: A variable is assigned a fresh GUID two or three times and never read. A rounding expression computes a constant that is also never read. The GUID is generated at runtime rather than baked into the file, so it does not itself vary the script hash. What does vary it is the randomized variable names and the randomized target filename, plus the user path, which is constant across our three samples only because they came off the same analysis host.

Table 2 lists all three dropped launchers. They differ in variable names, in which comment marker they open with, in the exact mix of junk statements, and in the target filename. Structurally they are the same file: 15 lines each, the same techniques, and a single call operator invocation at the end. Two of our three samples open with the same marker, which suggests a small pool rather than a per-build random string, though three samples is thin evidence for that.

Campaign Dropped script Opening marker Start-Process target
1 chz2n4005zp.ps1 # runtime_init C:\Users\<user>\AppData\Roaming\IVLllIrsaR.exe
2 2p1qkqquesx.ps1 # kernel32_init C:\Users\<user>\AppData\Roaming\wWLCxH.exe
3 4nwsb3sor1s.ps1 # runtime_init C:\Users\<user>\AppData\Roaming\YGqlCAFGaLidSU.exe
Table 2. The three dropped launchers, captured from our own detonation. The decimal arrays decode to absolute paths rather than environment variable expansions, so the account name shown is the analysis host and not a victim.

Recovered payload cipher

The final payload never exists on disk in decrypted form. It is stored as a byte array inside a named .NET resource in the GraftLoader assembly and decrypted in memory immediately before injection. Figure 15 shows the routine.Recovered payload cipher

Figure 15. The payload decryption routine
Figure 15. The payload decryption routine

The construction is an autokey stream cipher: an XOR against a repeating ASCII key, then a subtraction of the next ciphertext byte. Four details make it awkward to reimplement from a casual read.

  • The transformation is performed in place, so the value subtracted at each position is the ciphertext byte that has not yet been processed, not the plaintext byte behind it.
  • Indices wrap modulo the buffer length, so the last iteration reads back into position zero.
  • The loop bound is inclusive, which means it runs one iteration more than the buffer length.
  • The buffer is then resized down by one byte, so the stored resource is always exactly one byte longer than the payload it yields.

Written out, the operation at each position is the ciphertext byte exclusive-ORed with the key byte at the cyclic index, minus the following ciphertext byte, taken modulo 256.

for (int i = 0; i <= buf.Length; i++)
{
    buf[i % buf.Length] = (byte)(((buf[i % buf.Length] ^ key[i % key.Length])
                                  - buf[(i + 1) % buf.Length] + 256) % 256);
}
Array.Resize(ref buf, buf.Length - 1);

The stored resource is a container, not a bare array, so an extractor has to reach the inner byte array behind its type code and length first. Feed the whole resource to the cipher and you get noise.

Given a payload dump, the key falls out of the same relation read backwards: the key byte at each cyclic index equals the ciphertext byte exclusive-ORed with the sum of the plaintext byte and the following ciphertext byte, modulo 256. The length must be searched. We swept one to 40 and took the shortest that stayed consistent across the whole file, which found all three inside a second.

Each recovered key then decrypts its resource from scratch and reproduces the payload byte-for-byte. The Campaign 2 key independently matches the value sitting in the deobfuscated constructor, which is what gave us confidence in the other two.

Campaign Resource name Key Resource size Payload SHA-256 of decrypted payload
1 jARLOA4Rr bvsXdsw 286,209 286,208 5fc93c19290f66365b0f6f43c7a506d17dc2853f0018a01e49aaa324c2fee375
2 tSkc3nzxY UVqkDdLWQ 525,825 525,824 808a02060e1ba1cc125fcc3dbcc7416e5955a575378a537d7bb276d0ec4932cb
3 M5OjL1PU7 xuURKsQVBKUgyf 422,913 422,912 005b34cd35534aa697f23faabcba9063fe744f80f4e150ed77e2833c44a303f6
Table 3. Payload resources and their decryption keys. Sizes are in bytes. Key lengths of 7, 9, and 14 bytes are consistent with a generator emitting a random alphanumeric key per build.

Injection: Five modes, two used

Slot 0 selects how the decrypted payload runs. Mode 4 loads it reflectively in the current process. Modes 0 through 3 perform classic process hollowing, and the value selects the host: the loader’s own image for mode 0, MSBuild.exe for mode 1, vbc.exe for mode 2, and RegSvcs.exe for mode 3.

Figure 16 shows the selection and the target resolver, which resolves the three .NET framework binaries through the runtime directory at execution time rather than hardcoding full paths.

Figure 16. Delivery mode selection and the hollowing target resolver
Figure 16. Delivery mode selection and the hollowing target resolver

Campaign 1 selects mode 3 and hollows a freshly spawned RegSvcs.exe. Campaigns 2 and 3 both select mode 0 and hollow a new copy of their own process. Modes 1, 2, and 4 are present in the code of every build and selected by none of them. The in-process branch also carries a fallback: If the reflective load throws, it falls through to mode 0 hollowing. This means a build configured for mode 4 can still end up hollowing.

The hollowing itself is textbook, but the API resolution is not. Rather than importing them, GraftLoader resolves each function by name through a delegate factory in its static constructor: CreateProcessA, ZwUnmapViewOfSection, VirtualAllocEx, WriteProcessMemory, ReadProcessMemory, the thread context getters and setters including their WOW64 variants, and ResumeThread. Routing all of that through a generic helper removes the usual static giveaway, which is a managed assembly declaring exactly those imports.

The three payloads

That is the last thing the loader does. What it hands off to is the only part of the chain with nothing in common between builds: a packed native stub, an off-the-shelf commercial remote access tool (RAT), and a bespoke .NET RAT.

Campaign 1: An unidentified packed stub

The Campaign 1 payload is a 286,208-byte 32-bit PE with a single executable section and no data directories populated at all, meaning no imports and no relocations. It is therefore not position-independent in the usual sense; it uses absolute call targets against its preferred image base. The compilation timestamp reads December 2017, which is inconsistent with the rest of the toolset and is most likely faked.

The section is not uniformly encrypted. A sliding entropy window puts the first 8 kilobytes between 5.9 and 6.8 bits per byte, with the entry point inside that range, and everything after it at 7.95. The low-entropy prefix is the decryptor and the high-entropy remainder is the body it unpacks. The decryptor resolves its API addresses by walking the process environment block (PEB) rather than importing them, which is why the import directory is empty.

Public sandbox verdicts have tagged this architecture as Formbook or XLoader, which we could not corroborate. We are describing it as Formbook-consistent and explicitly not calling it Formbook.

Campaign 2: Remcos RAT 7.2.5 Pro

The Campaign 2 payload is Remcos, the commercial remote administration tool sold by Breaking Security. Decrypting the tSkc3nzxY resource with the recovered key yields the executable directly, and its settings resource confirms both the product and the operator configuration. Remcos stores that configuration in an RCDATA resource encrypted with RC4 under a key held in the same resource behind a single length byte.

RT_RCDATA / SETTINGS   659 bytes, RC4 key length 140
 
  C2 primary      84.38.129.31:9095:0
  C2 secondary    84.38.129.31:8085:0
  Campaign tag    Rmc-T423KN
  Bot label       RemoteHost
  Install name    remcos.exe
  Keylog file     logs.dat
  Max log size    100000

The import table matches the capability set the product advertises, from keyboard hooks and the clipboard family through the GDI capture chain to the waveIn functions for microphone capture. Plaintext strings name the product, the vendor domain, and the watchdog module. The C&C endpoint was not reachable during our analysis. This tells us nothing about when the campaign ran. We have no telemetry that would order the three branches in time, so we are not claiming any one is older than another.

Remcos earns its place here by being a known quantity. Reading its configuration out of the decrypted resource is what proved the extraction chain works end to end.

Campaign 3: LXBASE

The Campaign 3 payload is where the engineering went. It is a 422,912-byte .NET assembly named LX_PYLD_FED77E72, version 1.0.0.17, carrying 267 types and 1,563 methods.

It is protected very differently from the loaders that deliver it. Roughly three-quarters of the type names and half of the method and field names are replaced with random tokens, but the strings are left entirely in the clear. Every log message, every command name, every browser path, and every constant survive, which is the opposite of the loader protection scheme where names are readable and strings are encrypted. The residual metadata still names the root namespace as LxClient. Reading the payload therefore takes a fraction of the effort the loaders took, and the difference is consistent with the payload and the delivery chain coming from separate development pipelines.

One thing this build does not have is a C&C server. The only network endpoints recoverable from it are a loopback default and a private-range address used as a local fallback. Either the operator configures the endpoint at deployment time through a path we did not identify, or the sample we hold was never fielded. Readers should be careful not to treat Campaign 3 as an observed intrusion on the strength of this file alone.

The capability surface is broad and, judging by the internal logging, actively maintained:

  • A reverse proxy channel with connect, data, and disconnect message types, which gives the operator a tunnel through the victim network.
  • A ZIP-based plugin loader that accepts uploaded assemblies at runtime, chunked over the command channel, so the operator can add capabilities after infection without redeploying.
  • Hidden desktop sessions with dedicated launch profiles for Chrome, Edge, Brave, Opera, Opera GX, 360 Browser, Discord, and Telegram, including variants that start from a fresh profile.
  • Keylogging, with output rendered as HTML.
  • Credential and cookie theft across 23 Chromium forks, from the four Chrome release channels to Vivaldi, CocCoc, Sogou, and 2345, plus a separate Gecko path that reads Firefox profiles through nss3.dll.

LXBASE maintains a compact mapping of browsers to their elevator service interfaces and vtable offsets, invoking the appropriate decryption function to extract cookies by impersonating a legitimate caller. Decrypted cookies are relayed via a dynamically named pipe from a helper masquerading as a cryptography library, with detection largely reliant on the unusual instantiation of the browser elevator and brittle hardcoded offsets that can break with browser updates.

Two smaller details say something about the author. The assembly carries a list of 18 analysis-tool process names, each obfuscated with its own single-byte XOR key, covering debuggers, disassemblers, decompilers, network proxies, and a .NET deobfuscator. It checks them against the running process list alongside a debugger-attached check and exits if it finds a match. Separately, the code contains 82 calls to Environment.Exit carrying 54 distinct exit codes, so a sample that bails out early during analysis reports which check failed. Both are cheap, and both are aimed at a reverse engineer rather than at a scanner.

What ties the builds together

Nothing above proves a shared toolkit on its own. Loaders converge on the same techniques all the time. What makes this cluster a single operation is the set of artifacts in Table 4 that are not merely similar but identical across samples that were built separately, were protected separately, and deliver unrelated payloads.

Artifact Evidence Present in
Anti-sandbox sleep value A single ldc.i4 19004 instruction per loader, and zero occurrences anywhere else in the sample set 3 of 3 Stage 1 loaders
Crop constants The same two literals, 177 and 225, subtracted from the carrier dimensions 3 of 3 Stage 1 loaders
Stage 2 decryption bug The same key index wrap against the string length rather than the encoded byte array 3 of 3 Stage 1 loaders
Orphaned designer resource A 5,566-byte WinForms designer resource, MD5 c5534d2fa03d9da7ffcc1e73f31309fb, under one identical name that matches no type in any of them 3 GraftLoaders plus the Campaign 3 Stage 1 loader
Embedded control resource 8,870 bytes, MD5 eba61d4abe68877195e98bd67ea4cd55, stored under three different names 3 of 3 GraftLoaders
Payload cipher Identical in place XOR and subtract routine, identical wraparound, identical trailing byte trim 3 of 3 GraftLoaders
Configuration format 43 fields on a double pipe, 42 real slots, identical slot-to-feature mapping, different values 3 of 3 GraftLoaders
PowerShell launcher Same marker pool, same character code arithmetic, same unused decoy variables 3 of 3 dropped scripts
Hollowing target list MSBuild.exe, vbc.exe, RegSvcs.exe, in that order, in every build 3 of 3 GraftLoaders
Table 4. Artifacts that are identical rather than merely similar across independently built samples

The fourth row is the strongest of these. The 5,566-byte resource sits under a randomized-looking name that matches no type in any of the four assemblies carrying it, which reads like protector metadata. However, it is actually an orphaned WinForms designer resource acting as a build-environment fingerprint from a shared source repository or generator. While assembly names, obfuscation schemes, and target architectures vary across campaigns, shared quirks like a 42-slot layout, a key-index off-by-one error, and this identical leftover resource confirm the binaries originate from a single underlying toolkit.

Detection and hunting guidance

Because SHADOW-WATER-84's payloads change per build, but the delivery toolkit doesn't, every technique below targets the loader's behavior rather than any one payload's hashes or infrastructure. This list is ordered roughly by signal to noise, wherein each one survives a payload change.

The sleep constant

A rule keyed on the immediate value 19004 appearing in .NET IL, or a behavioral rule on any startup sleep between 18,900 and 19,100 milliseconds followed by a resource read and a reflective assembly load.

The CMSTP profile

A content rule matching either the phrase This section contains additional comments to increase file size or a service named CorpVPN inside any file with an .inf extension, particularly one written to a temp directory and then consumed by cmstp.exe with the auto install switch. Combining both strings raises confidence further. There is effectively no legitimate signal competing with either.

Run values that are PowerShell command lines

A registry Run value whose data is a full powershell.exe invocation pointing at a script in the user temp directory, rather than a path to an executable. Pair it with a hidden, system, read-only copy of a recently executed binary in the roaming application data directory for higher confidence.

The launcher script shape

PowerShell scripts in a temp directory whose only executable statement is an indirect call operator invocation, whose strings are built from chained [char] casts and decimal arrays, and which assign a fresh GUID to a variable that is never read. Any two of those together is already unusual. All three in a 15-line script referenced from a Run value is decisive.

Bitmap resources that are really PE files

Entropy is the obvious screen here and it is weaker than it looks. Measured over the red, green, and blue channels of the decoded pixels, the six SHADOW-WATER-84 carriers land between 4.76 and 5.83 bits per byte. A genuine icon shipped in one of the same decoys measures 4.11. Two-thirds of a bit is not much of a threshold to hang a rule on, and measuring the stored resource instead of the decoded pixels pushes every PNG carrier close to eight regardless of content.

A cheaper test catches half the carriers with no false positives: Walk any Bitmap resource in a managed assembly in column-major order, emit the red, green, and blue channels of each pixel, and check the first two bytes for the MZ signature and the e_lfanew field for a valid PE header offset. A real image will not satisfy both. This finds all three of the first-stage carriers, because those are stored unencrypted.

It will not find the second-stage carriers. Those are cropped, four-channel, length-prefixed, and encrypted, so their first bytes are the length field rather than a signature. For those, the tell is the length prefix itself: Read the first pixel as a little-endian integer and check whether it is a plausible assembly size that also fits inside the remaining pixel data. Combining the two checks covers all six. Use entropy to decide which resources are worth walking, not to decide which are malicious.

Browser elevator instantiation from a foreign process

Creation of a Chrome, Chrome Beta, Edge, Brave, or Avast Secure Browser elevator COM object by any process that is not the corresponding browser. This one generalizes well beyond this operation and will surface App-Bound Encryption bypasses in unrelated families.

Static payload extraction

For responders holding a GraftLoader sample, the routine in Figure 15 and the keys in Table 3 make payload recovery a desk exercise. Enumerate the managed resources, take the byte array whose name is a short random token, and run the routine against the key recovered from the constructor. The result is the final payload with no detonation required.

Conclusion

The INF file is a text file sitting in a temp directory. The launcher is a text file referenced from a registry value that any endpoint agent already watches. The sleep constant is a plain immediate in managed IL. The interface offsets in LXBASE are constants in a dictionary initializer. None of that is hidden from anything except a scanner reading files.

Loader-as-a-service is our reading but is not a measurement. One toolkit, under three builds and three unrelated payloads, is what we actually have. A single actor running three campaigns fits it too, as does a builder that leaked. As of writing, we have seen no marketplace listing, pricing, or separate infrastructure per campaign, and only one of the three samples carries a configured C&C server at all.

The asymmetry holds either way. Whoever owns this toolkit put the engineering budget into delivery. The payload changes between builds but the loader does not.

SHADOW-WATER-084 and Operation LoremDropalso say something about what comes next. A secondary payload downloader and a configurable decoy dialog already sit in every build with their switches off, needing new configuration values rather than new code. Somebody will eventually turn them on.

Proactive security with TrendAI Vision One™

TrendAI Vision One™ is the industry-leading AI cybersecurity platform that centralizes cyber risk exposure management, security operations, and robust layered protection. 

TrendAI Vision One™ Threat Intelligence Hub

The TrendAI Vision One™ Threat Intelligence Hub provides the latest insights on emerging threats and threat actors, exclusive strategic reports from TrendAI™ Research, and the TrendAI Vision One™ Threat Intelligence Feed in the ‌TrendAI Vision One™ platform.

Emerging Threats:

GraftLoader Loader-as-a-Service Delivers Remcos and LXBASE

Threat Actors:

SHADOW-WATER-084

TrendAI Vision One™ Intelligence Reports (IoC sweeping):

GraftLoader Loader-as-a-Service Delivers Remcos and LXBASE

TrendAI Vision One™ customers can retrieve the indicators associated with this activity for retrospective sweeping across their environments.

Hunting queries

The following starting points can be adapted in the TrendAI Vision One™ Search App. They are written against the durable behaviors described above rather than against file hashes.

// Run value data that is a PowerShell command line
eventSubId: TELEMETRY_REGISTRY_SET AND
registryRoot: HKEY_CURRENT_USER AND
registryKey: /CurrentVersion\\Run/ AND
registryValue: /powershell.*\.ps1/
// cmstp.exe auto install against an INF in a temp directory
processFilePath: /cmstp\.exe/ AND
processCmd: /\/au/ AND
processCmd: /\\(Temp|temp)\\/
// Defender exclusion added for a user-writable path
processCmd: /Add-MpPreference/ AND
processCmd: /-ExclusionPath/ AND
processCmd: /(AppData|Temp|Downloads)/
// Hollowing targets spawned by a non-developer parent
processFilePath: /(RegSvcs|MSBuild|vbc)\.exe/ AND
NOT parentFilePath: /(devenv|msbuild|dotnet|cmd|powershell)\.exe/

More hunting queries are available for TrendAI Vision One™ customers with the Threat Insights entitlement enabled.

TrendAI Vision One™ Threat Insights

To stay ahead of evolving threats, TrendAI™ customers can access TrendAI Vision One™ Threat Insights, which provides the latest findings from TrendAI™ Research on emerging threats and threat actors. Threat Insights coverage for this activity includes the emerging threat write-up on SHADOW-WATER-84 and the associated Operation LoremDrop intelligence report.

The TrendAI Vision One™ platform is the only AI-powered enterprise cybersecurity platform that centralizes cyber risk exposure management, security operations, and robust layered protection. This comprehensive approach helps predict and prevent threats, accelerating proactive security outcomes across the entire digital estate. Backed by decades of cybersecurity leadership and TrendAI™ Cybertron, the first truly proactive AI for cybersecurity, it delivers proven results: a 92% reduction in ransomware risk and a 99% reduction in detection time.

MITRE ATT&CK mapping

Nine of the 29 rows are visible only after the GraftLoader protection is stripped and are marked as such. The rest are observable in the samples as shipped.

Tactic Technique Observed behavior
Initial Access T1566.001 Spearphishing Attachment Decoy executables delivered inside archives
Execution T1204.002 User Execution: Malicious File Victim runs the decoy application
Execution T1059.001 PowerShell Obfuscated launcher script and the Defender exclusion command
Execution T1106 Native API Hollowing through dynamically resolved kernel32 and ntdll functions
Persistence T1547.001 Registry Run Keys Run value whose data is a full PowerShell command line
Priv. Escalation T1548.002 Bypass User Account Control cmstp.exe-driven elevation with an automated confirmation dialog
Defense Evasion T1218.003 CMSTP Signed Connection Manager Profile Installer runs attacker commands
Defense Evasion T1027.003 Steganography Stage 1 and Stage 2 stored as raw pixel data in bitmap resources
Defense Evasion T1027.009 Embedded Payloads Final payload stored as an encrypted byte array in a managed resource
Defense Evasion T1140 Deobfuscate or Decode Files Two distinct stream ciphers recovered and reproduced
Defense Evasion T1620 Reflective Code Loading Reconstructed assemblies loaded through AppDomain.Load and CallByName at two stages
Defense Evasion T1055.012 Process Hollowing RegSvcs.exe in Campaign 1, self-hollowing in Campaigns 2 and 3
Defense Evasion T1562.001 Disable or Modify Tools Add-MpPreference exclusion for the decoy path
Defense Evasion T1497.003 Time Based Evasion 19,004 ms sleep in Stage 1, configurable sleep in Stage 2
Defense Evasion T1564.001 Hidden Files and Directories Hidden, system, read-only attributes on the persistence copy
Defense Evasion T1222.001 File Permissions Modification ACL denying delete, write, and ownership change on the copy
Defense Evasion T1036.005 Masquerading Loader filenames imitating system utilities, helper module named after a Windows library
Defense Evasion T1622 Debugger Evasion 18 XOR-obfuscated analysis-tool names checked against the process list
Discovery T1057 Process Discovery Watchdog enumerates processes on a timer
Credential Access T1555.003 Credentials from Web Browsers Stored credentials harvested across 23 Chromium forks and Firefox
Credential Access T1539 Steal Web Session Cookie App-Bound Encryption bypass through the browser elevator interface
Collection T1056.001 Keylogging Present in both Remcos and LXBASE
Collection T1113 Screen Capture GDI capture chain in Remcos, hidden desktop capture in LXBASE
Collection T1123 Audio Capture waveIn family imports in Remcos
Collection T1115 Clipboard Data Clipboard API family in Remcos
Command and Control T1219 Remote Access Software Hidden desktop sessions and remote shell in LXBASE
Command and Control T1090 Proxy Reverse proxy tunnel in LXBASE
Command and Control T1105 Ingress Tool Transfer Runtime plugin upload in LXBASE, dormant download branch in GraftLoader
Command and Control T1071.001 Web Protocols WinINet- and urlmon-based HTTP client in Remcos
Table 5. MITRE ATT&CK coverage across the SHADOW-WATER-084 chain and its three payloads

Indicators of compromise (IoCs)

Names given for the loader stages are assembly names read from metadata, not filenames on disk, since those stages are never written to disk. The launcher script hashes are ours rather than durable indicators, because the generator randomizes variable names and the target filename on every drop. We advise hunting the script shape instead.

The list of indicators of compromise (IoCs) for each campaign can be found here.