ある研究者の手記

セキュリティとかゲームとかプログラミングとかそのへん

IPアドレスやドメイン名のブラックリストを取得・管理するGo言語のライブラリを作った

だいたいタイトルの通りなのですが、色々なサイトで公開されているブラックリストを取得して、トラフィックログなどとの照合に使うことを目的とした badman というライブラリを作成しました。Blacklisted Address and Dmain name Manager です。

github.com

少し前だとネットワークのセキュリティ監視をするといったら、だいたいsnortのようなネットワーク型IDSを使うことが多かったと思います。しかし近年だと殆どの通信がHTTPS化されているということもあり、現状だとネットワーク型IDSはあまり有効な手法と言えなくなってしましました。代わりにできることの一つとしては、ネットワークのフロー情報(IPアドレス、ポート番号、プロトコルややりとりされたデータサイズなど)やDNSの問合せ+応答のログを蓄積しておいて、マルウェアのCommand & Controlサーバや詐欺サイトとの通信が発生していなかったのかを検証することです。この方法だとWebなどの通信が暗号化されていたとしても、不審な通信があったかを発見できます。ただ、継続的に発生する通信ログの検証をするたびにブラックリスト提供サイトからリストをダウンロードしてくると、提供サイトや自分のネットワークにも負荷をかけてしまいます。そこで一度ダウンロードしたものを中長期的に使い回せるようにと考えて、このライブラリを作成しました。

なお、ライブラリになっているのはログの入力方法やログの内容が環境によって大きく異ると考えられたからです。具体的なアーキテクチャ例は後で説明します。

どう使うものか

Go言語に親しい方だとだいたい以下のコードを見てもらえるとイメージがつくのではと思います。

package main

import (
    "bufio"
    "log"
    "os"

    "github.com/m-mizutani/badman"
    "github.com/m-mizutani/badman/source"
)

func main() {
    man := badman.New()

    if err := man.Download(source.DefaultSet); err != nil {
        log.Fatal("Fail to download:", err)
    }

    // ipaddrs_in_traffic_logs.txt contains IP address line by line
    fd, err := os.Open("ipaddrs_in_traffic_logs.txt")
    if err != nil {
        log.Fatal("Fail to open a file:", err)
    }
    defer fd.Close()

    scanner := bufio.NewScanner(fd)
    for scanner.Scan() {
        entities, err := man.Lookup(scanner.Text())
        if err != nil {
            log.Fatal("Fail to lookup:", err)
        }

        if len(entities) > 0 {
            log.Printf("Matched %s in %s list (reason: %s)\n",
                entities[0].Name, entities[0].Src, entities[0].Reason)
        }
    }
}

このコードでやっていることは以下のとおりです。

  1. ブラックリストを提供しているサイトから複数のリストをダウンロードして自分用のレポジトリを作る
  2. IPアドレスのリストが含まれている ipaddrs_in_traffic_logs.txt を開いて1行(1 IPアドレス)ずつとりだす
  3. とりだしたIPアドレスがレポジトリに含まれていたか検証する

このサンプルだと1.のブラックリストをダウンロードしてくるところは毎回実行されてしまいますが、本来はダウンロードしたものをローカルに一度保存し、それを使い回すということを想定しています。保存する方法は A) シリアライズされたデータをファイルに書き込んで、次回以降はそのファイルを読み込む、 B) 永続性のあるデータストア(現在パッケージに取り込んでいるのはAWSのDynamoDBのみ)をバックエンドに使う、の2通りがあります。詳しい使い方についてはレポジトリの README を御覧ください。

アーキテクチャ

AWSで使うことを前提に、2つのアーキテクチャ例を紹介します。それぞれ badman をライブラリとして使って独自にプログラムを作り、動作させることを想定しています。

サーバレスで使う場合

f:id:mztnex:20200103151337p:plain

このケースでは、2つのLambda関数を利用しています。 1番目(左側)の関数はブラックリストを定期的に取得し、シリアライズされたブラックリストデータをS3に保存します。 2番目(右側)のLambda関数は、トラフィックログファイルがS3にアップロードされた際のObjectCreatedイベントによって呼び出されます。 そしてLambda関数はシリアライズされたブラックリストデータとログファイルの両方をダウンロードし、トラフィックログのIPアドレスブラックリスト内に存在するかどうかを確認します。 存在する場合、ラムダはSlackなどの通信ツールを介して管理者に通知します。

このアーキテクチャの利点はスケーラビリティと価格の柔軟性です。Lambdaは(上限はあるものの)トラフィックログの到着件数の増減に応じてスケールアウト・スケールインするのでログ量に応じてリソースが足りなくなったり、逆に過剰にリソースを割り当てたり、というのを心配する必要がありません。さらに、ログ量が少なければ料金も自動的に少なくなるので、あまりコストをかけたくない小さい規模のネットワークの監視をするなどのケースでおすすめです。

一方で、不利な点は遅延です。ログの流量によりますが、S3にアップロードする前にログをある程度まとめることになるのでそのバッファリングをしている時間は遅延が発生します。経験則だと概ね数分〜十数分になります。もしリアルタイム性が必要なら以下のサーバ型モデルを採用したほうが良いかもしれません。

サーバ上で使う場合

f:id:mztnex:20200103152259p:plain

このアーキテクチャでは常時稼働しているプログラムをホスト上(この例ではAWS EC2)で動かします。主な利点は、リアルタイム性のためのストリーム処理です。 先述したとおり、サーバレスモデルではある程度ログをまとめてからS3にアップロードする必要があるため、ログの発生から処理完了までに数分の遅延が発生します。常時稼働させるサーバプログラムを使ってデータを絶えず流し込むことで、遅延を最小化します。

このプログラムはfluentdのforward用のサービス*1 を動かしている事を想定しており、fluentd経由でトラフィックログを受信します。 その後、badmanを使用して、ブラックリストに含まれているIPアドレストラフィックログを確認します。 ブラックリストは定期的に更新されることを想定しています。プログラムを実行しているホスト(この場合はEC2)がクラッシュしても回復できるように、DynamoDBをデータ保存用のレポジトリとして使用します。

このアーキテクチャの欠点はリソースの割当が難しいことです。おそらくログの流量がボトルネックになるので、その最大値に合わせてホストのリソースを設定する必要があります。さらにトラブルやいつもと違うイベントでログの流量が大幅に増える可能性がある場合は、それについても気をつける必要があります。これを解決するためには自動的なスケーリングなどの仕組みと組み合わせて使う必要があります。

利用してもらうにあたっての注意

ブラックリストを提供しているサイトはそれぞれ異なるポリシーを持っています。そのため、環境や組織によっては利用条件を満たさない可能性があるため注意してください。使えるブラックリスト提供サイトを限定するために、利用者が自分で使うサイトを選択することもできるようになっています。また、自分で badman が提供しているInterfaceを満たす構造体を実装すれば、独自のサイトや非公開のデータストアからデータを取り込むことも可能です。

簡単にですが各サイトのポリシーについてもREADMEにまとめてありますので、そちらもご参照下さい。

ちなみに

昔、似たような mdstore というツールを実は作っていたんですが、今回はGoでえいやと作り直しました。理由としてはLambdaで動かしたかったので、1) Lambda+マネージドサービスだけでよりステートレスに動かす仕組みを作りたかった、 2) 概ねnodejsより速く動くGoにすることで、実行時間を短くして安く済ませたかった、などです。