Building on CISA’s Salt Typhoon YARA Rules: Stairwell finds 637 New Variants
In late August, CISA published a joint security advisory titled “Countering Chinese State-Sponsored Actors Compromise of Networks Worldwide to Feed Global Espionage System.” The report outlines a widespread and long-running campaign by a Chinese-state sponsored APT actor, referred to as Salt Typhoon (also tracked as GhostEmperor and other aliases).
These actors have been targeting global telecommunications, government, and military infrastructure since at least 2021, primarily by exploiting vulnerabilities in network devices and edge routers. The campaign focuses on persistent access and stealthy exfiltration, often leveraging tools like custom SFTP clients, packet capture utilities, and tunneling protocols to move laterally and extract data through compromised routers.
As part of their advisory, CISA included two YARA rules intended to aid in the detection of related tooling. One of the rules matched a single file in our internal dataset. We used that match as a jumping-off point to perform variant discovery and explore ways to extend detection coverage across a broader set of related samples.
What We Observed
Of the two published YARA rules, one had no matches across our internal corpus, while SALT_TYPHOON_CMD1_SFTP_CLIENT matched a single sample:
f2bbba1ea0f34b262f158ff31e00d39d89bbc471d04e8fca60a034cabe18e4f4
Using that sample, we ran variant discovery across our platform to identify structurally and behaviorally similar files. This yielded over a dozen close variants, including:
- 69c2083e7d401e0fe61230c271e94b84e4c1e8edf180501ae56d072282525db0
- ef5caf3c20317f6e5a50cab2a318f9d148f2bd801bd3dd540afed5411d3bb938
- d1f7bc8e97e5621bea311692e930208edc63aa6f07a514feee3afe4373ac5559
- 0e8ed0c02275824da54dbf82cbc408460728ab0d1f5cdbb5285241f7716208a2
- (and more…)
Interestingly, many of these variants were not detected by the original YARA rule, suggesting there may be opportunities to improve the rule’s coverage without sacrificing precision. Of course, we can’t speak to how the rule performs in other environments, but within our corpus, we saw gaps worth exploring.
The Art and Challenge of Writing YARA Rules
Creating strong YARA rules is always a balancing act between:
- Generalization (detecting multiple variants),
- Specificity (avoiding false positives),
- And resilience (withstanding minor changes or obfuscation)
The original rule appeared tailored to match very specific byte patterns or strings from the known sample. This is often a safe initial approach, especially in public reporting where false positives can be a concern. However, it can limit applicability across a malware family.
Our goal was to explore how a more flexible rule might look by using shared structural traits across variants, stable string artifacts or byte patterns, and optional metadata anchors (e.g., section names, third-party libraries).
Our Updated Detection Logic
Using the known match from the CISA YARA rule as a starting point, we applied variant discovery across our internal corpus to surface similar samples. From this original hash, we generated a new detection rule using our in-house tooling, which analyzes and extracts common strings and structural anchors across a given malware cluster.
The resulting rule, ST_cmd1_SFTP_Variants, focuses on key string artifacts, library references, and file paths found across the cluster. Its goal is not to replicate the original CISA rule’s precision but to cast a wider net across the malware family, surfacing both known and adjacent variants with high confidence.
rule ST_cmd1_SFTP_Variants {
meta:
author = "stairwell_auto"
date = "2025-09-10"
description = "Detects Go-based malware similar to SALT_TYPHOON_CMD1_SFTP_CLIENT"
hash = "f2bbba1ea0f34b262f158ff31e00d39d89bbc471d04e8fca60a034cabe18e4f4"
strings:
$s1 = "Program running in background: %sdoaddtimer: P already set in timerexecutable file not found in $PATHforEachP: sched.safePointWait != 0illegal base64 data at input byte in \\u hexadecimal character escapemspan.ensureSwept: m is not lockedout of memory allocating allArenasreflect: ChanDir of non-chan type reflect: Field index out of boundsreflect: Field of non-struct type reflect: string index out of rangeruntime.SetFinalizer: cannot pass runtime: g is running but p is notruntime: netpollBreak write failedschedule: spinning with local workslice bounds out of range [%x:%y:]slice bounds out of range [:%x:%y]too many references: cannot splicex protocol version mismatch: %d.%d177635683940025046467781066894531252006-01-02T15:04:05.999999999Z07:0088817841970012523233890533447265625attempt to clear non-empty span setfile type does not support deadlinefindrunnable: netpoll with spinningflate: corrupt input before offset gif: can't read graphic control: %sgreyobject: obj not pointer-alignedmheap.freeSpanLocked - invalid freemismatched begin/end of activeSweepnetwork dropped connection on resetno such multicast network interfacepersistentalloc: align is too largepidleput: P has non-empty run queuereflect.MakeSlice of non-slice typeruntime: close polldesc w/o unblockruntime: inconsistent read deadlineryuFtoaFixed32 called with prec > 9too many Questions to pack (>65535)traceback did not unwind completelytransport endpoint is not connectedzlib: invalid compression level: %d) is larger than maximum page size () is not Grunnable or Gscanrunnable" ascii
$s2 = "github.com/jezek/xgb.(*Conn).readResponses.func1" ascii
$s3 = "h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=" ascii
$s4 = "/usr/lib/go-1.19/src/os/user/lookup.go" ascii
$s5 = "/home/clay/go/pkg/mod/github.com/kbinani/[email protected]/internal/util/util.go" ascii
$s6 = "Stowaway/agent/initial.SoReusePassive.func1" ascii
$s7 = "*protocol.SSHTunnelRes" ascii
$s8 = "/usr/lib/go-1.19/src/runtime/cgo/asm_amd64.s" ascii
$s9 = "/usr/lib/go-1.19/src/runtime/chan.go" ascii
$s10 = "github.com/jezek/xgb.(*Conn).readResponses.func3" ascii
$s11 = "github.com/kbinani/screenshot/internal/util.CreateImage.func1" ascii
$s12 = "/usr/lib/go-1.19/src/internal/fmtsort/sort.go" ascii
$s13 = "rsa-sha2L9" ascii
$s14 = "github.com/jezek/xgb/xproto.init.19" ascii
$s15 = "/usr/lib/go-1.19/src/runtime/preempt.go" ascii
$s16 = "golang.org/x/[email protected]/ssh/client.go" ascii
$s17 = "golang.org/x/[email protected]/unix/zsyscall_linux_amd64.go" ascii
$s18 = "monitor capture CAP"
$s19 = "export ftp://%s:%s@%s%s"
$s20 = "main.CapExport"
$s21 = "main.SftpDownload"
$s22 = ".(*SSHClient).CommandShell"
$aes = "aes.decryptBlockGo"
$buildpath = "C:/work/sync_v1/cmd/cmd1/main.go"
condition:
(uint32(0) == 0x464c457f or (uint16(0) == 0x5A4D and
uint32(uint32(0x3C)) == 0x00004550) or ((uint32(0) == 0xcafebabe)
or (uint32(0) == 0xfeedface) or (uint32(0) == 0xfeedfacf)
or (uint32(0) == 0xbebafeca) or (uint32(0) == 0xcefaedfe)
or (uint32(0) == 0xcffaedfe))) and
5 of them
}
Running this rule across our dataset resulted in: 637 total matches
- Including the original CISA match
- And 636 structurally similar variants
Across most matches, structural similarity was extremely high, which we visualized using Hilbert Curve analysis within our platform. Ten of the matches are shown in the image below:

However, also among the matches were edge cases like binaries compiled from Stowaway and evilginx2. Both are open-source tools frequently used in red team operations but also widely adopted by threat actors in real-world attacks. While distinct from the SFTP tooling described in the CISA report, both matched based on shared Go build traits, overlapping third-party libraries, and similar runtime string artifacts. Stowaway provides multi-hop tunneling and command execution capabilities, while evilginx2 enables phishing campaigns that bypass MFA via session hijacking. Their presence in the match set highlights how variant discovery can surface dual-use or operationally adjacent tools that pose meaningful risk in practice.
Why This Matters
CISA’s report provided a strong foundation for defenders by including IOCs and detection logic. By applying variant discovery, we were able to build on that work to surface additional samples and extend visibility across a wider swath of activity.
This is a great example of how public threat intelligence can be operationalized. First, by identifying the initial match, then using Stairwell to enrich your data and discover variants, and finally by tuning detections to capture the evolving scope of related activity.
Even a single matched file can unlock a broader understanding of a threat actor’s toolkit if we follow the breadcrumbs. In this case, CISA’s release gave us an entry point, and our variant discovery engine helped map the rest.
As defenders, it’s crucial that we not only consume public intel but also use it to refine our own detections and expand coverage. Whether it’s Salt Typhoon or the next campaign, pairing shared threat reports with Stairwell is a powerful way to stay ahead.