detection-labs/spicy-whois
GitHub: detection-labs/spicy-whois
基于 Spicy 框架的 Zeek WHOIS 协议分析器,将 WHOIS 查询与响应解析为结构化日志,支持域名和网络资源的元数据提取。
Stars: 0 | Forks: 0
# WHOIS (RFC 3912) 协议分析器
基于 Spicy 的 [Zeek](https://zeek.org) WHOIS (RFC 3912) 协议分析器。
## 详细说明
WHOIS 是一种基础的 TCP 请求/响应协议:客户端发送一行查询,服务器返回自由格式的文本并关闭连接。
该分析器对交互的双方采用智能解析,生成结构化的 `whois.log`。
它将**查询**分类为 `domain`、`ipv4`、`ipv6` 或 `asn`,随后读取**响应**(上限为 64 KB)并扫描其中的注册局/RIR 字段:`owner`、`status`、`origin AS`、`registration`、`update` 和到期日期、域名服务器以及滥用投诉联系方式。
## 功能
- 将 WHOIS 查询和结构化的响应元数据记录到 `whois.log`
- 通过双向签名实现动态协议检测 (DPD)
- 响应时间追踪(从请求到响应的时间差)
- 针对协议异常(空请求、异常大的查询)输出 Weird 日志
- 支持 UTF-8/IDN(已在 JP、CN、KR 的 WHOIS 服务器上测试)
## 检测用例(示例)
- ** Sinkhole / 域名扣押** — 状态为 `serverHold` 或 `clientHold` 表示该域名已被注册局冻结。
- **路由情报** — 网络查询中的 `origin_as` 是 BGP 过滤的输入源;标记 origin AS 与预期对等连接不符的路由对象。
- **新基础设施** — 在您的回溯时间窗口内的 `registered` 日期可标记新建立的域名;较短的 `registry_expiry`(1 年注册期)会增强该信号。
- **基础设施扩展** — `name_server` 将域名与其 DNS 托管绑定;通过共享的域名服务器扩展到相关域名。
## 环境要求
- Zeek 6.1.0(内置 Spicy 1.9.0)或更高版本
- 构建该分析器需要 C++ 工具链和 libpcap 头文件:
* `gcc g++ make cmake libpcap-dev`
* 与任何 zkg Spicy 分析器一样,代码为 Spicy 源码,并在安装时进行编译
* 注意:官方的 [`zeek/zeek` 容器镜像](https://hub.docker.com/r/zeek/zeek) 缺少这些依赖,因此请先安装,否则构建将失败并提示 `pcap.h: No such file or directory`
## 安装
使用 [zkg](https://docs.zeek.org/projects/package-manager/) 包,源自 [Zeek Package Source](https://github.com/zeek/packages):
```
zkg install spicy-whois
```
## 事件
```
event WHOIS::request(c: connection, is_orig: bool, query: string)
```
每次客户端查询时触发,其中 `query` 包含去除了行终止符的字符串。
```
event WHOIS::reply(c: connection, is_orig: bool, data: string)
```
每次响应时触发一次,其中 `data` 包含完整的服务器文本(读取直到连接关闭,上限为 64 KB)。
尽管上述事件返回的是原始字节,但 `WHOIS::log_whois(rec: WHOIS::Info)` 是执行分析器解析的地方:每次连接它都会生成组装好的 `WHOIS::Info` 记录(包含已分类的查询和提取的响应字段),该记录将被写入 `whois.log`。
有关字段请参见 [WHOIS 应答模式](#whois-answer-schema)。
## 示例输出
使用测试 pcap 运行,并通过 `jq` 美化打印 `whois.log`:
```
zeek -C -r testing/Traces/whois-domain.pcap whois.hlto scripts/__load__.zeek LogAscii::use_json=T
jq --color-output . whois.log
```
**domain** 查询 (`whois-domain.pcap`) — 注册商、EPP `status` 代码、域名服务器、滥用投诉联系方式:
```
{
"ts": 1779334478.346291,
"uid": "Cm3FuO2WPLUSPqUolb",
"id.orig_h": "192.168.1.231",
"id.orig_p": 63154,
"id.resp_h": "192.34.234.30",
"id.resp_p": 43,
"query": "domain cloudflare.com",
"query_type": "domain",
"resource": "CLOUDFLARE.COM",
"owner": "Cloudflare, Inc.",
"registered": "2009-02-17T22:07:54Z",
"updated": "2024-01-09T16:45:28Z",
"registry_expiry": "2033-02-17T22:07:54Z",
"name_server": [
"ns3.cloudflare.com",
"ns4.cloudflare.com",
"ns5.cloudflare.com",
"ns6.cloudflare.com",
"ns7.cloudflare.com"
],
"status": [
"clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited",
"clientTransferProhibited https://icann.org/epp#clientTransferProhibited",
"clientUpdateProhibited https://icann.org/epp#clientUpdateProhibited",
"serverDeleteProhibited https://icann.org/epp#serverDeleteProhibited",
"serverTransferProhibited https://icann.org/epp#serverTransferProhibited",
"serverUpdateProhibited https://icann.org/epp#serverUpdateProhibited"
],
"abuse_contact": "registrar-abuse@cloudflare.com",
"reply_time": 0.025169849395751953,
"reply_size": 3719
}
```
**network** 查询 (`whois-net.pcap`) — 依据 `query_type` 转换的相同记录结构,此处是填充了 `server_name` 和 `origin_as` 的 RIR `inetnum`,并省略了仅适用于域名的字段:
```
{
"ts": 1779334777.802331,
"uid": "CBdloO3gjjCrOi6Q5l",
"id.orig_h": "192.168.1.231",
"id.orig_p": 64829,
"id.resp_h": "193.0.6.135",
"id.resp_p": 43,
"query": "95.217.0.1",
"query_type": "ipv4",
"server_name": "RIPE",
"resource": "95.217.0.0 - 95.217.15.255",
"owner": "ORG-HOA1-RIPE",
"origin_as": "AS24940",
"registered": "2023-12-12T12:40:45Z",
"updated": "2023-12-12T12:40:45Z",
"status": [
"ASSIGNED PA"
],
"reply_time": 0.16294193267822266,
"reply_size": 3800
}
```
## 分析器:附加、确认与端口
一个连接只有在经历两个步骤后才会被记录:首先分析器**附加**到其上,然后解析器**确认**字节数据符合 WHOIS 协议。
**附加**发生在 43/tcp 端口。`Analyzer::register_for_ports` 将分析器绑定到该端口,因此在 43/tcp 上的每一个连接在开始时(即任何负载被解析之前)都会附加该分析器。
**确认**在解析器中独立发生在每一端。成功解析的查询行会调用 `spicy::accept_input()`;携带数据的响应同样会调用此方法。任意一端的确认即生效,因此即使没有响应的客户端查询依然会标记该连接。任何一端的解析失败会转而调用 `zeek::reject_protocol()`。
是确认机制而非端口匹配,决定了在 `conn.log` 中设置 `service=whois`。43/tcp 上的非 WHOIS 流量依然会附加分析器,但永远不会确认,因此 `service` 会保持为空。
完整路径为 **端口 → 附加 → 解析 → `accept_input()` 确认 → `service=whois`。**
### DPD 签名
位于 [`scripts/dpd.sig`](scripts/dpd.sig) 中的签名是第三种独立机制:一种针对非标准端口的基于内容的附加路径。
WHOIS 没有固定的字节模式或固定偏移的标头可供匹配,因此该签名将客户端与服务器端的匹配规则进行组合,并针对 `testing/Traces/` 中捕获的字节进行了调优。只有在客户端查询匹配时(`requires-reverse-signature`),服务器端的规则才会触发,从而避免了由于其他文本协议的响应中带有偶然出现的关键字而引发的误报。
使用 `tcp-state originator`/`responder` 而不带 `established`,这与核心分析器保持一致;负载仅存在于握手之后,因此 `established` 是多余的。
**客户端(发起方) — 以 CRLF 结尾的单行查询:**
- 字符类涵盖了域名/IP/ASN 字符,以及标点符号(`- . @ = / + : ,`)和文字空格,因此像 `-T dn,ace example.de` 这样的 RIPE 风格标记查询也能匹配。
- 类中**不包含 `\s`**。早期版本曾包含它,会默默匹配内部的 `\r`/`\n` —— 结果导致多行负载(`foo.com\r\nbar.com\r\n`)和纯 CRLF 泛洪被识别为有效的单行查询。去掉 `\s` 即可拒绝这些情况。
- 排除了下划线:它曾导致 SSH 标语(`SSH-2.0-libssh_…`)被匹配。
- 保留了 `\x80-\xff` 以支持 IDN/CJK(中日韩)查询。
**服务器端(响应方) — 关键字匹配,受 `requires-reverse-signature` 控制:**
- 匹配真实注册局/注册商/RIR 响应中存在的关键字。
- 包含 `route6?:`(不仅仅是 `route:`)以及 `origin:`/`source:`,从而弥补了之前无法检测到来自 RIPE/RADB 的 IPv6 `route6:` 对象的缺陷。
### 非标准端口上的 WHOIS
要解析 43/tcp 以外端口的 WHOIS,请添加该端口,以便分析器在连接开始时附加到该端口,这与它在 43/tcp 上的路径完全相同:
```
redef WHOIS::ports += { 4343/tcp };
```
## 解析限制与边界
截断边界可防范畸形或恶意流量:
- **请求行** ([`whois.spicy`](analyzer/whois.spicy)) — 可打印字节(`\x09`、`\x20`–`\x7e`、以及用于 IDN 的 `\x80`–`\xff`),以可选的 CR 和必需的 LF 结尾。空查询会引发 `whois_empty_request`;超过 **512 字节**的查询会引发 `whois_oversized_request`(该行依然会被解析——此异常本身就是一种信号)。
- **响应主体** — 读取直到连接关闭,上限为 **64 KB** (`&size=65536 &eod`);前 64 KB 会被解析,超过上限的字节将被丢弃,因此 `reply_size` 会显示为截断后的值。
- **字段提取** ([`main.zeek`](scripts/main.zeek)) — 响应按 LF 拆分,每一行按首个 `:` 划分;键名转为小写,值首尾空格被去除,并跳过空值。单值字段采用**先到先得**原则;`status` 和 `name_server` 累积为集合(`name_server` 转为小写以进行去重),并受 64 KB 上限的限制。
## WHOIS 应答模式
应答表现为两种形式,它们都被映射到一组基于 `query_type` 转换的通用字段中:
- **domain** 响应(注册商/注册局数据)
- **network** 响应(RIR inetnum/route/ASN 对象)
读取值时请务必结合 `query_type` —— 同一列在不同的类型下包含不同的元素(例如 `owner` 对于域名是注册商,对于网络则是 `mnt-by` 维护者)。
| 字段 | Domain 响应 | Network 响应 | 为何对防御者重要 |
|-------|-----------------|------------------|------------------------------|
| `query` | 查询字符串 | 查询字符串 | 查找的内容 |
| `query_type` | `domain` | `ipv4` / `ipv6` / `asn` | 将注册商查询与路由情报查询区分开 |
| `server_name` | — | 来源注册局 (RIPE, ARIN…) | 由哪个数据库应答 |
| `resource` | `Domain Name` | `NetRange` / `CIDR` / `inetnum` / `route` | 响应描述的对象 |
| `owner` | `Registrar` | 组织 / `mnt-by` 维护者 | 谁控制该资源 |
| `origin_as` | — | `OriginAS` / `origin:` | [CCC RIPE 演讲](#inspiration) 中重点提到的 BGP 过滤输入源 |
| `registered` | `Creation Date` | `RegDate` / `created:` | 年龄 — 新注册具有可疑性 |
| `updated` | `Updated Date` | `last-modified` | 近期重新指向/接管的信号 |
| `registry_expiry` | `Registry Expiry Date` | — | 短期(1 年)注册是一种排查线索 |
| `name_server` | `Name Server`(集合) | — | DNS 托管 + 通过共享 NS 扩展到相关域名 |
| `status` | EPP 代码 | — | `serverHold`/`clientHold` = 已扣押/已 Sinkhole |
| `abuse_contact` | `Registrar Abuse Contact Email` | — | 滥用报告 + 无视投诉的“防弹”注册商指纹识别 |
| `reply_time` | 请求→响应时间差 | 请求→响应时间差 | 延迟 — 隧道化/滥用信号 |
| `reply_size` | 总字节数 | 总字节数 | 数据量大小,无需存储完整的数据块 |
## 协议参考
- [RFC 3912 - WHOIS 协议规范](https://datatracker.ietf.org/doc/html/rfc3912)
- [Wireshark WHOIS 协议解析器](https://gitlab.com/wireshark/wireshark/-/blob/master/epan/dissectors/packet-whois.c)
## 许可证
BSD-3-Clause,详见 [`COPYING`](COPYING)。
### 作者
* Craig P ([@detection-labs](https://github.com/detection-labs))
### 致谢
* [Corelight](https://github.com/corelight)
* [Wireshark](https://gitlab.com/wireshark/wireshark)
* [tcpdump](https://github.com/the-tcpdump-group)
### 灵感来源
* [*第 38 届混沌通信大会, 38c3: “用于互联网路由策略的 WHOIS 协议,或:通过 TCP/43 传输的明文是如何进入路由器配置的”*](https://media.ccc.de/v/38c3-the-whois-protocol-for-internet-routing-policy-or-how-plaintext-retrieved-over-tcp-43-ends-up-in-router-configurations)
标签:Rootkit, Spicy, WHOIS, Zeek, 协议解析器, 网络流量分析