- 网络安全
- 网络
- IDS
【免费下载链接】zeek
Zeek is a powerful network analysis framework that is much different from the typical IDS you may know.
本指南以 Zeek 仓库中base/protocols/dns脚本包(含 index.rst 所描述的__load__.zeek、consts.zeek、main.zeek、check-event-handlers.zeek四个文件)为核心,系统讲解 Zeek 如何对 Domain Name System (DNS) 协议进行开箱即用的分析:从脚本包的加载顺序、DNS 数值码点(QTYPE/QCLASS/RCCODE/Opcode 等)到人类可读字符串的映射表,再到日志记录DNS::Info的字段定义、查询/响应的配对跟踪状态机与可调优的运行时选项。读完本文,你将能够读懂dns.log的每一列来源,知道如何通过redef/option调整 DNS 分析行为,并能依据源码级证据扩展自己的 DNS 分析脚本。
脚本包结构:四个文件如何协同工作
Zeek 的脚本包以__load__.zeek为统一入口。scripts/base/protocols/dns/__load__.zeek的内容非常精简,仅有三行@load指令:
@load ./consts @load ./main @load ./check-event-handlers也就是说,加载base/protocols/dns等价于按以下顺序加载三个子模块:
| 文件 | 职责 |
|---|---|
| consts.zeek | 定义 DNS 分析所需的类型(数值码点)、错误码与字段常量,是"helper file",供其他 DNS 分析脚本复用 |
| main.zeek | 基础 DNS 分析脚本:跟踪并记录 DNS 查询及其响应,定义日志流DNS::LOG与核心数据结构 |
| check-event-handlers.zeek | 校验脚本:当某些 DNS 事件处理器因配置原因永远不会被触发时,向用户发出警告 |
三个文件均声明module DNS;,所有导出符号都位于DNS::命名空间下。consts.zeek是纯数据层(只定义常量与映射表),main.zeek是逻辑层(注册日志流、挂接事件、维护配对状态),check-event-handlers.zeek是健康检查层,职责边界非常清晰。
consts.zeek:DNS 数值码点的权威映射表
consts.zeek(源码)是理解dns.log中qtype_name、qclass_name、rcode_name、opcode_name等可读字段的唯一事实来源。这些字段之所以能自动变成字符串,正是因为每张映射表都带有&default兜底函数:当遇到表中未收录的新码点时,会返回query-N、rcode-N、qclass-N这类占位字符串,保证日志字段永不缺失。
标量常量
| 常量 | 值 | 语义 |
|---|---|---|
DNS::PTR | 12 | RR TYPE 值,域名的指针记录(反向解析) |
DNS::EDNS | 41 | OPT 伪资源记录的 RR TYPE 值,代表 EDNS(0) 扩展 |
DNS::NONE | 254 | 表示"无类别"的 class 值,用于动态更新(RFC 2136) |
DNS::ANY | 255 | QTYPE 值,表示请求所有记录 |
操作码(Opcode)常量
consts.zeek定义了DNS::DNS_OP_QUERY = 0、DNS::DNS_OP_IQUERY = 1、DNS::DNS_OP_SERVER_STATUS = 2、DNS::DNS_OP_NOTIFY = 4、DNS::DNS_OP_DYNAMIC_UPDATE = 5、DNS::DNS_OP_DSO = 6,并配套两张映射表:
DNS::opcodes:标准 DNS 操作码 → 可读名,如[0]="query"、[5]="dynamic-update"、[6]="dso";DNS::netbios_opcodes:NetBIOS Name Service(NBNS,RFC 1002)专用操作码 → 可读名,如[0]="netbios-query"、[5]="netbios-registration"、[8]="netbios-refresh"。
main.zeek的set_session钩子中正是依据msg$is_netbios标志来决定查哪张表,从而正确渲染 NetBIOS 查询的 opcode 名称。
查询类型表DNS::query_types
这是 Zeek 维护的一张覆盖面很广的 QTYPE 映射表(源码位置),从经典类型到 DNSSEC、再到现代加密 DNS 类型均有收录。节选如下(完整定义以源码为准):
| 码点 | 名称 | 码点 | 名称 |
|---|---|---|---|
| 1 | A | 28 | AAAA |
| 2 | NS | 33 | SRV |
| 5 | CNAME | 35 | NAPTR |
| 6 | SOA | 43 | DS |
| 12 | PTR | 46 | RRSIG |
| 15 | MX | 48 | DNSKEY |
| 16 | TXT | 64 | SVCB |
| 41 | OPT | 65 | HTTPS |
| 250 | TSIG | 257 | CAA |
| 252 | AXFR | 255 | *(任意类型) |
此外还收录了WINS/WINS-R(65281/65282)、INTEGRITY(65521)等私有或扩展码点。dns_request事件处理中,qtype_name直接通过query_types[qtype]获得。
类别表DNS::classes与错误码表DNS::base_errors
DNS::classes覆盖 CLASS/QCLASS 字段:[1]="C_INTERNET"、[2]="C_CSNET"、[3]="C_CHAOS"、[4]="C_HESIOD"、[254]="C_NONE"、[255]="C_ANY";DNS::base_errors覆盖除 TSIG/EDNS 之外的标准 RCODE:[0]="NOERROR"、[1]="FORMERR"、[2]="SERVFAIL"、[3]="NXDOMAIN"、[4]="NOTIMP"、[5]="REFUSED"、[6]="YXDOMAIN"、[8]="NXRRSet"、[9]="NOTAUTH"、[16]="BADVERS"、[17]="BADKEY"、[18]="BADTIME"、[19]="BADMODE"、[23]="BADCOOKIE"、[3842]="BADSIG"等,未分配码点(11–15)显式标注为unassigned-N。
DNSSEC 相关表
DNS::algorithms:DNSKEY/DS/RRSIG 记录中的签名算法,如[1]="RSA_MD5"、[5]="RSA_SHA1"、[8]="RSA_SHA256"、[10]="RSA_SHA512"、[13]="ECDSA_curveP256withSHA256"、[15]="Ed25519"、[16]="Ed448"、[254]="PrivateOID";DNS::digests:DS 记录使用的摘要类型,如[1]="SHA1"、[2]="SHA256"、[3]="GOST_R_34_11_94"、[4]="SHA384";DNS::edns_zfield:EDNS 扩展头中 Z 字段的取值,[0]="NOVALUE"、[32768]="DNS_SEC_OK"(表示接受 DNSSEC RR)。
SVCB/HTTPS 参数表DNS::svcparam_keys
DNS::svcparam_keys依据 RFC 9460 定义 SVCB/HTTPS 记录的 SvcParam 键:[0]="mandatory"、[1]="alpn"、[2]="no-default-alpn"、[3]="port"、[4]="ipv4hint"、[5]="ech"、[6]="ipv6hint"。源码注释明确要求"Keep in sync with src/analyzer/protocol/dns/DNS.h SVCPARAM_Key",即与 C++ 分析器侧的枚举保持一致,这也是验证映射正确性的关键线索。
main.zeek:查询/响应跟踪与 dns.log 的生成
main.zeek(源码)是整个 DNS 分析的核心。它的主要工作分为三层:注册日志流、维护连接级配对状态、通过事件/钩子持续填充日志记录。
日志流与端口注册
redef enum Log::ID += { LOG }; const ports = { 53/udp, 53/tcp, 137/udp, 5353/udp, 5355/udp } &redef; event zeek_init() &priority=5 { Log::create_stream(DNS::LOG, Log::Stream($columns=Info, $ev=log_dns, $path="dns", $policy=log_policy)); Analyzer::register_for_ports(Analyzer::ANALYZER_DNS, ports); }要点:
- 新增枚举
DNS::LOG作为日志流标识; - 创建日志流时指定
$columns=Info(列结构)、$ev=log_dns(写日志前触发的可观察事件)、$path="dns"(即dns.log)、$policy=log_policy(默认策略钩子); - 通过
Analyzer::register_for_ports让 DNS 分析器监听一组"众所周知"端口,默认集合为{ 53/udp, 53/tcp, 137/udp, 5353/udp, 5355/udp },覆盖标准 DNS、NetBIOS(137)、mDNS(5353) 与 LLMNR(5355)。这是一个&redef常量,可通过redef DNS::ports += { ... };扩充。
日志记录结构DNS::Info
DNS::Info是dns.log的列定义(源码位置),逐列含义如下:
| 字段 | 类型 | 是否记录 | 语义 |
|---|---|---|---|
ts | time | 是 | 该连接上最早观测到 DNS 协议消息的时间 |
uid | string | 是 | 传输 DNS 消息的连接的唯一标识 |
id | conn_id | 是 | 连接的 4 元组(两端地址/端口) |
proto | transport_proto | 是 | 传输层协议(UDP/TCP) |
trans_id | count | 是(可选) | 发起方分配的 16 位事务 ID,用于配对响应 |
rtt | interval | 是(可选) | 查询到响应开始之间的往返时延 |
query | string | 是(可选) | 查询的域名 |
qclass/qclass_name | count / string | 是(可选) | 查询类别值及其可读名 |
qtype/qtype_name | count / string | 是(可选) | 查询类型值及其可读名 |
rcode/rcode_name | count / string | 是(可选) | 响应码值及其可读名 |
AA/TC/RD/RA | bool | 是(默认 F) | 权威应答、截断、期望递归、可用递归四个标志位 |
Z | count | 是(默认 0) | RFC 1035 规定的 3 位保留字段(DNSSEC 时可能非零) |
answers | vector of string | 是(可选) | 应答区中的资源描述集合 |
TTLs | vector of interval | 是(可选) | 与answers对应的缓存生存期 |
rejected | bool | 是(默认 F) | 查询是否被服务器拒绝 |
opcode/opcode_name | count / string | 是(可选) | 请求/响应的操作码值及可读名 |
total_answers | count | 否(可选) | 应答区资源记录总数 |
total_replies | count | 否(可选) | 应答+权威+附加区资源记录总数 |
saw_query/saw_reply | bool | 否(默认 F) | 是否已观测到完整查询/响应 |
另有两个仅当加载特定 policy 脚本时才出现的可选字段:加载 scripts/policy/protocols/dns/auth-addl.zeek 后会出现auth(权威应答)与addl(附加应答);加载 scripts/policy/protocols/dns/log-original-query-case.zeek 后会出现original_query(保留原始大小写的查询名)。这种"字段按需扩展"的机制是 Zeek 脚本生态的典型模式。
连接状态DNS::State与待配对消息队列
DNS::State(源码位置)按连接跟踪 DNS 查询状态:
type State: record { pending_query: Info &optional; # 尚未配对的单条查询(性能快路径) pending_queries: PendingMessages &optional; # 按事务 ID 索引的未配对查询队列 pending_replies: PendingMessages &optional; # 按事务 ID 索引的未配对响应队列 };其中PendingMessages是table[count] of Queue::Queue——即以事务 ID 为键、值为base/utils/queue.zeek提供的 FIFO 队列。之所以引入队列而非单条记录,是因为同一连接上可能并发出现多个共享同一事务 ID 的查询/响应(例如大型 AXFR 区域传输过程中会有大量同 ID 消息)。
该状态通过redef record connection += { dns: Info &optional; dns_state: State &optional; };挂接到每个连接记录上,并在set_session钩子中随连接生命周期创建、通过Conn::register_removal_hook(c, finalize_dns)注册连接移除回调。
配对逻辑:set_session钩子
set_session钩子(源码位置)是查询/响应配对的核心,由dns_message事件以&priority=5触发,并传入is_query = ! msg$QR。其分支逻辑可以归纳为:
- 多播目的地短路:若响应方地址属于
DNS::multicast_subnets(默认{ 224.0.0.0/4, ff00::/8 }),则不做配对,直接将saw_query与saw_reply置真,让dns_end立即记录该消息。这是因为 mDNS/LLMNR 的响应来自不同连接,按事务 ID 配对会得到错误结果; - 查询方向:若该事务 ID 已有待配对响应,则直接取队列头部响应与之配对;否则新建会话并放入
pending_query(快路径)或pending_queries队列; - 响应方向:优先与
pending_query中同 ID 的查询配对;否则查pending_queries队列;仍无匹配则放入pending_replies等待后续查询; - 填充公共字段:响应方向补充
rcode/rcode_name、total_answers、total_replies、rejected(rcode != 0且无查询消息时置真);双向都填充opcode/opcode_name。
超限兜底:两个运行时选项
为防止异常流量(如服务器损坏、Zeek 丢包、AXFR 进行中)导致内存膨胀,main.zeek提供两个可用option调整的阈值(enqueue_new_msg函数中生效):
DNS::max_pending_msgs(默认50):单个事务 ID 的待配对查询/响应达到该数量后,放弃继续匹配,先把队列中已有的未配对消息全部写日志再清空重建;DNS::max_pending_query_ids(默认50):待配对查询/响应横跨的不同事务 ID 数量达到该值后,认为这些消息永远无法配对,整体记录后丢弃。
应答收集:do_reply钩子与各dns_*_reply事件
main.zeek为每种资源记录类型实现了对应的dns_*_reply事件处理器(均以&priority=5挂接),它们把 RR 的具体内容格式化为字符串后统一交给DNS::do_reply钩子:
dns_A_reply/dns_AAAA_reply/dns_A6_reply:输出地址字符串;对动态更新消息额外带上ans$query前缀;dns_TXT_reply/dns_SPF_reply:逐条输出TXT 长度 内容/SPF 长度 内容,多条以空格连接;dns_NS_reply、dns_CNAME_reply、dns_MX_reply、dns_PTR_reply、dns_SRV_reply:输出名称/目标;dns_SOA_reply:输出soa$mname(主域名服务器);dns_NAPTR_reply:编码 order/preference/flags/service/regexp/replacement;- DNSSEC 系列:
dns_RRSIG(RRSIG 覆盖类型 签名者,根签名者显示为<Root>)、dns_DNSKEY(算法)、dns_NSEC(NSEC 查询 下一名称)、dns_NSEC3/dns_NSEC3PARAM、dns_DS(算法与摘要类型)、dns_SSHFP(十六进制指纹)、dns_LOC(size/精度); - 动态更新:
dns_dynamic_update_pre与dns_dynamic_update_del依据 qclass/qtype 组合生成NameNotInUse、NameInUse、NoRRSet、RRSetExists、RRSet *、RR ...等描述。
DNS::do_reply钩子(默认实现)在收集阶段完成以下工作:填充query(若尚未设置)、AA/RA标志、计算rtt(network_time() - c$dns$ts,为 0 时删除以免误报)、追加answers与TTLs。do_reply是一个hook,意味着用户脚本可以在其默认实现之前/之后插入额外逻辑。
记录时机:dns_end与finalize_dns
dns_end事件(&priority=5)负责置位saw_query/saw_reply;另一重载(&priority=-5)在两者均为真时执行Log::write(DNS::LOG, c$dns)并清除,从而保证"查询与响应成对后只记一行";DNS::finalize_dns是Conn::RemovalHook,在连接状态到期时被调用,把仍滞留的pending_query、pending_queries、pending_replies全部写日志,避免状态被丢弃导致日志缺失。
事件与钩子速查
| 符号 | 类型 | 用途 |
|---|---|---|
DNS::log_dns | event(rec: Info) | 每条记录写入日志框架前触发,供用户脚本访问/修改DNS::Info |
DNS::set_session | hook(c, msg, is_query) | 每次建立 DNS 会话(查询或响应)时调用,可做附加初始化 |
DNS::do_reply | hook(c, msg, ans, reply) | 各类dns_*_reply收集应答的统一入口 |
DNS::log_policy | Log::PolicyHook | 日志流的默认策略钩子 |
DNS::finalize_dns | Conn::RemovalHook | 连接移除时冲刷未配对消息 |
check-event-handlers.zeek:预防"静默失效"的事件处理器校验
check-event-handlers.zeek 的职责非常独特:当全局选项dns_skip_all_addl为真时,附加区(additional section)相关事件不会被分析器触发,如果用户脚本仍然使用了这些事件,会产生"看起来没问题、实际上从不执行"的静默失效。该脚本在zeek_init(&priority=20,早于一般脚本)中检查dns_TSIG_addl、dns_EDNS_addl、dns_EDNS_ecs、dns_EDNS_tcp_keepalive、dns_EDNS_cookie五个事件是否被用户处理,是则输出Reporter::warning;同时警告dns_TKEY事件在dns_skip_all_addl下ans参数将不含数据。这体现了 Zeek 脚本框架中"可观测性优先"的设计习惯——宁可显式警告,也不让脚本静默失效。
结合 policy 脚本的扩展用法
base层的 DNS 分析默认只记录基础列,仓库在scripts/policy/protocols/dns/目录提供以下按需加载的增强脚本:
- auth-addl.zeek:为
DNS::Info增加auth与addl字段,记录权威区与附加区的应答; - log-original-query-case.zeek:增加
original_query字段,保留查询名的原始大小写(对检测基于大小写差异的隐蔽通道有价值); - detect-external-names.zeek 与 disable-opcode-log-fields.zeek:前者检测外部域名解析,后者可在不需要 opcode 字段时缩减日志体积。
典型使用方式是在local.zeek或自定义脚本中@load policy/protocols/dns/auth-addl。这些脚本通过redef扩展DNS::Info记录或挂接DNS::log_policy,展示了 base 层为上层策略预留的扩展点。
实战验证与调试建议
- 跑通基线:对任意包含 DNS 流量的 pcap 运行
zeek -r capture.pcap(或zeek -C -r ...),检查生成的dns.log是否包含qtype_name、rcode_name等可读列,并对照 doc/scripts/base/protocols/dns/main.zeek.rst 中DNS::Info的字段定义逐一核对; - 观察配对行为:对正常流量检查
rtt与trans_id的对应关系;对 mDNS/LLMNR 流量(多播地址)应能看到逐条独立记录而非配对记录,这正是multicast_subnets逻辑生效的表现; - 调整分析范围:若需监控自定义端口上的 DNS,使用
redef DNS::ports += { 5353/udp };;若担心异常流量导致队列膨胀,可调低DNS::max_pending_msgs或DNS::max_pending_query_ids; - 扩展日志字段:在脚本中
hook DNS::log_dns(或直接event DNS::log_dns)读取/改写rec,即可在记录落盘前附加自定义信息;需要权威/附加区信息时加载policy/protocols/dns/auth-addl。
从源码结构看,base/protocols/dns的设计目标可以概括为:以 consts 提供"数字 ↔ 语义"的确定性翻译,以 main 提供健壮的跨连接配对状态机,以 hook/event 暴露全部扩展点,再以 check-event-handlers 兜底配置陷阱——四个文件各司其职,共同构成 Zeek 对 DNS 协议分析的开箱即用能力。
- 网络安全
- 网络
- IDS
【免费下载链接】zeek
Zeek is a powerful network analysis framework that is much different from the typical IDS you may know.
相关推荐
Zeek DNS 协议分析脚本(base/protocols/dns/main.zeek)深度解析:日志字段、查询/响应关联与源码实现
Zeek DNS 协议分析脚本(base/protocols/dns/main.zeek)深度解析:日志字段、查询/响应关联与源码实现 导读 scripts/b
网络安全网络IDSZeek 中 SMB 协议分析包(base/protocols/smb)加载机制与脚本架构深度解析
Zeek 中 SMB 协议分析包(base/protocols/smb)加载机制与脚本架构深度解析 本文以 Zeek 仓库中的 doc/scripts/base
网络安全网络IDSZeek base/packet-protocols 包解析:__load__.zeek 加载机制与 30+ 数据链路/隧道协议分析器全景
Zeek base/packet protocols 包解析:__load__.zeek 加载机制与 30+ 数据链路/隧道协议分析器全景 base/packe
网络安全网络IDS
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考