So, a bit of a watercooler conversation prompted this post.. Essentially, the common complaint is, “When an alert fires, how does the analyst know what exactly it is firing for?” outside of interpreting the raw logic (if there is even access to the logic). I came across a publicly available one recently and figured I’d pick at it a bit. This is not to pick on the rule, the author or the vendor - all of these three things seem just fine - I want to dig into how such a rule might be surfaced to an analyst and some of the challenges that may result from how it is displayed. This also includes how a detection engineer might contextualize that rule in the SIEM for the receiving analyst. Lots of variables! The rule that fired in this context, and what prompted this friendly debate, is MALWARE StealC C2 firing to a ticket saying “credentials were stolen”. To my eye, those are different claims. Malware C2 and Stolen Credentials can sometimes be packaged within the same event - but not always. Only one of these was actually evaluated by the rule. So let’s get into that. I read the pinned rule text alongside the published protocol analysis to see what it supports.
Here’s the TL;DR:
ThreatLabz’s May 2025 analysis describes StealC V2’s JSON-over-HTTP operations: create, upload_file, done and loader. The first request, create, carries a build tag and HWID; the response carries an access_token and collection settings. Embedded-string material and message-encryption material have different roles. Neither is the session token or collection policy. Bitsight’s two pinned Suricata rules test HTTP request shape, not decrypted JSON or accepted tokens. They don’t recover encryption material. Its YARA rule combines format and size checks with string and byte predicates. A match doesn’t mean anything was decrypted or executed. Write down what matched. Make the theft claim separately, or not at all. In short, contextualize your detections.
Background
On June 24, 2026, Bitsight TRACE described supporting a Europol and Microsoft DCU action against both families under Operation Endgame: 47 domains and 182 C2 IPs acted on, 79 of them StealC. Bitsight dates StealC’s sale as a service to early 2023 and the v1/v2 split to March 2025: multipart bodies for v1, JSON over plain HTTP for v2. It expects operators to retool. I haven’t checked whether that infrastructure is back. This isn’t a claim about the family today or anything (although whether this rule needs to still be active or not based on that ambiguity - another debate!).
Four Things Called “the Key”
Zscaler describes strings stored in Base64 and decrypted with a hardcoded RC4 key. The builder doesn’t change that key; the seller manages updates. That’s the local-string role. Message protection is another role, using what the report calls standard RC4.
The server returns an access_token in the create response for later requests. Its error table maps an empty token to error5, a malformed or unknown one to error1. That’s authorization, not cryptography. The sources I inspected don’t actually establish token generation or validity rules. They don’t establish a lifetime either.
Collection settings are the fourth role: flags such as self_delete and take_screenshot, alongside lists of browsers and plugins, plus files to target. They say what was requested. There’s also a random key-value pair for per-message variability. That JSON “key” isn’t crypto material.
And Trying to Infer Versions Isn’t Super Helpful
Zscaler’s analysis describes different encryption arranges int he templates it examined: the 2.1.1 template shares an RC4 key between string decryption and network communication, while 2.2.4 uses separate keys. Its references to when network encryption was implemented and enabled don’t establish one unambiguous release boundary.
Thus, the practical limit here is actually simpler: the HTTP signature doesn’t identify a specific build. A StealC C2 match alone cannot establish a version number or key arrangement. Keep those fields unknown unless separate evidence supports them.
What the Pinned Network Rules Evaluate
Both rules at commit fd4af64b8cee use alert http and flow:to_server,established, from $HOME_NET to $EXTERNAL_NET [80, 443]. The C2 rule, sid:3082112804, requires POST and two header contents, with a URI matching pcre:"/^\/[a-f0-9]{16}(?:[a-f0-9]{4})?\.php$/U". It has no JSON field predicates, decryption routine or token-validation step. A match doesn’t identify the documented operation or establish what the collection flags mean.
HTTP buffers require a parsed request; listing port 443 doesn’t decrypt TLS. Zscaler reported V2 HTTP on port 80 only. The DLL rule, sid:2261307656, tests a GET for a named library under a hex path. Zscaler’s feature matrix lists supporting-DLL downloads as V1 behavior dropped by its analyzed V2. The signature itself has no v1/v2 condition. That request pattern is consistent with the reported older behavior; it isn’t a version identifier or a coverage guarantee for either lineage.
Watch the last C2 predicate: negated content for User-Agent without an http_header modifier of its own. The earlier header contents each have one. The official payload reference and HTTP-buffer reference explain why the matching buffer matters. I haven’t compiled or run this legacy-form rule, and those pages don’t settle its exact behavior here. I’d retain the written predicate rather than treat this static review as proof that a parsed User-Agent header was absent.
And the File Rule
win_stealc requires MZ at offset zero and pe.is_pe, with filesize < 2MB. It also requires five specific patterns and 2 of ($nlohmann, $builder, $hwid). YARA defines that last expression as at least two of the set. The required patterns include an app-bound-encryption property name and a self-delete fragment, plus two chunking names and a byte pattern.
Its description mentions RC4/Base64 C2 and a Chrome ABE v20 bypass. Those predicates don’t establish successful decryption or execution. A bypass isn’t proven either. Its 2026-05-04 metadata date isn’t a compatibility guarantee for later builds. The file is licensed CC BY-NC-SA 4.0.
So What Should the Analyst Actually See?
Back to the watercooler question. That table is half the answer: what each piece of evidence supports, and what it doesn’t. The other half goes on the ticket itself. Next to whatever matched, I’d add four lines and leave them open until somebody closes them with evidence:
Traffic decrypted: not established
Session authenticated: not established
Collection executed: not established
Exfiltration completed: not established
A confident alert name doesn’t close any of them. Neither does an enabled take_screenshot flag, a reported success opcode or an enrichment note that says “exfiltration succeeded.” A flag is a request, an opcode is one request’s result and a note is somebody’s assertion. And a flag you never recovered is unavailable, not off. Each line closes when evidence for that specific step turns up, and the ticket should point at it.
That’s what “contextualize your detections” looks like in practice. The rule tells the analyst what it saw. Those four lines tell them what it didn’t.
The Handover
- Carry rule identity. Record the SID and exact
msg, plus the commit or revision. Add direction and layer, with an evidence reference. Inspect what matched; the friendly name is a label. - Keep the four roles apart. String material, message material, session token and collection policy need separate fields, even when values coincide, as the 2.1.1 and 2.2.4 rows show. Reference sensitive artifacts by pointer. Don’t paste key or token bytes into a ticket.
- Corroborate theft independently. A reported
successopcode is one request’s result; a flag requests work. Connection or byte counts don’t prove completion. Establish what transfer records or endpoint evidence support before writing “confirmed theft.” None was gathered here. - Unknown isn’t benign. Those four open claims mean unestablished, not necessarily “clean”. Investigate and contain under your response policy while corroboration runs. Don’t suppress a signature flagging C2-shaped traffic because it can’t prove every later step.
- Test the enrichment. A renamed alert or a version defaulted from a family name shouldn’t close any of those claims. Neither should an enabled collection flag. Feed your pipeline a few test alerts built to tempt it, and check that all four still come out open.
Conclusion
The alert is still worth investigating. I’d keep the strength of the conclusion matched to the evidence, and put a regression test around that translation. Hope it helps!