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 Steal 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.
What I Built to Check My Reasoning
The local handover has sixteen stdlib-only unit tests around a reporting contract. No network or cryptography. No PE parsing or rule engine. Run it from the Technical Writing folder with python _pipeline/test_stealc_evidence_contract.py; --json prints seven categories. Each observation carries a kind and evidence reference, with source and template fields. Two roles can share a synthetic reference without being merged. Key/token bytes are rejected as values, but that’s an input policy, not a secret scanner. Reference fields still accept arbitrary strings.
Collection flags accept integer 0 or 1 in these fixtures; an absent value stays unavailable. That is my deliberately strict teaching schema, not a StealC server-validation rule. The kind and provenance fields record assertions; they don’t authenticate the evidence.
The part I care about is what the report refuses to fill in:
{
"unresolved_claims": {
"traffic_decrypted": null,
"session_authenticated": null,
"collection_executed": null,
"exfiltration_completed": null
}
}
These four fields stay null regardless of inputs. An evidence reference saying “Exfiltration succeeded” doesn’t change that; neither does an enabled collection flag or a reported success opcode. The tests check preservation of this intentional policy. They don’t validate source authenticity, correlate observations or adjudicate corroborating evidence. This is a teaching handover, not production validation or a finding about how other pipelines behave. An analyst must assess the stronger claims separately.
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. Four null fields mean unestablished, not 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 turn an unresolved outcome into success. Neither should an enabled collection flag. The local handover has my fixtures.
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!