4、Zeek(Bro)脚本基础:Zeek事件驱动模型、Zeek脚本语法(事件、函数、变量)、Zeek日志分析

好,咱们进入第四讲。说实话,Zeek(以前叫Bro)这套东西,我第一次接触的时候也觉得有点怪——跟Snort那种规则匹配的思路完全不一样。但用久了你会发现,它的设计哲学其实非常优雅。

Zeek的核心是什么?是事件驱动。说白了,它不是在那儿傻乎乎地一条一条比对特征,而是等着网络里发生“事情”。一个TCP连接建立了,一个HTTP请求过来了,一个DNS响应返回了——这些都是事件。Zeek会捕捉这些事件,然后触发你写的脚本去处理。

我个人习惯把Zeek比作一个“网络世界的监听者”。它不主动说话,但耳朵特别好使。你告诉它:“嘿,如果看到有人访问某个IP,就记下来。”它就会照做。

4.1 事件驱动模型:Zeek的灵魂

为什么说事件驱动是灵魂?你想想看,传统的IDS要一条一条规则去匹配,流量大了CPU直接飙到100%。Zeek不一样,它只在你关心的事件发生时才会干活。平时?它就在那儿挂着,几乎不占资源。

Zeek内置了上百种事件类型。我列几个最常用的:

事件名称 触发时机 典型用途
new_connection 检测到新TCP连接 记录连接建立
http_request 捕获到HTTP请求 分析URL、User-Agent
dns_request 捕获到DNS查询 检测恶意域名
ssl_established TLS握手完成 检查证书异常
conn_state 连接状态变化 判断连接是否正常关闭

嗯,这里要注意:事件是有顺序的。比如一个HTTP请求,一定是先触发new_connection,然后才是http_request。我在项目中遇到过有人写脚本时搞反了顺序,结果数据一直取不到,排查了半天才发现是事件没对上。

4.2 Zeek脚本语法:其实没那么复杂

很多人一看到Zeek脚本就头大,觉得语法怪。其实它的设计很简洁,核心就三样东西:事件、函数、变量。

4.2.1 变量

Zeek的变量分两种:全局变量和局部变量。全局变量用global声明,局部变量用local。我建议你尽量用local,除非你真的需要跨事件共享数据。

# 全局变量,所有事件都能访问
global suspicious_ips: set[addr] = set();

# 局部变量,只在当前事件内有效
event http_request(c: connection, method: string, uri: string, ...)
{
    local my_local_var: string = "临时数据";
    # 只在当前事件内使用
}

我曾经踩过一个坑:在多个事件里用了同一个全局变量,结果并发访问时数据全乱了。后来我加了个锁机制才解决。所以啊,全局变量要慎用。

4.2.2 函数

函数就是封装逻辑用的。Zeek的函数定义挺直观:

function is_suspicious_ip(ip: addr): bool
{
    # 检查IP是否在黑名单中
    if (ip in suspicious_ips)
        return T;
    return F;
}

event http_request(c: connection, method: string, uri: string, ...)
{
    if (is_suspicious_ip(c$id$orig_h))
    {
        print fmt("发现可疑IP: %s", c$id$orig_h);
    }
}

你看,函数可以返回布尔值、字符串、甚至自定义的记录类型。我个人习惯把复杂的判断逻辑都封装成函数,这样事件处理函数里就干净多了。

4.2.3 事件处理函数

这是Zeek脚本的核心。每个事件对应一个处理函数,格式固定:

event 事件名称(参数列表)
{
    # 你的处理逻辑
}

举个例子,检测DNS隧道攻击:

event dns_request(c: connection, msg: dns_msg, query: string, qtype: count, qclass: count)
{
    # 检测超长域名,可能是DNS隧道
    if (|query| > 50)
    {
        print fmt("疑似DNS隧道: %s -> %s", c$id$orig_h, query);
    }
}

为什么会检测超长域名?因为正常的DNS查询很少超过50个字符。攻击者为了在DNS里塞数据,会把域名搞得很长。这是我在一次红蓝对抗中发现的技巧。

4.3 Zeek日志分析:从数据中找线索

Zeek默认会生成很多日志文件。我挑几个最重要的说说:

日志文件 记录内容 分析价值
conn.log 所有网络连接 基础流量分析
http.log HTTP请求/响应 Web攻击检测
dns.log DNS查询/响应 恶意域名、隧道检测
ssl.log TLS握手信息 证书异常、加密流量分析
notice.log 告警事件 安全事件汇总

日志格式是TSV(制表符分隔),可以直接用cut、awk等工具分析。比如我想看所有HTTP请求的URL:

cut -f 3,9 http.log | head -20

或者用Zeek自带的zeek-cut工具:

zeek-cut ts uid id.orig_h id.resp_h uri < http.log

嗯,这里有个小技巧:notice.log里的告警是Zeek自己生成的,但很多时候你需要自定义告警规则。比如检测到某个IP在短时间内发起大量连接:

event new_connection(c: connection)
{
    local src_ip = c$id$orig_h;
    
    # 假设我们有一个函数统计连接频率
    if (connection_rate(src_ip) > 100)
    {
        NOTICE([$note=High_Connection_Rate,
                $msg=fmt("IP %s 连接频率过高", src_ip),
                $conn=c]);
    }
}

核心要点:Zeek的日志分析不是看单条日志,而是看模式。单个HTTP请求可能没问题,但100个请求都指向同一个IP,那就值得怀疑了。

4.4 实战:写一个简单的检测脚本

咱们来写一个完整的脚本,检测SSH暴力破解。思路很简单:如果同一个源IP在短时间内发起大量SSH连接,就告警。

# 保存为 detect_ssh_brute.zeek

module DetectSSHBrute;

export {
    # 阈值:10秒内超过5次SSH连接
    global ssh_threshold: count = 5;
    global time_window: interval = 10sec;
    
    # 记录每个IP的连接次数
    global ssh_attempts: table[addr] of count &create_expire=time_window;
}

event new_connection(c: connection)
{
    # 只关注SSH端口(22)
    if (c$id$resp_p == 22/tcp)
    {
        local src = c$id$orig_h;
        
        # 更新计数
        if (src in ssh_attempts)
            ++ssh_attempts[src];
        else
            ssh_attempts[src] = 1;
        
        # 检查是否超过阈值
        if (ssh_attempts[src] > ssh_threshold)
        {
            NOTICE([$note=SSH_Brute_Force,
                    $msg=fmt("SSH暴力破解检测: %s 在 %s 内发起 %d 次连接", 
                             src, time_window, ssh_attempts[src]),
                    $conn=c]);
        }
    }
}

提示:脚本里的&create_expire=time_window是个好东西。它会让表项在指定时间后自动过期,省得你手动清理。我刚开始写Zeek脚本时不知道这个,结果内存越占越多,最后把服务器搞崩了。

4.5 避坑指南

最后分享几个我踩过的坑:

  • 事件参数别搞错:每个事件的参数是固定的,多一个少一个都不行。我建议你写之前先查一下官方文档的事件原型。
  • 注意大小写:Zeek是大小写敏感的。event和Event是两个东西。我曾经因为把new_connection写成New_Connection,脚本死活不触发。
  • 日志别忘开:默认情况下,Zeek只记录部分日志。如果你需要自定义日志,记得用Log::write函数手动写入。
  • 性能问题:事件处理函数里别做太重的操作。比如查数据库、写文件这些,尽量异步处理。否则流量一大,Zeek会丢包。

好了,这一章的内容就这些。Zeek脚本其实不难,难的是理解事件驱动的思维方式。你多写几个脚本,慢慢就习惯了。下一章咱们聊聊如何用Zeek检测更复杂的攻击,比如SQL注入和XSS。