ある研究者の手記

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

AWSにおけるお手軽セキュリティ監視運用のはじめかた 2019(Cookpad Tech Bookより)

はじめに

  • この文章は技術書典7で頒布されたCookpad Tech Bookに寄稿させてもらったものの転載になります。基本的に加筆などはしていませんが、一点だけSecurityHubに関する記述に間違いがあった(自動通知の機能がない、という誤った情報を記載していた)のでその点だけ修正しています。
  • 御存知の通り、2019年12月初旬に開催されたAWS re:Invent 2019において色々な新しいサービスが発表されました。なかでもAmazon DetectiveAmazon Fraud Detectionはセキュリティ関連サービスの中で注目すべき存在になっており、今後のAWSセキュリティ監視の中で重要なポジションになる可能性があります。この文書は2019年9月ごろ書かれたものなので当然それらの内容には触れていませんが、それについてはまたいずれ別の形でアウトプットしたいと考えています。

クラウドの時代になってもセキュリティのやっていきは必要です。クラウド上だと多くのマネージドサービスが提供されているためオンプレミスの環境よりもセキュリティの機能を実現しやすい側面はありますが、それでもまだすべてを頼り切ってよいほど進化しているとはいえないのが現状です。やらないといけないセキュリティの機能はさまざまですが、ここではAWS、およびセキュリティ監視(セキュリティ被害の発生の兆候を検知し分析すること)に焦点を当てて、マネージドサービスを活用したセキュリティ監視の小さなはじめかたについて解説したいと思います。また、マネージドサービスの初期設定方法についての解説は世の中に多くありますが、継続的に使っていくためにはどうすればいいか?ということを説明したものはあまりないため、ここではそういった運用に焦点をあてて説明します。

セキュリティ監視

この文章における「セキュリティ監視」とは「自分達が運用しているシステムやサービスに発生しうるインシデントの兆候を検知し、それを分析して監視対象の環境に影響があるかを判断すること」とします。 インシデントとはある人物による作為的な攻撃によって、システムやサービスに何かしら変化が発生し、それによってビジネスや仕事に悪い影響が発生することとします。 わかりやすい例として、業務用PCがマルウェアに感染してしまったり、サービスに侵入されてしまう、という状況を想像してもらえるとよいかと思います。

侵入口を潰したり攻撃そのものを遮断したりする「防御」とは異なり、何かが起きた際にそれを迅速に発見し対応できるようにすることを目的としています。

なぜセキュリティ監視するのか

はじめにそもそもなぜセキュリティ監視が必要なのか?という理由について簡単に説明したいと思います。

  • すべての攻撃を完璧に防ぐのは不可能である: 「監視などするまえにそもそも防いでしまえばいいのでは」という説はありますが、システムに侵入しようとする攻撃の手法やそれをとりまく環境は日々変化し続けており、それに完璧に追従するのは困難です。特に攻撃に利用される脆弱性は次々と新しいものが発見されており、長年使われていたオープンソースのソフトウェアでも例外ではありません。攻撃を防ぐためにはどのような攻撃がくるかということをあらかじめ知っている必要があり、日々状況が更新されていくような環境では防御だけにすべてを頼るのは厳しいといえます。
  • 防御の対策は利便性を阻害しがちである: それでも防御を重要視していこうとすると、今度はユーザーの利便性が阻害されてしまいます。防御とは攻撃と疑われる行為を発見したらそれを止める対策です。防御でなるべく多くの攻撃を止めようとすると、ちょっとでも不審な動きがあればその動きを遮断するような方向に調整しなければなりません。それによって正規のユーザーによる正常な動作も不審とみなされてしまい(これを False Positive と呼びます)システムやサービスへのアクセスを止められてしまいます。極端な例として、パスワードを一度間違えたら1日はアクセスできなくなってしまうサービスを想像してみてください。うかつなパスワードを設定していても突破される可能性は低くなりますが、本来のユーザーによる正規のアクセスも妨げてしまいます。もちろんこのような措置が必要な場合もありますが、多くの場合は利便性とのバランスを考えなければなりません。

以上の理由により、防御となる対策を入れることはもちろん重要なのですが、防御だけですべての攻撃から守ろうとするのはあまり現実的ではありません。監視と併用することによって利便性、安全性、コストのバランスのとれたセキュリティを実現していくことができるようになります。

セキュリティ監視を構成する「検知」と「分析」(と「対応」)

セキュリティ監視というプロセスは主に「検知」と「分析」、そして「対応」という3つのフェイズからなっています。これらはそれぞれがキッチリと分離しているわけではなく、検知と分析が被る部分、分析と対応が被る部分がそれぞれあり、互いに影響しあっていると考えてもらえればと思います。

f:id:mztnex:20191223202935p:plain
aa

セキュリティの製品やサービスでは検知に特化したもの、分析に特化したもの、両方の性質を兼ね備えたものなど、いろいろな種類のものがあります。ただ、今回は特に検知と分析がそれぞれわかれたサービスについて説明します。

用語の定義

  • インシデント : ある人物による作為的な攻撃によって、システムやサービスに何かしら変化が発生し、それによってビジネスや仕事に悪い影響が発生すること
  • イベント : サービスやシステム内で発生する事象を一括にした呼び方。Webサービスへのアクセスからプロセスの起動など、粒度はさまざま
  • アラート : セキュリティ監視のためのプロダクトやサービスが、インシデントの可能性があるイベントを発見し、それについて発報したもの
  • ログ : イベントとして発生した事象の詳細を記録として残したもの

f:id:mztnex:20191223203132p:plain
セキュリティ監視の説明に出てくる要素の関係性。抽象化しているためこのモデルに当てはまらないものもあるが、おおよその関係性ということでご容赦願いたい

検知

監視している対象のサービスやシステム内で発生したイベントの情報から、セキュリティ上問題がある事象をみつけだすことを「検知」とします。たとえばアンチウィルスソフトウェアはディスクに保存されたファイルの内容からマルウェアを検知するというのが分かりやすい事例かと思います。他にもイベントが記録として残ったログを集めて分析し、不審な行動を探し出すSIEM(Security Information and Event Manager)も検知の機能があるといえます。また、機械ではなく人間がログや現在進行中のイベントなどをみて根性で怪しい現象に気づく、というのも検知にあたります(定常的にこれをやるのはあまりお勧めできません)。

しかし、これらはあくまで「可能性」であり実際にサービスやシステムに影響を及ぼしていると断定した形で報告されることは稀です。そこで、発見された事象が本当にインシデントなのかを判断する作業が必要になります。

分析

検知された事象が実際にサービスやシステムに影響を及ぼしたのかを調べるのが「分析」です。検知のためのサービスやプロダクトはインシデントの可能性が高い事象を発見してくれますが、これはあくまで可能性にすぎません。 本来は検知の段階でインシデントであることを確定させてから通知してくれればいいのですが、残念ながら現代のセキュリティ監視のサービスやプロダクトをそのまま使うだけでインシデントを正確に発見するのは困難です。 これはなぜかというと、組織ごとにルールやサービス・システムの運用方法の違い、業務内容の違いがあるからです。

たとえば、AWSのコンソールに普段は見られない国に所属しているIPアドレスからのアクセスがあり、ログインに成功したとします。 後述する Amazon GuardDuty はこういった通常見られない不審なアクセスをアラートとして発報してくれます。 このアラートではIDとパスワードなどの認証情報が盗まれ、第三者が不正にAWSのコンソールにログインした可能性を示唆しています。 では、このアラートがあがった時点でそれをインシデントと断じることができるでしょうか。

ある組織のルールとして「業務は必ずオフィス内で実施すること」と決まっていたとしたら、自国外からのログインはかなりの確度でインシデントとみていいでしょう。 しかし、リモートワークが前提の組織であったとしたらどうでしょうか。スタッフは場合によっては他の国にふらっと旅行にいって、旅先で仕事をしているかもしれません。 そうした場合、通常と違う国からのアクセスが一概に危険とは言い難いです。また、ルールが厳しい前者の組織であってもたまたま出張にでていて例外としてアクセスしていた可能性もあります。 このように組織のルールや内部事情などによって発見された「インシデントの可能性があるもの」が本当にインシデントなのかを、他のデータと突き合わせながら判断するフェーズを「分析」と呼んでいます。

分析では業務内容などに照らし合わせてアラートの判定をする他に、サービスやシステムのログをみてアラートの内容を精査するというアプローチがあります。 さきほどの例から考えると、直接本人に「今、旅行中ですか」と尋ねる方法もありますが、一方で他の社内システムのログを検索することで「しばらく前からいつもと違う国で仕事していたのだな」ということを確認できます。(もちろん、AWSへのアクセスとその社内システムの認証系が別になっていることが前提ですが) 他にも「ある人物による不審な動きが観測された」というようなアラートがあったときに、具体的にどういった動きがあったのかを確認できれば、それが通常の業務の範囲内なのか、それとも逸脱した行為をしているのかということを判断できます。

こういった判断を重ね、発報されたアラートから本当に自組織に対して被害を及ぼすインシデントを発見する、というステップが必要です。実際にインシデントと判断できるものを発見した場合、「対応」のフェーズに移っていきます。

対応(余談)

「対応」は実際に何かしらの被害が発生した際、被害状況をまとめる、被害を軽減する、被害から回復させるといった行動です。 もちろんこれはサービスやシステムを運用する上で重要な対策ではあります。 ただ、やり方がサービスやシステムの構成に大きく依存しており、さらに直接的にこれを支援するAWSのサービスは(今の所)あまりないようなので、今回の解説からは省きます。

ただ今回ひとつ触れておきたいのは、冒頭で述べたとおり分析と対応はきれいに分離されている訳ではない、ということです。 実際の現場では、インシデントと確証はないがかなり怪しいとなったら影響の少ない範囲ですばやく対応を開始する場合も多いです。 これについては筆者もそこまで経験が豊富ではないためあまり多くは語れませんが、発生している事象のリスクと周りに対する影響などを判断しつつ、バランスをとりながら対応することは必ずしも容易ではありません。

セキュリティ監視に使えるAWSサービスとその役割

前置きが長くなりましたが、今回のテーマである「セキュリティ監視を既存(2019年9月現在)のAWSでどうミニマムに実現できるか?」に話を移していきたいと思います。 「AWS セキュリティ」でググってみていただくと分かると思いますが、当然ながらセキュリティは監視だけにあらずで、さまざまなサービスがでてくると思います。 今回はここまで説明した監視だけに絞り、サービスそのものと構成の例について説明したいと思います。 まず次の4つのサービスについて簡単に説明します。各サービスの詳細についてはそれぞれ公式ドキュメントをご覧ください。

  • Amazon GuardDuty
  • AWS Security Hub
  • CloudTrail
  • VPC Flow Logs

検知に役立つAWSサービス

Amazon GuardDuty

Amazon GuardDuty はAWS上で起こる不審な挙動を検出してくれるサービスです。 詳細な動作について細かなアルゴリズムなどは公開されていませんが、一部設定のセキュリティ上の問題をみつけてくれる他、機械学習を利用して通常と異なる動作(EC2インスタンスによる普段と異なる通信、AWSコンソールへの不審なアクセス)を検知してくれます。 具体的な検知内容については https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-active.html をご覧ください。

GuardDutyは基本的にはCloudTrailのログ、VPC Flow Logs、そして内部DNSの問い合わせデータを利用して検知をしており、それらのデータから見えないイベントは見ることができないようです。 たとえば、インスタンス内部に悪性プログラムが静かに潜んでいるのを検出するといったようなことはできないはずです。(通信が発生すれば別ですが) そのためAWS内で発生するすべてのインシデントを網羅できるわけではないのですが、 それでもCloudTrailから見えるAWSリソースの操作、 およびVPC内部での通信状況に関しては監視できるため、 AWS利用の中でそれなりに大きな部分をカバーできるといえます。 また、利用料金も全体のAWS利用料のおおよそ1%程度になるといわれており、 一般的な商用セキュリティサービス・プロダクトに比べてかなりリーズナブルになっています。

AWS Security Hub

AWS Security Hub はGuardDutyをはじめとするAWS、あるいはサードパーティのセキュリティ監視検知結果を統合する機能を持っています。 また、AWSのベストプラクティスを元にしたCIS(Center for Internet Security)ベンチマークという機能もあり、AWS上のセキュリティ関連設定の不備について通知してくれます。

AWS Security Hub はそれ自身が検知をする機能を提供しているわけではないのですが、サードパーティのセキュリティ監視サービスやプロダクトからのアラートを統合してくれる点に強みがあります。 セキュリティ監視をするうえで手間がかかることのひとつは複数のサービスやプロダクトを利用する際に、ちゃんと統合して使えるようにすることです。 これはSIEM(Security Information & Event Manager)という製品がより上位の機能を提供しているので、状況によってはSIEMを使ったほうがよいかもしれませんが、Security Hubの方が機能が限定的な代わりに多くの場合SIEMより安価なので、今回は Security Hubを軸として説明します。 仮にGuardDutyしか使っていない状況だったとしても、今後セキュリティ監視に利用するサービス・プロダクトを容易に追加するために最初から導入しておくのがお勧めです。

分析に役立つ(データを蓄積してくれる)AWSサービス

AWS上で分析をするにあたり、なるべく多くのログを保存できたほうがいいのはいうまでもありません。 しかしサービスなどから出力されるログは環境によってまちまちで、一概に説明するのは難しいものです。 そのため、ここでは共通して利用したほうがよいサービスについてのみ解説します。

CloudTrail

CloudTrailはAWS上での操作をAPI単位で記録するサービスです。 AWSではコンソールを含むすべての操作がAPI経由で実行されており、そのログを追うことでどのリソースがいつ作成、変更、削除されたのかを追いかけることができます。

CloudTrailはAWS上でのユーザーの挙動を追跡したり、リソースに関する履歴を調べるのに役立ちます。 アラートの分析をしていると「このユーザーは何をしようとしていたのか」「このリソースはいつ誰が作ったのか」などを確認する必要がたびたびあります。 そのユーザーやリソースについて自分達が記憶していたり、すぐに聞いて分かる状態であればいいのですが、ちょっと前のことになると人は記憶を失ってしまうため、ちゃんとログに頼って検証できる状態にしておくのが望ましいです。 CloudTrailは利用しているアカウントの全リージョンに対し一括有効化できる機能があるため、ひとまず有効化しておいてよいサービスかと思います。

VPC Flow Logs

VPC Flow LogsはVPC内の通信を記録してくれるサービスです。 記録と言ってもその名のとおりフローの情報のみ、つまり送信元IPアドレス、宛先IPアドレス、ポート番号、転送データ量、パケット数、といったような通信の一部の情報のみで、通信の内容については取得されません。 もし通信の内容を取得したい場合は、VPC Traffic Mirroringを利用できます(詳しくは @{Webサービスへのログを取得する} で解説します)

VPC Flow Logsは現状だと一括で有効化する方法は提供されておらず、VPCごとに個別に設定をしていく必要があります。 全VPCで設定を有効化したい場合は、AWS Configを使ってFlow Logsが有効になっていないか探すか、(手前味噌ですが)次のようなツールを使って一気に有効化するというのがよさそうです。

AWSにおけるセキュリティ監視の構成例

では、実際にこれらのサービスをどのように使うかを、検知と分析それぞれの観点から説明したいと思います。 それぞれのサービスを有効化する方法などは公式ドキュメントやWeb上の記事に多く解説がありますので、ここでは触れません。 代わりにどのような構成で使うのがよさそうかという、筆者の視点からみたベストプラクティスについて説明します。

検知のための構成

f:id:mztnex:20191223204328p:plain
検知のための構成

検知をするためのお勧めの最小構成が上記の図です。 Security Hubを中心として、GuardDutyを有効にし、さらに他のセキュリティ監視サービスやプロダクトを使っており、Security Hubとの統合機能が提供されているのであれば、それも統合していきます。 GuardDutyは有効化されていれば自動的に統合されるはずですが、念のためSecurity HubのSummaryページから統合されているか確認してみてください。 これによって検知に関する情報がSecurity Hubに集約され、Security Hubのダッシュボードを見るだけで、対応すべきアラートの一覧を確認できます。 セキュリティ監視サービス・プロダクトが2、3程度だと実感しにくいかもしれませんが、コンソールが別々に分離しているとサービスやプロダクトの数が増えることで作業の手間が指数関数的に煩雑化していきます。 セキュリティ監視の対応(特にアラートの分析まで)は日常のルーチンワークになりがちなので、こういったコストを低減できるうちに取り組んでおくことに価値があります。 Security HubのFinding(本文書でいうところの「アラート」)はCloudWatch Eventsからイベントを発行できるため、Lambdaでそれを受信し各所に通知するなどの自動対応に繋げることができます。

分析のための構成

f:id:mztnex:20191223203527p:plain
分析のための構成

分析のための構成はよりシンプルです。 ここまでで説明したCloudTrail、およびVPC Flow LogsをそれぞれS3 Bucketに保存します。 図中では同じS3 Bucketに保存していますが、監視対象の規模が大きい場合はオブジェクト数が多くなるため別Bucketにするのがよいでしょう。 それぞれ、標準でS3 Bucketに直接ログを出力する機能があるので、それを利用してください。

そして保存したログを検索する方法についてですが、まずはAmazon Athenaを利用することをお勧めします。 詳しい説明は公式ドキュメントなどに譲りますが、簡潔に説明するとS3 Bucket上にあるログデータなどを並列で一気に読み込み、記述したSQLのような命令にしたがって検索、集計、表示をするサービスです。

このサービスが今回のセキュリティ監視に適している理由として、サービスの料金が読み込んだデータ量の従量課金である、ということが挙げられます。 セキュリティ監視における分析は重要なタスクですが、業務時間において四六時中発生している状態は望ましくありません。(もしアラートがひっきりなしに発生している、という状況であれば @{対応するアラートを選別する} をご参照ください) そのため事業内容のデータ分析などと違い、特定の瞬間のみログを検索できるような仕組みのほうがリーズナブルです。 Athenaはこういった要件によくマッチしていることもあり、まずセキュリティ監視の仕組みを作る初期段階にお勧めできます。 具体的な検索の手順などについては次のドキュメントをそれぞれご参照ください。 特に「S3パスの日付を指定して、ある特定の日付からのみ検索する」というテクニックを使うことで、検索範囲を狭め(=読み込むデータ量を減らし=安い料金で)検索結果もより高速に取得することができるようになります。

さらに一歩進んだ監視をしたい場合

今回紹介したのはあくまで最初に取り組む、いわば初期装備とでもいうべき構成となっています。 いずれ何らかの形で改めて紹介したいと考えていますが、セキュリティ監視をより充実させるためにできそうな取り組みのポイントだけ取り上げたいと思います。

また、解説という体になってはいますが、筆者もまだ試行錯誤を続けている段階のものが多くあるため、よい解決方法などをお持ちの方がいたらぜひご教示ください。

Webサービスへのログを取得する

AWSを利用する目的として、かなり多くの方々が「Webサービスの提供」を挙げられるのではないかなと想像しています。 Webサービスを提供している場合「攻撃のための侵入口」としてWebサービスそのものが利用されるのは現代においては自明といってもいいでしょう。 そのため、自分達が提供しているWebサービスまわりのリソースについてのアラートが発報された場合、Webサービスでどのようなことが起こったのか調査できるように、ログを取得・保管しておくことが望ましいです。

通常、セキュリティ監視が目的でなかったとしてもWebサービスに関連するログの一部は取得している場合が多いのではないかと想像しています。 特に次の3点については監査やトラブルシューティングのために何らかの形で保管していることが多いと思われるため、それらをセキュリティ監視の分析でも使えるようにするというのが最初のステップかと考えられます。

  • Reverse Proxy、Load BalancerなどWebアプリの手前の中継機能のログ
  • HTTPサーバなどのミドルウェアのログ(あれば)
  • Webアプリ本体のログ

多くの場合、これらのログは送信元のIPアドレスやリクエストが送信された対象のホスト、URI、一部のクエリなどが含まれており、分析に最低限必要な情報は取得できます。 しかし一方でPOSTリクエストなどに含まれるBodyのデータは見えなかったり、リクエストのヘッダ部分の情報が一部欠落しているケースが多く、そういった部分に攻撃コードが含まれるような場合は追跡が難しくなってしまいます。 (これらの欠落してるデータはデータ量が多い、センシティブなデータが含まれる、というケースがあるので当然といえば当然なのですが) これについては筆者も最適解はまだわかっていませんが、現状AWSサービスで解決するとしたら次の2つを検討します。

  • AWS WAFを使う: AWS WAF(Web Application Firewall)は防御の機能だけでなく、どのようなリクエストが発生してどのルールにマッチしたか・しなかったのか、をログとして残すことができます。ログを残す目的だけ(=トラフィックを止めないの)であればAWS WAFはCloudFront、あるいはALBに関連付けするとすぐ使えるようになるので、既存Webサービスがこれらを使っていた場合は非常に容易に展開ができます。ただし現状(2019年9月現在)はリクエストのBodyの内容は取得できないため、POSTなどによる攻撃の追跡は難しいという欠点があります。
  • VPC Traffic Mirroringを使う: VPC Traffic Mirroring機能はWebサービスに限らずEC2インスタンスなどENI(Elastic Network Interfaces)の通信をパケットレベルですべてコピーし、別のインスタンスなどに送信できるサービスです。完全な通信のコピーを取得できますが、パケット単位で通信が送られてくるため、HTTPリクエストの形に直すためにはツールなどでうまくTCPセッションを復元してあげる必要があります。また通信量も莫大になるため、よりシビアにコスト計算をしたり、運用時のパフォーマンスを考えねばならず、全体的に負荷は高めといえます。

どちらの方法もかなりの量のデータを取り扱うことになるため、どの程度の料金が発生するかなどを事前によく試算してから利用することを強くお勧めします。

その他使えそうなAWSサービス

今回は紹介しませんでしたが、その他セキュリティ監視に利用できそうなサービスをご紹介します。

  • Amazon Macie: S3上に個人情報などセンシティブなデータが保存されていないかをチェックしてくれるサービスです。セキュリティ監視の文脈では有効なサービスではあったのですが、残念ながら2019年9月現在では us-east-1 (バージニア)と us-west-2 (オレゴン)のみでしか利用できないので今回は対象外にしました。
  • Amazon Inspector: EC2インスタンスにインストールされている脆弱なパッケージなどを報告してくれるサービスです。どちらかというと予防の側面が強かったため今回は取り上げませんでしたが、これも利用できると分析時に「この攻撃に対して対象のEC2インスタンスは脆弱だったか?」という検証がしやすくなるため、セキュリティ監視に取り込む検討をしてもよさそうです。

インスタンス内部の情報を取得する

Webサービスに対するリクエスト同様に、今回のセキュリティ監視の構成で取得できていないものとしてインスタンス内部のイベントに関するログがあります。 GuardDutyは「あるインスタンスで不審な活動があった」ということまでは教えてくれますが、インスタンスの中のどのプロセスがその不審な活動をしたか、までは教えてくれません。 (このあたりについて詳しく知りたい場合はAWSの責任共有モデルを調べてみることをお勧めします) そのため、インスタンスの中のイベントのログは自分で取得しないと、どのプロセスが不審な活動をしたのかがわからず、それがインシデントなのか判断できずじまい、ということになりがちです。

これを解決するような製品やOSSは近年増えてきています。 osquery、sysdig、auditbeatなどのOSSも、本格的な運用の話は国内であまり聞こえていませんが、開発も盛んになってきている印象です。 この領域については筆者もいまだ試行錯誤しているため、明確な構成や運用のベストプラクティスなどを語れる立場にありません。 ただ、実際にこのようなログを取り込んだいた場合のアラート分析は著しく捗ることはわかっているため、今後も取り組みを続けたいと考えています。

対応するアラートを選別する

検知の仕組みを入れた後に起こりがちなのが、アラートが大量すぎて対応しきれなくなるというものです。 冒頭でも述べたようにセキュリティ監視は監視する対象のサービス、システム、業務によってやり方が大きく変わってくるため多くの監視サービスやプロダクトは多様なアラートを発報します。 「セキュリティ」というものに取り組んでいると、どうしてもセキュリティ監視サービスやプロダクトが発報したアラートについてひととおり目を通して安全であることを確認しないと気がすまない、という状況に陥りがちです。 確かに些細に見えるアラートが大きなインシデントの兆候を示していた、というケースもありえなくはありません。 しかしそれを理由にすべてのアラートをこと細かに分析してチェックしようとしはじめると現場が疲弊してしまい、本来見るべきだったものを見逃してしまうリスクの方が大きくなります。 これを回避するためには必ずアラートのチューニングをしていく必要があります。 アラートのチューニングにおいて重要になることを2つほど説明させてください。

  1. アラートの内容を機械的に判断して要・不要を判断する: アラートのリスクを判断するときは基本的に人間の直感などに頼らず、判断するための根拠があるはずです。その根拠は同種類の別のアラートにも適用できるはずであり、それをちゃんと言語化しルールとして次のアラートに活かしていかなければなりません。たとえばいつもと違う国からアクセスがあった、というアラートがあってもそれが本人が持っているデバイスからであるという確認がとれれば、それがアメリカからでもイギリスからでも大差はないはずです。(緊張関係にある国だった場合はまた別かもしれませんが、それも言語化すべきルールといえます)このルールをできれば監視のシステム自体に組み込めれば担当者の負荷も劇的に下がると思われますが、それができないシステムも多いので、その場合は基準を作って担当者間で共有するだけでも機械的に対処できることになり、負荷は下げられると考えられます。
  2. 機械的な判断に必要な情報を(なるべく自動的に)揃える: アラートの内容を機械的に判断するためには、ログをはじめとしてアラート外の情報との組み合わせが必要になるケースが多くあります。これらの外部情報の取得作業をいかに簡易化できるかというのも、ある意味チューニングの一部といえます。アラートが発生した際、何を基準に判断するかが明確になっていれば、その判断に必要な情報は何か?という問いの答えも明確になるはずです。何が必要かわかっているなら、あとは機械的(自動的ならなおよし)に取得できるはずで、これによって自動的にアラートの判断ができないまでも、担当者の負荷を下げることにはつながるはずです。

まとめ

AWSをはじめとするクラウドサービスではセキュリティのマネージドサービスが多く提供されており、セキュリティを向上させるための土台はオンプレミス環境より豊富になっていると思います。 しかし一方で、マネージドサービスの個別の使い方についての説明は多く世の中に出回っているものの、運用まで含めた統合的な使い方の説明はあまりないなと常々思っていたため、今回文章にしてみた次第です。 まだ拙い内容なのでわかりにくい部分なども多いと思うのですが、今後もこうした情報発信をしていきたいなと思いますので、ぜひお気持ちのある方々と一緒にやっていければなと思う今日この頃です。

セキュリティアラートの自動対応の種類についてまとめた

最近、セキュリティアラートの自動対応に対する機運が高まっているので、どういった種類のものがあるかについて整理のため自分なりにまとめてみました。なおここでの「アラート」は「セキュリティシステムや人間がある程度の確度をもってセキュリティ侵害が発生した、あるいは発生し続けていると判断したもの」を指しています。

disclaimer

  • 基本的に筆者の知識・経験に基づいて書いてます。もっとこういうのあるよ、というのがあれば教えて下さい
  • アラートの検知などの話はしません
  • 各対応の細かい実装方法についても話しません(環境・製品・サービスなどに大きく依存するため)
  • アラートの対象となるホスト(被害を受ける側)については、特に断りがない限りサーバ環境とクライアントPC環境の両方を想定して書いています

お題は以下の通りです。

  • Notification (通知)
  • Investigation (調査)
  • Preservation (保全)
  • Block (遮断)
  • Remediation (修復)
  • Quarantine (隔離)

これらから1つだけ選ぶ、というよりは状況などに応じて複数を併用することを想定しています。

Notification (通知)

  • 概要: アラートが検知されたことを関係者に通知する。EメールやSlackなどチャットに流すこともあるし、PagerDutyのように通知先や対応状況の共有などをサポートするツールを利用することもできる
  • 利点
    • 対象ホストに対して(基本的に)全く干渉しないため誤検知だとしてもホスト・サービスなどに対して影響しない
  • 課題
    • (当然だが)人間が対応を開始するまではホストはアラートで示された状況から何も変化しないので、もし本当に危険に晒されている状況で人間が対応を開始できなければ危険度は高くなっていく
  • 検討事項
    • 基本的には人間が対応をすることが前提であるため、誰がいつどのように対応するか?ということまで含めてちゃんと設計する。例えば「通常業務時間内で対応する」というのであれば原則として深夜や土日に検知されたアラートについては通知がきても翌営業日ないし週明けにしか対応できない、というようなことを組織内で握っておく必要がある
    • ホストに影響がないからといってどんなアラートでも通知しようとすると流量が増えていき人間が疲弊する。また、意図的に増やそうとしなくてもセキュリティシステムなどの更新により新しいアラートが検知されて流量が増える可能性もある。そのためアラート数を少なくするような仕組みとセットで考えるとよい

Investigation (調査)

  • 概要: アラートに出現した識別情報(IPアドレスドメイン名、ユーザ名、ファイルのハッシュ値などなど)に対して関連情報を収集する。例えば不審な通信のアラートが検知された場合、外部の脅威情報DB(VirusTotalやabuse.chなど)を参照することで、その通信先がC&C(Command & Control)サーバとして利用されていたのかどうかを知ることができる。他にもあるユーザが不審な行動をしたというアラートが検知された場合、組織内で利用しているサービスやシステムのログを参照することで、当該ユーザがアラートの前後に何を、どこから実施していたのかがわかる。これによって、アラートのリスク評価などの時間を短縮できる。なお、調査のみを単独で実施するというよりは通知などと組み合わせることでより効果を発揮する
  • 利点
    • 調査をする過程において判断が必要となる手順はあまりないため、誰がやっても同じ結果になる。そのため自動化の恩恵(時間の短縮、ミスの防止)を十全に受けることができる。
  • 課題
    • 特になし
  • 検討事項
    • 外部の情報を参照する場合はAPIなどを使って検索をすることになるが、そのような脅威情報を提供してくれるサービスは無料版だとかなり厳しくAPIの利用制限が定められている場合が多い。したがって、アラートが大量に発生している状況ですべてのアラートについて調査しようとするとあっという間に利用制限に引っかかる。特に自動化していると気づかずにDoSのようになってしまって、あげくBANされる可能性もあるため流量の制限などは事前によく検討しておく必要がある。なお有料版にすることで制限緩和してくれるサービスもあるが、だいたいはそこそこいいお値段である
    • 上述のポイントにも絡むが、1つのアラートに対してどこまで深堀りするかも考える必要がある。例えば、あるIPアドレスが過去にマルウェアを配布していたという情報を得た際、どのような種類のマルウェアなのかを知ることでよりリスク評価が容易になる。ただ、そのような紐付けされた情報をたぐり始めると際限がなくなってしまい、APIの制限にひっかかったりシステムの負荷が上がってしまうという問題がある。そのため、無闇に関連する情報を探すのではなく、アラートに含まれる識別情報の意味と文脈を考えて、ちゃんと調査する対象を選んだりどこまで調べるかを考えなければならない

Preservation (保全)

  • 概要: アラートの対象となっているシステムから調査に必要そうな情報を探して別の場所に保全する。集める情報としてはシステムログ、ミドルウェアやシステムの設定情報、動いているプロセスやネットワークの状況といったものが挙げられる。フォレンジックアーティファクト収集ツールを自動で実行しデータを別の場所へ転送するイメージ
  • 利点
    • 通知、調査に次いでホストに影響を及びさない(ただし情報取得時に負荷のかかる手段を使うと、可用性の面で影響があるかもしれない)
    • 収集したい情報が大量であったとしてもアラートが検知された近辺でのみ収集することで、データ転送やストレージなどのコストを下げられる
  • 課題
    • 通知などと同様にホストに干渉しないため進行中の脅威があった場合に阻止してくれるものではない
    • 侵害を受けたホストの場合、保全の対応をする前にrootkitなどを仕込まれている可能性があるため、ファイル情報やプロセス情報などが改ざんされたものになっている恐れがある
  • 検討事項
    • セキュリティ侵害を受けた対象ホストから保全した情報の削除・変更ができるとまずいので、権限の設定やアーキテクチャをちゃんと考える必要がある
    • 課題の部分でも述べたとおりアラート発生後だと正しい情報を取得できない可能性がある。自動化によって対応開始の時間を短縮でき人間が手動で対応するよりは良いが、アラートもセキュリティ侵害発生直後に発見されているとは限らず、後手に回っているのは間違いない。そのため多少データ転送やストレージのコストがかかったとしても、常時必要な情報を収集する仕組みを検討する価値はある
    • 厳密な証拠保全の観点から言うと、自動保全の工程で微細な証拠を消してしまう(例えば証拠が残っているディスクの一部の領域を上書きしてしまうなど)可能性もあるので、そういった工程のなかで影響がないように留意する

Block (遮断)

  • 概要: 現在進行系で攻撃を受けている際に、攻撃元になっている相手を遮断して攻撃による影響を排除すること。つまりBAN。主に外部にサービスを提供するサーバ環境で必要な場合が多い。狙ってやってくるDoS攻撃だけでなく、悪意のあるなしに関わらずシステムに想定外の負荷をかけるような通信や、不正な行動(リスト型攻撃など)を弾くといった場面でも有効な場合がある
  • 利点
    • 可用性の点でいうと、サービス側の性能向上や負荷がかかる部分の修正といった作業無しですぐ効果がでる
  • 課題
    • サービスの場合、すべての通信を遮断するとただのサービスダウンになってしまうので攻撃者の識別子(IPアドレスやユーザ名)を指定した上で遮断することになる。しかし、どちらも攻撃者からすると比較的簡単に用意できるため、ボットネットなどを使って数の暴力で押されると弱いという問題がある
    • 概要にも書いたとおり、あくまで可用性への影響低減や不正な行動の抑制に対して効果があるものであり、任意コード実行などの攻撃が成功したあとに遮断しても無意味である(遮断しても別の場所から打ち込まれて死亡するだけなので)
  • 検討事項
    • BANした対象をどの程度の期間BANし続けるかも考えておく必要がある。IPアドレスのような識別子はNATによって複数人が同時に使っていたり、アドレスのつけ変わりによって別の人が利用したりすることはよくある。永久にBANし続けると無関係の人まで巻き込んでしまうため、攻撃による影響低減とのバランスをとって検討しなければならない
    • サービス内でBANする場合は比較的柔軟に遮断ルールを持てる(はずだ)が、マネージドサービスやセキュリティ製品をつかってネットワークのレイヤでBANする場合は保持できるルール数に上限があったりするので注意が必要である
    • あくまで筆者の経験則や聞いた話だが、いきなりボットネットで大規模攻勢に晒されるということはあまり多くはない気がするため、短期的には遮断しつつ、その間に別の対応を進めるというような併用が効果的かもしれない

Remediation (修復)

  • 概要: 攻撃、あるいは悪意のないミスなどによって変更されてしまったファイルや設定などを含むシステムをあるべき状態に戻すこと。サーバ上のコンテンツ復旧や、マルウェアに汚染されたホストからマルウェアを取り除いて正常な状態に戻すといったようなことも含める。また、ホストだけではなくクラウドを含むマネージドサービスについても、管理者が禁止している設定内容になっていた場合に自動的に修復させる、といった用法もある。概念としてまとめてしまったが、具体的な方法については修復対象によって大きく異る。
  • 利点
    • 攻撃によるコンテンツや設定などの改変に対しては、(場当たり的にではあるが)可用性を高めることができる。機密性や完全性が損なわれている状態だとしても、とにかく可用性を維持したいというようなものに有効
    • (主に悪意のない)意図しない設定変更などに対しては、認可の粒度の粗さを補うことができる。つまり「Aという項目に対して変更する権限をあたえる」までしか認可の設定ができないシステムにおいて「Aという項目はXかYという内容になっていなければならない」というルールを強制したい場合に、「Aという項目がZになっていた場合に自動的にXに戻す」というような対応をすることで、擬似的にルールを実現できる。
  • 課題
    • 攻撃による改変に対しては、根本原因を取り除くわけではない。改変が発生しているということはホスト上の様々な箇所が改変されている可能性があり、それらを全て適切に修復するのはかなり困難である。例えばサーバの場合だとコンテンツや設定だけでなく、同時にバックドアが仕込まれている可能性が高い。またPC環境でマルウェアに感染していた場合、昨今のマルウェアは感染とほぼ同時に複数の悪意あるコードをホスト上のあちこちに仕込み、1つや2つ駆除されたとしても別のマルウェアが再度汚染を再開させてもとの状態に戻ってしまう。したがって攻撃に対しては場当たり的であると言わざるを得ない。
    • 主に悪意のない、意図しない変更に対する修復についても便利ではあるものの、一度は意図しない状態になってしまう(完全に変更を防げるわけではない)という点に留意する必要がある。そのため、根本的には認可の粒度をより細かくできるようにする、というアプローチの方が重要である。
    • クラウドのような環境であれば問題が起こったホストをまるごと潰して、全く同じものを再度作り直すという自動化をすることで、ホスト上にどのような改変がされていたとしても完全にその影響は除去できる。ただし、同じものが再度作られるということは当然攻撃を成功させた脆弱性もそのまま再現されるので、やはり根本解決には至らない
  • 検討事項
    • 課題で述べたとおりあくまで場当たり的な対応であり、修復単体で対策しても可用性を少し高める程度である。並行して根本的な(おそらくは人間による)対応を実施するということを念頭に置いて自動化を設計する必要がある。
    • フォレンジックの観点で考えるといろいろな痕跡を破壊する可能性があるため、それを踏まえた上で実施するかどうかを検討する必要もある。

Quarantine (隔離)

  • 概要: 攻撃を受けたとされるホストをネットワークから切り離し、攻撃による内外への影響を閉じ込める。攻撃を受けただけという状況よりは、攻撃が成功してすでに侵入されている、といった状況で被害の拡大を防ぐのに有効な一手になる。
  • 利点
    • ほとんどの攻撃がネットワーク越しに発生する&社内向けシステムもほとんどがネットワークによってアクセスされることを考えると、攻撃元からの指示を止めつつ内部システムや外部サイトへの影響を完封できる。攻撃の進行を食い止めるための強力な方法である
  • 課題
    • ネットワークから切り離されるため、可用性に対しての影響が大きい。サーバの場合はサービスの提供が全くできなくなるし、PC環境の場合は操作している人の作業が強制的に中断される可能性がある。サーバの場合はサービスが冗長化してあって対象のホストを急に隔離しても大丈夫&すぐに代替のサーバが用意できる、というようなアーキテクチャになっている必要がある。PC環境の場合は事前にそのような対応をすることについての合意形成が必要になる
    • 課題と言うほどではないかもしれないが、ほとんどの場合があくまでホスト単位での隔離なので、すでに攻撃が横展開されていた場合は影響を完封できないことには留意したい
  • 検討事項
    • 課題で述べたとおり、他の対策に比べて非常に影響が大きく安易に実施できない。そのため自動的に実施する場合は、対象ホストの重要度(機密性の高い情報がある、強い権限を持っている、など)を考慮したり、攻撃が進行中であることを示す複数の証拠をもとに発動させるなど、慎重に条件を検討する必要がある
    • ネットワーク通信を遮断するため、C&Cサーバとの通信も遮断される。これは影響を極小化するという意味ではいいが、フォレンジックの観点からは追跡が難しくなるとも言える。これについてはどちらが良いかという諸説がありつつ、ビジネス的な判断要素も多分に絡まってくる。そういった高度な判断が必要と考えると、やはりあまり自動化に向かないかもしれない

Amazon S3上のログのスキーマを管理していい感じに使うGo言語用ツールを作った

あまりにニッチすぎるから全く需要ないかなと思ったんですが、世の中に3人くらいは同じ悩みを抱えた人がいるかなー?と思ったので一応ブログをしたためておきます。

github.com

これはなに

タイトルでちょっと雑に書きましたが、もう少し正確言いうとAmazon S3上に保存したログのフォーマットやスキーマの一部をコード上で定義し、管理するためのフレームワーク in Go言語です。以下のように定義を書いた後、S3のバケット名、キーを指定するとパース済みの構造体を返してくれます。ここでは1行ずつのJSON形式で保存されたログが your-bucket バケットhttp_log/ 以下に入っており、さらに ts というフィールドに時刻情報が入っていることを定義しています。

   pipeline := rlogs.Pipeline{
        Ldr: &rlogs.S3LineLoader{},
        Psr: &parser.JSON{
            Tag:             "ts",
            TimestampField:  rlogs.String("ts"),
            TimestampFormat: rlogs.String("2006-01-02T15:04:05"),
        },
    }

    reader := rlogs.NewReader([]*rlogs.LogEntry{
        {
            Pipe: pipeline,
            Src: &rlogs.AwsS3LogSource{
                Region: "ap-northeast-1",
                Bucket: "your-bucket",
                Key:    "http_log/",
            },
        },
    })

実際の読み取りはこんな感じ。

   ch := reader.Read(&rlogs.AwsS3LogSource{
        Region: "ap-northeast-1",
        Bucket: "your-bucket",
        Key:    "http_log/log.json",
    })

    for q := range ch {
        if q.Error != nil {
            log.Fatal(q.Error)
        }
        values := q.Log.Values.(map[string]interface{})
        fmt.Printf("[log] tag=%s time=%s src=%v\n", q.Log.Tag, q.Log.Timestamp, values["src"])
    }

rlogsの中には自分自身の需要によりJSONVPC FlowLogs、CloudTrailのそれぞれのログに対応するパーサーを用意していますが、自分で実装することもできます。

なんで必要なのか

S3はオブジェクトストレージという性質上、当然ながら保存するときのログの中身・構造は一切問われません。そのためフォーマットやスキーマという概念を(ほぼ)無視してログの保存ができますが、一方でログを利用する際には別途中身をパースする必要があります。

保存しているログが2、3種類だけしかない、あるいは保存する際にちゃんと共通のスキーマに落とし込んでいる、というような場合はこれ以上お話することはないのですが、だいたいの場合現実はそうはいかないかなと思っています。特にS3にログを保存するときのメリットは「高可用でログの一次保存ができる」という点が大きいです。そのため、ログを保存する前にあれこれやってエラーなどがおきて可用性が下がってしまうというのは、アーキテクチャの旨味が少なくなってしまうかなと思います。

ということからログを利用する際にはどのバケット、どのプレフィクスにどんなログが入っているかを知っている必要があります。ログを利用するツール・システムが1つだけならそこに定義などを書いておけばいいのですが、実際には様々なことにログを利用したくなるため、自分は共通化したいと考えました。そこで共通利用できるようにして、ついでにログの取得+パースの機能もつけようと思ったのがこのフレームワークです。

どう使うのか

組織内などで1つレポジトリを作り、そこにフォーマットやスキーマの定義を書いて、それをGoのパッケージとして呼び出す、というような使い方を想定しています。例えば your-git-server.example.com のようなサーバがあって your-git-server.example.com/someone/logparser のようなレポジトリを用意した場合、以下のような感じで定義を書いておきます。

package logparser

func NewReader() *rlogs.Reader {
        logEntries := []*rlogs.LogEntry{
                // ログの取り込み方+パースの仕方+場所の定義を
                {
                        Pipe: rlogs.Pipeline{
                                Ldr: &rlogs.S3LineLoader{},
                                Psr: &parser.JSON{
                                        Tag:             "your.log",
                                        TimestampField:  rlogs.String("time"),
                                        TimestampFormat: rlogs.String("2006-01-02T15:04:05Z"),
                                },
                        },
                        Src: &rlogs.AwsS3LogSource{
                                Region: "ap-northeast-1",
                                Bucket: "your-backet",
                                Key:    "logs/",
                        },
                },
               // いろいろ書く
        }
        return rlogs.NewReader(logEntries)
}

そのあと、実際にログを利用するコード内で以下のように呼び出します。

package main

import (
        "your-git-server.example.com/someone/logparser"
)

func main() {
    reader := logparser.NewReader()
    ch := reader.Read(&rlogs.AwsS3LogSource{
        Region: "some-region",
        Bucket: "your-bucket",
        Key:    "http/log.json",
    })

    for q := range ch {
        if q.Error != nil {
            log.Fatal(q.Error)
        }
        values := q.Log.Values.(map[string]interface{})
        fmt.Printf("[log] tag=%s time=%s src=%v\n", q.Log.Tag, q.Log.Timestamp, values["src"])
    }

}

どう使っているか

以前、会社のブログにも書いたですが、今の仕事ではおよそ20種類ぐらいのログをこのフレームワークを利用して取り込み、いろいろなことに利用しています。このあたりの話はまたいずれ改めて会社のブログなどに書くかもしれません。

クラウドネイティブなハニーポットを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のみで運用したいのですが、詳しくは今後の課題に後述

テキスト出力されたログファイルから元のログフォーマットを分析するツールを作った

タイトルの通りなのですが、昔ちょっとやっていたテーマに関連したツールをGo言語の練習がてら作ってみました*1

ログファイルから元のログフォーマットを分析するとは

ここで言うログのフォーマットというのは所謂フォーマット文のことを指します。

log.Printf("Requested from %s", ipAddr)

このコードから以下のようなログが出力されます。

2018/05/23 23:25:00 Requested from 10.0.2.1
2018/05/23 23:25:10 Requested from 192.168.1.5
2018/05/23 23:25:24 Requested from 10.0.1.5

元になったフォーマットは %s の部分にIPアドレスらしきものが埋め込まれて下図のようなテキストとして出力されます。この例は非常に簡単なので下から上を推測するのは容易ですが、内容が複雑になってくると「これ値なのか固定文なのかどっちだ?」ということがまれによくあります。この下の出力から上のフォーマット文(に近いもの)を推測するのが今回作成したツールになります。このツールは 1) すでに出力されたログファイルからフォーマットを推定する、そして 2) 推定したフォーマットを利用し、そのフォーマットに該当するログがログファイルのどのあたりに出現したのかを示す という2つの機能を実装しています。

なんでこんなツールが必要なのか

実際には、正規化・構造化されたログデータのみを扱う環境であればこのツールは不要ですが、以下のような状況で役立ちます。

  • ログの全体像を把握したい場合 : セキュリティ分析の文脈で特に多いと思いますが、今まで見たことのない大量のログをみてそこから知見を導き出さないと行けない場合があります。そういったときにひたすら less コマンドで眺めようとしても人間には厳しいので、全体としてどういうログがあるのか? そしてどういう分布をしているのか? ということがわかると分析のとっかかりが非常に楽になります。特にセキュリティ分析で必要なのは多くの場合全体の99%を占める通常のサービスに関するログではなく、何か異常が起こったポイントになります。異常が起こった際のログというのは通常見られないエラーや処理が発生しやすいため、異常なログ=珍しいフォーマットのログがどこに出現するのかを把握できると、そこにまず注目して分析するという足がかりを作ることができます。
  • テキスト形式で出力されるログを再利用しないとならない場合 : すでにサービスなどが稼働しておりテキストではログを出力するという場合、そのログを正規表現にかけるなどして中に含まれている値を抽出する必要があります。仕様書がある場合はいいですが、そうでない場合はソースコードを見るか、もしくは正規表現を書く→網羅でいているか確認する→正規表現を直す、みたいなことを繰り返さないとならずかなり面倒です*2。このツールだと抜くべき値の正規表現の推定まではしてくれませんが、既存のログから分かる範囲ではどこまでやればいいかを網羅できるので作業的に楽になります。

使い方

Go言語を使う環境が整っていれば go get github.com/m-mizutani/logptn でインストールされます。

GitHub - m-mizutani/logptn: Generate Log Format from real log data

例として(短いものですが)以下のようなログをツールに入力してみます。

$ cat test.log
Feb  1 07:56:49 pylon sshd[5153]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.0.3  user=root
Feb  1 07:56:51 pylon sshd[5153]: Failed password for root from 192.168.0.3 port 7176 ssh2
Feb  1 07:56:51 pylon sshd[5153]: Connection closed by 192.168.0.3 [preauth]
Feb  1 08:01:26 pylon sshd[5156]: Invalid user upload from 192.168.0.3
Feb  1 08:01:26 pylon sshd[5156]: input_userauth_request: invalid user upload [preauth]
Feb  1 08:01:26 pylon sshd[5156]: pam_unix(sshd:auth): check pass; user unknown
Feb  1 08:01:26 pylon sshd[5156]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.0.3
Feb  1 08:01:28 pylon sshd[5156]: Failed password for invalid user upload from 192.168.0.3 port 51058 ssh2
Feb  1 08:01:28 pylon sshd[5156]: Connection closed by 192.168.0.3 [preauth]
Feb  1 08:05:01 pylon CRON[5159]: pam_unix(cron:session): session opened for user root by (uid=0)
Feb  1 08:05:01 pylon CRON[5159]: pam_unix(cron:session): session closed for user root
Feb  1 08:05:54 pylon sshd[5162]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.0.3  user=root
Feb  1 08:05:56 pylon sshd[5162]: Failed password for root from 192.168.0.3 port 33005 ssh2
Feb  1 08:05:56 pylon sshd[5162]: Connection closed by 192.168.0.3 [preauth]
Feb  1 08:10:28 pylon sshd[5165]: Invalid user mythtv from 192.168.0.3
Feb  1 08:10:28 pylon sshd[5165]: input_userauth_request: invalid user mythtv [preauth]
Feb  1 08:10:28 pylon sshd[5165]: pam_unix(sshd:auth): check pass; user unknown
Feb  1 08:10:28 pylon sshd[5165]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.0.3
Feb  1 08:10:30 pylon sshd[5165]: Failed password for invalid user mythtv from 192.168.0.3 port 59978 ssh2
Feb  1 08:10:30 pylon sshd[5165]: Connection closed by 192.168.0.3 [preauth]
Feb  1 08:15:01 pylon CRON[5168]: pam_unix(cron:session): session opened for user root by (uid=0)
Feb  1 08:15:01 pylon CRON[5168]: pam_unix(cron:session): session closed for user root
Feb  1 08:15:26 pylon sshd[5171]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=10.2.3.4  user=root
Feb  1 08:15:28 pylon sshd[5171]: Failed password for root from 10.2.3.4 port 60733 ssh2
Feb  1 08:15:28 pylon sshd[5171]: Connection closed by 10.2.3.4 [preauth]
Feb  1 08:17:01 pylon CRON[5173]: pam_unix(cron:session): session opened for user root by (uid=0)
Feb  1 08:17:01 pylon CRON[5173]: pam_unix(cron:session): session closed for user root
Feb  1 08:20:35 pylon sshd[5177]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=10.2.3.4  user=root
Feb  1 08:20:37 pylon sshd[5177]: Failed password for root from 10.2.3.4 port 44877 ssh2
Feb  1 08:20:37 pylon sshd[5177]: Connection closed by 10.2.3.4 [preauth]
Feb  1 08:25:01 pylon CRON[5180]: pam_unix(cron:session): session opened for user root by (uid=0)
Feb  1 08:25:01 pylon CRON[5180]: pam_unix(cron:session): session closed for user root
Feb  1 08:25:16 pylon sshd[5183]: Invalid user user from 10.2.3.4

このデータを入力させると以下のような出力をします。

./logptn test.log
2018/05/20 13:30:55 arg:test.log
     4 [4ffb267b] Feb  1 *:*:* pylon sshd[*]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=*  user=root
     4 [845f4659] Feb  1 *:*:* pylon sshd[*]: Failed password for root from * port * ssh2
     6 [847ccf35] Feb  1 *:*:* pylon sshd[*]: Connection closed by * [preauth]
     3 [de051cd9] Feb  1 08:*:* pylon sshd[*]: Invalid user * from *
     2 [8e9e2a13] Feb  1 08:*:* pylon sshd[*]: input_userauth_request: invalid user * [preauth]
     2 [22190c74] Feb  1 08:*:* pylon sshd[*]: pam_unix(sshd:auth): check pass; user unknown
     2 [83fba2bf] Feb  1 08:*:* pylon sshd[*]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.0.3
     2 [f1ba83ea] Feb  1 08:*:* pylon sshd[*]: Failed password for invalid user * from 192.168.0.3 port * ssh2
     4 [e4a6f815] Feb  1 08:*:01 pylon CRON[*]: pam_unix(cron:session): session opened for user root by (uid=0)
     4 [5256845b] Feb  1 08:*:01 pylon CRON[*]: pam_unix(cron:session): session closed for user root

この出力では、左から「そのフォーマットが出現した回数」「フォーマットID」「推定されたフォーマット」になっています。また、推定されたフォーマットにおいて、値として埋め込まれると思われる部分を * という記号に置き換えています。この例ではサンプルが少ないため、IPアドレスの部分が * になっていないものもありますが、サンプル数が増えるとこれも * に置き換わります。上記は人間が読みやすいテキスト形式での出力になっていますが、別のプログラムで扱えるようにjson形式でも出力できます。

./logptn test.log -d sjson | jq . 
{
  "formats": [
    {
      "segments": [
        "Feb  1 ",
        null,
        ":",
        null,
        ":",
        null,
        " pylon sshd[",
        null,
        "]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=",
        null,
        "  user=root"
      ],
      "count": 4
    },
(snip)

さらに、そのフォーマットがログ全体のどの行数あたりに現れたのかをHTML形式で一覧にして表示することもできます。

$ ./logptn  ./var/log/secure -d heatmap -o secure.html

上記のコマンドでログのフォーマット、およびそれが何行目あたりに出現したのかを示すヒートマップを作成できます。ヒートマップは左が推定されたフォーマット、上のヘッダが行数(何行目〜何行目)、右が合計ログ数になっています。下の画像がちょっと小さくて見づらいですが、HTMLファイル自体はここからもダウンロード可能です。

f:id:mztnex:20180520141045p:plain 大きい画像

性能

計算量としては O(NM) になり、N がログファイルに含まれるログの総数、 M が推定されたフォーマットの数になります。いろいろなログファイルで試してみましたが M は10〜100ぐらいに収束するのでだいたいは N 、つまりログの総数が影響します。雑にしか計測していませんが、M=40 ほどになるデータに対して MacBookPro Early 2015 (2.7 GHz Intel Core i5) で動かして、おおよそ30,000 logs/sec 程度で動作しました。多分コード的にはもっと最適化できるんですが、まだそこまでは手を付けてません。

動作の仕組み

このあとはどうやってフォーマットを作成しているかという話なので、興味のある方だけどうぞ。

f:id:mztnex:20180524021502p:plain

すでに出力されているテキストログから元になったログフォーマットを推定する話は昔から研究としてありますが*3、今回 logptn で実装したのは非常に簡易なアルゴリズムになっています。昔は自分もいろいろとこねくり回した方法を考えたのですが、どれだけ複雑なアルゴリズムを使ったところで「まあ所詮は推測にすぎないよね」という割り切りを得たので、極力シンプルに実装しました。

手順

このフォーマット推定のアルゴリズムは4段階に別れており、それぞれ順番に解説します。

Phase1) Import logs

このアルゴリズムはバッチ型(ある程度の固まった量のログデータをまとめてから処理するタイプ)になります。一方で次々到着するログを逐次的に処理するオンライン型やストリーム型と呼ばれる手法もありますが、今回はもとになるデータセットは事前に決まっている(あとから増えない)ものとします。

取り込みに関しては全く難しいことはしておらず、現在は完全に1行1ログとして分割して切り分けています。全体の流れとしては複数行のログでも対応できないことはないアルゴリズムになっていますが、複数行で切り分ける基準がログごとに様々すぎるので、現状では対応していません。

Phase2) Chunking

データを1つずつのログ(現在は1行のテキスト、と同じ)に分割したあとは、そのログに含まれる単語などに分解する Chunking を実施します。これはログ内に埋め込まれる値は単語などの短い単位として出力される、という前提をおくことで、値として出力される単語の長さが違うことでフォーマットがばらつくのを防ぐのが目的です。例えば 0.0.0.0255.255.255.255 という2つが別のログに現れたとしても同じ「IPアドレス」として認識してほしいですが、これを一文字ずつ比較しようとすると2つはかなり異なる文字列長になります(0.0.0.0 が7文字、255.255.255.255 が15文字)もちろんこういう文字列長の違いをうまく吸収してくれるアルゴリズムなら気にしなくていいのですが、だいたいは問題を単純化するために事前にログを単語(ここではchunkとよんでいます)に分割しています。

分割については、これが自然言語的な英語であれば単純に空白で区切ればいいのですが、ログファイルというのは様々な記号が含まれてそれによって区切られている場合もあるので、なかなか空白だけというわけにはいきません。実際にはある特定の記号が出てきた場合に区切るという実装にしており、現状 logptn だと \t!,:;[]{}()<>=|\\*\"' がデフォルトの区切り文字となっています。これらのうちどれかがでてきたらchunkとして切り分けてきます。(詳しくはこちら を参照)この記号の選び方は完全に自分の経験に基づくヒューリスティックなものなので明確な根拠はないですが、まあだいたいうまくいっている感じです。一方、これらの記号にもとづいてこのフェーズである程度正しく文を分割できることが前提となっているため、日本語のようなマルチバイト文字のログについてはおそらくうまくいきません。

また、ヒューリスティックを入れていいと考えるなら正規表現などによって日付やURL、Eメールアドレスなど値として埋め込まれると考えられるような形式の値をを特定して切り出してしまえばより精度があげられます。実際、それを見越して正規表現でそういった機能も実装したのですが、Goの正規表現モジュールが想像以上に遅くて厳しかったので、現状デフォルトの機能としては外しています。(一応、 --enable-regex オプションを使うと有効化はされます)

実際にこの方法で文を分割すると、以下のようになります。

  • Before: pam_unix(cron:session): session closed for user root
  • After: pam_unix, (, cron, :, session, ), :, , session, , closed, , for, , user, , root

Phase3) Clustering

Chukingされたログが出揃ったら次は類似しているログをクラスタリングします。クラスタリングも非常にシンプルなアルゴリズムを使っています(名前知らないだけで既存のクラスタリング手法かもですが)。以下の手順をログ1つずつ順番に試します。

  1. Chunk長(1つのログから生成されたChunkの数)が同じクラスタがなかったら手順終了
  2. 同じChunk長のクラスタ全部に対して距離を計算する
  3. クラスタとログの距離は、クラスタの中心になっているログとどれくらい近いかで計算する。ログ間の距離は全体でChunkが一致する割合を見て、割合が高いほど近いと判断する。
  4. クラスとの距離がthreshold(デフォルトでは0.7)を超えていたら、もっとも近いクラスタに組み込まれて手順終了
  5. もしthresholdを下回るクラスタのみだったらそのまま終了

クラスタに組み込まれずに終了した場合は、そのログを元に新しいクラスタを生成します。これをすべてのログに対して実施します。

Phase4) Estimate Format

クラスタが生成されたらあとはフォーマットを推定するのみです。これも非常にシンプルなアプローチで、クラスタ内のすべてのログに対して積をとっているイメージです。クラスタ内のログを L1, L2, ... , Ln としたとき、まず L1L2 で積をとって、L' を生成し、そのあとは LiL' で積をとる、という処理を繰り返します。

  • L1: Requested from 10.0.2.3
  • L2: Requested from 192.168.0.1
  • L': Requested from null

同じクラスタだとChunkの長さがすべて同じになるので、互いのログのChunkを先頭から比較します。同じ内容であればそのまま、もし異なる内容であれば null とします。この null がフォーマットの中で値が入ると考えられる部分となります。このあと L3 と比較する際は null の部分はどのChunkと比較しても必ず null になります。

この操作をクラスタ内のすべてのログに対して実施すると、最終的にそのクラスタのすべてのログに適合できるフォーマットが生成(推定)できるということになります。

この手法の弱点

前述したとおり、このアルゴリズムはChunkingがある程度正しくできていて、かつ1 Chunk=埋め込まれる値になることが強い前提となっています。そのため、たとえばChunkに分割されやすい任意長の文字列が登場するようなログ(極端に言えば、例えばユーザによる入力をそのままログに書き出すようなログ)に対しては非常に低い精度になると見込まれます。また任意長でないとしても同様にChunk分割によって1つの値が複数に分割されてしまうようなログには耐性がないと言えます。

また、現状のアルゴリズムだと複数のクラスタから同じフォーマットが生成される可能性があります。クラスタを生成する時のアルゴリズムがわりと雑なのでたまたま距離の計算でしきい値を超えてしまったなどの場合に、本来同じクラスタであるべきログが2つ以上のクラスタに分離してしまいます。これについてはまだすっきりした方法を思いついてないですが、生成後のフォーマット同士を比較してマージするというような処理が必要かなと考えています。

参考文献

*1:言い訳がましいのですがまだGo言語まともに書き始めて1ヶ月ぐらいという有様なので実装物について、流儀的にこういうの違うよ、とかGoならこういうふうにも書けるよ、みたいなコメントは大歓迎です

*2:そんな環境のほうがおかしいだろというツッコミはあると思いますが、稀によくあるシチュエーションでした。特に前職

*3:参考文献として幾つか論文へのリンクを貼っておきました

2018年春アニメはネット配信だけで視聴できるのか

背景

先日、会社でアニメの雑談していたときに「そういえば今時のネット配信はテレビ放映中のアニメのカバレッジってどれくらいなんだろう?」と気になりました。だいぶ配信が整備されてきたようなそうでもないような…。

個人的には四半期に一度せっせと録画の準備をしたり、HDDの残り容量を気にしていろいろケアしたり、録画に失敗したりというので消耗するのはそろそろやめたかったのでなるべくならネット配信で見てしまいたい派です。ということで、現実的にどれくらいの視聴ができるのかというのを知るべく、2018年春アニメのネット配信状況をまとめて見ました。

結果

2018年春アニメ ネット配信一覧 - Google スプレッドシート

※ 雑に作った&後から情報が更新されることもあるようなので、間違いなどあればtwitterなどで指摘してもらえればと思います

結論から言うと「完全にネット配信だけで生きていくことはできないが、頑張ればわりといける」です。

まとめはある程度配信数を揃えていそうなサイトで、かつ月額定額制で(少なくともその期にやってるアニメは)定額のうちに入っているものを対象にしたつもりです。(ニコニコ、DMMなどは1週間で無料期間が終わってPay per viewになるので除外しました)

以下、まとめていた時の気づき。

  • 配信数はバンダイチャンネルdアニメストアの二大勢力。しかしそれでも30本ちょいで今季から新しく始まった72作品の半分に届いていない
  • 日中や夕方にやるような本来子供向けのアニメはあまり配信されていない傾向にある。そのためみたいコンテンツが深夜アニメ枠ならかなりがカバーされている
  • AmazonビデオやNetflixは二大勢力には数で劣るものの、独占配信のコンテンツがいくつかある

Web系企業に転職して最高だったという話をしたい

11月にSI企業からCookpadにセキュリティエンジニアとして転職して1ヶ月たったのですが、いろいろ感銘をうけたのでその気持を忘れぬうちに文章に残しておきたいと思います。

disclaimer

  • 個人の主観であり、客観的にSI企業が悪いとかWeb系が良いとか言っているわけではありません。
  • かなり前職disな話っぽくなってしまっていますが、そこは企業としての性質の違いだとご理解いただければ幸いです。
  • 当該企業からはお金を頂いています。予めあしからずご了承ください。

しがらみが少ない

CookpadはWeb系の中でもかなり規模が大きい方だとは思うのですが、それでも前職のグローバル含めた規模のおそらく1/1000ぐらいであり、自分にとってはとても風通しの良い体質に思えます。

新しく何かを始めようとするときも、関係する人と立ち話で「こんな感じにしようと思うんだけどどうですかね」みたいなところをざっと決めて作りながら物事を進めていく…というやり方だと感じています。Rough Consensus and Running Codeという世界ですね。もちろんこの1ヶ月の間に細かい手戻りも何度かあったりしたのですが、ちゃんと合意を取りながらすすめると言ったやりかたを超えるスピード感で仕事をするのが重要と感じています。(もちろんこれは対象の規模などにもよりますが)

また、サービスなどに直結するような内容でも素早く動いていくことを重視しているように感じています。前職では何かしようとするとだいたい2〜3つ以上の力学というか外圧のようなものがあり、それをまずどうにかしないといけませんでした。グループ内部ではわりと好きにやらせてもらっていたのですが、その外に出ようとした瞬間に壁を突破したり調整したりが必要でそこで疲弊してしまうことが少なくありませんでした。現職では担当者同士ですぐ話をして次のステップへ進めるというサイクルが短く回っているため、むしろ振り落とされないよう頑張ってついていかねばと思う場面も少なくありません。

強いエンジニアが多い

とりあえず、右を見ても左を見ても豪傑ばかりという印象です。

前職でも研究所に学術的研究に秀でた方は多くいらっしゃいましたが、正直なところエンジニアリングが得意という人は全体でもかなり少なかったです。本当はエンジニアリングも得意だけど、そういうのが仕事の内容的に見えてこないというだけだったかもしれませんが、全体としてエンジニアリングにあまり積極的でない空気感を感じていました。

当然ながら現職ではエンジニアリングは非常に重要な位置づけとなっており、これを蔑ろにすると(多分)人権を失います。象徴的なイベントとして、入社後にちょうど社内ISUCONが開催され私も参加させてもらったのですが、出張や休暇などでいない人を除いて全技術職が参加必須というのに驚きました。技術職と言っても様々な分野の方(インフラ、サーバサイド、フロントエンド、モバイルアプリ、研究職)がいらっしゃるわけですが、皆さんほぼ基本的な技術は一通りできるのは当たり前で、その上で自分の得意分野を活かして競技に参加していました。

また、CTOが「今日一日業務が止まるのはとても手痛いが、それでもやる価値がある」と言っていたのがとても印象的で、ちゃんとエンジニアの育成に力を入れているんだなと思いました。

セキュリティの分野についても入り組んだ攻撃や防御の話に通じている人はあまりいませんが、実践のサービス開発と運用の部分については長年の蓄積を持つ人が多く、学ばせてもらうところが多くあります。そういった環境に身を置けるというのはエンジニアとしてありがたいことだと思います。

当事者意識があり、サービス・環境の改善にとても前向き

自分たちが作っている・使っている環境を常に良くしていこうという文化を強く感じます。前職の場合、基本的には本社なり別部署が決めたものを(それがどんなに使いづらいものでも)言われるがままに使うというのが基本でした。規模の大きさを考えると仕組みや使うものを決める人と使う人を分けたほうが仕事が明確になって良いのでしょうが、使いづらさやだめなところがいつまでも改善されないというフラストレーションがありました。

現職場では自分達が使うものを自分たちで選び、時には作るといったことをするため、常に「どうすると良くなるか?」といった方向を見ているなと感じています。これは今あるものを良くするためにも重要なことだし、その良くなったものをベースにまた次の新しいことを始めるのにも活きてきます。また、そうするべきかという議論や検討はもちろん必要ですが、ソフトウェアであれば自分で作ってしまっても良いわけで、ものづくりが好きな自分としてはそれも魅力の一つになっています。

自由にオープンソースにコミットできる

正直、これが一番強烈だったかもしれせん。

入社する前から外部に対してアウトプットしていくことは評価にも含まれており望ましいという話は聞いていたのですが、実際にとても自由な状況でした。OSSに関するポリシーを見てみると "従業員は自分の良識に基づいて、業務時間中に開発したソフトウェアをOSSで公開できる" という説明を見つけ、あまりの神々しさに見た瞬間目が潰れるかと思いました。目がーっ!

前職では知財やそれに準じる成果物が非常に厳しく管理されており、私的時間に書いたコードですらOSSとして公開あるいはcontributeするのに内部レビューと承認が必要でした。おかげでOSS的活動が好きな自分としては少なからずストレスではあったのですが、それが完全に開放され翼を授かった気持ちです。というかむしろ、前職との違いに頭がクラクラしています。

おかげで、これまでgithub上でPRを送るというOSS活動に縁がなかったのですが、先日始めてPRを送りmergeされてました。実に大したことじゃないんですが、ささやかながら嬉しい気持ちになっています。今後は自分で書いているOSSだけでなく、他のOSSにも積極的に貢献していきたいと考えてます。

f:id:mztnex:20171203114238j:plain

その他

あまり本質ではないのですが、その他感動したことなどをいくつか。

いわゆる今時の仕組みを使った開発・運用

社内で動いているシステムやフレームワークを見て「おお、これが噂に聞いたgitでconfigのバージョン管理をして自動デプロイされるというあれか…!」と感動していました。github eterpriseまでは前職でも使っていたのですが、普通にコードのバージョン管理をするだけで(それでも社内ではかなり珍しい感じでしたが)そういった今時っぽい使い方はしていませんでした。まあがっつり運用というほどではなかったので、そこまでする必要がなかったと言えばそうなんですがね…。

開発マシンなど

マシン自体は前職もそこまで悪くなかったですが、現職ではさらにもう一回り上のスペックになっています(MacBookPro メモリ16GB)。基本、全員が4Kディスプレイを使っています。

キーボードやマウスについても、自分は最近 RealforceLogicool G900 がベストな組み合わせなので「ちょいと値は張るけど新しく職場用に自分で買うかー」と思っていました。が、この話をしたら「いや、普通に発注してくださいよw」と言われするっと申請したらしゅっと支給されてとても感動しました。

無限のコーヒーやスナックがある

まったく本質ではないし、前職にいたときもコーヒー買うお金をケチったことなどないのですが、やはり無料で供給されると思うと気分が全く違いますね。ここしばらくカフェインをコードに変換する仕事をしていました。

おわりに

まだ転職してからたかだか1ヶ月なのでこれから見えてくる大変なことや苦労も色々あるとは思うのですが、今のところ職務内容も含めて、転職してよかったと思える生活をしています。

この記事は別にリクルート目的というわけではないので特にリンクなどは貼りませんが、現職ではセキュリティエンジニアだけでなくいろんな職種を募集しているので、興味のある方は気軽に声をかけてもらえればと思います。

追記

コメントとか見てたら「給与や労働時間の話がないのは闇」って書かれていて、みんなよく訓練されてるなぁと感心してしまいました。気になる方も多いと思うので一応追記しておきます。

  • 給与は前職で「独身一人暮らしが雑に出費しても貯金できる程度」もらってると書きましたが、そこからちょい上乗せするくらいもらっています。転職時にあんまり給与交渉しなかったので場合によってはもっとあがっていたかもしれません。
  • 勤務時間は基本1日8時間となっていてフレックスタイム制です。残業時間は先月で確か20時間ちょいぐらいだったと記憶していますが、これは働きはじめでいろいろ慣れていなかったこともあり、効率的にやればもっと短縮できそうではあります。