Network Firewallで Suricata互換のIPSルールを試してみた

マネージドでシグネチャー型のIPSが使えますよ
2021.05.07

僕はまだNetwork Firewallの真の力を知らなかった

こんにちは、のんピ です。

最近、Network Firewallのブログを2本書きました。

しかし、Network Firewallのココが凄いポイントである、Suricata互換のIPSルールにまだノータッチでした。

また、Suricata互換のIPSルールについてまとまった日本語の資料があまりなかったので、 せっかくなので、気合を入れて今回はSuricata互換のIPSルールを深掘りしていこうと思います。

ちなみに、IPSにロマンを感じるのは私だけでしょうか。 通信を検知して、振る舞いや大量のシグネチャーと照らし合わせて、不正な通信を見つけ出してやるという働きっぷりにロマンを感じられずにはいられません。 単純にIPSのシグネチャーを見るだけで興奮します
※ 同じロマンネットワーク機器としてはL3スイッチがあります。

いきなりまとめ

  • Suricataはシグネチャー型のIPS
  • 同じSuricata互換のIPSのシグネチャーを使い回すことができる
  • そもそも、Network Firewallのステートフルエンジン自体がSuricata互換
  • Domain Listや5-tapleでは設定できなかったシグネチャー毎に優先度をつけることができる
  • ホワイトリスト運用、ブラックリスト運用のどちらも実装しやすい
  • CloudWatch Logs Insightsを用いることで、いつ、どのシグネチャーが多く検知されたのか容易に把握することができる

Suricataとは

Suricataの特徴

まずは、Suricataについての説明からしていきます。

Suricataとはコミュニティが管理するIDS/IPSです。

Suricata is a high performance Network IDS, IPS and Network Security Monitoring engine. It is open source and owned by a community-run non-profit foundation, the Open Information Security Foundation (OISF). Suricata is developed by the OISF.

What is Suricata

IDS/IPSにはシグネチャー型とアノマリ型と大きく分けて2つの種類がありますが、Suricataはシグネチャー型です。

シグネチャー型は事前に用意されたシグネチャーと照らし合わせて、シグネチャーとマッチしたらアラートをあげたり、通信をブロックする方式です。
例えば、Webサーバの.htaccessにアクセスするために、httpのリクエストに.htaccessという文字列が含まれていたら、通信をブロックするといったものです。

一方でアノマリ型は、通信の振る舞いを見て、不審な通信の場合はアラートをあげたり、通信をブロックする方式です。
例えば、大量のSYNパケットが短時間に大量に送られて来たときは、SYN Flood攻撃と判断して、通信をブロックするといったものです。

AWS Network FirewallはSuricata互換と記載されている通り、同じSuricata互換のIPSで使用してたシグネチャーを使い回しすることが可能です。 そのため、既にSuricata互換のIDS/IPSを使っているのであれば、スムーズに導入できるかと思います。
※ 投稿時点では、AWS公式のシグネチャーセットのようなものはありませんでした。

なお、SuricataでSSLのデコード処理は出来ません。https通信の中身を確認した上で検知・ブロックしたいなどの要件がある場合は、AWS Marketplace上で提供されているIPSの仮想アプライアンスなど、3rdパーティの製品を利用することになります。
例) httpsのクエリーの文字列で検知したい場合など

連携例として、Suricata公式ドキュメントには、Symantec SSL Visibilityが掲載されていました。

As Suricata itself cannot decrypt SSL/TLS traffic, some organizations use a decryption product to handle this. This document will offer some advice on using Suricata with the Symantec SSL Visibility appliance (formerly known as BlueCoat).

21.1. Symantec SSL Visibility (BlueCoat)

シグネチャーの書き方

Suricataのシグネチャーのフォーマットは大きく分けて以下の3つに分かれています。

  • action
  • header
  • options

実際のシグネチャーで色分けすると以下のようになります。

drop tcp $HOME_NET any -> $EXTERNAL_NET any (msg:”ET TROJAN Likely Bot Nick in IRC (USA +..)”; flow:established,to_server; flowbits:isset,is_proto_irc; content:”NICK “; pcre:”/NICK .USA.[0-9]{3,}/i”; reference:url,doc.emergingthreats.net/2008124; classtype:trojan-activity; sid:2008124; rev:2;)

それぞれの役割、書き方について以降、説明していきます。

action

drop tcp $HOME_NET any -> $EXTERNAL_NET any(msg:”ET TROJAN Likely Bot Nick in IRC (USA +..)”; flow:established,to_server; flowbits:isset,is_proto_irc; content:”NICK “; pcre:”/NICK .USA.[0-9]{3,}/i”; reference:url,doc.emergingthreats.net/2008124; classtype:trojan-activity; sid:2008124; rev:2;)

actionはシグネチャーに一致したら、どのような処理を行うのかを指定する部分です。 Suricataで定義されているactionは以下の通りです。

説明
alert アラートを出す
pass パケットの検査を中止し、通信を許可する
drop パケットをドロップしてアラートを出す
reject 一致したパケットの送信者にRST/ICMP unreach errorを送信する

なお、AWS公式ドキュメントに記載がある通り、Network Firewallでは、alertpassdrop以外の値は指定出来ません。

header

headerは以下の4つのセクションに分かれています。

  • protocol
  • source and destination
  • ports
  • direction

順番に説明していきます。

protocol

drop tcp $HOME_NET any -> $EXTERNAL_NET any (msg:”ET TROJAN Likely Bot Nick in IRC (USA +..)”; flow:established,to_server; flowbits:isset,is_proto_irc; content:”NICK “; pcre:”/NICK .USA.[0-9]{3,}/i”; reference:url,doc.emergingthreats.net/2008124; classtype:trojan-activity; sid:2008124; rev:2;)

protocolは通信のプロトコルを指定する部分です。 Suricataで定義されているprotocolは以下の通りです。httpなどアプリケーション槽のプロコトルにも対応しています。

  • tcp
  • udp
  • icmp
  • ip (「all」または「any」を表します)
  • http
  • ftp
  • tls (sslを含む)
  • smb
  • dns
  • dcerpc
  • ssh
  • smtp
  • imap
  • modbus (Network Firewallでは使用不可)
  • dnp3 (Network Firewallでは使用不可)
  • enip (Network Firewallでは使用不可)
  • nfs (Network Firewallでは使用不可)
  • ikev2
  • krb5
  • ntp
  • dhcp

source and destination

drop tcp $HOME_NET any -> $EXTERNAL_NET any (msg:”ET TROJAN Likely Bot Nick in IRC (USA +..)”; flow:established,to_server; flowbits:isset,is_proto_irc; content:”NICK “; pcre:”/NICK .USA.[0-9]{3,}/i”; reference:url,doc.emergingthreats.net/2008124; classtype:trojan-activity; sid:2008124; rev:2;)

source and destinationは、通信の送信元と、送信先を指定する部分です。 左側が送信元のネットワーク、右側が送信先のネットワークです。
IPアドレス(IPv4とIPv6の両方がサポート)とIPアドレスの範囲を割り当てることができます。また、以下のように演算子を使うことが出来ます。

演算子 説明
../.. IPアドレスの範囲 (CIDR表記)
! 否定/例外
[.., ..] グループ

入力例は以下の通りです。

意味
! 1.1.1.1 1.1.1.1以外の全てのIPアドレス
![1.1.1.1, 1.1.1.2] 1.1.1.1と1.1.1.2以外の全てのIPアドレス
[$EXTERNAL_NET, !$HOME_NET] EXTERNAL_NETに定義したネットワークの範囲 と HOME_NETに定義したネットワークの範囲以外
[10.0.0.0/24, !10.0.0.5] 10.0.0.5以外の10.0.0.0/24

否定演算子があるのはかなり熱いですよね。10.0.0.0/16以外からの通信のようにセキュリティグループや、NACLでは設定出来なかったシグネチャーを作成することができます。

また、一般的には自分のネットワークの範囲を示す$HOME_NETと自分以外のネットワークの範囲を示す$EXTERNAL_NETという2つの変数を定義して使用することが多いです。

ports

drop tcp $HOME_NET any -> $EXTERNAL_NET any (msg:”ET TROJAN Likely Bot Nick in IRC (USA +..)”; flow:established,to_server; flowbits:isset,is_proto_irc; content:”NICK “; pcre:”/NICK .USA.[0-9]{3,}/i”; reference:url,doc.emergingthreats.net/2008124; classtype:trojan-activity; sid:2008124; rev:2;)

portsは通信のポートを指定する部分です。 左側が送信元のポート、右側が送信先のポートです。

source and destination同様に以下のように演算子を使うことが出来ます。

演算子 説明
: ポート範囲
! 否定/例外
[.., ..] グループ

入力例は以下の通りです。

意味
[80, 81, 82] ポート番号が80, 81, 82
[80: 82] ポート番号が80から82
[1024: ] ポート番号が1024から最大ポート番号まで
!80 ポート番号が80以外
[80:100,!99] ポート番号が99を除く80から100
[1:80,![2,4]] ポート番号が2と4を除く1から80

注意点としては、指定したポートは通信で使用されるプロトコルを自動で判別しないことです。

例えば、httpsの通信を検知するために、以下のようにプロトコルをhttp、ポートは443を指定しても検知されません。
これはhttpをtcp/443で通信した場合として判別されます。

alert http $HOME_NET any -> $EXTERNAL_NET 443 (msg:”DETECTED https”; flow:to_server, established; sid:1000001; rev:1;)

httpsの通信を検知する場合は、以下のようにtlsを使って記述します。

alert tls $HOME_NET any -> $EXTERNAL_NET any (msg:"DETECTED https"; flow:to_server, established; sid:1000001; rev:1;)

direction

drop tcp $HOME_NET any -> $EXTERNAL_NET any (msg:”ET TROJAN Likely Bot Nick in IRC (USA +..)”; flow:established,to_server; flowbits:isset,is_proto_irc; content:”NICK “; pcre:”/NICK .USA.[0-9]{3,}/i”; reference:url,doc.emergingthreats.net/2008124; classtype:trojan-activity; sid:2008124; rev:2;)

directionは通信の方向性を指定する部分です。種類は以下の通り2つあります。

演算子 意味
-> source -> destination sourceからdestionationへの通信
<> source <> destination sourceからdestionationへの通信
destionationからsourceへの通信

ちなみに<>とした場合は、リクエストとレスポンスの通信、それぞれで検知されることになります。 そのため、ほとんどのシグネチャーでは->が使われます。

options

optionsは数が多いので、以下の通り、よく使われるものを抜粋して説明します。

  • sid
  • rev
  • msg
  • priority
  • metadata
  • content
  • nocase
  • depth / offset
  • isdataat
  • startswith / endswith
  • dotprefix
  • flow
  • http
  • ssl/tls

sid

drop tcp $HOME_NET any -> $EXTERNAL_NET any (msg:”ET TROJAN Likely Bot Nick in IRC (USA +..)”; flow:established,to_server; flowbits:isset,is_proto_irc; content:”NICK “; pcre:”/NICK .USA.[0-9]{3,}/i”; reference:url,doc.emergingthreats.net/2008124; classtype:trojan-activity; sid:2008124; rev:2;)

sidはoptionsで唯一必須の項目です。sidはシグネチャーに独自のIDを付与します。そのため、他のシグネチャーと重複することはできません。
sidは一意にする必要があるため、以下のようにユーザーが作成したシグネチャーに割り当てる範囲は決められています。

sidの範囲 割り当て対象
1000000-1999999 ユーザーが作成したシグネチャー
2000000-2099999 Emerging Threatsのオープンシグネチャーセット
2100000-2103999 フォークされたEmerging Threatsのシグネチャー
2200000-2200999 Suricataデコーダのイベント
2210000-2210999 Suricataストリームのイベント
2220000-2299999 Suricataの予約領域
2800000-2899999 Emerging Threats Pro Full Coverage Ruleset

参考: Emerging Threats Threats SID Allocation

また、sidoptionsの最後または、revが存在する場合は最後から2番目に記述することがほとんどです。

rev

drop tcp $HOME_NET any -> $EXTERNAL_NET any (msg:”ET TROJAN Likely Bot Nick in IRC (USA +..)”; flow:established,to_server; flowbits:isset,is_proto_irc; content:”NICK “; pcre:”/NICK .USA.[0-9]{3,}/i”; reference:url,doc.emergingthreats.net/2008124; classtype:trojan-activity; sid:2008124; rev:2;)

revはシグネチャーのバージョンを表します。シグネチャーを変更した場合は、revの値をインクリメントします。

msg

drop tcp $HOME_NET any -> $EXTERNAL_NET any (msg:”ET TROJAN Likely Bot Nick in IRC (USA +..)”; flow:established,to_server; flowbits:isset,is_proto_irc; content:”NICK “; pcre:”/NICK .USA.[0-9]{3,}/i”; reference:url,doc.emergingthreats.net/2008124; classtype:trojan-activity; sid:2008124; rev:2;)

msgはシグネチャーがマッチした際に、出力する文字です。

priority

alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"DETECTED dev.classmethod.jp"; http.host; content:"dev.classmethod.jp"; startswith; endswith; priority:1; flow:to_server, established; sid:1000001; rev:1;)

priorityはシグネチャーの優先度を表します。1〜255の範囲の値で指定しますが、1〜4の数字が最もよく使用されます。優先度の高いシグネチャーが最初に検査されます。最も高い優先度は1です。
優先度を上手く使用すれば、どのルールにも当てはまらなかったものは、drop/passといったホワイトリスト/ブラックリスト運用が可能になります。

metadata

alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"DETECTED dev.classmethod.jp"; http.host; content:"dev.classmethod.jp"; startswith; endswith; flow:to_server, established; sid:1000001; rev:1; metadata:created_at 2021_05_05, updated_at 2021_05_05;)

metadataはシグネチャーに任意の情報を付与します。Suricataのアラートにも表示されるため、ログを集計するときのキーとして使用することもできます。

content

alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"DETECTED dev.classmethod.jp"; http.host; content:"dev.classmethod.jp"; startswith; endswith; flow:to_server, established; sid:1000001; rev:1;)

contentはパケットまたはストリームのペイロードの内容の検査文字列です。普通の文字はもちろん、改行や:"などの特殊文字、メタ文字は16進表記で表現することができます。
16進表記は大文字で書くのが慣例です。以下は16進表記で入力した例です。

|61| = a
|61 61| = aa
|41| = A
|21| = !
|0D 0A| = Windowsの改行コード
|22| = "
|3B| = ;
|3A| = :
|7C| = |

検索範囲としては、デフォルトではペイロードのすべてが対象になり、部分一致するものを見つけようとします。
また、検索の際は大文字と小文字が区別されます。

否定を意味する!も使用することできます。以下のシグネチャーでは、Firefoxの使用バージョンが3.6.13でない場合にアラートを出します。

alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"Outdated Firefox on Windows"; content:"User-Agent|3A| Mozilla/5.0 |28|Windows|3B| "; content:"Firefox/3."; distance:0; content:!"Firefox/3.6.13"; distance:-10; sid:9000000; rev:1;)

nocase

alert tcp $EXTERNAL_NET any -> $HTTP_SERVERS $HTTP_PORTS (msg:\".htpasswd access attempt\"; flow:to_server,established; content:\".htpasswd\"; nocase; sid:210503; rev:1;)"

nocaseは大文字と小文字の区別を無視する際に使用するオプションです。上のようにcontentの後ろに配置します。

depth / offset

alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"DETECTED dev.classmethod.jp"; http.host; content:"dev.classmethod.jp"; depth:18; offset:0; flow:to_server, established; sid:1000001; rev:1;)

depthはペイロードの先頭から何バイトまでが検索対象かを指定します。
offsetはペイロードのどのバイトからチェックされるかを指定します。

上の例だと、先頭の0バイトから18バイトまでをチェック対象となります。 depthoffsetを使用することで、ペイロードの一部分のみが検索対象となり、処理負荷軽減につながります。

isdataat

alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"DETECTED .jp"; http.host; content:"jp"; isdatat:!1,relative; flow:to_server, established; sid:1000001; rev:1;)

isdataatはペイロードの特定の部分にまだデータがあるかどうかを確認することができます。relativeというオプションを使用して、最後に一致した文字列の後にまだデータがあるかどうかを確認します。 否定を意味する!も使用することできます。
上の例では、HTTP Hostヘッダーの末尾が.jpである場合に検知されます。

startswith / endswith

alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"DETECTED dev.classmethod.jp"; http.host; content:"dev.classmethod.jp"; startswith; endswith; flow:to_server, established; sid:1000001; rev:1;)

startswithcontentで指定した文字列が先頭であることを指定します。正規表現で言うところの、^です。
endswithcontentで指定した文字列が末尾であることを指定します。正規表現で言うところの、$です。

どちらもcontentの後ろに記述する必要があります。

通常、contentで指定した文字列はペイロード内を部分一致検索をしますが、startswithendswithを組みあせて使うことで、完全一致で検索することができます。
上の例では、HTTP Hostヘッダーがdev.classmethod.jpである場合に検知されます。

dotprefix

alert dns any any -> any any (msg:"DETECTED .classmethod.jp dns query"; dns.query; dotprefix; content:".classmethod.jp"; sid:1000001; rev:1;)

dotprefixは、contentに含まれる.をワイルドカードとして扱います。
上の例では、DNSクエリがdev.classmethod.jpであっても検知されます。

flow

drop tcp $HOME_NET any -> $EXTERNAL_NET any (msg:”ET TROJAN Likely Bot Nick in IRC (USA +..)”; flow:established,to_server; flowbits:isset,is_proto_irc; content:”NICK “; pcre:”/NICK .USA.[0-9]{3,}/i”; reference:url,doc.emergingthreats.net/2008124; classtype:trojan-activity; sid:2008124; rev:2;)

flowは、通信の方向を指定することができます。以下のようにflow:<オプション>で、サーバー/クライアントのどちらからの通信か、3ウェイ・ハンドシェイクの後なのかを指定することができます。

オプション 説明
to_client サーバーからクライアントへのパケットを照合する
to_server クライアントからサーバーへのパケットを照合する
from_client クライアントからサーバーへのパケットを照合する(to_serverと同じ)。
from_server サーバーからクライアントへのパケットを照合する(to_clientと同じ)。
established 確立された接続に対して照合する
not_established 確立されていない接続のパケットに対して照合する。
stateless 確立された接続の一部であるパケットとそうでないパケットを照合する
only_stream 再構築されたパケットまたは確立された通信中のパケットを照合する
no_stream 再構築されたパケットまたは確立された通信中のパケット以外と照合する
only_frag フラグメントから再構成されたパケットを照合する
no_frag フラグメントから再構成されていないパケットを照合する

http

今までcontentで指定した文字列がペイロードに含まれるかと言う説明をしていましたが、ペイロードのどこなのかの指定をしていませんでした。 httpを使うと、HTTPトラフィックの特定の部分のみをチェックするよう指定することができます。例えば、リクエストURI、Cookie、HTTPリクエストまたはレスポンスの本文などが指定できます。

httpは以下に記載するとおり、多くの指定方法があります。

値                       説明 通信方向         
http.uri リクエストURIを検索対象とします。
ペイロードが、「GET/index.html HTTP/1.0\n」のときに、「index.html」があるかどうか検索したい場合は、「http.uri; content:/index.html;」 とします。
リクエスト
http.uri.raw リクエストURIを検索対象とします。
http.uriとの違いは16進数表記で書かれた文字を変換せずそのまま解釈するところです。
|%20|という文字があった時、http.uriではスペースと認識しますが、http.uri.rawでは、そのまま|%20|という文字として認識します。
リクエスト
http.method HTTPのメソッド部分(GET、POST、PUT、HEAD、 DELETE、TRACE、OPTIONS、CONNECT、およびPATCH)を検索対象とします。
GETメソッドを検知したいのに、「content:"GET"」としてしまうと、htmlファイル名に"GET"が含まれたときに意図しない検知をしてしまいます。
リクエスト
http.request_line HTTPリクエスト全てを検索対象とします。 リクエスト
http.request_body HTTPリクエストボディを検索対象とします。 リクエスト
http.header HTTPヘッダーを検索対象とします。 両方向
http.header.raw HTTPヘッダーを検索対象とします。
http.headerは末尾の空白とタブ文字はすべて削除しますが、http.header.rawはそのまま取得できます。
両方向
http.cookie cookieを検索対象とします。 両方向
http.user_agent HTTPリクエストヘッダーのUser-Agentヘッダーを検索対象とします。 リクエスト
http.host HTTP hostヘッダーを検索対象とします。 リクエスト
http.host.raw HTTP hostヘッダーを検索対象とします。http.hostはすべて小文字であるように正規化されますが、http.host.rawは大文字、小文字を区別します。 リクエスト
http.accept HTTP Acceptヘッダーを検索対象とします。 リクエスト
http.accept_lang HTTP Accept-Encodingヘッダーを検索対象とします。 リクエスト
http.accept_enc HTTP Accept-Languageヘッダーを検索対象とします。 リクエスト
http.referer HTTP Refererヘッダーを検索対象とします。 リクエスト
http.connection HTTP Connection ヘッダーを検索対象とします。 リクエスト
http.content_type HTTP Content-Typeヘッダーを検索対象とします。 両方向
http.content_len HTTP Content-Lengthヘッダーを検索対象とします。 両方向
http.start HTTPリクエストまたはレスポンスの開始を検索対象とします。 両方向
http.protocol HTTPリクエストまたはレスポンスのプロトコルフィールドを検索対象とします。 両方向
http.header_names HTTPヘッダーの名前を検索対象とします。
ヘッダーが存在しないことを確認したり、ヘッダーが特定の順序に並んでいるか確認したりする場合に便利です。
両方向
http.stat_msg HTTPステータスメッセージを検索対象とします。 レスポンス
http.stat_code HTTPステータスコードを検索対象とします。 レスポンス
http.response_line HTTPレスポンス全てを検索対象とします。 レスポンス
http.header HTTPヘッダーを検索対象とします。
なお、cookieなど一部の独自の値については他の値で取得する必要があります。
両方向
http.header.raw HTTPヘッダーを検索対象とします。http.headerはすべて小文字であるように正規化されますが、http.header.rawは大文字、小文字を区別します。 両方向
http.cookie cookieを検索対象とします。 両方向
http.server HTTP Serverヘッダーを検索対象とします。 レスポンス
http.location HTTP locationヘッダーを検索対象とします。 レスポンス
file_data HTTPレスポンスボディを検索対象とします。 レスポンス

ssl/tls

ssl/tlsはSSL/TLSのハンドシェイクのさまざまなプロパティを検索対象とします。よく利用されるものを抜粋して以下に記載します。

説明
tls.cert_subject TLS/SSL証明書のSubjectフィールドを検索対象とします。
tls.sni TLS/SSL通信をしているServer Name Indication(SNI)フィールドを検索対象とします。
tls_cert_expired 証明書の有効期限が切れている場合、trueを返します。
tls_cert_valid 証明書の有効期限が切れていない場合、trueを返します。
tls.version 使用されているTLSのバージョンを返却します。
TLS 1.2が使われているか確認する際は、「tls.version:1.2;」とします。
ssl_version 使用されているSSL/TLSのバージョンを返却します。
TLS 1.2が使われているか確認する際は、「ssl_version:tls1.2;」とします。

Network Firewall上での評価フロー

Network Firewallはステートレスエンジンと、ステートフルエンジンとの2つの検査エンジンがあります。そして以下の通り、ステートフルエンジン自体がSuricata互換です。
そのため、ステートフルエンジンで動作する、Domain Listや、5-tupleで設定するルールについても、Suricataのシグネチャーに置き換えて書くことができます。

The stateful engine takes rules that are compatible with Suricata, an open source intrusion prevention system (IPS). Suricata provides a standard rule-based language for stateful network traffic inspection.

そもそも、ステートレスエンジンとステートフルエンジンの関係性は? という方は以下の図をご覧ください。

抜粋: Network Firewall stateless and stateful rules engines

評価のフローとしては以下の通りです。

  1. ステートレスエンジン内で設定した優先度順に評価する
    • 設定したどのルールにも合致しなかった場合は、デフォルトアクションで設定した処理に進む
    • ステートレスエンジンで設定できる処理は、以下の通り
      • alert
      • pass
      • drop
      • Forward to stateful rule groups
  2. ステートレスエンジンから転送された処理をステートフルエンジンで順番に評価する
    • ステートフルエンジンで設定できる処理は、以下の通りで、上から優先度が高い
      • pass
      • drop
      • aler

また、Network Firewallが互換性のあるSuricataのバージョンは、以下の通り、バージョン5.0.2です。

A stateful rule group is a rule group that uses Suricata compatible intrusion prevention system (IPS) specifications. Suricata is an open source network IPS that includes a standard rule-based language for stateful network traffic inspection. AWS Network Firewall supports Suricata version 5.0.2.

Stateful rule groups in AWS Network Firewall

上述したSuricataの説明で参考にしたドキュメントも、Suricata 5.0.2の公式ドキュメントになります。

やってみた

キャパシティの計算

まずは、キャパシティの計算をする必要があります。Network Firewallには設定できるルール数にキャパシティという制限(クォーター)があります。

キャパシティはファイアウォールポリシー1つにあたり以下のようになっています。

  • ルール数の上限
    • ステートレスルールグループ: 10,000ルール
    • ステートフルルールグループ: 30,000ルール

キャパシティの変更は作成後はできず、再作成するしかないので、今後増えるであろうルールを予測して設定する必要があります。

今回のSuricata互換のIPSルールは、1行あたり1つキャパシティを消費します。 そのため、文字数の上限(上限1,000,000バイト)もありますが、最大30,000行のルールを設定することができます。

仮に、キャパシティの上限を超えてしまうと、更新しようとしたタイミングで、StatefulRules capacity exceededと表示されます。

今回は、以下の通り、httpもしくはhttpsでdev.classmethod.jpに通信したときに、アラートを出すというを設定しようと思います。 設定ルールが2行あるので、必要なキャパシティは2になります。ただ、今回は少し余裕を持って、10で設定しておきます。

./ips.rules

alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"DETECTED http://dev.classmethod.jp"; http.host; content:"dev.classmethod.jp"; startswith; endswith; flow:to_server, established; sid:1000001; rev:1;)
alert tls $HOME_NET any -> $EXTERNAL_NET any (msg:"DETECTED https://dev.classmethod.jp"; tls.sni; content:"dev.classmethod.jp"; startswith; endswith; flow:to_server, established; sid:1000002; rev:1;)

CDKでデプロイ

いつものごとくCDKでやっていきます。 構成は前回と同じで以下の通りです。

CDK内部では、Suricataのシグネチャーを記載したファイルを読み込むようにしました。

./lib/app-stack.ts

    // Read IPS rules
    const ipsRules = fs.readFileSync("./ips.rules", "utf8");

    // Create Network Firewall rule group
    const networkfirewallRuleGroupIps = new networkfirewall.CfnRuleGroup(
      this,
      "NetworkfirewallRuleGroupIps",
      {
        capacity: 10,
        ruleGroupName: "IpsRuleGroup",
        type: "STATEFUL",
        ruleGroup: {
          rulesSource: {
            rulesString: ipsRules,
          },
          ruleVariables: {
            ipSets: {
              HOME_NET: {
                definition: [vpc.vpcCidrBlock],
              },
              EXTERNAL_NET: {
                definition: ["any"],
              },
            },
          },
        },
      }
    );

デプロイ後、セッションマネージャーでEC2インスタンスにログインして、curlhttp://dev.classmethod.jpと、https://dev.classmethod.jpにアクセスしました。 ログは以下の通りで、通信は正常にできています。

sh-4.2$ curl -I http://dev.classmethod.jp
HTTP/1.1 301 Moved Permanently
Server: awselb/2.0
Date: Fri, 07 May 2021 07:29:52 GMT
Content-Type: text/html
Content-Length: 134
Connection: keep-alive
Location: https://dev.classmethod.jp:443/

sh-4.2$ curl -I https://dev.classmethod.jp
HTTP/2 200
date: Fri, 07 May 2021 07:30:07 GMT
content-type: text/html; charset=utf-8
content-length: 169005
set-cookie: AWSALB=szpO81N0hPEcO6Z4YcGajE9EEO1mrqvcHEBE48ojUcFPilgJ68oLHLf9fGWuENoogoUUncxwyoPjiYxAaloHoqqSe1Ow7Pg0V9RUGXBvLzcnlbm1cnIi63xgXTIN; Expires=Fri, 14 May 2021 07:30:07 GMT; Path=/
set-cookie: AWSALBCORS=szpO81N0hPEcO6Z4YcGajE9EEO1mrqvcHEBE48ojUcFPilgJ68oLHLf9fGWuENoogoUUncxwyoPjiYxAaloHoqqSe1Ow7Pg0V9RUGXBvLzcnlbm1cnIi63xgXTIN; Expires=Fri, 14 May 2021 07:30:07 GMT; Path=/; SameSite=None; Secure
server: nginx/1.18.0
vary: Accept-Encoding
x-amzn-requestid: 6c343df9-51a9-49aa-aed7-210817682b69
x-amzn-remapped-content-length: 169005
x-amz-apigw-id: e8nXYEvDNjMFo4Q=
cache-control: max-age=300
etag: "2942d-baiRZkF+/OR2k69LHWf0LUyDoHg"
x-powered-by: Express
x-amzn-trace-id: Root=1-6094ec2f-3ee1cbf2743b46aa0e4d8a48;Sampled=0
via: 1.1 0c2ca767ecc2f5a180d1781f16f1e2f3.cloudfront.net (CloudFront), 1.1 fd95d915cb5f672e4b8b3613a0dde9ea.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-C4
vary: Accept-Encoding
x-cache: Hit from cloudfront
x-amz-cf-pop: NRT12-C2
x-amz-cf-id: TNOymqDo6NUGNUgdtKb_vZkMlf_JdHhD70HGslAAflcj_6gutdCGbQ==
age: 33
expires: Fri, 07 May 2021 07:35:07 GMT
strict-transport-security: max-age=31536000; includeSubDomains
accept-ranges: bytes

CloudWatch Logsに出力しているAlertを確認してみると、以下の通り2つログが出力されていました。

{
    "firewall_name": "NetworkFirewall",
    "availability_zone": "us-east-1a",
    "event_timestamp": "1620372592",
    "event": {
        "timestamp": "2021-05-07T07:29:52.667151+0000",
        "flow_id": 429088379510758,
        "event_type": "alert",
        "src_ip": "10.0.1.150",
        "src_port": 3481,
        "dest_ip": "75.2.71.201",
        "dest_port": 80,
        "proto": "TCP",
        "tx_id": 0,
        "alert": {
            "action": "allowed",
            "signature_id": 1000001,
            "rev": 1,
            "signature": "DETECTED http://dev.classmethod.jp",
            "category": "",
            "severity": 3
        },
        "http": {
            "hostname": "dev.classmethod.jp",
            "url": "/",
            "http_user_agent": "curl/7.61.1",
            "http_method": "HEAD",
            "protocol": "HTTP/1.1",
            "length": 0
        },
        "app_proto": "http"
    }
},
{
    "firewall_name": "NetworkFirewall",
    "availability_zone": "us-east-1a",
    "event_timestamp": "1620372606",
    "event": {
        "timestamp": "2021-05-07T07:30:06.627396+0000",
        "flow_id": 980871418901001,
        "event_type": "alert",
        "src_ip": "10.0.1.150",
        "src_port": 9696,
        "dest_ip": "99.83.185.173",
        "dest_port": 443,
        "proto": "TCP",
        "tx_id": 0,
        "alert": {
            "action": "allowed",
            "signature_id": 1000002,
            "rev": 1,
            "signature": "DETECTED https://dev.classmethod.jp",
            "category": "",
            "severity": 3
        },
        "tls": {
            "sni": "dev.classmethod.jp",
            "version": "UNDETERMINED",
            "ja3": {},
            "ja3s": {}
        },
        "app_proto": "tls"
    }
}

「VPCエンドポイントは不要なのか検証してみた」のリベンジ

Suricata互換のIPSルールを使えば、自分で細かい設定をする必要があるとは言え、Domain Listも使えるし、優先度も設定できるしということで、最高ですね!!

ということで、前回企画倒れになってしまった「Network FirewallとNAT Gatewayを使えば、VPCエンドポイントは不要なのか検証してみた」にリベンジしてみようと思います。
最早、私を突き動かしているのはロマンです。

早速、作成した環境からSSMエンドポイントを削除し、IPSルールを以下のように変更してから、CDKで再度デプロイしました。

./ips.rules

pass tls $HOME_NET any -> $EXTERNAL_NET any (msg:"DETECTED https://ssm.us-east-1.amazonaws.com"; tls.sni; content:"ssm.us-east-1.amazonaws.com"; startswith; endswith; flow:to_server, established; priority:1; sid:1100001; rev:1;)
pass tls $HOME_NET any -> $EXTERNAL_NET any (msg:"DETECTED https://ssmmessages.us-east-1.amazonaws.com"; tls.sni; content:"ssmmessages.us-east-1.amazonaws.com"; startswith; endswith; flow:to_server, established; priority:1; sid:1100002; rev:1;)
pass tls $HOME_NET any -> $EXTERNAL_NET any (msg:"DETECTED https://ec2messages.us-east-1.amazonaws.com"; tls.sni; content:"ec2messages.us-east-1.amazonaws.com"; startswith; endswith; flow:to_server, established; priority:1; sid:1100003; rev:1;)
drop tls $HOME_NET any <> $EXTERNAL_NET any (msg:"not matching any allowlisted FQDNs"; flow:to_server, established, only_stream; priority:254; sid:1200001; rev:1;)
drop ip $HOME_NET any <> $EXTERNAL_NET any (msg:"not matching any allowlisted FQDNs"; flow:to_server, established; priority:255; sid:1200002; rev:1;)

すると、以下ご覧の通り、セッションマネージャーで接続できて、pingや許可したドメイン以外のhttp/httpsの通信は出来ないことが確認できました!!
リベンジ成功です!!!

それでは設定したルールの説明をします。 大枠としては、SSMに必要な通信だけは許可して、それ以外の通信はドロップするように指定しています。

まず、1行目から3行目SSMに必要なFQDNを許可します。ポイントは3行目と4行目にあるflow:to_serverです。
4行目のflow:to_server, established, only_stream;はSSMの通信で再構成前のパケットを許可するためです。仮に、flow:to_server, established;としてしまうと、再構成前のパケットも検知対象となってしまい、マネージドインスタンスとして登録されません。
5行目は許可していない通信を明示的にドロップするように指定します。

提供されているシグネチャーの適用

参考までに、ネット上に公開されているSuricata互換のシグネチャーを適用してみます。適用するシグネチャーはProofpointが提供しているEmerging Threatsです。

今回は、https://rules.emergingthreats.net/open/suricata/emerging-all.rules の中の以下シグネチャーを適用してみました。

./ips.rules

alert http $EXTERNAL_NET any -> $HOME_NET any (msg:"ET WEB_CLIENT Mozilla Firefox nsTreeSelection Element invalidateSelection Remote Code Execution Attempt"; flow:established,to_client; content:"document.getElementById(|27|treeset|27|)"; nocase; content:"view.selection"; nocase; distance:0; content:"invalidateRange"; nocase; distance:0; reference:bid,41853; reference:cve,2010-2753; classtype:attempted-user; sid:2013144; rev:2; metadata:affected_product Web_Browsers, affected_product Web_Browser_Plugins, attack_target Client_Endpoint, created_at 2011_06_30, deployment Perimeter, signature_severity Major, tag Web_Client_Attacks, updated_at 2016_07_01;)

alert http $EXTERNAL_NET any -> $HOME_NET any (msg:"ET WEB_CLIENT Download of PDF With Compressed Flash Content"; flowbits:noalert; flow:established,to_client; content:"stream"; content:"|0A|CWS"; within:5; fast_pattern; pcre:"/stream(\x0D\x0A|\x0A)CWS/"; flowbits:set,ET.flash.pdf; reference:url,www.symantec.com/connect/blogs/analysis-zero-day-exploit-adobe-flash-and-reader; reference:url,blog.zynamics.com/2010/06/09/analyzing-the-currently-exploited-0-day-for-adobe-reader-and-adobe-flash/; classtype:misc-activity; sid:2012907; rev:3; metadata:affected_product Web_Browsers, affected_product Web_Browser_Plugins, attack_target Client_Endpoint, created_at 2011_05_31, deployment Perimeter, signature_severity Major, tag Web_Client_Attacks, updated_at 2016_07_01;)

alert http $EXTERNAL_NET any -> $HOME_NET any (msg:"ET WEB_CLIENT Possible Adobe Multimedia Doc.media.newPlayer Memory Corruption Attempt"; flow:to_client,established; content:"PDF-"; depth:300; content:"this.media.newPlayer|28|null"; nocase; distance:0; content:"util.printd"; nocase; within:150; reference:url,www.metasploit.com/redmine/projects/framework/repository/revisions/7881/entry/modules/exploits/windows/fileformat/adobe_media_newplayer.rb; reference:url,vrt-sourcefire.blogspot.com/2009/12/adobe-reader-medianewplayer-analysis.html; reference:bid,37331; reference:cve,2009-4324; classtype:attempted-user; sid:2010495; rev:13; metadata:affected_product Web_Browsers, affected_product Web_Browser_Plugins, attack_target Client_Endpoint, created_at 2010_07_30, deployment Perimeter, signature_severity Major, tag Web_Client_Attacks, updated_at 2016_07_01;)

cdk diffをすると、以下のように正しくシグネチャーの差分情報が出力されました。

> npx cdk diff   
Stack AppStack
Resources
[~] AWS::NetworkFirewall::RuleGroup NetworkfirewallRuleGroupIps NetworkfirewallRuleGroupIps
 └─ [~] RuleGroup
     └─ [~] .RulesSource:
         └─ [~] .RulesString:
             ├─ [-] pass tls $HOME_NET any -> $EXTERNAL_NET any (msg:"DETECTED https://ssm.us-east-1.amazonaws.com"; tls.sni; content:"ssm.us-east-1.amazonaws.com"; startswith; endswith; flow:to_server, established; priority:1; sid:1100001; rev:1;)
pass tls $HOME_NET any -> $EXTERNAL_NET any (msg:"DETECTED https://ssmmessages.us-east-1.amazonaws.com"; tls.sni; content:"ssmmessages.us-east-1.amazonaws.com"; startswith; endswith; flow:to_server, established; priority:1; sid:1100002; rev:1;)
pass tls $HOME_NET any -> $EXTERNAL_NET any (msg:"DETECTED https://ec2messages.us-east-1.amazonaws.com"; tls.sni; content:"ec2messages.us-east-1.amazonaws.com"; startswith; endswith; flow:to_server, established; priority:1; sid:1100003; rev:1;)
drop tls $HOME_NET any <> $EXTERNAL_NET any (msg:"not matching any allowlisted FQDNs"; flow:to_server, established, only_stream; priority:254; sid:1200001; rev:1;)
drop ip $HOME_NET any <> $EXTERNAL_NET any (msg:"not matching any allowlisted FQDNs"; flow:to_server; priority:255; sid:1200002; rev:1;)
             └─ [+] alert http $EXTERNAL_NET any -> $HOME_NET any (msg:"ET WEB_CLIENT Mozilla Firefox nsTreeSelection Element invalidateSelection Remote Code Execution Attempt"; flow:established,to_client; content:"document.getElementById(|27|treeset|27|)"; nocase; content:"view.selection"; nocase; distance:0; content:"invalidateRange"; nocase; distance:0; reference:bid,41853; reference:cve,2010-2753; classtype:attempted-user; sid:2013144; rev:2; metadata:affected_product Web_Browsers, affected_product Web_Browser_Plugins, attack_target Client_Endpoint, created_at 2011_06_30, deployment Perimeter, signature_severity Major, tag Web_Client_Attacks, updated_at 2016_07_01;)

alert http $EXTERNAL_NET any -> $HOME_NET any (msg:"ET WEB_CLIENT Download of PDF With Compressed Flash Content"; flowbits:noalert; flow:established,to_client; content:"stream"; content:"|0A|CWS"; within:5; fast_pattern; pcre:"/stream(\x0D\x0A|\x0A)CWS/"; flowbits:set,ET.flash.pdf; reference:url,www.symantec.com/connect/blogs/analysis-zero-day-exploit-adobe-flash-and-reader; reference:url,blog.zynamics.com/2010/06/09/analyzing-the-currently-exploited-0-day-for-adobe-reader-and-adobe-flash/; classtype:misc-activity; sid:2012907; rev:3; metadata:affected_product Web_Browsers, affected_product Web_Browser_Plugins, attack_target Client_Endpoint, created_at 2011_05_31, deployment Perimeter, signature_severity Major, tag Web_Client_Attacks, updated_at 2016_07_01;)

alert http $EXTERNAL_NET any -> $HOME_NET any (msg:"ET WEB_CLIENT Possible Adobe Multimedia Doc.media.newPlayer Memory Corruption Attempt"; flow:to_client,established; content:"PDF-"; depth:300; content:"this.media.newPlayer|28|null"; nocase; distance:0; content:"util.printd"; nocase; within:150; reference:url,www.metasploit.com/redmine/projects/framework/repository/revisions/7881/entry/modules/exploits/windows/fileformat/adobe_media_newplayer.rb; reference:url,vrt-sourcefire.blogspot.com/2009/12/adobe-reader-medianewplayer-analysis.html; reference:bid,37331; reference:cve,2009-4324; classtype:attempted-user; sid:2010495; rev:13; metadata:affected_product Web_Browsers, affected_product Web_Browser_Plugins, attack_target Client_Endpoint, created_at 2010_07_30, deployment Perimeter, signature_severity Major, tag Web_Client_Attacks, updated_at 2016_07_01;)

デプロイ後、マネージメントコンソールからルールグループを確認すると、以下のようになっていました。分量が多くなると、マネージメントコンソールで管理をするのはちょっと大変そうですね。

検知したシグネチャーの集計

IPSで様々な通信を許可/拒否できることを確認しましたが、どういった通信が検知されたのか気になりますよね。 Network FirewallのアラートログをCloudWatch Logsに出力していたので、今回はCloudWatch Logs Insightsで集計してみます。

CloudWatch Logs Insightsのクエリは以下ブログを参考にしました。

Network Firewallのアラートを出力していたCloudWatch Logsを選択し、以下クエリを実行します。

fields @timestamp, @message
| sort @timestamp desc
| limit 20

直近20件のログが抽出されています。

次はどのシグネチャー毎の検知数を確認してみます。 以下のクエリで、シグネチャーのメッセージであるevent.alert.signatureをカウントします。

fields @message
| stats count(event.alert.signature) by event.alert.signature

以下の通り、シグネチャー毎の検知数が確認できました。運用担当の方もこんなに簡単に集計できると嬉しいのではないでしょうか。

Network FirewallはSuricataを使いこなすと楽しくなる

Suricata互換のルールを使うことによって、一気にできる幅が広がったような感覚です。 IPS機能実装するのがめんどくさい...といった方も、Emerging Threatsなど公開されているSuricataのルールから試してみてはいかがでしょうか。

私自身はこのGWはNetwork Firewallと格闘して、ようやく友達になれたような気がします。これからもNetwork Firewallを上手く活用していこうと思います!!

以上、東京オフィスの のんピ(@non____97)でした!