SIEM, SOC & Detection

Hunting CLOSEDQUORUM: A Process Allowlist for LLM-API Command and Control

Dark cyberpunk illustration of a circular stone chamber at night with four tall glowing cyan monoliths around the rim, each sending a thin amber beam inward to a single small black cube resting on the wet reflective floor.

Name the programs on your network that are allowed to call a large language model API. Most teams cannot produce that list, and it has stopped being a governance question.

On September 22, Cisco Talos published its analysis of CLOSEDQUORUM, a 16.4 MB Windows binary written in Go that carries no command-and-control server. It sends host context to four commercial model providers - DeepSeek, Qwen, Mistral and Google Gemini - asks each one what to do next, counts the replies, and runs whichever action wins the vote. The outbound traffic that drives it goes to hostnames thousands of legitimate applications reach every day.

How the vote works

The implant constrains each model to a four-value decision schema: steal, inject, persist, or move. Each provider's answer increments a counter in a Go map[string]int, and the highest count wins. Ties resolve by a fixed provider order, DeepSeek first, then Qwen, Mistral and Gemini. The distribution build ships no handler for move, so lateral movement is declared but not implemented.

The three actions that do work are conventional. steal dumps LSASS, browser credential stores and cryptocurrency wallets. inject uses process hollowing or APC injection. persist writes a Windows Update value under the current user's Run key, a scheduled task, or a WMI event subscription backed by C:\Windows\Temp\wmi.ps1. Collected material goes to an operator-controlled Discord webhook, encrypted with AES-256-GCM under a daily-rotating key, split into 1,900-byte segments and posted at one-second intervals.

Two timing details matter more than the payloads. The binary waits five minutes before it starts, and then polls the model providers at randomized five to fifteen minute intervals. Talos states the purpose plainly: the delay and the jitter reduce exposure to short-lived sandbox analysis.

Who this is for, and who can skip it

This applies to you if Windows endpoints or servers in your estate can open outbound TLS to arbitrary internet hosts and nothing in your stack records which process opened the connection. That describes most small estates and a surprising number of large ones.

You can stop reading if your egress already runs through an authenticated proxy that enforces a per-process or per-identity allowlist and you keep those logs for thirty days. You have the control and the evidence. Everyone else is being asked to detect a beacon whose destination is a hostname their developers depend on.

One honest limit on the story. Talos reports no confirmed deployment in the wild. The six SHA256 hashes it published are development builds, the public sample carries placeholder API keys and will not run, and the code dates to June 2026. Blocking those hashes buys you nothing. The technique is the finding, and the technique is cheap to copy.

You cannot block the destination

Talos reaches the same conclusion and says so directly: "The most useful detection strategy is still to focus on behavioral characteristics, rather than domain blocking." Deny api.openai.com across the estate and you break the coding assistant, the summarizer in the helpdesk tool, and three scripts someone in finance wrote. The destination is load-bearing.

Constrain the source instead. In a 200-seat company, the number of programs with a defensible reason to call a model API is small, and you can write it on one screen: a browser, an editor, whatever your vendors embed, and perhaps a build agent. Everything else reaching those hostnames is either shadow AI you need to know about or a beacon. Both are worth an alert.

This inversion is the whole control. The catch is that you have to derive the list from your own environment rather than guess it, or the first week of alerts will bury you and you will switch the rule off.

Derive the list from telemetry you already hold

Two paths get you the same pairing of process and destination. Pick whichever matches what you already run.

With Defender for Endpoint

Thirty days of DeviceNetworkEvents will produce the draft allowlist. The RemoteUrl column carries the FQDN, and the initiating process columns carry the path, the file name and the signer's company name, which is what lets you separate a signed editor from something in a temp directory.

let providers = dynamic(["api.openai.com", "api.anthropic.com", "api.deepseek.com",
                         "api.mistral.ai", "generativelanguage.googleapis.com",
                         "openrouter.ai", "dashscope.aliyuncs.com"]);
DeviceNetworkEvents
| where Timestamp > ago(30d)
| where RemoteUrl has_any (providers)
| summarize calls = count(),
            hosts = dcount(RemoteUrl),
            devices = dcount(DeviceName),
            first_seen = min(Timestamp),
            last_seen = max(Timestamp)
        by InitiatingProcessFolderPath,
           InitiatingProcessFileName,
           InitiatingProcessVersionInfoCompanyName
| order by calls desc

With Sysmon and any log destination

Sysmon gives you the same pairing for free. Event ID 22 records the DNS query and the process that made it, which survives TLS and needs no inspection appliance. Add one rule group and ship the events to wherever you already keep logs.

<RuleGroup name="llm-egress" groupRelation="or">
  <DnsQuery onmatch="include">
    <QueryName condition="end with">api.openai.com</QueryName>
    <QueryName condition="end with">api.anthropic.com</QueryName>
    <QueryName condition="end with">api.deepseek.com</QueryName>
    <QueryName condition="end with">api.mistral.ai</QueryName>
    <QueryName condition="end with">generativelanguage.googleapis.com</QueryName>
    <QueryName condition="end with">openrouter.ai</QueryName>
    <QueryName condition="end with">dashscope.aliyuncs.com</QueryName>
  </DnsQuery>
</RuleGroup>

Keep that list of hostnames under review. It is the part of this control that rots, because a new provider your developers adopt is a new blind spot until somebody adds the line.

Score whatever the allowlist does not cover

An allowlist tells you who is approved. It does not tell you whether the unapproved process that appeared this morning is a beacon or a curious accountant. CLOSEDQUORUM's polling schedule answers that, and the schedule is hard for an operator to disguise without giving up responsiveness. A person calls a model API in bursts, during working hours, with gaps ranging from seconds to hours. An implant on a five to fifteen minute timer produces gaps that all fall inside a fifteen-minute band, round the clock.

One detail will break a naive implementation. A single CLOSEDQUORUM polling round emits four DNS queries within a few seconds, one per provider, so most raw inter-query gaps are near zero and the beacon scores as noise. Collapse queries closer together than sixty seconds into one round first, then measure the gaps between rounds. This script does that, then reports the fraction of gaps that land in the five to fifteen minute window.

PROVIDERS = ("api.openai.com", "api.anthropic.com", "api.deepseek.com",
             "api.mistral.ai", "generativelanguage.googleapis.com",
             "openrouter.ai", "dashscope.aliyuncs.com")
BURST = 60          # queries closer than this are one polling round
LO, HI = 300, 900   # the five to fifteen minute window

def rounds(times):
    out = [times[0]]
    for t in times[1:]:
        if (t - out[-1]).total_seconds() > BURST:
            out.append(t)
    return out

for image, rows in sorted(seen.items(), key=lambda kv: -len(kv[1])):
    times = sorted(t for t, _ in rows)
    r = rounds(times)
    gaps = [(b - a).total_seconds() for a, b in zip(r, r[1:])]
    span = (times[-1] - times[0]).total_seconds() / 3600
    band = sum(1 for g in gaps if LO <= g <= HI) / len(gaps) if len(gaps) >= 6 else None

    if image.lower() in allowlist:
        verdict = "allowlisted"
    elif band is not None and band >= 0.9 and span >= 6:
        verdict = "BEACON - isolate and triage"
    elif band is None:
        verdict = "too few rounds to score"
    else:
        verdict = "unapproved - confirm owner"

Run against a day of Sysmon Event ID 22 records containing a browser, an editor with an AI assistant, a one-off Python script and a simulated implant, it separates them cleanly:

process                                       q  rounds  hosts  in-band  hours  verdict
C:\Windows\Temp\WindowsUpdate.exe           576     144      4     100%   23.8  BEACON - isolate and triage
...\Microsoft VS Code\Code.exe              111      40      1       8%    8.4  allowlisted
...\Chrome\Application\chrome.exe            65      30      1       3%    8.9  allowlisted
...\Python312\python.exe                      2       1      1        -    0.0  too few rounds to score

The two approved programs score 8 and 3 per cent in-band, so the timing test would have separated them from the implant even with an empty allowlist. The four distinct provider hostnames under one process path is the second signal: legitimate software picks a provider and stays with it.

Tune one thing before you deploy this. The 90 per cent threshold and the six-hour minimum span are deliberately strict so the rule stays quiet. Loosen the band to 120 to 1,800 seconds if you want to catch timers that are not copied from this sample, and expect to hand-review the results for a week.

What to do with a sample, and the tool Talos shipped alongside

Talos released the hunting framework it used to find this binary. CAIRN, the Cognitive Artifact Intelligence Research Network, is MIT-licensed Python on GitHub. It hunts for what Talos calls cognitive artifacts - prompt templates, provider endpoints, API key patterns, tool-calling syntax and jailbreak strings left inside binaries - and it works from VirusTotal metadata alone, without downloading or detonating anything.

Its twenty-six YARA rules are organized in three tiers, and the first two are the useful ones for a defender rather than a researcher. T1 covers nine primitive artifacts such as provider endpoints and tool-calling syntax. T2 covers eight co-occurring behavioural patterns. T3 attributes to nine known families. Point T1 and T2 at whatever your EDR has quarantined over the past year. The rules match on strings that any AI-integrated sample carries, so they will flag builds nobody has named yet.

This trajectory has a paper trail. Google's Threat Intelligence Group documented PROMPTFLUX and PROMPTSTEAL in November 2025: the first called the Gemini API to rewrite its own VBScript hourly, the second queried Qwen2.5-Coder to generate collection commands and was used by APT28 against targets in Ukraine. Both asked a model to write code. CLOSEDQUORUM asks a model to decide. Talos summarizes the distance between them in one line: "There is an autonomy escalation arc, and it is changing fast."

Write down the programs allowed to reach a model API

The work here is an afternoon, and it does not need a product. Turn on Sysmon Event ID 22 or query thirty days of DeviceNetworkEvents. Read the process list that comes back. Approve the handful that belong, which is also the moment you find out how much shadow AI you were already running. Alert on the rest, and score anything unapproved by its polling rhythm before you wake anybody up.

CLOSEDQUORUM is a prototype, and nobody has yet pointed one at a real target. Do the work anyway, for a duller reason. Model provider egress turned into a legitimate, encrypted, high-volume channel out of your network faster than anyone wrote a policy for it. The first campaign that uses that channel properly will not announce itself, and the telemetry that would have caught it only helps if you were already keeping it.

We build open-source tools for exactly this class of problem, including an AI packet analyzer for the capture side of the same question. If you would rather have a second set of eyes on the egress policy before you turn the alert on, that is a short conversation.

Want to try our open-source security tools?

We build open-source tools that automate the network and exposure checks teams keep meaning to script. Browse them on GitHub, or book a session to walk through your egress policy before you turn the alert on.