baldoseri/tracehound
GitHub: baldoseri/tracehound
一款被动式网络威胁传感器,通过透明的计算证据来发现 C2 流量、DNS 隧道和恶意行为,并将告警映射至 MITRE ATT&CK 框架。
Stars: 0 | Forks: 0
# tracehound
**一款被动网络传感器,用于发现命令与控制(C2)流量、DNS 隧道和数据渗出——并展示其每次调用背后的计算逻辑。**
[](https://github.com/baldoseri/tracehound/actions/workflows/ci.yml)
[](https://goreportcard.com/report/github.com/baldoseri/tracehound)
[](https://pkg.go.dev/github.com/baldoseri/tracehound)
[](LICENSE)
tracehound 从捕获文件或实时接口读取数据包,将其组装成
流(flow),使用 **JA4** 对 TLS 客户端进行指纹识别,并报告映射到
**MITRE ATT&CK** 的攻击者行为。它是一个单一的静态二进制文件——没有 libpcap,没有 cgo,没有运行时
依赖,且 Web 仪表板被直接编译进可执行文件中。

*真实输出,非模型演示——该动画由 `make demo-gif` 生成,
该命令会重新运行传感器并重绘其打印的所有内容,因此不会过时。*
## 三十秒内试用
```
git clone https://github.com/baldoseri/tracehound && cd tracehound
make demo
```
这会构建二进制文件,生成包含真实攻击者
行为的合成捕获文件,并对其进行分析。无需网络,也无需下载恶意软件样本。
对于实时仪表板,在 17 秒内重放 33 分钟的捕获内容:
```
make dashboard # then open http://localhost:8080
```
或者使用 Docker,除了 Docker 之外不需要安装任何东西:
```
docker compose up demo
```
### 输出内容
```
[HIGH ] TH-0002 Probable DNS tunnelling to exfil.example
2026-03-14T09:22:47Z 10.0.0.66 -> 10.0.0.1:53 score 0.95
ATT&CK: T1071.004, T1048.003, T1572
10.0.0.66 issued 90 queries under exfil.example, 100% of them for names
never repeated, averaging 46 characters of subdomain at 4.52 bits/char
entropy. Legitimate resolution reuses names and caches; this pattern only
makes sense if the name itself is the payload.
avg_entropy_bits=4.515 avg_subdomain_len=45.889 domain=exfil.example
max_subdomain_len=46 queries=90 queries_per_min=95.106 txt_null_ratio=1
unique_names=90 unique_ratio=1
[HIGH ] TH-0001 Periodic beaconing to 198.51.100.23:443
2026-03-14T09:27:50Z 10.0.0.66 -> 198.51.100.23:443 score 0.96
ATT&CK: T1071.001, T1573
10.0.0.66 opened 28 connections to 198.51.100.23:443 at a mean interval of
60.7s with 6% jitter. Regularity at this level is characteristic of
automated check-in rather than user activity.
connections=28 interval_cv=0.059 interval_mad_ratio=0.048
interval_mean_s=60.696 jitter_pct=5.931 periodicity_score=0.952
size_consistency=0.997
```
每个告警都带有产生该告警的数值。如果分析师看不出工具触发警报的 *原因*,
他们就会停止信任该工具,因此在这里展示计算过程是一项硬性要求,
而不是可有可无的点缀。
## 检测内容
| 规则 | 检测项 | 信号 | ATT&CK |
|---|---|---|---|
| `TH-0001` | C2 Beaconing | 连接间隔的离散度,以及请求大小的一致性 | [T1071.001](https://attack.mitre.org/techniques/T1071/001/), [T1573](https://attack.mitre.org/techniques/T1573/) |
| `TH-0002` | DNS 隧道 | 名称唯一性、子域名长度、香农熵、TXT/NULL 比率 | [T1071.004](https://attack.mitre.org/techniques/T1071/004/), [T1048.003](https://attack.mitre.org/techniques/T1048/003/), [T1572](https://attack.mitre.org/techniques/T1572/) |
| `TH-0003` | 垂直端口扫描 | 单台主机上被访问的不同端口数 | [T1046](https://attack.mitre.org/techniques/T1046/) |
| `TH-0004` | 水平扫描 | 单个端口上被访问的不同主机数 | [T1046](https://attack.mitre.org/techniques/T1046/), [T1018](https://attack.mitre.org/techniques/T1018/) |
| `TH-0005` | 数据渗出 | 已完成流上的出站/入站字节不对称性 | [T1041](https://attack.mitre.org/techniques/T1041/) |
| `TH-0006` | 新设备 | 之前未见过的主机的首次流量 | — |
| `TH-0007` | 罕见 TLS 栈 | 在具有共享基线的网络上,仅被一台主机使用的 JA4 指纹 | [T1573](https://attack.mitre.org/techniques/T1573/) |
## 为什么 JA4 是有趣的部分
TLS 加密的是有效负载,而不是握手。客户端提供的密码套件、扩展和签名算法的精确集合和排序是其 TLS *栈* 的属性,而不是其流量的属性——因此它能经受住加密、代理和域前置。
在实践中,JA4 哈希可以识别应用程序。Chrome 看起来与 curl 不同,curl 看起来与 Go 运行时不同,Go 运行时看起来又与 Cobalt Strike beacon 不同。“此网络上的一台主机开始使用没有其他主机使用的 TLS 栈进行通信”是被动传感器能提供的信号最强、噪声最低的发现之一。
ClientHello 解析器是针对网络传输格式手工编写的,而不是委托给 `crypto/tls`,因为 `crypto/tls` 只会解析它愿意协商的握手——而最值得进行指纹识别的握手恰恰是那些古怪的握手。
有两个细节很容易出错,并在此得到了妥善处理:
- **GREASE** (RFC 8701) 值被从每个列表中剔除。客户端随机注入这些值,专门为了破坏忽略它们的中間人设备;如果保留它们,就会导致同一客户端每次连接的指纹都发生变化。
- ** fragmented hello 被重新组装。** 当前的 Chrome 或 Firefox 如果携带混合后量子密钥共享,其 hello 数据会超过一个 TCP 段。如果传感器只解析第一个有效负载数据包,就会在无形中停止对那些你最想看到的现代客户端进行指纹识别。这里有一个测试,它将握手数据 **按每个段一个字节** 输入,因为将握手拆分成最小段落是逃避内联检查的长期惯用手法。
## 工作原理
```
capture ─────▶ decode ─────▶ flow table ─────▶ detectors ─────▶ alerts
pcap file gopacket bidirectional beaconing ATT&CK-mapped
AF_PACKET zero-alloc 5-tuple, LRU dns-tunnel + evidence
layer parser expiry port-scan │
│ exfiltration ▼
└────────▶ JA4 / JA3 ──────▶ inventory HTTP API + SSE
ClientHello embedded dashboard
reassembly
```
整个数据路径运行在 **单个 goroutine** 上。在单个普通核心可以解码的数据包速率下,跨 worker 分片的同步成本超过了被分片的工作负载,而且单线程 pipeline 更容易进行逻辑推理和确定性测试。扩展应该在捕获层进行——每个 RSS 队列一个 pipeline——而不是在循环内部。
### 值得阅读代码的设计决策
**流过期是 O(expired),而不是 O(total)。** 显而易见的实现方式是在计时器上扫描每个条目,而这恰好在表很大时发生退化——也就是说,在你最想检测到的扫描或泛洪期间。相反,每个流都被串联到一个 intrusive recency list 上,因此当头部过期时,清理操作会直接从头部弹出。
→ [`internal/flow/table.go`](internal/flow/table.go)
**Beaconing 会取两种离散度度量中更好的一个。** 变异系数能捕捉漂移;绝对中位差能容忍漏报的签到。真实的 beacon 会跳过间隔,而一个翻倍的间隙就足以让标准差膨胀到完全掩盖原本的模式。
→ [`internal/detect/beacon.go`](internal/detect/beacon.go)
**DNS 隧道赋予唯一性最高的权重。** 这是隧道无法避免的唯一属性:每个携带走私数据的包都必须使用一个新的名称,否则缓存会将其吞没,导致通道无法工作。
→ [`internal/detect/dnstunnel.go`](internal/detect/dnstunnel.go)
**稀有性首先需要建立基线。** 在捕获的早期,每个主机都只贡献了一个指纹,所以 *所有东西* 都是“仅被一台主机使用”。检测器在看到明确被共享的 TLS 栈之前拒绝做出判断——否则它会控告整个网络。这是一个真实的误报,是通过重放演示捕获文件发现的。
→ [`internal/detect/inventory.go`](internal/detect/inventory.go)
**解析器将输入视为敌对。** 边界检查游标使得每一次越界读取都会失败而不是触发 panic,因此解析器以直线型代码运行,只需在最后进行一次有效性检查。这里包含一个模糊测试目标(fuzz target),因为发生 panic 的网络解析器就是一种远程拒绝服务攻击。
→ [`internal/fingerprint/clienthello.go`](internal/fingerprint/clienthello.go)
## 性能
在 AMD Ryzen 9 3900X 上测量,使用 `go test -bench . -benchmem`:
| 操作 | 耗时 | 内存分配 |
|---|---:|---:|
| 流表更新,现有流 | 68 ns | **0** |
| 拒绝非 TLS 有效负载 | 15 ns | **0** |
| ClientHello 解析 + JA4 + JA3 | 1.16 µs | 16 |
| 完整 pipeline,端到端 | **~1,050,000 packets/sec** | — |
指纹识别路径最初耗时 4.1 µs 并进行 51 次分配。`fmt.Sprintf("%04x")` 之前为每个密码套件分配一次内存;手动格式化这些半字节(nibble)使其速度提升了 3.6 倍。
数据包解码使用 gopacket 的 `DecodingLayerParser` 处理预分配的层结构体,而不是 `gopacket.NewPacket`,因为后者会为每个包的每一层分配一个新对象,这在限速(line rate)场景下会主导性能分析结果。
## 如何确立正确性
单元测试对于检测器来说是必要但不充分的——一个低到可以对任何东西都触发的阈值也能通过它自己的测试。因此,演示捕获文件兼作检测测试工具:**生成器声明了它植入的内容**,而集成测试要求每一个植入的行为都能正确识别出对应的主机并被输出,*并且六个良性主机中没有一个被诬告*。
```
--- PASS: TestReplayFindsEveryPlantedBehaviour
found TH-0001 10.0.0.66 sev=medium score=0.96 Periodic beaconing to 198.51.100.23:443
found TH-0002 10.0.0.66 sev=high score=0.95 Probable DNS tunnelling to exfil.example
found TH-0003 10.0.0.99 sev=high score=0.56 Port scan: 121 ports on 10.0.0.10
found TH-0004 10.0.0.99 sev=medium score=0.15 Network sweep: port 445 across 60 hosts
found TH-0005 10.0.0.66 sev=medium score=1.00 Large outbound transfer (17.9 MiB)
found TH-0007 10.0.0.66 sev=medium score=0.60 Rare TLS fingerprint
--- PASS: TestReplayDoesNotAccuseBenignHosts
6 benign hosts, none reported above info severity
```
这个误报测试才是最重要的。任何检测器都可以通过降低阈值来触发;困难的是在面对紧挨着攻击的普通流量时保持沉默。
覆盖率:`flow` 97%,`fingerprint` 91%,`pipeline` 85%,`detect` 83%。
CI 还额外运行了竞态检测器,在每次 Pull Request 时对 TLS 解析器进行 90 秒的模糊测试,针对五个平台进行交叉编译,以及一个端到端演示——如果有任何规则停止触发,构建就会失败。
## 用法
```
tracehound replay Analyse a capture file
tracehound sniff -i Capture live (Linux; needs CAP_NET_RAW)
tracehound gen-demo Write a synthetic capture containing known attacks
```
常用标志(flag):
| 标志 | 含义 |
|---|---|
| `-listen :8080` | 提供实时仪表板和 JSON API 服务 |
| `-speed 120` | 以 120 倍实时速度重放,以便逐步显示检测结果 |
| `-json` | 以 JSON Lines 格式输出告警,用于通过管道输入 SIEM |
| `-min-severity high` | 仅报告重要内容 |
| `-home-nets 10.0.0.0/8,192.168.0.0/16` | 定义“内部”网络(默认为 RFC 1918) |
实时捕获需要 `CAP_NET_RAW` 权限。请严格赋予该权限,而不是以 root 身份运行:
```
sudo setcap cap_net_raw,cap_net_admin=eip ./bin/tracehound
```
### JSON API
| 端点 | 返回内容 |
|---|---|
| `GET /api/alerts?limit=&min_severity=` | 告警,最新优先 |
| `GET /api/devices` | 带有 JA4 指纹的被动资产盘点 |
| `GET /api/flows?limit=` | 活动流表 |
| `GET /api/stats` | 吞吐量和检测器计数器 |
| `GET /api/attack` | 观察到的 ATT&CK 技术及其计数 |
| `GET /api/stream` | Server-sent events,每个告警对应一个事件 |
## 限制
在此明确说明,因为夸大其词的安全工具还不如功能少但诚实的工具:
- **实时捕获仅限 Linux。** 它使用纯 Go 实现的 AF_PACKET。所有平台都可以重放捕获文件,无论如何这都是更好的开发工作流,因为它是可复现的。
- **除了 ClientHello 之外,没有 TCP 流重组。** 这对于指纹识别已经足够;但对于有效负载的协议分析则不够。
- **不对 IP 分片进行重组。** 但 *会* 遍历 IPv6 扩展头部链,因此第一个分片可以正常解码;非初始分片不包含传输层头部,会被计为无法解码,而不是被错误解析。
- **公共后缀表是不完整的。** 内置了大约六十个常见的双标签后缀,因此 `a.example.co.uk` 可以正确归类到 `example.co.uk` 下。完整的 Public Suffix List 虽然详尽,但代价是增加一兆字节的嵌入数据以及长期的更新义务,而对于一个评分由熵主导的检测器来说,这并不划算。
- **随着证据的增加,告警会重复。** 完全相同的证据会被直接抑制,严重程度的升级会立即报告,但如果某个发现的数值不断攀升,则会在每个冷却周期内重申一次。可以使用 `-min-severity` 进行调节。
- **检测器阈值是针对合成流量进行调整的。** 它们只是一个起点,而不是针对您网络的校准。
## 路线图
- 用户可扩展的 YAML 规则(受 Sigma 启发),以便无需重新编译即可编辑阈值和 ATT&CK 映射
- SQLite 持久化,让发现能在重启后保留
- QUIC 支持——JA4 已经编码了传输层信息,但解码器尚未读取 QUIC
- JA4S/JA4H(服务器和 HTTP 变体)
## 许可证
MIT — 详见 [LICENSE](LICENSE)。
合成捕获仅使用 [RFC 5737](https://www.rfc-editor.org/rfc/rfc5737) 和
RFC 1918 文档地址,因此它绝不会与真实基础设施混淆,也不会被重放以攻击真实基础设施。本仓库不包含任何真实的网络流量。
标签:AMSI绕过, EVTX分析, Go, Ruby工具, TLS指纹识别, 威胁检测, 日志审计, 网络安全, 网络流量分析, 请求拦截, 隐私保护