【日本初紹介】Zeek・Spicyの使い方まとめ

概要

はじめまして。イノベーションセンター所属の鄭(GitHub: nbhgytzheng)です。2021年入社し、現在はテクノロジー部門のOsecT-Ops プロジェクトに所属して、OsecTの開発・運用業務に取り組んでいます。

今回はOsecTで利用しているZeek(ネットワーク・セキュリティ・モニタリングツール)とZeekのプロトコル拡張ツールであるSpicy(Zeekで利用するC++のパーサーをC++で記述することなく簡易に生成できるツール)の概要及び使い方・利用例を紹介します。探した限り日本語での使い方の紹介記事がないため、今回が日本初紹介です。本記事の目標は、ZeekとSpicyを使って任意のプロトコルを含むトラフィック解析ができるようになることです。

OsecTとは

OsecTとは工場などの制御システム(OT; Operational Technology)のセキュリティリスクを可視化・検知するサービスです。 多様化する工場システムのセキュリティ脅威に対して、パケット解析するセンサー機器を設置するだけで、OTシステムへの影響なく、ネットワークの可視化と脅威・脆弱性検知ができます。早期にリスク感知できる状態を作り、工場停止による損失を未然に防ぐことができます。詳しくは過去のブログ記事に書いているので、興味がある人はぜひ見てください。(OsecTリリースOsecT前編OsecT後編

Zeekとは

Zeekとはネットワークを監視し、トラフィックを解析して、IPアドレス、MACアドレスやプロトコルなどの情報をログとして出力するOSSです。Zeekは下記のような基本ログ(conn.log, dns.log, dhcp.logなど)を出力します。また必要に応じて、プラグインを利用することで出力ログの追加ができます。

# 生成のログの例:dns.log

#separator \x09
#set_separator  ,
#empty_field    (empty)
#unset_field    -
#path   dns
#open   2023-03-14-16-15-20
#fields ts  uid id.orig_h   id.orig_p   id.resp_h   id.resp_p   proto   trans_id    rtt query   qclass  qclass_name qtype   qtype_name  rcode   rcode_name  AA  TC  RD  RA  Z   answers TTLs    rejected    pkts
#types  time    string  addr    port    addr    port    enum    count   interval    string  count   string  count   string  count   string  bool    bool    bool    bool    count   vector[string]  vector[interval]    bool    int
1539457598.376546   CHTBxA4O2ypY1PxYPg  fd00:f81d:f5f:6b92:fd0d:9399:3d28:3984  5353    ff02::fb    5353    udp 0   -   _ipp._tcp.local 1   C_INTERNET  12  PTR -   -   F   F   F   F   0   -   -   F   -
1539459646.378861   COMnQY2JaJZ3TllMYb  fd00:f81d:f5f:6b92:fd0d:9399:3d28:3984  5353    ff02::fb    5353    udp 0   -   _ipp._tcp.local 1   C_INTERNET  12  PTR -   -   F   F   F   F   0   -   -   F   -
1539463246.380625   Cgu5Zw1ASoGNZ2UGok  fd00:f81d:f5f:6b92:fd0d:9399:3d28:3984  5353    ff02::fb    5353    udp 0   -   _ipp._tcp.local 1   C_INTERNET  12  PTR -   -   F   F   F   F   0   -   -   F   -
1539466846.382677   C5X9v4snqSDPMj3u7   fd00:f81d:f5f:6b92:fd0d:9399:3d28:3984  5353    ff02::fb    5353    udp 0   -   _ipp._tcp.local 1   C_INTERNET  12  PTR -   -   F   F   F   F   0   -   -   F   -
...

Spicyとは

プラグインを利用することで、出力ログを増やせますが、必要なログを出力するプラグインがないこともあります。例えば、以下の要件を満たすプラグインは存在しません。

  • Zeekが対応しているプロトコルの出力ログ内に追加の情報を加えて出力したい場合
  • Zeekが対応していないプロトコルの情報をログに出するしたい場合

ここで登場するのがSpicyです。SpciyとはZeekで利用するC++のパーサーをC++で記述することなく簡易に生成するためのツールです。Spicyを利用すれば、ログ情報の追加やZeek及びプラグインで解析できないプロトコルの解析ができるようになります。

Zeek・Spicyによるプロトコル解析

ここまではZeek, Spicyの概要について紹介しました。本章ではZeek, Spicyの利用方法について、環境構築からコーディングまで紹介します。

1. ZeekとSpicyのインストール

はじめに、Zeek, Spicyの実行環境を構築します。今回はUbuntu 22.04.1をベースに環境構築の方法を示します。その他のOSでのインストール方法は公式サイトを参照してください。

1.まずはZeekのリソース(GitHub)を取得するため、gitコマンドをインストールします。

~$ sudo apt install git

2.次にgitコマンドを利用して、Zeekのリソースをクローンします。

~$ git clone --recursive https://github.com/zeek/zeek -b v5.0.0

3.Zeekのリソースはcmakeコマンドを利用して自動インストールできるので、cmakeコマンドと関連パッケージをインストールします。

~$ sudo apt-get install cmake make gcc g++ flex libfl-dev bison libpcap-dev libssl-dev python3 python3-dev swig zlib1g-dev

4.最後にクローンしたZeekディレクトリへ移動し、以下のコマンドを実行すれば自動でインストールされます。makeは状況に応じて1時間以上かかる場合もあります。最後のmake installはスーパーユーザーで実行しないと、エラーになるため注意してください。

~$ ./configure --build-type=Release
...一部省略...
================================================================

-- Configuring done
-- Generating done
-- Build files have been written to: /home/xxx/zeek/build

~$ make
...一部省略...
Consolidate compiler generated dependencies of target zeek-archiver
make[3]: Leaving directory '/home/xxx/zeek/build'
[100%] Built target zeek-archiver

~$ sudo make install
...一部省略...
make[1]: Leaving directory '/home/xxx/zeek/build'

5.以上でZeekのインストールは完了です。以下のコマンドでZeekが利用できることを確認します。

# パス追加
~$ export PATH="$PATH:/usr/local/zeek/bin"
# zeekコマンド確認
~$ zeek -version
zeek version 5.0.0

6.Spicyのインストールは公式サイトでDEBファイルを取得し、以下のコマンドを順に実行すればインストールが完了です。

~$ sudo dpkg --install xxx.deb (例:spicy-dev.deb)
Selecting previously unselected package spicy.
(Reading database ... 212653 files and directories currently installed.)
Preparing to unpack spicy-dev.deb ...
Unpacking spicy (2.4.8) ...
Setting up spicy (2.4.8) ...

7.インストールが完了したら、以下のコマンドでSpicyが利用できることを確認します。

# パス追加
~$ export PATH="$PATH:/opt/spicy/bin/spicyc"
# spicyコマンド確認
~$ spicyz -version
1.3.16
~$ spicyc -version
spicyc v1.5.0 (d0bc6053)

以上でZeekとSpicyの実行環境の構築は完了です。

2. プロトコル仕様の確認

SpicyではIPベースのプロトコルだけでなく、イーサネットベースのプロトコルのパーサーも作成できます。今回はIPベースのUDPプロトコルを例に説明します。IPベースのプロトコル構造は以下のようになっています。IPベースでSpicyを利用する場合、Spicyが自動的に青い部分(イーサネットヘッダー、IPヘッダー、TCP/UDPヘッダー)を除き、オレンジ色の部分を処理します。

まず、ログに出力する内容を決めるために、解析対象のプロトコル仕様を確認します。プロトコル仕様はGoogleなどで検索して見つけられるケースもありますが、プロトコルに関わる各種団体・協会経由で仕様書を取得するケースもあります。プロトコル仕様からポート番号(今回の例では1111)と以下のようなパケットフォーマット定義を用意して、Spicyでのパーサー実装に進みます。

項目名/フィールド名 サイズ(オクテット) 内容
header1 2 プロトコルヘッダー
header2 1 プロトコルヘッダー
command 2 リクエストのタイプを表す
subCommand 2 実行する動作を表す

次にパーサーで利用するSpicyファイル・evtファイル・Zeekファイルの作成方法について紹介します。

3. Spicyファイルを作成

本節では以下のテンプレートと前節で取得したプロトコル情報(ポート番号・パケットフォーマット定義)を利用したSpicyファイルの基本的な作成方法を紹介します。Spicyファイルでは、通信プロトコルのプロトコルフォーマットを定義します。

# spicyテンプレート
module MYPROTOCOL;

import zeek;
import spicy;

public type Message = unit {
        PROTOCOL_FIELD1: FIELD_TYPE &size=1;
        PROTOCOL_FIELD2: FIELD_TYPE &size=2;
        ...

        on %done { print self; zeek::confirm_protocol();}
};

テンプレートの各部分を解説します。

  • module MYPROTOCOL;:任意のモジュール名を宣言
    • 多くの場合、解析対象のプロトコル名を記述します。
  • import xxx:Spicy関数の取り込み
    • 基本的には、Spicyの基本関数が入っている”spicy”とconn.logのservice列にプロトコル名を出力ための”zeek”が必要です。
  • public type Message = unit {}:プロトコルのデータ部分を格納する変数
    • データ部分はこのMessageに渡されます。
  • PROTOCOL_FIELD1: FIELD_TYPE &size=1;:データ部分をブロックごとに分解
    • 書き方はFIELD_TYPEによって異なるため公式サイトを参照してください。
  • on %done { print self; zeek::confirm_protocol();}:Spicyが解析を終えたときに実行する関数
    • print self;:解析データの全てをprint(debug用)
    • zeek::confirm_protocol();:解析が終わったことをZeekに通知

そして、前節で取得したプロトコル情報を利用して、テンプレートを改造すると以下のようなSpicyファイルが作成できます。ここで、subCommandまでが必要なデータとした場合、最後にtmpというPROTOCOL_FIELDを追加で作成し、subCommand以降のデータをtmpに入れることができます。

module protocol_name;

# Spicyを単独で実行する場合は下の import zeek をコメントアウトする
import zeek;
import spicy;

public type Message = unit {
        header1:    bytes &size=2;
        header2:    bytes &size=1;
        command:    bytes &size=2;
        subCommand: bytes &size=2;
        tmp:        bytes &eod;

        # Spicyを単独で実行する場合は zeek::confirm_protocol(); 部分をコメントアウトする
        on %done { print self; zeek::confirm_protocol();}
};

実行すると、以下の出力が得られます。(Spicyのみ実行したい場合は上記のコードをコメントに従って変更する必要があります)

# 実行結果
~$ printf '\x00\x00\x01\x02\x02\x03\x03\x11\x11\x11\x11\x11\x11\x11' | spicy-driver test.spicy
[$header1=b"\x00\x00", $header2=b"\x01", $command=b"\x02\x02", $subCommand=b"\x03\x03", $tmp=b"\x11\x11\x11\x11\x11\x11\x11"]

4. evtファイルを作成

本節では以下のテンプレートとプロトコル情報(ポート番号・パケットフォーマット定義)・Spicyファイルを利用して、evtファイルの作成方法を紹介します。evtファイルでは解析したいプロトコルを指定します。

protocol analyzer spicy::MYPROTOCOL over UDP:
    parse with MYPROTOCOL::Message,
    port PORT_NUMBER/udp;

import MYPROTOCOL;

on MYPROTOCOL::Message -> event MYPROTOCOL::message($conn, self.PROTOCOL_FIELD1, ...);

例えば、以下のようにプロトコルのポート番号(1111)、プロトコル名(protocol_name)、Zeekに渡したいプロトコルフィールド(header1、header2など)を記述します。

protocol analyzer spicy::protocol_name over UDP:
    parse with protocol_name::Message,
    port 1111/udp;

import protocol_name;

on protocol_name::Message -> event protocol_name::message($conn, self.header1, self.header2, ...);

evtファイルを作成後、コンパイルしてオブジェクトファイルであるhtloファイルを生成します。

~$ spicyz -o test.hlto test.spicy test.evt
~$ # test.hltoが生成される

5. Zeekファイルを作成

最後に、以下のテンプレートを利用して、Zeekファイルの作成方法を紹介します。Zeekファイルでは、Spicyの処理結果をログに出力します。

# Spicy側のパーサーを利用するための宣言
module MYPROTOCOL;

# 実行開始前に必要な変数の宣言(global変数)
export {
    redef enum Log::ID += { LOG };
    
    # この変数の内容はlogに書き出す
    type Info: record {
        ts:     time &log;
        uid:        string &log;
        id:     conn_id &log;
        ## MYPROTOCOL data.
        PROTOCOL_FIELD1:    string &log;
        ## MYPROTOCOL data.
        PROTOCOL_FIELD2:    string &log;
        ## MYPROTOCOL data.
        PROTOCOL_FIELD3:    string &log;
        ...
        ...

        final_block: count &optional;
        done: bool &default=F;
    };

    global log_myprotocol: event(rec: Info);
}

global expected_data_conns: table[addr, port, addr] of Info;

redef record connection += {
    myprotocol: Info &optional;
};

# $pathで出力ファイル名を指定する
event zeek_init() &priority=5
    {
    Log::create_stream(MYPROTOCOL::LOG, [$columns = Info, $ev = log_myprotocol, $path="myprotocol"]);
    }

# logに書き出す時の動作を書く関数
event MYPROTOCOL::message(c: connection, PROTOCOL_FIELD1: string, PROTOCOL_FIELD2: string, PROTOCOL_FIELD3: string, ... )
    {

    local info: Info;
    info$ts  = network_time();
    info$uid = c$uid;
    info$id  = c$id;
    info$PROTOCOL_FIELD1 = PROTOCOL_FIELD1;
    info$PROTOCOL_FIELD2 = PROTOCOL_FIELD2;
    info$PROTOCOL_FIELD3 = PROTOCOL_FIELD3;
    ...
    ...
    c$myprotocol = info;
    
    # info内の変数を全部logに書き出す
    Log::write(MYPROTOCOL::LOG, info);
    }

テンプレートの各部分を解説します。

  • module MYPROTOCOL;:Spciyで作成したパーサーをインポートするために宣言
  • export {…};:Zeekで利用する変数の宣言
    • ログに出力したい項目はここで指定します。
  • event zeek_init() &priority=5 {…}:出力ログのパスを指定
  • event MYPROTOCOL::message(…) {…}:パーサーの処理結果をログに出力
    • ログに出力するタイミングで加工することもできます。

上記のテンプレートと今までの結果を利用し、プロトコル名(protocol_name)、出力したいフィールド(header1など)、出力したいログファイルの名前($path="protocol_name")を書き換えることで、以下のようなZeekファイルを作成できます。

# Spicy側のパーサーを利用するための宣言
module protocol_name;

# 実行開始前に必要な変数の宣言(global変数)
export {
    redef enum Log::ID += { LOG };
    
    # この変数の内容はlogに書き出す
    type Info: record {
        ts:     time &log;
        uid:        string &log;
        id:     conn_id &log;
        ## MYPROTOCOL data.
        header1:    string &log;
        ## MYPROTOCOL data.
        header2:    string &log;

        final_block: count &optional;
        done: bool &default=F;
    };

    global log_protocol_name: event(rec: Info);
}

global expected_data_conns: table[addr, port, addr] of Info;

redef record connection += {
    protocol_name: Info &optional;
};

# $pathで出力ファイル名を指定する
event zeek_init() &priority=5
    {
    Log::create_stream(protocol_name::LOG, [$columns = Info, $ev = log_protocol_name, $path="protocol_name"]);
    }

# logに書き出す時の動作を書く関数
event protocol_name::message(c: connection, header1: string, header2: string )
    {

    local info: Info;
    info$ts  = network_time();
    info$uid = c$uid;
    info$id  = c$id;
    info$header1 = header1;
    info$header2 = header2;
    c$protocol_name = info;
    
    # info内の変数を全部logに書き出す
    Log::write(protocol_name::LOG, info);
    }

6. テスト用Pcapで出力ログの確認

最後にテスト用Pcapを使って出力ログを確認します。

~$ zeek -Cr cclink_ief_basic.pcap test.hlto test.zeek
~$ # protocol_name.logが生成される

生成されたログの中身を確認すると、以下のようになっています。最後の16進数の部分(P\x00 \x00など)は今回出力したい部分です。今回は16進数のデータを出力しただけですが、Zeekファイルを作り込むことで人間が解釈できる文字列として出力もできます。

#separator \x09
#set_separator  ,
#empty_field    (empty)
#unset_field    -
#path   protocol_name
#open   2023-06-12-15-53-06
#fields ts      uid     id.orig_h       id.orig_p       id.resp_h       id.resp_p       header1       header2
#types  time    string  addr    port    addr    port    string  string
1655284124.859924       C3KZkP3l8QTJaS2vg7      172.16.134.128  61450   172.16.134.255  61450   P\x00   \x00
1655284124.953994       CG3NWX3t9Qf1Xrezl5      172.16.134.129  61450   172.16.134.128  61450   \xd0\x00        \x00
1655284125.375608       C3KZkP3l8QTJaS2vg7      172.16.134.128  61450   172.16.134.255  61450   P\x00   \x00
1655284125.484180       CG3NWX3t9Qf1Xrezl5      172.16.134.129  61450   172.16.134.128  61450   \xd0\x00        \x00
1655284125.500485       C3KZkP3l8QTJaS2vg7      172.16.134.128  61450   172.16.134.255  61450   P\x00   \x00
1655284125.610011       CG3NWX3t9Qf1Xrezl5      172.16.134.129  61450   172.16.134.128  61450   \xd0\x00        \x00
...
...

Zeek・Spicyの活用例

ここまで、Zeek, Spicyの使い方まで紹介しました。本章ではイメージを深めるため、OsecTで実装したZeekが対応していないプロトコルの追加と、Zeekの基本ログへの情報追加の2つの例を紹介します。

CC-Linkファミリーへの対応

今回は市場ニーズを踏まえた上で、Zeekが対応していないOTプロトコル(CC-Linkファミリー)に対応しました。具体的なプロトコルはCC-Link IE FieldとCC-Link IE ControlとCC-Link IE Field Basicです。実装したコードは公開しているため、興味がある方は以下のリンクを参照ください。

CC-Link IE Field Basicの出力ログは以下のようになっています。ペイロードの奥深くまで情報を取得することもできますが、今回はパケットの種類(cyclicDataRes、cyclicDataReq)と使っているコマンド(cyclic)をログに出力しました。

#separator \x09
#set_separator  ,
#empty_field    (empty)
#unset_field    -
#path   cclink-ief-basic
#open   2023-05-27-00-52-06
#fields ts      uid     id.orig_h       id.orig_p       id.resp_h       id.resp_p       pdu     cmd     number  ts_end
#types  time    string  addr    port    addr    port    string  string  int     time
1655284124.953994       CIAp8bugKIZRVpAYk       172.16.134.129  61450   172.16.134.128  61450   cyclicDataRes   -       222     1655284149.499713
1655284124.859924       Ckkc3929guO41BnpSa      172.16.134.128  61450   172.16.134.255  61450   cyclicDataReq   cyclic  222     1655284149.392238
#close  2023-05-27-00-52-06

CC-Link IE FieldとCC-Link IE Controlの出力ログは以下のようになっています。ここではパケットの種類(select、scanなど)と種類別の情報(0x0001など)をログに出力しました。今回はテスト用Pcapを自作したため、全て0x0001になっています。

#separator \x09
#set_separator  ,
#empty_field    (empty)
#unset_field    -
#path   cclink-ie
#open   2023-03-15-16-56-36
#fields ts  src_mac dst_mac service pdu_type    cmd node_type   node_id connection_info src_node_number number  ts_end
#types  time    string  string  string  string  string  string  int string  string  int time
1667903833.066101   00:11:11:11:11:11   00:00:00:00:00:01   cclink_ie_control   select  -   -   -   -   0x0001  61  1667903833.134207
1667903833.065821   00:11:11:11:11:11   00:00:00:00:00:01   cclink_ie_control   scan    -   -   -   -   0x0001  48  1667903833.129023
1667903833.064742   00:11:11:11:11:11   00:00:00:00:00:01   cclink_ie_control   connectAck  -   -   -   -   0x0001  61  1667903833.133590
1667903833.065511   00:11:11:11:11:11   00:00:00:00:00:01   cclink_ie_control   connect -   -   -   -   0x0001  62  1667903833.134085
1667903833.067818   00:11:11:11:11:11   00:00:00:00:00:01   cclink_ie_control   nTNTest -   -   -   -   0x0001  53  1667903833.131018
1667903833.068939   00:11:11:11:11:11   00:00:00:00:00:01   cclink_ie_control   dummy   -   -   -   -   0x0001  57  1667903833.133957
1667903833.065083   00:11:11:11:11:11   00:00:00:00:00:01   cclink_ie_control   collect -   -   -   -   0x0001  61  1667903833.133231
1667903833.064936   00:11:11:11:11:11   00:00:00:00:00:01   cclink_ie_control   launch  -   -   -   -   0x0001  40  1667903833.132990
1667903833.066240   00:11:11:11:11:11   00:00:00:00:00:01   cclink_ie_control   token   -   -   -   -   0x0001  57  1667903833.133351
#close  2023-03-15-16-56-36

Zeekの基本ログへの情報追加

DHCPv4はZeekが対応しているプロトコルですが、出力されるログの情報量に過不足がありました。そのため、Zeek, Spicyを利用して必要のない情報を削除・必要な情報を追加しました。実装したコードは公開しているため、興味がある方はMYDHCPを参照ください。

デフォルトの出力ログは以下のようになっています。

#separator \x09
#set_separator  ,
#empty_field    (empty)
#unset_field    -
#path   dhcp
#open   2023-06-16-18-23-59
#fields ts      uids    client_addr     server_addr     mac     host_name       client_fqdn     domain  requested_addr  assigned_addr   lease_time      client_message  server_message  msg_types       duration
#types  time    set[string]     addr    addr    string  string  string  string  addr    addr    interval        string  string  vector[string]  interval
1624301983.013489       Cah1vz3icJOQ3kUtk4,C6uWeo2ldOmwsjKUj7   10.0.2.15       10.0.2.2        00:00:20:b0:60:b0       kali    -       -       10.0.2.15       10.0.2.15       86400.000000    -       -       REQUEST,ACK     0.000309
#close  2023-06-16-18-23-59

そして、Zeek, Spicyによって改造後のログは以下のように変更しました。本来のログのMACアドレスとタイムスタンプをそのまま利用し、SrcIP,Hostname,Parameter,ListClassIdを追加しました。

#separator \x09
#set_separator  ,
#empty_field    (empty)
#unset_field    -
#path   mydhcp
#open   2023-06-16-18-23-59
#fields ts      SrcIP   SrcMAC  Hostname        ParameterList   ClassId
#types  time    addr    string  string  vector[count]   string
1624301983.013489       0.0.0.0 00:00:20:b0:60:b0       kali    1,2,6,12,15,26,28,121,3,33,40,41,42,119,249,252,17      -
#close  2023-06-16-18-23-59

おわりに

今回はZeekとSpicyの概要及び使い方・利用例について紹介しました。この記事を参考に、プロトコルのパーサーを実装、ログ出力までできるようになれば幸いです。そして、Zeek, Spicyの活用例の章で紹介したスクリプトはすでにOsecTへ実装しリリース済です。OsecTもこれからZeek, Spicyを利用し、対応プロトコルの拡張などを続けていくので、興味を持たれた方はぜひご連絡ください。

© NTT Communications Corporation All Rights Reserved.