- OpenSSH サーバー
sshd のシグナルハンドラの競合状態により、認証前の制限時間 LoginGraceTime 内で、未認証クライアントがデフォルト設定のサーバー上でリモートコード実行を引き起こせる可能性がある
- この脆弱性は 2006 年の CVE-2006-5051 の**回帰(regression)**であり、2020 年 10 月の OpenSSH 8.5p1 のコミットが
sigdie() から安全策を削除したことで、8.5p1 以上 9.8p1 未満に再び発生した
- glibc ベースの Linux では
syslog() が malloc()・free() のようなasync-signal-unsafe 関数を呼び出すため、サンドボックス化されていない sshd の privileged コードで、未認証の root RCE につながる可能性がある
- 実験は i386 仮想マシンと約 10ms のパケットジッタを持つ安定したネットワークで実施され、Debian 12.5.0 OpenSSH 9.2p1 では平均約 10,000 回の試行と
MaxStartups=100、LoginGraceTime=120 の条件で、root shell 取得まで約 6〜8 時間を要した
- OpenSSH は 2024 年 6 月 6 日のコミット
81c1099 で修正しており、更新や再コンパイルが難しい場合は LoginGraceTime 0 によって RCE は防げるが、MaxStartups 接続枯渇 DoS のリスクは残る
脆弱性が発生する箇所
- OpenSSH
sshd の問題は、認証前に動作するSIGALRM ハンドラから始まる
- クライアントが
LoginGraceTime 内に認証しない場合、SIGALRM ハンドラが非同期に呼び出される
- このハンドラが
syslog() のような async-signal-safe ではない関数を呼び出す
- デフォルト値は
LoginGraceTime=120 秒で、古い OpenSSH バージョンでは 600 秒だった
- この脆弱性は CVE-2006-5051 の回帰である
- CVE-2006-5051 は 2006 年に Mark Dowd が報告した、OpenSSH 4.4 より前のシグナルハンドラ競合状態である
- 2020 年 10 月の OpenSSH 8.5p1 のコミット
752250c が、sigdie() から #ifdef DO_LOG_SAFE_IN_SIGHAND を誤って削除した
- バージョンごとの影響範囲は明確に分かれる
- OpenSSH 4.4p1 未満: CVE-2006-5051 または CVE-2008-4109 関連パッチがバックポートされていなければ脆弱
- OpenSSH 4.4p1 以上 8.5p1 未満:
sigdie() が安全な _exit(1) 呼び出しに変更されており、この競合状態には脆弱ではない
- OpenSSH 8.5p1 以上 9.8p1 未満: 安全策の削除により再び脆弱になった
影響を受ける環境と例外
- リモート悪用の対象はglibc ベースの Linuxである
- glibc の
syslog() は内部的に malloc() や free() のような async-signal-unsafe 関数を呼び出す
- 脆弱なコードは
sshd の privileged コード内にあり、サンドボックス化されず full privilege で実行される
- その結果、未認証のリモート root コード実行が可能になる
- 他の libc や OS は調査対象に含まれていない
- OpenBSD は脆弱ではない
- OpenBSD の
SIGALRM ハンドラは syslog() の代わりに syslog_r() を呼び出す
syslog_r() は OpenBSD が 2001 年に作成した、より async-signal-safe なバージョンである
リモート悪用研究の前提
- この競合状態をリモートから悪用するには、3 つの問題を解決する必要がある
SIGALRM が適切なタイミングで割り込んだ際に、sshd を不整合な状態に残すコード経路が必要である
- そのコード経路に到達し、適切なタイミングで中断される確率を高めなければならない
- リモートのネットワーク環境でもそのタイミングを合わせる必要がある
- 研究は最新の防御機構にすぐ挑むのではなく、古い OpenSSH の i386 環境から始め、最近のバージョンへ拡張された
- 実験条件には明確な限界がある
- bare-metal サーバーではなく仮想マシンのみを対象とした
- ネットワークは約 10ms のパケットジッタを持つ比較的安定したリンクだった
- エクスプロイトの複数の部分はさらに改善できる
- amd64 向けのエクスプロイト作業は始まっているが、より強い ASLR のためはるかに難しい
古い OpenSSH を対象にした実験
-
Debian 3.0r6, OpenSSH 3.4p1
- 対象は
SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 で、Debian 3.0r6 の 2005 年環境である
- この Debian バージョンは privilege separation がデフォルトで有効化された最初の Debian バージョンであり、当時の主要な脆弱性パッチが適用されていた
- 悪用は
free() の中断とheap の不整合状態を利用する
- 公開鍵パースコード内の
free() 呼び出しを SIGALRM で中断する
- その後、
packet_close() 内の別の free() で不整合な heap 状態を利用する
- glibc 2.2.5 には Solar Designer の
unlink() 手法に対する強化がなかった
- 攻撃は
__free_hook を上書きし、heap 上の shellcode アドレスへ実行フローを切り替える
- この Debian バージョンには ASLR も NX もない
- タイミング改善後も平均約 10,000 回の試行が必要だった
MaxStartups=10、LoginGraceTime=600 の条件で、リモート root shell 取得まで平均約 1 週間かかった
-
Ubuntu 6.06.1, OpenSSH 4.2p1
- 対象は
SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3 で、Ubuntu 6.06.1 の 2006 年環境である
- CVE-2006-5051 に依然として脆弱な最後の Ubuntu バージョンである
- glibc 2.3.6 は malloc 系関数のエントリ時に mandatory lock を取得するため、malloc 中断後に別の malloc 呼び出しを悪用する方法は deadlock につながる
- 最終的な悪用経路はPAMを利用する
pam_start() が sshd のグローバルな sshpam_handle ポインタを設定する
_pam_add_handler() が中断されると、未初期化の next フィールドが残る可能性がある
pam_end() が SIGALRM ハンドラから呼び出されると、任意ポインタを free() に渡せる可能性がある
- glibc の古い
unlink() 手法は防がれているため、Malloc Maleficarum の House of Mind fastbin 版を使用する
- fake arena を
sshd の .got.plt に向け、_exit() エントリを heap shellcode のアドレスで上書きする
- この Ubuntu の heap はデフォルトで実行可能である
- 平均約 10,000 回の試行が必要だった
MaxStartups=10、LoginGraceTime=120 の条件で、リモート root shell 取得まで平均約 1〜2 日かかった
- 運の悪い攻撃者は、root shell を得る前に 10 本の
MaxStartups 接続をすべて deadlock させる可能性がある
Debian 12.5.0, OpenSSH 9.2p1 の実験
-
syslog() と glibc malloc 経路
- 対象は
SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2 で、Debian 12.5.0 の 2024 年 current stable 環境である
- この環境は CVE-2006-5051 の回帰に脆弱である
- このバージョンの
SIGALRM ハンドラは packet_close() や pam_end() を呼び出さず、syslog() 経路へ進む
grace_alarm_handler() が sigdie() を呼び出す
sigdie() は sshlogv() と do_log() を経由して syslog() を呼び出す
- Debian glibc 2.36 の
syslog() は、初回呼び出し時に malloc を呼び出す
__localtime64_r() 経路で __tzfile_read() が呼び出される
fopen() が FILE 構造体のために malloc(304) を呼び出す
- 内部 read buffer のために
malloc(4096) も呼び出す
- glibc malloc は 2017 年 10 月以降、単一スレッド環境では mandatory lock を取得しない
sshd のような単一スレッドプロセスでは、malloc 競合を悪用できる可能性が生じる
-
ASLR 条件と i386 の制約
- Debian 12.5.0 の i386 環境にはASLR の弱点がある
sshd の PIE、heap、大半のライブラリ、stack は通常ランダム化される
- glibc 自体は常に
0xb7200000 または 0xb7400000 にマップされる
- glibc のアドレスは 50% の確率で当てられる
- エクスプロイトは glibc が
0xb7400000 にマップされていると仮定する
- このアドレスの方が
0xb7200000 よりわずかに多かったためである
-
heap の不整合と FILE 構造体の悪用
- 選択された malloc 経路は、大きな free chunk を 2 つに分割する split 経路である
- 返却される chunk と remainder chunk が生成される
SIGALRM が remainder chunk が unsorted list に接続された後、size フィールド初期化前に割り込むと、heap の不整合が発生する
- 攻撃者は以前の heap allocation の残存データを通じて、remainder chunk の size フィールドを制御する
- remainder chunk を実際より大きく見せ、別の heap chunk と重ね合わせる
- SIGALRM ハンドラ内の malloc がこの chunk を使うことで、heap メモリが破壊される
- 標的は
__tzfile_read() 内の fopen() が heap に割り当てるFILE 構造体である
- 制限付きの heap corruption により、FILE 構造体の
_vtable_offset 1 バイトを上書きする
- glibc の libio 関数が、vtable ポインタを通常位置ではなく別の offset から探すようになる
- 攻撃者はその位置にある fake vtable pointer と
_codecvt pointer を、以前の heap allocation の残存データで制御する
- i386 glibc では、この手法により
__fread_unlocked() 中に任意の関数ポインタ __fct を呼び出せる
- amd64 glibc では
_vtable_offset は使われていないように見える
-
heap レイアウトと 27 個の race window
- 単一の小さな race window だけでは競合状態に勝ちにくいため、27 組の large hole と small holeを作成する
- 28 組では
PACKET_MAX_SIZE 256KB を超える
- 最終パケットは
malloc(~4KB), malloc(304), malloc(~4KB), malloc(304) の順序を強制する
- 各 large hole の split が適切なタイミングで中断されると、SIGALRM ハンドラの
fopen() が対応する small hole を FILE 構造体として割り当てる
- heap レイアウトの構築には
sshd の公開鍵パースコードを利用する
cert_parse() と cert_free() 経路で、ほぼ任意の malloc() と free() シーケンスを実行する
- memory leak を見つけられなかったため、tcache chunk を一時的なbarrier chunkとして使う
- 5 種類の公開鍵 packet を送信する
- a/ 制御できない heap allocation が tcache chunk に入るようにする
- b/ 27 組の large/small hole と barrier chunk を作成する
- c/ fake header、fake footer、fake vtable、
_codecvt pointer を書き込んでおく
- d/ ほぼ 256KB の大きな文字列を割り当て・解放して、hole を unsorted list から各 malloc bin へ移動させる
- e/ 最後の malloc シーケンスを強制し、27 個の小さな race window を開く
-
最新版向けのタイミング戦略
- 古いバージョンで使われたフィードバックベースのタイミング戦略は、OpenSSH 9.2p1 では通用しなかった
- 5 回目の公開鍵パースには約 10ms かかり、large race window が大きすぎた
- OpenSSH 7.8p1 で導入された
user_specific_delay() が応答を最大約 9ms 遅延させ、既存のフィードバックを壊した
- 新しい戦略は、2 種類の意図的なエラー応答時間を比較する
- 公開鍵パース直前にエラーを発生させる packet を送る
- 公開鍵パース直後にエラーを発生させる packet を送る
- 2 つの応答時間差から、最後の公開鍵パース時間を測定する
- この戦略により、平均約 10,000 回の試行で競合状態に勝てた
MaxStartups=100、LoginGraceTime=120 の条件で、競合状態に勝つまで平均約 3〜4 時間かかった
- ASLR のため、リモート root shell 取得までは平均約 6〜8 時間を要した
amd64 エクスプロイトの進捗状況
- amd64 の対象は Rocky Linux 9 である
- 対象イメージは
Rocky-9.4-x86_64-minimal.iso
- OpenSSH 8.7p1 はこのシグナルハンドラ競合状態に脆弱である
- glibc は ASLR の弱点により 2MB 境界の倍数にマップされ、partial pointer overwrite がより強力になる
- Rocky Linux 9 の glibc 2.34
syslog() は内部で __open_memstream() を呼び出す
- heap 上に FILE 構造体を
malloc() する
calloc()、realloc()、free() も呼び出し、追加の余地を与える
- heap corruption primitive、heap に割り当てられる 2 つの FILE 構造体、glibc アドレスの 21 個の固定ビットを踏まえると、amd64 でも悪用可能だと見ている
- 想定時間は i386 の 6〜8 時間より長いが、1 週間未満を見込んでいると述べている
- Ubuntu 24.04 についての別の観察もある
- Ubuntu 24.04 は
sshd child の ASLR を再ランダム化せず、起動時に 1 回だけランダム化する
- 原因は
rexec_flag を無効にする systemd-socket-activation.patch に追跡されている
- 一般的には良くない選択だが、この脆弱性では SIGALRM ハンドラ内の
syslog() が初回 syslog() 呼び出しではないため malloc 関数を呼び出さず、悪用を防ぐ
- 関連パッチ: https://git.launchpad.net/ubuntu/+source/…
パッチと緩和策
- OpenSSH は 2024 年 6 月 6 日のコミット
81c1099 でこの競合状態を修正した
- 81c1099:
sshd(8) に問題のある client behavior を penalise する機能を追加
- async-signal-unsafe コードを
sshd の SIGALRM ハンドラから listener process に移し、同期的に処理する
- この修正は大きなコミット
81c1099 と、さらに大きな defense-in-depth コミット 03e3de4 の上にあるため、バックポートが難しい可能性がある
- バックポートが難しい場合は、
sshsigdie() から async-signal-unsafe コードを削除またはコメントアウトし、_exit(1) だけを呼び出すよう変更できる
- 更新や再コンパイルが不可能な場合は、設定ファイルで
LoginGraceTime を 0 に設定できる
- この設定はこのアドバイザリのリモートコード実行を防ぐ
- その代わり、すべての
MaxStartups 接続が枯渇するDoSには脆弱になる
公開スケジュール
- 2024-05-19: OpenSSH 開発者に連絡し、その後パッチとレビューが繰り返された
- 2024-06-20:
distros@openwall に連絡
- 2024-07-01: coordinated release date として公開
1件のコメント
Hacker News の意見
興味深いことに、RCE の修正はほぼ1か月前に公開の場で「紛れ込んで」いたように見えた
PerSourcePenalties が有効になると、sshd(8) は認証前の子セッションプロセスの終了状態を監視し、認証失敗の繰り返しや sshd のクラッシュといった条件をクライアントアドレスに対して一定時間のペナルティとして記録する
https://github.com/openssh/openssh-portable/commit/81c1099d2...
攻撃者に何かを教えてしまうリバースエンジニアリング可能なパッチというより、バイナリ構造を変えて特定の脆弱性を取り除き、そのエクスプロイト系統全体も緩和する副作用があるようで、かなり巧妙に見える
上の変更はゴミのような接続を処理するために予告されていた機能で、競合状態に勝ちにくくしてこの脆弱性も緩和するだけ
以前の議論: https://news.ycombinator.com/item?id=40610621
人々がスレッドの最初のコメントだけ読んで賛成票を入れ、誤った印象だけを持って去っているのではないかと気になる
OpenSSH のリリースノートの一節が興味深い
「ASLR が有効な 32ビット Linux/glibc システムで、悪用に成功することが実証された。実験室条件では、攻撃はサーバーが許可する最大数まで連続接続を平均6〜8時間維持する必要がある。64ビットシステムでも可能だと考えられているが、まだ実証されていない。このような攻撃は改善される可能性が高い。」
https://www.openssh.com/releasenotes.html
バグが入った diff [1] を見ると、分析上の問題は
sigdie()が#ifdef DO_LOG_SAFE_IN_SIGHANDで囲まれていた形から、sshsigdie()がsshlogv()を直接呼ぶ形にリファクタリングされ、#ifdef が抜け落ちたこと何が防げただろうか。プルリクエストをもっと多くの人が見るべきだったのか。世界中がセキュアな接続に依存しているソフトウェアが、実質的に2人 [2] で保守されているように見えるのは驚き
[1] https://github.com/openssh/openssh-portable/commit/752250caa...
[2] https://github.com/openssh/openssh-portable/graphs/contribut...
この場合、
#ifdefがなぜ必要なのかコメントがあれば助けになったかもしれない。例えば「ここコードは非同期シグナルセーフでなければならず、ロック状態は不確定な可能性がある」といった具合ただし正直なところ、
getrlimitもこの一覧にはない: https://man7.org/linux/man-pages/man7/signal-safety.7.htmlそれでも非同期シグナルセーフ関連のコメントが付いたコードを削除または修正していれば、レビューで目に留まった可能性はある。引用されたコードには
SAFE_IN_SIGHAND程度しか、このコードがシグナルハンドラ内で安全である必要を示唆していないsyslog関数を使うようシステムをリファクタリングしたため、このコードを書いた人が変更は安全だと単純に仮定した可能性があるOpenBSD の ssh 開発者たちが実際にはサポートしていると主張していない他のプラットフォームでは、依然として非同期シグナルに安全でない関数を使っている点を忘れていたか、知らなかったのかもしれない
オープンソース開発者から何かを受け取る権利があるわけではない。彼らもミスはするし、メンテナーやレビュアーを何人置くかも自分たちで決められる
https://gist.github.com/richhickey/1563cddea1002958f96e7ba95...
#ifdefを削除した人たちのようにしないことリリースノートも読む価値あり: https://www.openssh.com/releasenotes.html
これは実のところ、興味深いシグナル競合状態バグの一種です。脆弱性報告書によると、「OpenBSD は特に脆弱ではない。SIGALRM ハンドラーが、OpenBSD が 2001 年に作成した非同期シグナルにより安全な
syslog()版であるsyslog_r()を呼び出すため」とのことです。つまり、シグナル安全性の緩和策が OpenBSD 開発者に、シグナルハンドラー内へ些細ではないコードを入れさせ、そのコードが他のシステムへ移植されると安全でなくなった、ということです。一般的な知見と Unix コードの慣習に従い、シグナルハンドラー内のコードを最小限にするリファクタリングをしていれば、このバグは避けられたはずです。
時間が経つと、推移的な呼び出しのどこかに非同期シグナル安全でない呼び出しが混ざるのはあまりにも簡単であり、その経路がシグナル文脈から到達可能であることも常に明確とは限らないためです。
自分の OpenSSH インスタンス群をアップグレードした後、それらは glibc ではなく musl にリンクされていたので、musl の
syslog(3)もアロケーションを行うのか、したがって同じ方法で容易に悪用可能なのかを確認してみました。見たところ、そうではなさそうです: https://github.com/bminor/musl/blob/master/src/misc/syslog.c
そこにあるのはすべてスタック上のものか、ロックで再入を防ぐ静的変数です。
{d,sn,vsn}printf()の呼び出しも musl ではアロケーションしませんが、glibc ではする可能性があります。何か見落としているでしょうか?それでも
sigalrm内でデッドロックすると接続のクリーンアップを妨げる可能性があるため、サービス拒否につながり得ます。FreeBSD 向けのパッチが出ています。
影響を受けるかどうかは明確ではありません。既知の悪用は glibc でのみ可能で、FreeBSD は glibc を使っていませんが、安全側に倒すのがよいでしょう。
https://www.freebsd.org/security/advisories/FreeBSD-SA-24:04...
報告書によると、sshd を更新または再コンパイルできない場合、設定ファイルで
LoginGraceTimeを 0 に設定するだけで、このシグナルハンドラー競合状態を修正できるとのことです。この場合 sshd は、すべての
MaxStartups接続が使い尽くされるサービス拒否には脆弱になりますが、このアドバイザリで示されたリモートコード実行に対しては安全になります。したがって、
sshd_configにLoginGraceTime 0を設定すれば緩和策になるようです。それなら、むしろ悪化するのでは?
Debian 12 向けのパッチが出ており、Debian 11 は影響を受けません。
https://security-tracker.debian.org/tracker/CVE-2024-6387
apt updateとupgradeを実行したところ、アップグレードされたのは OpenSSH パッケージだけでした。本当に素晴らしい発見だと思う
自分が直接その仕事をしている立場ではないが、セキュリティ研究で「勝つ」には、単一の問題を見つけて修正してもらったり報酬を受け取ったりするのではなく、リモートアクセスまでつながるチェーン全体を見つけなければならない、という空気をよく感じる
単一の穴、たとえばメモリ破壊ひとつやサンドボックス脱出ひとつを見つけるだけでも十分であるべきではないかと思う。今は小さな問題が多すぎて、本当に人々に真剣に受け止めてもらったりバグバウンティを支払ってもらったりするには、最後までつながるハックを示さなければならないのかもしれない
たとえば、あるアプリが誤った信頼済み入力を受け取るとクラッシュするが、そのアプリの性質上、攻撃者にさらされることが意図されておらず、現実的にもそうなることがないなら、ほとんどの場合それは単なるバグであって、セキュリティバグとは見なされない。直せればよいが同じレベルではなく、こういうものは見つけるのもそれほど難しくない
だからこの事例のような「本物の」セキュリティバグと、セキュリティ上の影響がないバグを区別する必要があり、問題が悪用可能であることを証明する作業が非常に重要になる
セキュリティ上の影響がないバグは際限なく存在するだろうから、こうした証明要求がすぐになくなるとは思えない
これは設計上そういうもので、ユーザーはラムダ関数まで含めて何でもシリアライズし、デシリアライズできる。自分のライブラリは、信頼された出所のデータ処理だけを意図している
自分の知る限り、このライブラリを信頼できないデータの処理に使っている人はいない。ある人気ライブラリが設定ファイルを読むために自分のライブラリを使っているが、彼らは設定ファイルを信頼済みデータと見なしている。そして他人が自分のライブラリをどう使うかを取り締まるのは自分の仕事ではない
この場合、自分のプロジェクトにリモートコード実行の脆弱性があるとして、最高優先度のCVEを登録するのが正しいのだろうか?
報酬は常に前者のカテゴリに支払われる。後者のカテゴリの報告は、概念実証や悪用可能性の証明がなければ、むしろ評判やシグナルを損なうこともある
特定の条件が満たされるまでは悪用可能にならない弱点は、ほぼ常に存在する。Pwn2Ownのような大会でも、複数の脆弱性をつなげて最終的にデバイスを乗っ取り、何年もパッチされないまま残るケースがよく見られる。研究者が影響度を最大化するために、そうした弱点を長く寝かせておくこともある
悲しいが、それが現実だ
OpenSSH リリースノート: https://www.openssh.com/txt/release-9.8
アップグレードできない、またはしたくない人向けの最小パッチ: https://marc.info/?l=oss-security&m=171982317624594&w=2