The AI produced malware kill switch
Blogincident-response

The AI produced malware kill switch

In part one we walked through WindowsAudit.exe, the .NET apphost backdoor we found running as LocalSystem on our client’s network. We covered how it got onto the host, how it persisted, and the surface area of what it could do once it was there.

This is part two. Part one was the malware. Part two is everything that came after: what we did with the binary in front of us, the other organizations we ended up identifying as victims of the same attacker, and the architectural decision in the C2 protocol that we only noticed once the rest of the work was already done.

Unpacking the binary

WindowsAudit is shipped as a .NET apphost: a native, statically linked launcher that stages and runs a managed DLL with the appropriate .NET runtime. The first thing we did once samples were on disk was peel that wrapper apart, extract the embedded WindowsAudit.dll, and run it through a decompiler.

Inside, in plaintext, sitting next to Program.Main, was a Discord bot token. No obfuscation, no XOR layer, no environment lookup. The same string was baked into every copy of the binary on every infected host.

A Discord bot token is a credential. Whoever holds it can authenticate to Discord as the bot: connect to the gateway, read the channels the bot is in, download attachments, post messages. We treated it as exactly that: a stolen credential the attacker had unwittingly handed us by leaving it in the agent.

The config also contained credentials to an MQTT broker which was used as a second C2 channel, however we will not focus on that in this article.

Building the monitor

We were cautious about how we used the token. The client had already blocked the C2 communications at the firewall level, taking every infected host’s bot session offline. Our working assumption was that the attacker ran each campaign from a dedicated bot account — so if we authenticated and the bot went from “all instances offline” to “one instance back online,” they might notice something had reconnected. To avoid that, we built a small monitoring service on our own infrastructure: it authenticated with the token, pulled channel message history, downloaded attachments, and indexed everything it saw.

That caution turned out to be overly cautious. As we’ll get to further down the post, the attacker was running every campaign through the same single bot account, with the bot’s identity shared across every infected host. An extra session showing up under that identity would have been indistinguishable from any other infected machine reconnecting, and would not have stood out at all. But we didn’t know that at the time, and the monitor we built was sufficient regardless.

The token’s permissions mattered. The bot’s configured permissions did not include the ability to list guild members or channel members, so we had no immediate way to enumerate who else was in the server or pull a roster of connected users. What the token did give us was read access to the channels the bot had been added to: their message history, their attachments, and the ability to tail new messages as they arrived. Within those constraints, the monitor gave us four useful things:

  • Bus channel history. Because the monitor never registered with the Discord gateway, we never saw the operator’s slash commands as live interaction events: those go from Discord to a connected bot session and don’t otherwise touch the channel. What we did see was every JSON message the bot had posted to the “bus” channel as a result, every response, and every attachment.
  • Snowflake metadata. Every Discord ID (guild, channel, message, user, bot) encodes a millisecond-precision creation timestamp in its upper bits. From snowflakes alone we could date the creation of the guild the attacker was using as a console, the bot account, and every channel and message inside the server. Stacked together it gave us a hard timeline of the attacker’s tooling that they had no way to forge or scrub after the fact.
  • Live activity tailing. This turned out to be simpler than expected. The bot never spoke on its own: no pings, no heartbeats, no scheduled status updates, nothing. Every bus channel message was the downstream consequence of the operator typing a slash command into their Discord client. So a new message arriving in the channel was, on its own, a near-perfect signal that the operator was at the keyboard. The monitor watched for that and paged us in near-real-time, which let us correlate the operator’s working hours, timezone, and which victims they tended to revisit.
  • Tool outputs. When commands like netscan, ad_users, dump_lsass, or screenshot produced output too large to fit in a Discord message, the agent uploaded the result as an attachment. Those attachments stayed in the channel. We pulled and archived all of them.

The stager channel also gave us something operationally useful. The malware’s update mechanism worked by splitting the agent binary into multiple GZip-compressed chunks, then posting each chunk as a Discord message attachment in the stager channel for the infected hosts to download. The chunk messages would arrive first, one attachment per part, followed by a manifest message declaring the version string and the total number of parts. Any infected host that needed to restore or update itself would collect the chunks, use the manifest to verify all parts were present, reassemble them in order, decompress, and execute. In practice this meant that every time the attacker pushed a new build of the malware to their victims, a fresh set of manifest and chunk messages would appear in the channel. Our monitor alerted us each time this happened.

Payload release notification

From there, our automation would pull the new sample, unpack the apphost wrapper, extract the embedded DLL, decompile it, and pull the hardcoded configuration — bot token, guild ID, channel IDs, MQTT credentials — along with hashes of every component. Each new build fed directly into new detections without any manual steps.

Config extraction output

Occasionally the operator would also fumble a slash command and send it as a regular text message from their own personal Discord account rather than through the bot. Those messages sat in the channel history alongside everything else. Everybody, meet n0rad_:

n0rad_ Discord user

What we had after downloading everything was effectively the operator’s own loot dump: sortable, searchable, dated. Internal subnet maps. AD enumeration dumps with domain names we didn’t recognize. Hostnames, usernames, file listings, hashes, screenshots of other people’s desktops.

Each of those was a different victim.

Finding the other victims

The attachments were the part that turned this from a single-customer incident into something with an externally-facing footprint.

Tool outputs are full of identifiers. AD dumps name the domain. netscan results expose internal subnets, but more usefully they expose hostnames, and corporate hostnames are often a giveaway, with naming conventions, asset tags, and location codes that are recognizable on sight to anyone who’s worked in IR for any length of time. Inventory commands returned licensed-product names and registered company strings. Screenshots embedded the user’s wallpaper, taskbar, browser tabs, and open documents.

For each candidate organization we built a small dossier: which agent IDs in the channel almost certainly belonged to them, what the operator had already done on those agents, what data had been exfiltrated, and roughly when the compromise had started (snowflakes again: the earliest message bearing that agent’s ID is a hard upper bound on dwell time). In all, we positively identified 25 unique victims in the data, with some still unidentified. We quickly realized that this was not a small campaign, and that this threat actor was actively working away to compromise as many organizations as possible, and appeared to have domain admin access in multiple orgs — a level of access that could support ransomware deployment. All while we watched from our kibana dashboard.

compromised domain controllers

Because several victims were based in Israel, we coordinated notification through CERT-IL, Israel’s national computer emergency response team. The response work that came after is not something we’re going to write up publicly. The reverse engineering and the monitoring were a means to an end. The end was getting other people unstuck.

Then we looked at the protocol itself

It was only after the monitoring infrastructure was up, the victims were identified, and the notifications were in flight that we went back to the recorded channel data and started looking at the protocol itself: the shape of the messages, rather than the contents flowing across them.

Three key observations were made:

  1. The bus channel was not just carrying responses from the bots back to the operator. It was also carrying commands to them: JSON messages with a type, requestId, targetAgent, and a payload. Agents read those messages, matched targetAgent against their own hostname or machine GUID, and executed whatever the payload told them to do.
  2. The sender of every one of those command messages (the message author metadata in the Discord logs) was the same bot account whose token we already held. The token wasn’t just for receiving commands, it could be used to send them too. It was the identity actually posting the commands the agents were acting on.
  3. The bot itself was handling the operator’s slash commands. When the operator typed /exec in their Discord client, Discord delivered the interaction to a connected bot session, and the bot serialized that interaction into the JSON envelope and posted it onto the bus channel for every other infected host to consume.

The flaw

WindowsAudit.exe is monolithic. Every infected machine runs the same binary, and that binary runs two services in parallel: a bot service that connects to the Discord gateway and registers the slash commands, and an agent worker that subscribes to the bus channel and executes anything addressed to it. Discord allows multiple gateway connections to share the same bot token (it’s how sharding works for high-traffic bots), so every infected host is simultaneously a “controller” registering the operator’s slash commands and a “victim” listening on the bus channel.

When the operator types /exec, the interaction doesn’t go anywhere special. Discord routes it to whichever infected host happened to win the gateway lottery for that interaction. That host then writes the command onto the bus channel as the bot. Every other infected host reads it off the bus channel and runs it without any verification of its authenticity, no signing involved.

So the protocol fundamentally requires the bots to be able to write to the bus channel, a huge design flaw.

Pull one sample, unpack, decompile, read the token. You can post commands to the bus channel and every infected host will execute them. There is no separate C2 server for the operator to harden. There is no C2 server. Every running copy of the binary is the C2 server. And the credential to act as the C2 server is shipped with each one.

We never exploited this vulnerability, although one could theoretically uninstall the attacker’s agent from every infected host using this power. The monitoring work needed nothing more than read access, and posting anything to the bus channel ourselves would have rendered in the operator’s own Discord client just as visibly as it would have on the agents, which was exactly the wrong outcome when the goal was a quiet feed of their activity. The point of writing the flaw up here isn’t that it was operationally useful to us; it’s that the architecture handed the entire botnet to anyone willing to spend an afternoon with a decompiler.

Why this design happened

To be honest:

Skill issue

The author wanted the C2 to look like normal Discord traffic. Discord whitelists itself out of most corporate egress filtering, slash commands give you a free polished UI, and attachments give you a free file-transfer plane. On paper, it made sense. But Discord’s bot model is designed around a single bot service running in a trusted environment, with the token kept secret. The author didn’t have a single trusted environment; they had N untrusted endpoints, and they wanted any of them to be able to act as the controller. So they gave them all the same token.

The moment you do that, you’ve collapsed the trust boundary. The bot identity no longer represents “the operator”; it represents “anyone running this binary.” And because Discord’s permission model only understands identities, the protocol cannot tell the difference between an instruction from the operator and an instruction from a malware analyst with a decompiler and a curl command.

You can basically smell the vibe-coding in this setup.

What we kept, what we didn’t

We’ve intentionally left specific tokens, channel IDs, snowflakes, and credentials out of this post. They were useful operationally, briefly, but publishing them now would do nothing except potentially burn collaborators or out the affected organizations.

Why are we publishing this?

You may be asking yourself why we published this. Why not sit on it and collect intel as long as we could? After sharing the information to the Israeli CERT, they published key details about the Discord operation publicly, effectively blowing our cover. If we had waited any longer, the window to warn remaining victims would have closed. Publishing the analysis now lets defenders detect the same indicators before the attacker adapts.

IOCs

filenamesha256first seen in versionfirst seen at
e_sqlite3.dlldccbabb2bc7e7d4302c44d9ce41b70721a7d0914fa4d289e2f340d39766ad1021.5.752026-04-27T08:21:20.970000+00:00
RemoteAdmin.Shared.dll0a9ae498b83c252cd43d91cc622d69466abd62084eefccaab8335cdf44411a221.5.752026-04-27T08:21:20.970000+00:00
RemoteAdmin.Shared.dll2cfe5e2c348cf97d9080592491e90f1e07aff452c42602777eaaa59d03a4c5d91.5.932026-04-29T10:36:01.286000+00:00
RemoteAdmin.Shared.dll7160efc4f5e077d9402eed1d5c6176a906b421650f3341eb2740e52f54b175031.5.772026-04-27T09:02:20.634000+00:00
RemoteAdmin.Shared.dll8c604239f01521dc3da404431b1220a7932ab01daf9794ad788112ea3d7a9d4d1.5.892026-04-29T08:31:01.267000+00:00
Renci.SshNet.dllce705466f6425837743583744335acc4d3bdcfb801a5998ec0c0026124fe46ed1.5.752026-04-27T08:21:20.970000+00:00
SQLitePCLRaw.batteries_v2.dlle2709fda3ee4137dcea3398221f0afcd6241db0ea6fa55fd31a33610be78cf021.5.752026-04-27T08:21:20.970000+00:00
SQLitePCLRaw.core.dllc33995427edd44fa641cf702df8b63cc82cb7054dd984dc8277d15ee7c9588741.5.752026-04-27T08:21:20.970000+00:00
SQLitePCLRaw.provider.e_sqlite3.dll2e7315a35cb86213200654b717f8cbe3c7643a6bec4a106f22fc8c744a94906c1.5.752026-04-27T08:21:20.970000+00:00
Telegram.Bot.dll156c249523f1b2903ab9275ac23f8354d5bb0a5fc19bd61fc7ee7727f266be4e1.5.752026-04-27T08:21:20.970000+00:00
WindowsAudit_1.5.75.exe2d7e6192c5ea9ee21f618177fd1f048d45b2b0788cbbf1621e22a855862ad0ab1.5.752026-04-27T08:21:20.970000+00:00
WindowsAudit_1.5.77.exe2a328e50c2399651c0d6637ed721aa2b061a708931355a29961fdf7f65486b841.5.772026-04-27T09:02:20.634000+00:00
WindowsAudit_1.5.78.execfbbf42abeea234bf95fffc4a020495a61bdace7aa08258ebb6e119118d6efdc1.5.782026-04-28T07:42:58.434000+00:00
WindowsAudit_1.5.79.exe47aa4d3cb3b237285087a2d24f573b0537f16d8985e333f9441eada37381542d1.5.792026-04-28T07:35:34.803000+00:00
WindowsAudit_1.5.80.exe20117f3850eb17749e5ad83c93a84cc5e0b78cf739b31cb6cbf868fc773c57821.5.802026-04-28T11:25:33.566000+00:00
WindowsAudit_1.5.83.exe6addd580b560f5d2fa835b90f12568b0bd66786930c232cfc9f026c99e9c6eec1.5.832026-04-29T05:07:31.719000+00:00
WindowsAudit_1.5.84.exe9409c66ec4adcb45bdd55a48202f88de90acda8fec8395a5bb06d2f3bbbb0efa1.5.842026-04-29T05:29:12.513000+00:00
WindowsAudit_1.5.86.exebd44bf07413c7d93db6edb1c785c4cc07c102a86edec120b97ff8beb5c892a2b1.5.862026-04-29T05:41:20.666000+00:00
WindowsAudit_1.5.88.exeb3d44d3edcb592d841eb27a72259166df2df2b7478cfeacc50e9a5bc5a2bbfbc1.5.882026-04-29T08:11:34.397000+00:00
WindowsAudit_1.5.89.exe242431f78a5e864186fdecc9e47264b0f28275199532be780575370833a263351.5.892026-04-29T08:31:01.267000+00:00
WindowsAudit_1.5.90.exedc3749d7faa8673d2449a2851eb8d314d85082e61923f99c242c969be9f5ecf31.5.902026-04-29T08:44:07.624000+00:00
WindowsAudit_1.5.91.exed0d5fa1fbaa3915da95ef5d281ae52a91f7203253b87b2c05b913510a00d75391.5.912026-04-29T09:09:57.244000+00:00
WindowsAudit_1.5.93.exebeb17265331aa33be56e540b26cfac6ead634b708565a17294e831de01fc77dc1.5.932026-04-29T10:36:01.286000+00:00
WindowsAudit_1.5.94.exe51e1e45f0c9cd434750774736aec3ed55e278d3190d64011ccde6c10d7a6dd2e1.5.942026-04-30T07:37:54.208000+00:00
WindowsAudit_1.5.95.exe52c7d8b4fc6486cd9eb137cb3c11016eac70439f46d55ab3b698bca6560080611.5.952026-04-30T08:07:49.367000+00:00
WindowsAudit_1.5.96.exe23b7fa0710b26840f4e70759556f8e36d27bbcb26cc79dd4a7dcce7724d52eb71.5.962026-04-30T08:26:13.540000+00:00
WindowsAudit.deps.json4cdba85bf51015394c969caa03aae1f233d722a0cfacccdeadfebf30cab6c98a1.5.752026-04-27T08:21:20.970000+00:00
WindowsAudit.dll17bace642882d0cdf7be77150fbaae35b61f61d89074ad1cfeaa73adf73bd98c1.5.842026-04-29T05:29:12.513000+00:00
WindowsAudit.dll29f8dfb3890bae3938be1b9746ebb43622d8b23525f9fcde1b92fcdb085d897f1.5.882026-04-29T08:11:34.397000+00:00
WindowsAudit.dll35b8421f6ffa5f2447665d26cf7a9d9025eb31c9e46d6d478cbee20e9bd0134f1.5.752026-04-27T08:21:20.970000+00:00
WindowsAudit.dll3632e7857b987df0a651d66dccb6dfa9155cfbeb03dd9d0f973a3732fc127ca91.5.792026-04-28T07:35:34.803000+00:00
WindowsAudit.dll4467a8bacda7711a956cc43d1b354313a1d4c1ff6192d932d75c386a0066e3571.5.942026-04-30T07:37:54.208000+00:00
WindowsAudit.dll5a02f2020b9e8c57075c3695b7760f00a507da0db3eeb54f996043a6801ca7cb1.5.862026-04-29T05:41:20.666000+00:00
WindowsAudit.dll6ac0102ca55e930184e2b2fa294ca07256702551108a9c256a2a40dfd584b7df1.5.952026-04-30T08:07:49.367000+00:00
WindowsAudit.dll750a46ce1815b36479965f6e8017f4d1af387f1d02932c19764b6379834b4ce91.5.912026-04-29T09:09:57.244000+00:00
WindowsAudit.dll76510b3638eff30040f9a8ab4c88b17bc9860d82af16edb9b264c5612d73c2f41.5.892026-04-29T08:31:01.267000+00:00
WindowsAudit.dll76d08ddcce33b40e3f48306c9da2cf744bf2b41bae4301ea94453d6dd3cd16961.5.932026-04-29T10:36:01.286000+00:00
WindowsAudit.dll87344a768cc219b2a9d916579991847900dd0a138b9fcd47fd2e3f0ced74db1b1.5.902026-04-29T08:44:07.624000+00:00
WindowsAudit.dll8f58ea4c69d2e419c6bff84db02aa9dff497ba18b09b3c167983240b3b729abc1.5.832026-04-29T05:07:31.719000+00:00
WindowsAudit.dllc1410a1f2ce40b5dda06f0507276fce088a43c9be1050aa827388769bd378c4a1.5.772026-04-27T09:02:20.634000+00:00
WindowsAudit.dlle48055ee1a4d4ee892c2be669445f48c8f27352daaf076b0335639f88952ec261.5.782026-04-28T07:42:58.434000+00:00
WindowsAudit.dllea8e6ab13e51a1f9216597c1d3836c42d8f4b42351625c576949f87cbd6034281.5.802026-04-28T11:25:33.566000+00:00
WindowsAudit.dllf84d382bac73d6a65d423e94ff4691866c186c5d5be143b338d7068cdd92bc701.5.962026-04-30T08:26:13.540000+00:00
chrome_decrypt_helper.dll486dd528e0497ae7eff6aae6a289740a6c231fae85c0adf279dc18a4fac2db341.5.752026-04-27T08:21:20.970000+00:00
chromelevator.exee857298fd2f8d1c7d48780769433f33e7b3ceaae5ea5a74c13ce8c10bcc7b6901.5.752026-04-27T08:21:20.970000+00:00
WinSATSvc.exe2c4208d795f8b1b7e44051d487a68843a6ba47308c95548bcea15de40953b7d31.5.772026-04-27T09:02:20.634000+00:00
WindowsAudit.runtimeconfig.jsond57dc3928bbe3658d4b7162169a7432ba5ced2ed09a425397ee0cdccb969bc231.5.752026-04-27T08:21:20.970000+00:00

This is part two of two. Part one covers the malware itself: the apphost wrapper, the persistence chain, and the WinSAT secondary stage.