ある研究者の手記

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

クラウドネイティブなハニーポットをAWS上に作ってみた話

TL;DR

  • AWSのマネージドサービスを活用して低インタラクション型のハニーポット環境を作った
  • コストも月々約$15で運用可能
  • コマンド3回ぐらいで誰でもデプロイできるようになっているので興味があれば使ってみてくれよな

f:id:mztnex:20190210154448p:plain

背景

という感じで昔クラウド上で運用していたハニーポットのことをふと思い出したのですが、仕事で多少AWSのサービスを理解した今だったらもうちょっとまともに実装できそうだよなぁ、実装するならインスタンスで完結するんじゃなくてクラウドのマネージドサービスちゃんと使って消耗しない作りにしたいよなぁ、と考えているうちに気持ちが高まってきたのでやりました。

また真面目な話としては、自分も情報セキュリティを生業としているので自分自身で脅威情報を収集する手段を持っていたかった、というのがあります。警視庁による定点観測NICTによる監視報告などから、どのようなポートにどういう攻撃が来ているか、ということを知ることができますが、どのような環境で観測されているかという情報は非公開です。肌感覚ですが、こういったアクセスは対象となるネットワークによって異なる傾向があるように思えるため、自分で把握できる環境の情報をえる手段があると良いなと思っていました。

ちなみに「クラウドネイティブ」という言葉の意味は誰が言っていることが正しいのかはよくわかりませんが、「極力マネージドサービスを使って構築する」ぐらいの気持ちで使っています。あしからず。

今回扱うハニーポットの仕組み

ハニーポットと一口に言っても、特定のWebサービスの振りをしたりするものや、仮想環境をまるっと用意して侵入した攻撃者がどういう行動をするか観測するものなど様々なのですが、今回は低インタラクション型なハニーポットを実装しました。やっていることは至極単純で、TCPのsynパケットが来た場合に偽装syn+ackパケットのみを打ち返し、攻撃者側のプログラムにTCPがEstablishしたと誤認させます。そして、その後に続くパケットを収集することで攻撃者がどんな通信を投げてくるのか観測します。

f:id:mztnex:20190210171416p:plain

以前はC++でこれを実装していたのですが、近代的な機能を使おうとするといろいろ面倒というのもあったのでこれを機にGoで書き直しました。lurkerという名前で実装しており、gopacket というライブラリでパケットキャプチャ、および偽装パケットの生成と送信をしています。前述したとおり低インタラクション型なので、攻撃者の挙動を深追いすることはできないのですが、以下のようなメリットがあります。

  • スケーラビリティが高い: 主な動作が 1) SYNパケットの応答を打ち返す、 2) パケットをキャプチャして保存する、という2つのみなので非常に軽快に動作します。今回はAWSの構成の制約上やっていませんが、大昔に大学の研究室で数百単位のIPアドレスを監視していた際も同じようなアーキテクチャで問題なく可動していました。
  • 侵入後の攻撃者の活動を心配する必要がない: 心配する必要がないというより、心配すらできない、が正しいかもしれません。実際に侵入させる系の高インタラクション型のハニーポットだと、侵入された後に外部に対して攻撃などの影響を及ぼさないようにするにはどうしたらいいか、ということを真剣に考える必要があります。場合によっては不正アクセスの片棒をかつぐ事になり厳しい怒られが発生する場合もありつつ、さりとてあまり行動(特に外部との通信)を制限しすぎると何も情報がえられない、という難しい問題があります。ですが、この仕組ではsyn+ackを打ち返すだけしかできないので、そのあたりの心配をする必要はありません。
  • 全ポートを監視できる: 通常ハニーポットを運用する場合、Webサービスを偽装する場合はport 80や443、sshを偽装する場合はport 22など、対象となるサービスによって監視するポートが自ずと狭まるものです。しかしこの仕組ではとりあえずどのTCPポートでも受信したパケットに対してはすべからく打ち返すので、全ポートに対するデータが収集できます。これの利点は、攻撃者がそもそもどこのポートに対してスキャンしようとしているかが分かることです。例えばsshTCPセッション確立後にProtocol version exchangeのパケット(SSH-2.0-OpenSSH_5.3 みたいなやつ)が送信されます。これを見ることでsshは標準ポートの22だけでなく、1022、2222、8022といったポートもスキャンされているという情報を知ることができます。

設計方針

なるべくマネージドサービスを使う

近年においては常識ですよ、と言われてしまうかもしれませんが、すぐに対象のサービスが使えるという以外にも一応以下の効果を期待したということを記しておきます。

  • 持続的な運用を丸投げできる: 完全なメンテナンスフリーというわけにはいきませんが、やはり日々の運用の部分を肩代わりしてもらえるというのは大きいです。特に今回作るものは毎日ガッツリ使うというよりは、日々データをためておいて必要なときに確認する、というユースケースを想定しています。そうなると自分で動かしていたサービスがいつの間にか落ちていて、気づいたら全くデータ取れていなかった…という悲しいことも起きがちです。もちろんちゃんと監視の仕組みを入れてアラート飛ばすなどすればいいのでしょうが、その対応の手間も安くはありません。あと、雑に自前でサービスを立ち上げるためにインスタンスを上げっぱなしにしていると中のパッケージが古くなって脆弱性を放置して…などということも起こりがちです。雑多な用途で作るサービスだからこそ、そのあたりの負荷をなるべく減らしたいという気持ちです。
  • 監視の仕組みが最初から組み込まれている: こちらも同様に自前で頑張れなくないですが、既存のマネージドサービスだと最初から豊富な監視の機能が提供されています。メッセージ配信サービス(例 Amazon Simple Notification Service)であれば流量など、サーバレス実行環境(例 AWS Lambda)であれば実行回数や実行にかかった時間などをCloudWatch metricsで確認したり、CloudWatch alarmを設定し特定の条件で通知を飛ばしたり、といったことが容易に実現できます(今回そこまでやってないけど)こういったものがデフォルトで提供されているのであまり自分で頑張らなくて良い、というのもマネージドサービスの利点です。

データ取得と分析の処理を分離する

以前に似たようなアーキテクチャを考えていたときは、基本的にデータの収集と分析を密結合させて、最終的な結果だけをfluentdに流す、という方法をとっていました。言わずもがなですが、その構成だと分析方法の追加や変更、削除をするたびにデータ収集の部分をいじる必要がでてきてしまい、うかつに触れなくなってしまいます。また、データ収集と分析の処理を同じ環境で実施しようとすることで、負荷の重い方がリソース(CPUやメモリ)の制約を受けやすくなってしまう、ということもあります。具体的には後述しますが、そういった理由からマネージドサービスなストレージであるSimple Storage Service (S3) にまずデータ(今回は通信を記録したpcapファイル)を投げ込み、その通信に何が含まれていたか?という処理は後ろ側に任せる、というような構成をとりました。

実装

というわけで実際の実装に関する解説をしたいと思います。実装は3パートに分かれていて、それぞれ sensor、backend、output と呼んでいます。それぞれCloudFormationをベースとして実装しており、実際のデプロイ方法についてはデプロイの節をご覧ください。

sensorパート

f:id:mztnex:20190211103017p:plain

ハニーポット本体が設置してあるパートです。さすがに「TCPのSYNパケットを受けて偽装したSYN+ACKを返す」という処理をマネージドサービスで実現するのは困難だったので、そこはEC2インスタンスを使いました。ハニーポットには管理用と監視用、2つのネットワークインターフェースを接続しています。EC2は自前で2つ以上のネットワークインターフェースを接続すると標準で割り当てられるパブリックIPアドレスが使えなくなってしまうため、Elastic IP Address を2つ確保し、それぞれのネットワークインターフェースに割り当てて*1います。これらは全てCloudFormationを使ってデプロイ可能なようにしています。( CloudFormationのテンプレート

テンプレートを見るとわかるかと思いますが、(かなり雑なのでちょっと恥ずかしいですが)ハニーポットのソフトウェア本体であるlurkerのバイナリをダウンロードして /etc/rc.local に起動スクリプトを仕込み、実行するところまでを記述してあります。なのでこのテンプレートをデプロイするだけで、AWSに対し外部からどのような攻撃やスキャンがされているのか、というデータを収集し始めることができます。

このホスト上で動作するlurkerの仕事は非常に単純です。

  • TCPのSYNパケットを観測したらSYN+ACKパケットを打ち返す
  • TCPUDPのフロー(IPアドレス+ポート番号の組み合わせ)ごとに観測したパケットを保持しておく
  • あるフローに対して1分以上通信が発生しなかったらそのフローのパケットデータをS3バケットにpcap形式で保存する

先述したとおり、gopacket というライブラリがかなりいろいろやってくれて、偽装パケットを作ったり、パケットをキャプチャしたり、pcapファイル形式にデータを変換してくれたりと、だいたいこれに乗っかってやりたいことができました。データ出力先は思い切って(というか面倒だったので)S3バケットに出力するようにしか作っていません。出力されたpcapファイルは以下のような感じで蓄積されています。現在運用している感触だと、だいたい1日あたり10000個弱、合計10MB弱のpcapファイルが生成されています。

f:id:mztnex:20190211150811p:plain

backendパート

f:id:mztnex:20190211103618p:plain

ここは単純に2つのマネージドサービスを展開しているのみで、あまり説明することはありません。sensorがデータをアップロードする先のS3、およびイベントを通知するための Simple Notification Service (SNS) が、このbackendパートになります。S3にファイルがアップロード(生成)されると、そのイベント通知がSNSに飛び、さらにそこからoutputパートにあるLambdaが呼び出されます。

強いていうとS3にはイベントが発生した際に直接Lambdaを起動する機能もあるので、SNSを使わないでもこの構成は実現可能です。それでもSNSを利用している理由としては、S3からのLambdaの直接呼び出しは1つの宛先しか設定できないために分析したい処理が増えたときに困る、そしてbackendパートとoutputパートをより疎結合にできる、などが挙げられます。

outputパート

f:id:mztnex:20190211103629p:plain

最後が収集したデータをもとに、なんらかの出力をするoutputパートになります。backendパートにおいてS3に生成されたファイルがSNSに通知され、それをトリガーにしてLambdaが起動します。Lambdaには生成されたファイルそのもののデータではなく、どのバケット、どのキーに対してファイルが生成されたかという情報が伝わるだけなので、LambdaがS3バケットにアクセスして対象ファイルをダウンロードしてpcapファイルの中身を分析する、という一般的なS3 + Lambdaの構成になっています。SNSで複数のLambdaに通知を飛ばせるため複数の分析用Lambdaを配置することもできますが、今回はサンプルとして1つだけ「pcapファイルの中身を荒くまとめてCloudWatch Logsに投げ込む」という例をご紹介します。

今回、分析用に使うLambdaは python + dpkt で実装しました。ハニーポットのソフトウェアであるlurkerと同じくGoで実装しても良かったのですが、gopacketがlibpcapに依存しておりLambda上でC libraryを使うのは骨が折れそうだったので、今回はネイティブにpcap読み取り機能をもつ(libpcapに依存しない)dpktを利用しました。Lambdaでやっている処理は以下のとおりです。

  1. S3からpcapファイルをダウンロードして読み取る
  2. 送信元IPアドレスや宛先ポート番号を取得しTCPのデータセグメントを再構築する
  3. CloudWatch Logsにデータを送信する

TCPはデータを再送などできる都合上、重複したデータをキャプチャしている可能性があるためそれを排除する必要があります。今回は特にsyn+ackだけ返してその後の通信を全くしない、というツールの特性上、通常のTCP/IPスタックであれば必ずデータの再送が発生します。今回は各pcapファイルに1つのフローしか入っていない、という条件なのでわりと雑多にストリームの再構成のコードを書いてみました(何か間違っていたらこっそり教えてください)これらの情報を取得した後、以下のようなJSONを生成してCloudWatch Logsに投げ込みます。

{
  "init_ts": 1549870119.960277,
  "last_ts": 1549870151.089438,
  "src_addr": "193.201.224.***",
  "dst_port": 22,
  "payload": "U1NILTIuMC1XaW5TQ1BfcmVsZWFzZV81LjcuNQ0K",
  "readable": "SSH-2.0-WinSCP_release_5.7.5\r",
  "s3path": "s3://***/pcap/2019/02/11/07/1549870119_193.201.224.***_172.30.2.***_23300_22.pcap"
}

CloudWatch Logsに投入したJSON形式のログは自動的にパースされ、CloudWatch insightを使って実用的な検索などができるようになっています。

デプロイ

先述したとおり、ご紹介した sensor、backend、outputのパートは全てCloudFormationで記述されていますので、awsコマンドだけでバーンと展開できるようになっています。

Prerequisite

以下のツールが必要になります。pythonは3系、awscliはここ最近のものなら動くんじゃないかなと思いますが、検証済みなのは以下の条件です。また、言わずもがなですがawscliはAPIキーなどのcredentialをセットアップする必要があります(詳しくはこちら

  • python >= 3.7
  • awscli >= 1.14.40

0) テンプレート等の取得

普通にgitでtemplateなどのセットを取ってきます。

$ git clone https://github.com/m-mizutani/aws-honeypot-templates.git
$ cd aws-honeypot-templates

1) backendパートのデプロイ

まず最初にbackendパートを設定します。これはsensorパートが送り先のS3バケットの名前を知っておく必要があること、そしてoutputパートがsubscribeするSNSのtopic名を知らないといけない、という依存関係によります。必要なパラメータを用意しコマンドを実行することで、S3バケットSNSのリソースが生成され必要な設定が変更されます。

必要なパラメータ

  • backend_stack_name: backendパートのスタック名です。任意の名前で問題ありません。
$ aws cloudformation deploy \
    --template-file backend.yml \
    --stack-name <backend_stack_name> \
    --capabilities CAPABILITY_IAM

デプロイが完了したら、以下のコマンドでS3バケットの名前を知ることができます。AWSのWebコンソールなどから参照しても問題ありません。

$ aws cloudformation describe-stack-resources --stack-name <backend_stack_name> | jq '.StackResources[] | select(.LogicalResourceId == "DataStore") | .PhysicalResourceId ' -r

2) sensorパートのデプロイ

次にsensorパートを同様にCloudFormationのテンプレートを使ってデプロイします。こちらは必要なパラメータが少々多くなります。

必要なパラメータ

  • sensor_stack_name: sensorパートのスタック名です。任意の名前で問題ありません(ただし他のスタック名とかぶらないように)
  • VpcId: センサーがデプロイされるVPCのIDです。 (例 vpc-1234xxxx) VPCおよびsubnetは事前に準備しておく必要があります
  • SubnetId: センサーが接続するsubnetのIDです (例 subnet-1234xxxx)
  • KeyName: センサーにセットするSSHキーの名前です。基本的にトラブルシュート用です (例 default)
  • S3Bucket: backendパートで作成したS3のバケット名を指定してください
$ aws cloudformation deploy \
    --template-file sensor.yml \
    --stack-name <sensor_stack_name> \
    --capabilities CAPABILITY_IAM \
    --parameter-overrides \
    VpcId=<VpcId> \
    SubnetId=<SubnetId> \
    KeyName=<KeyName> \
    S3Bucket=<S3Bucket>

3) outputパートのデプロイ

outputパートのテンプレート、およびLambdaで使うコードは output/cwlogs 以下に入っています。(他にも何種類か用意したかった気持ちのあるディレクトリ構成だ、というところだけお察しください)こちらはいくつかの処理が必要なので deploy.sh というスクリプトを実行しています。やっていることは中身を見ればわかりますが、1) pythonのパッケージインストール、2) pythonのコードをzipで固める、3) backendパートのリソース名を取得する、4) 実際にデプロイ、という流れになっています。

必要なパラメータ

  • region: AWS のリージョンを指定してください(backend、sensorと同じリージョンにしてください)もしbackend、sensorデプロイ時に特にリージョン指定していなければ aws configure get region で確認できます (例 ap-northeast-1)
  • backend_stack_name: backend パートをデプロイしたスタック名を指定してください
  • output_stack_name: outputパートのスタック名です。任意の名前で問題ありません(ただし他のスタック名とかぶらないように)
  • code_s3_bucket: Lambdaのコードを置くためのS3バケット名になります。backendパートで用意したS3バケットとは別のものが良いと思います(同じでだめなことはないけど)
  • code_s3_prefix: Lambdaのコードを置くためのS3キーのprefixになります。自動的に "/" が末尾に追加されるので、例えば functions とだけ指定してください。(自分で末尾に / をつけると空のディレクトリ名が作成さてしまいます)
$ cd output/cwlogs/
$ ./deploy.sh <region> <backend_stack_name> <output_stack_name> <code_s3_bucket> <code_s3_prefix>

CloudWatch Insightでキャプチャしたログを見てみる

ここまできたらデータ収集、データの蓄積、そして分析結果が一気通貫で動いている状態になっているはずです。CloudWatch insightで結果を確認してみましょう。東京リージョン(ap-northeast-1)を利用しているなら以下のリンクでコンソールが開くと思います。

https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#logs-insights:

コンソールを開くと以下のような画面になっているかと思います。 /honeypot/lurker-cwlogs-output となっているところがLogGroupの選択になります。/honeypot/<outputパートstack名> というLogGroupが作成されているはずなので、自分の環境にあった名前を選んでください。

f:id:mztnex:20190211165509p:plain

とりあえず「クエリの実行」をクリックしてもなんらかの出力は出てくると思いますが、例えば以下のようなクエリがわかりやすいかと思います。

fields @timestamp, src_addr, dst_port, readable, strlen(readable) as len
| filter len > 0
| sort @timestamp desc
| limit 20

クエリの意味としては、fields で項目を選択しつつ readable フィールドの文字列を取得して、 filter で文字列が0より大きいものを抽出しています。観測してみると、synパケットだけ飛んできていてペイロードデータが送られてきていないトラフィック(いわゆる普通のSYNポートスキャン)もそれなりにあるので、それを避けるためのクエリです。例として私の環境では以下のような出力がでてきました。

f:id:mztnex:20190211170026p:plain

どういうものが見えるのか?

この仕組を動かし始めてまだ2〜3日といったところですが、いくつか面白い物が見えていたので軽くご紹介します。画像は左から送信元IPアドレス、宛先ポート番号、そして送信されたペイロードとなります。

Hadoop YARN ResourceManager への攻撃

f:id:mztnex:20190211180853p:plain

現在観測している範囲だと、データが送信されている通信の半分以上がこの通信です。S-Owlさんのブログ記事によるとHadoop YARN ResourceManagerに任意のコードを実行できる脆弱性があり、これを探し回っている通信と見られます。

SSHへのアクセス

f:id:mztnex:20190211181300p:plain

これ上側は全部同じIPアドレスからきているのですが、protocol version exchangeで全部違ったクライアントを騙ってアクセスしてきていました。クライアントの種類によってアクセスを許可・遮断するようなツールやサーバがあったりすんですかね…。

バックドアらしきものへのアクセス

f:id:mztnex:20190211181535p:plain

ちょっと見づらいのですが、 /xw1.php というパスに対して h=die(@md5(F3bru4ry)); というデータをPOSTで送信してきています。他にも /xw.php/xx.php といったパスに対して同様の形式っぽいデータが送られているのを確認しました。具体的になんのツールなのかなどはわかっていませんが、パス名のパターンから考えるに攻撃者が設置したバックドアなどではないかと推察されます。

コスト

さて、冒頭にも書いていますが実際にこれを運用するとどのくらいのコストがかかるか、というのを計算してみました。前提条件は以下の通りで実測値をもとにしています。環境ごとに変化する可能性はありますが、大幅にずれることはないんじゃないかなと思います。(2019年2月現在の東京リージョンの価格情報をもとに計算しています)

  • 1日あたりに生成されるpcapファイルの数:10000個
  • 1日あたりの生成されるpcapファイルのサイズ合計:10MB
  • 1時間あたりのSNSに流れるデータサイズ:300KB
  • Lambda の1時間あたりの実行時間合計:350秒
  • S3のデータ保持期間:1年

f:id:mztnex:20190211153113p:plain

なにか抜けてる要素があるかもしれませんが、支配的なのはEC2インスタンスの使用料+EIPの使用料なのでまあそんなにズレはないかなと思います。ということで、月額約$15ほどでこの仕組を運用できることがわかりました。これは無料枠を使っていない前提なので、普段AWS上のリソースを使っていないアカウントならさらにお安くなります。価格帯的に高いか安いかは人によるでしょうが、趣味で動かすのであれば高すぎるということはないんじゃないかな、と思う次第です。

今後の課題

  • Elastic IP addressを自動更新するようにする : このような定点観測型のハニーポットの弱点はIPアドレスが固定だと攻撃者に気づかれて、そもそもアクセスを敬遠される、という恐れがあります。ただ、これはAWSを利用することで解決可能で、定期的にEIPをつけかえれば(同じ観測点ではないものの)攻撃者に観測点を常に特定されている、という状況を回避できます。一方でこの仕組を作ろうとするとCloudFormationと相性があまり良くない、という問題があります。スタックを削除する際に付け替えるために新たに取得したアドレスをちゃんと片付けられるようにしないとならず、それについてはちょっと頭を悩ませるところです
  • 使用するElastic IP addressの数を減らすAWS ではデフォルトでEIPの利用がアカウントごとに5個までと制限されています。この仕組は1つのセンサーをデプロイするだけで2つも消費してしまうため、最大で2つまでしか利用することができなくなっています。このことからEIPを1つだけで運用する、もしくはEC2 インスタンスに標準で付与されるIPアドレスを利用してうまくできないか、というのは検討の余地があるかなという状況です。
  • 自分のパケットがデータに含まれない問題をどうにかする : gopacket非常に便利ではあるのですが、どうも自分自身が射出したパケットはキャプチャできない、という構造になっているようです。なので現在収集されているpcapにはsyn+ackパケットが含まれておらず、pcapを読み込むツールによっては期待された動作をしない懸念があります。これはTCPの応答プロセスとキャプチャのプロセスを分けるなどしてうまくできないかなとは考えています。

*1:本当は1つのElastic IP Addressのみで運用したいのですが、詳しくは今後の課題に後述