Loading

流量分析的瑞士军刀:Zeek

Zeek (Bro) 是一款大名鼎鼎的开源网络安全分析工具。通过 Zeek 可以监测网络流量中的可疑活动,通过 Zeek 的脚本可以实现灵活的分析功能,可是实现多种协议的开相机用的分析。本文主要是将 Zeek 结合被动扫描器的一些实践的介绍,以及 Zeek 部署的踩过的一些坑。

安装

https://www.cnblogs.com/cscshi/p/16582933.html

Zeek 结合被动扫描器的玩法

被动扫描器的效果往往取决于流量的质量和数量,在我们的实际实践中,发现通过 Zeek 获取的流量占我们被动扫描器测试流量的绝大一部分。Zeek 对于 http 解析的日志都会存储在 /usr/local/zeek/logs 中。如果 Zeek 是启动状态,那么 http.log 的路径会在 /usr/local/zeel/logs/current 中,而历史日志则会被打包。如果使用 Zeek 去捕获流量的时候,日志往往会占很大的存储,所以要记得修改 Zeek 日志的存储路径,否则很容易就把系统盘塞满。

通过脚本自定义 http.log

http.log 中其实已经包含了丰富的字段,常见的一些字段如下:

# ts          uid          orig_h        orig_p  resp_h         resp_p
1311627961.8  HSH4uV8KVJg  192.168.1.100 52303   192.150.187.43 80

不过里面还有一些信息是缺失的,比如一些 http 请求头以及 POST 请求的请求体,为了添加这些字段,可以通过自定义 Zeek 脚本来实现,Zeek 脚本的能力真的非常强大,通过脚本其实有很多更高级的玩法。

添加请求头

@load base/protocols/http/main
module HTTP;
export {
    redef record Info += {
            header_host:    string  &log    &optional;
            header_accept:  string  &log    &optional;
            header_accept_charset:  string  &log    &optional;
            header_accept_encoding:  string  &log    &optional;
            header_accept_language:  string  &log    &optional;
            header_accept_ranges:  string  &log    &optional;
            header_authorization:  string  &log    &optional;
            header_connection:  string  &log    &optional;
            header_cookie:  string  &log    &optional;
            header_content_length:  string  &log    &optional;
            header_content_type:  string  &log    &optional;
    };
}
event http_header(c: connection, is_orig: bool, name: string, value: string) &priority=3
        {
        if ( ! c?$http )
                return;
        if ( is_orig )
                {
                if ( log_client_header_names )
                        {
                switch ( name ) {
                                case "HOST":
                                    c$http$header_host = value;
                                    break;
                                case "ACCEPT":
                                    c$http$header_accept = value;
                                    break;
                                case "ACCEPT-CHARSET":
                                    c$http$header_accept_charset = value;
                                    break;
                                case "ACCEPT-ENCODING":
                                    c$http$header_accept_encoding = value;
                                    break;
                                case "ACCEPT-LANGUAGE":
                                    c$http$header_accept_language = value;
                                    break;
                                case "ACCEPT-RANGES":
                                    c$http$header_accept_ranges = value;
                                    break;
                                case "AUTHORIZATION":
                                    c$http$header_authorization = value;
                                    break;
                                case "CONNECTION":
                                    c$http$header_connection = value;
                                    break;
                                case "COOKIE":
                                    c$http$header_cookie = value;
                                    break;
                                case "CONTENT-LENGTH":
                                    c$http$header_content_length = value;
                                    break;
                                case "CONTENT-TYPE":
                                    c$http$header_content_type = value;
                                    break;
                                }
            }
                }
        }

添加 POST 请求体

export {
    ## The length of POST bodies to extract.
    const http_post_body_length = 200 &redef;
}
redef record HTTP::Info += {
    postdata: string &log &optional;
};
event log_post_bodies(f: fa_file, data: string)
    {
    for ( cid in f$conns )
        {
        local c: connection = f$conns[cid];
        if ( ! c$http?$postdata )
            c$http$postdata = "";
        # If we are already above the captured size here, just return.
        if ( |c$http$postdata| > http_post_body_length )
            return;
        c$http$postdata = c$http$postdata + data;
        if ( |c$http$postdata| > http_post_body_length )
            {
            c$http$postdata = c$http$postdata[0:http_post_body_length] + "...";
            }
        }
    }
event file_over_new_connection(f: fa_file, c: connection, is_orig: bool)
    {
    if ( is_orig && c?$http && c$http?$method && c$http$method == "POST" )
        {
        Files::add_analyzer(f, Files::ANALYZER_DATA_EVENT, [$stream_event=log_post_bodies]);
        }
    }

通过上述的脚本就可以添加一些请求头以及 POST 请求的请求体,完整的脚本可以参考 http-custom。脚本编写完毕,需要通过 zeekctl 部署才能生效。

mv http-custom /usr/local/bro/share/bro/base/protocols
echo '@load base/protocols/http-custom' >> /usr/local/bro/share/bro/site/local.bro
zeekctl deploy

对于被动扫描器,我们目前的方案是通过 Filebeat 去采集日志然后输出给 Logstash 做处理,处理完毕之后再输出到 Kafka。
img

Filebeat 加 Logstash 适用于多种场景,在日常的各种日志采集场景都能派上用场。通过 Logstash 可以完成日志灵活的处理,因为 Logstash 里面包含了各种丰富的插件,几乎可以完成对于日志的任何操作。比如为了保证 POST 请求体保证传输的正确性,可以通过 base64 来进行编码。通过 logstash-filter-base64 可以遍历地实现字段的编码或者解码。通过 filter 中的 mutate 插件可以增加字段或者删除字段。

base64 {
     field => "postdata"
     action => "encode"
   }

通过这种方案还有一个优势就是我们还可以将我们的日志输出到别的地方,比如 es,这个也可以方便后续排查日志问题。

不过我在后面又发现了一种新的方案,可以通过 Zeek 的插件,将 http.log 直接输出到 Kafka,这个方案的优点主要是更高效,同时也节省了一些成本,毕竟 Logstash 需要的机器性能还是比较大的。对于这个方案主要是两个问题,第一个问题是首先需要处理好日志的格式,这样保证后续处理地便利性;第二个问题是如何将日志直接从 Zeek 输出到 Kafka。其实我是先解决了第一个问题再解决第二个问题的,因为第二个问题的处理的方式更灵活,得益于 Zeek 脚本的便利性,肯定是可以实现的。

img

metron-bro-plugin-kafka 是 Apache 官方的一个 Bro 的插件,不过因为 Zeek3.0.0 是可以兼容的,所以这个插件是可以使用的。这个插件有两种安装方式,一种是通过 bro-pkg (Bro 的官方包管理工具)来进行安装,另外一种则是通过手工安装。

安装 librdkafka

curl -L https://github.com/edenhill/librdkafka/archive/v0.11.5.tar.gz | tar xvzcd librdkafka-0.11.5/
./configure --enable-sasl
make
sudo make install

安装插件

./configure --bro-dist=$BRO_SRC
make
sudo make install

这里有一个坑就是安装文档根本就没有说 $BRO_SRC 是哪个路径,所以安装的时候总是报错,后来才弄清楚这个路径其实就是当初 Zeek 解压后的路径,即 Zeek 安装包的路径。

验证结果

zeek -N Apache::Kafka
Apache::Kafka - Writes logs to Kafka (dynamic, version 0.3)

接着就是将 http 的日志进行处理,因为在原始的 http.log 中有还多字段是我们并不需要的。在研究了官方文档之后,可以通过 Filters 可以定义一个新的日志文件,可以拷贝其它的日志输出到新的文件,可以自定义字段,方式比较灵活。另外还可以通过 Writer 可以将日志写入到 sqlite 数据库中。不过,这里我们主要是通过插件将日志写入到 Kafka。

我们的目标是获取 http.log 中的部分字段,所以可以通过 Filters 来实现日志文件的复制并且对日志字段进行过滤,基于 KafkaWriter 将日志文件直接写入到 Kafka 中。为了定义 Filter,在 /usr/local/zeek/share/zeek/base/protocols/http/main.zeek 的 zeek_init 函数中进行定义:

event zeek_init() &priority=5
     {
     Log::create_stream(HTTP::LOG, [$columns=Info, $ev=log_http, $path="http"]);
     Analyzer::register_for_ports(Analyzer::ANALYZER_HTTP, ports);
     local filter: Log::Filter =
         [
         $name="kafka-http",
         $include=set("host","id.resp_p","uri"),
         $writer=Log::WRITER_KAFKAWRITER
         ];
      Log::add_filter(HTTP::LOG, filter);
     }

另外,记得在 /usr/local/zeek/share/zeek/site/local.zeek 中定义 Kafka 的 topic 和 Broker:

redef Kafka::topic_name = "bro-test";
redef Kafka::kafka_conf = table(
    ["metadata.broker.list"] = "127.0.0.1:9092"
);

最后记得使用 zeekctl deploy 重新部署一下,这样脚本就生效了,日志就可以直接写入到 Kafka 中,大大提高效率。

posted @ 2022-08-13 14:24  锦瑟,无端  阅读(762)  评论(0编辑  收藏  举报