VirLabs Blog
CLOSEDQUORUM's paint is still wet
By Ivan Kwiatkowski ·
Cisco Talos recently published an analysis of CLOSEDQUORUM, a Windows implant that uses "LLMs as a C2", meaning several language models are queried to determine what action should be taken next. CLOSEDQUORUM provides host context, tallies model answers, and executes the selected action. VirLabs analyzed two samples listed by Talos. They tell an interesting story about experimentation around the intersection of AI and malware, but also about the distance between an architecture and its implementation: one sample simulates its model responses entirely, the other makes real requests and dispatches the resulting decision, yet contains substantial defects and never returns to the models after that first round.
My assessment is that these binaries represent an experiment in progress, likely improved over daily vibe-coding sessions, but not a production-ready, autonomous implant. Talos notes that it has no confirmation of deployment in the wild and did not observe complete end-to-end execution, which is consistent with the current state of this strain (some samples only contain dummy API credentials at this stage). I however disagree with their assessment regarding some of the implemented behavior, particularly the recurring decision loop described in the article.
A quickly evolving malware family
Talos presents its hashes as a build chain spanning roughly seven days. We examined these two members of that chain:
| SHA-256 | VirLabs analysis |
|---|---|
c4dc171f2513fcaf9d5ecc815a94aee4063b213ab380f80bd3ac422dee5205a7 | Link |
250d4fa37488af9b025333fa17705573d721467b203765bc360890b4f5a90cd7 | Link |
First, a word on the timeline. c4dc… has a compilation timestamp of 2026-Jan-05 23:54:24 and was uploaded on VirusTotal less than a minute later. Assuming that the date wasn't tampered with (and I think that's most likely), it would almost certainly mean that the malware author was submitting the samples herself (betraying extremely poor tradecraft). 250d… doesn't have a compilation date but was uploaded on the very next day by the same user. Despite these samples having been observed only one day apart, they already exhibit major differences. In c4dc…, the four model-query functions never contact an AI provider. They construct fixed local responses:
| Model label | Hardcoded action | Confidence |
|---|---|---|
| DeepSeek | analyze | 0.70 |
| Qwen | evade | 0.80 |
| Mistral | spread | 0.75 |
| Gemini | exfiltrate | 0.90 |
The selection routine accumulates confidence by action and since each action appears once, exfiltrate always wins. CLOSEDQUORUM's handler logs messages and enumerates local network interfaces, but it does not collect credentials or transmit stolen data. Separate Discord reporting code sends, or in fact fails to send the fixed decisions to a placeholder webhook. Other features have similarly modest implementations. The shellcode generator produces four NOP bytes, the loader maps its transformed output into executable memory then logs a message without transferring control to it. Several evasion functions resolve to a RET stub.
There is malicious intent and some substantive code here, including persistence routines, but the normal execution path is largely a blueprint of what the program hopes to become.
A day later, 250d… represents a significant step forward. The program makes provider-specific HTTP requests to DeepSeek, OpenRouter for Qwen, Mistral and Gemini. It extracts response content, parses decisions, counts votes and dispatches an action -- this is consistent with my experience of a coding agent delivering one more "implementation slice". This time, the sample really does connect LLM output to malicious routines. But how much of the proposed autonomous behavior does this implementation actually deliver?
It's not a decision loop if it only decides once
In 250d…'s main.main, VirLabs identifies one call to main.interModelDiscussion, which queries the four providers sequentially. The program selects a result then dispatches at most one action. With its initialized keepalive flag enabled, the sample then enters an infinite loop that sleeps and updates the delay in a variable called beaconDelay. Yet the loop does not beacon, query a model, refresh the target or dispatch another action.
flowchart TD
A[Initialize and select target] --> B[Query four models once]
B --> C{Selected action}
C -->|inject| D[Attempt injection]
C -->|persist| E[Install persistence]
C -->|steal| F[Collect data]
C -->|empty or unsupported| G[Sleep]
D --> G
E --> G
F --> G
G --> H[Update delay]
H --> G
The only relevant back edge is at 0x14025b183, which jumps to 0x14025b15a, the beginning of the sleep sequence. It never returns to the discussion call at 0x14025afad. Potentially, if the selected action installs the scheduled task successfully, Windows can launch a fresh process every five minutes which technically could count as breaking out of the loop. Sort of. Each new process would still get stuck the same way. This is still far away from actual autonomous AI operation control.
When the models fail
As far as I can tell, the fallback also differs from Talos's description. In the recovered decision structure, consensus is the model label. The action is inject, with process_hollow as its exploit type:
Model: consensus
Decision: inject
TargetProcess: explorer.exe
ExploitType: process_hollow
That fallback is reached when no provider response has been appended to the decision list. Transport failures or responses that cannot be parsed can produce this result. The program does not check HTTP status codes: if it cannot extract the expected provider response content, it returns the raw response body for another JSON parsing attempt. A valid JSON object containing unrelated fields could theoretically deserialize successfully into an otherwise empty decision. The program assigns the provider label and accepts it into the vote. There is no subsequent check that the action is nonempty or supported.
Consequently, a typical JSON authentication-error response can suppress the fallback and be interpreted as a vote for the empty string.
| Response outcome | Effect |
|---|---|
| A recognized action wins | Dispatch the corresponding routine |
| All four requests fail at transport or decision-parsing stages | Select fallback injection |
| Providers return accepted objects with empty decisions | Empty action can win; no handler runs |
An unsupported action wins, including the advertised move | No handler runs |
The models can make recommendations. The surrounding program still has to handle errors and validate those recommendations but so far those obligations only seem to be partially implemented.
Injection in progress
The injection routines provide another example of unfinished internals. main.generateShellcode returns a 32-byte buffer with the following execution prefix:
cld
and rsp, -16
call base+0xca
The remaining bytes contain a XOR key, a length byte and an encrypted body. There is no decoder to turn that body into executable instructions. The call jumps beyond the 32 bytes the program supplies. This results in undefined behavior for the early-bird APC injection path. The process-hollowing path has an independent problem: it retrieves the PEB's image-base field and writes the 32 bytes at that address, overwriting the beginning of the DOS header. It does not calculate an entry point, replace the image or redirect the suspended thread to the generated buffer and resumes the original thread.
Both paths leave STARTUPINFO.cb unset and ignore several API results. The malformed payload and absent execution redirection already establish deeper problems that valid API credentials (absent from the sample) would not solve.
Collection without exfiltration
The steal branch calls routines for LSASS dumping, browser databases and cryptocurrency wallets.
| Material | Implemented path |
|---|---|
| LSASS dump | Attempt dump, read file, encrypt, launch Discord exfiltration goroutine |
| Chrome, Edge and Firefox files | Copy to local staging paths |
| Ethereum, Exodus and MetaMask files | Copy to local staging paths |
Only the LSASS path reaches the file-encryption and data-exfiltration routines. VirLabs found no later scan, archive operation or callback that sends the staged browser and wallet files. And even the LSASS path does not validate that the dump succeeded, so it might very well proceed with empty data -- but this path is at least functional.
Overall, there is enough implemented functionality to take the project somewhat seriously, but also enough structural gaps to describe it as an unfinished prototype.
Conclusion
The speed of development and the contrast between these implementations make this look like someone vibe-coding an LLM-as-C2 experiment at breakneck speed. Looking at the negative image of this project, I think we can draw a few interesting conclusions. First, OpenAI and Anthropic are notably absent from the providers contacted by the sample, possibly indicating that their cyber guardrails are effective. Or that they're too expensive. The generally poor code quality leads me to believe that, if my vibe-coding hypothesis holds, the developer was not using frontier models. I would put my money (again, speculating) on an obliterated flash model running on a couple of DGX Sparks.
The final sample contains dummy_api_key values in lieu of actual API keys that would allow any LLM inference. This absence highlights why LLM-controlled malware might in fact be very difficult to achieve in practice: attackers simply have no way of meaningfully protecting these secrets. If I were to find keys in a sample, you better believe I'd be burning tokens with it, literally "imposing costs" I can measure on a threat actor for the first time ever.
Overall, the paint is still very fresh. Despite the rapid pace of development, it's almost certain there is now a version out there with substantial bug fixes... yet no further samples appeared after January. My best bet? The developer gave up and moved on.
Indicators of compromise (as provided by Cisco Talos)
eddbd0ecf7195d38fefae5b9d393abfa79e6f3f94bde19308ecef130a05a42e5
5191cf625dfc209a347f137b50aea199e82040fd5ee9086fb3e2de73c133f3cb
f5f1f8c3e7b883793800ab6ccf21b3e60bd0730f300b4595fe74a33adc17a63c
c13cea04f598e2b0c248d603a6e31bd13aabb64d8149c1b6a77b64e0b983a86f
c4dc171f2513fcaf9d5ecc815a94aee4063b213ab380f80bd3ac422dee5205a7
250d4fa37488af9b025333fa17705573d721467b203765bc360890b4f5a90cd7
