Skip to content
KitploitKITPLOIT
ツールブログ
提出
ツールブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
regreSSHion — これはCVE-2024-6387用に書いたPOCです。 | Kitploit
ツール/GitHubGitHub/teamos-hub/regresshion
脆弱性分析エクスプロイトペネトレーションテストレッドチーミングリモートアクセスツールバイナリエクスプロイト
GitHubteamos-hub/regresshion

regreSSHion

これはCVE-2024-6387用に書いたPOCです。

リポジトリを見る
12年前未レビュー

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有

Qualys Security Advisory

regreSSHion: OpenSSHサーバーにおけるRCE、glibcベースのLinuxシステム上 (CVE-2024-6387)

===================================================================== 目次

概要 SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 (Debian 3.0r6, 2005年)

  • 理論
  • 実践
  • タイミング SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3 (Ubuntu 6.06.1, 2006年)
  • 理論、その1
  • 理論、その2
  • 実践
  • タイミング SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2 (Debian 12.5.0, 2024年)
  • 理論
  • 実践
  • タイミング amd64エクスプロイトに向けて パッチと軽減策 謝辞 タイムライン

===================================================================== 概要

root@kitploit:~
必要なのは信仰の跳躍だけ
    -- The Interrupters, "Leap of Faith"

予備的注記: OpenSSHは世界で最も安全なソフトウェアの一つです。この脆弱性は、ほぼ完璧な実装における一度のミスです。その多層防御設計とコードは模範であり、インスピレーションを与えるものであり、OpenSSHの開発者の模範的な仕事に感謝します。

私たちはOpenSSHサーバー(sshd)において、シグナルハンドラの競合状態の脆弱性(CVE-2024-6387)を発見しました。クライアントがLoginGraceTime秒(デフォルト120秒、古いOpenSSHバージョンでは600秒)以内に認証しない場合、sshdのSIGALRMハンドラが非同期に呼び出されます。しかし、このシグナルハンドラは非同期シグナルセーフではないさまざまな関数(例: syslog())を呼び出します。この競合状態は、sshdのデフォルト設定に影響します。

調査の結果、この脆弱性は実際には2006年にMark Dowd氏によって報告されたCVE-2006-5051(「OpenSSH 4.4以前のシグナルハンドラ競合状態により、リモート攻撃者がサービス拒否(クラッシュ)を引き起こし、任意のコードを実行する可能性がある」)の回帰であることがわかりました。

この回帰は2020年10月(OpenSSH 8.5p1)に、コミット752250c(「OpenSSHのための改訂されたログ基盤」)によって導入されました。このコミットは、sshdのSIGALRMハンドラから直接呼び出される関数sigdie()から"#ifdef DO_LOG_SAFE_IN_SIGHAND"を誤って削除しました。言い換えれば:

  • OpenSSH < 4.4p1は、CVE-2006-5051に対するバックポートパッチが適用されていない場合、またはCVE-2006-5051に対する不正確な修正であるCVE-2008-4109に対するパッチが適用されていない場合、このシグナルハンドラ競合状態の影響を受けます。
  • 4.4p1 <= OpenSSH < 8.5p1は、このシグナルハンドラ競合状態の影響を受けません(CVE-2006-5051のパッチでsigdie()に追加された"#ifdef DO_LOG_SAFE_IN_SIGHAND"によって、この安全でない関数が安全な_exit(1)呼び出しに変換されたため)。
  • 8.5p1 <= OpenSSH < 9.8p1は、再びこのシグナルハンドラ競合状態の影響を受けます("#ifdef DO_LOG_SAFE_IN_SIGHAND"がsigdie()から誤って削除されたため)。

この脆弱性はglibcベースのLinuxシステム上でリモートから悪用可能であり、syslog()自体が非同期シグナルセーフでない関数(例: malloc()やfree())を呼び出します。これは認証されていないリモートコード実行をroot権限で引き起こします。sshdの特権コードに影響し、サンドボックス化されておらず、完全な権限で実行されるためです。私たちは他のlibcやオペレーティングシステムについては調査していません。ただし、OpenBSDは明らかに影響を受けません。なぜなら、そのSIGALRMハンドラはsyslog_r()を呼び出しますが、これはOpenBSDが2001年に発明したasync-signal-safeなsyslog()のバージョンだからです。

この脆弱性をリモートから悪用するために(私たちの知る限り、CVE-2006-5051はこれまでに正常に悪用されたことはありません)、私たちは2001年にMichal Zalewski氏によって発表された先見性のある論文「Delivering Signals for Fun and Profit」からインスピレーションを得ました。

https://lcamtuf.coredump.cx/signals.txt

それでも、私たちは直ちに3つの主要な問題に直面しました。

  • 理論的な観点から、適切なタイミングでSIGALRMによって中断された場合にsshdを矛盾した状態にする有用なコードパスを見つけ、その後SIGALRMハンドラ内部でこの矛盾した状態を悪用する必要があります。
  • 実用的な観点から、sshd内のこの有用なコードパスに到達する方法を見つけ、適切なタイミングで中断する可能性を最大化する必要があります。
  • タイミングの観点から、リモートからこの有用なコードパスを適切なタイミングで中断する可能性をさらに高める方法を見つける必要があります。

これらの3つの問題に集中し、最新のオペレーティングシステムの保護機構(特にASLRとNX)にすぐに立ち向かわなくて済むように、まずi386上の古いOpenSSHバージョンをエクスプロイトし、その後その経験に基づいて最近のバージョンをエクスプロイトすることにしました。

  • 最初に、"SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3"("debian-30r6-dvd-i386-binary-1_NONUS.iso"から)を取り上げました。これは、デフォルトで権限分離が有効になっている最初のDebianバージョンであり、当時の重大な脆弱性(特にCVE-2003-0693とCVE-2002-0640)に対してパッチが適用されています。

    このバージョンをリモートから悪用するために、free()の呼び出しをSIGALRMで中断し(sshdの公開鍵解析コード内)、ヒープを矛盾した状態にし、SIGALRMハンドラ内の別のfree()呼び出し中にこの矛盾した状態を悪用します。

    実験では、この競合状態に勝つには平均で約10,000回の試行が必要でした。つまり、600秒(LoginGraceTime)あたり10接続(MaxStartups)が受け入れられる場合、リモートルートシェルを取得するには平均で約1週間かかります。

  • 2番目に、"SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3"("ubuntu-6.06.1-server-i386.iso"から)を取り上げました。これは、CVE-2006-5051(「OpenSSH 4.4以前のシグナルハンドラ競合状態」)の影響を受ける最後のUbuntuバージョンです。

    このバージョンをリモートから悪用するために、pam_start()の呼び出しをSIGALRMで中断し、PAMの構造の1つを矛盾した状態にし、SIGALRMハンドラ内のpam_end()呼び出し中にこの矛盾した状態を悪用します。

    実験では、この競合状態に勝つには平均で約10,000回の試行が必要でした。つまり、120秒(LoginGraceTime)あたり10接続(MaxStartups)が受け入れられる場合、リモートルートシェルを取得するには平均で約1~2日かかります。

  • 最後に、"SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2"("debian-12.5.0-i386-DVD-1.iso"から)を取り上げました。これは現在のDebian安定版であり、CVE-2006-5051の回帰の影響を受けます。

    このバージョンをリモートから悪用するために、malloc()の呼び出しをSIGALRMで中断し(sshdの公開鍵解析コード内)、ヒープを矛盾した状態にし、SIGALRMハンドラ内の別のmalloc()呼び出し中(より正確にはsyslog()内)にこの矛盾した状態を悪用します。

    実験では、この競合状態に勝つには平均で約10,000回の試行が必要でした。したがって、120秒(LoginGraceTime)あたり100接続(MaxStartups)が受け入れられる場合、約3~4時間です。最終的に、リモートルートシェルを取得するには平均で約6~8時間かかります。なぜなら、glibcのアドレスを正しく推測できるのは半分の時間だけだからです(ASLRのため)。

この研究はまだ進行中です。

  • 仮想マシンのみを対象としており、ベアメタルサーバーではない、ほぼ安定したネットワークリンク(約10msのパケットジッタ)での実験です。
  • エクスプロイトのさまざまな側面は大幅に改善できると確信しています。
  • amd64エクスプロイトの作業を開始しましたが、これはより強力なASLRのためにはるかに困難です。

amd64での作業を開始してから数日後、sshdのSIGALRMハンドラにおけるデッドロックに関する以下のバグレポート(OpenSSHの公開Bugzilla内)に気付きました。

https://bugzilla.mindrot.org/show_bug.cgi?id=3690

したがって、私たちはすぐにOpenSSHの開発者に連絡し(このデッドロックが悪用可能な脆弱性によって引き起こされていることを知らせるため)、amd64の作業を保留し、このアドバイザリの作成を開始しました。

===================================================================== SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 (Debian 3.0r6, 2005年)


理論

root@kitploit:~
しかし、それは私らしくない、私は解放されている
    -- The Interrupters, "Haven't Seen the Last of Me"

このOpenSSHバージョンのSIGALRMハンドラはpacket_close()を呼び出し、それはbuffer_free()を呼び出し、さらにxfree()、そしてfree()を呼び出します。free()はasync-signal-safeではありません。


302 grace_alarm_handler(int sig) 303 { ... 307 packet_close();

329 packet_close(void) 330 { ... 341 buffer_free(&input); 342 buffer_free(&output); 343 buffer_free(&outgoing_packet); 344 buffer_free(&incoming_packet);

35 buffer_free(Buffer *buffer) 36 { 37 memset(buffer->buf, 0, buffer->alloc); 38 xfree(buffer->buf); 39 }

51 xfree(void *ptr) 52 { 53 if (ptr == NULL) 54 fatal("xfree: NULL pointer given as argument"); 55 free(ptr); 56 }

そこで、このDebianのglibc(2.2.5)のmallocコードを調べ始めました。free()への最初の呼び出しがSIGALRMによって中断され、SIGALRMハンドラ内の2回目のfree()呼び出し(上記の341-344行)中に悪用される可能性があるかどうかを確認するためです。このglibcのmallocは、2000年にSolar Designer氏が先駆的に開発したunlink()テクニックに対して強化されていないため、chunk_free()(free()から内部的に呼び出される)の中にすぐに興味深いコードパスを見つけました。


1028 struct malloc_chunk 1029 { 1030 INTERNAL_SIZE_T prev_size; /* 前のチャンクのサイズ(解放されている場合) / 1031 INTERNAL_SIZE_T size; / オーバーヘッドを含むバイト単位のサイズ / 1032 struct malloc_chunk fd; /* 二重リンクリスト -- 解放されている場合のみ使用 / 1033 struct malloc_chunk bk; 1034 };

2516 #define unlink(P, BK, FD)
2517 {
2518 BK = P->bk;
2519 FD = P->fd;
2520 FD->bk = BK;
2521 BK->fd = FD;
2522 } \

3160 chunk_free(arena ar_ptr, mchunkptr p) .... 3164 { 3165 INTERNAL_SIZE_T hd = p->size; / ヘッドフィールド / .... 3177 sz = hd & ~PREV_INUSE; 3178 next = chunk_at_offset(p, sz); 3179 nextsz = chunksize(next); .... 3230 if (!(inuse_bit_at_offset(next, nextsz))) / 前方統合 / 3231 { .... 3241 unlink(next, bck, fwd); .... 3244 } 3245 else 3246 set_head(next, nextsz); / inuseビットをクリア */ .... 3251 frontlink(ar_ptr, p, sz, idx, bck, fwd);

このコードパスを悪用するために、sshdのヒープを以下のレイアウトに配置します(chunk_X、chunk_Y、chunk_Zはmalloc()されたメモリチャンクであり、p、s、f、bはそれぞれprev_size、size、fd、bkフィールドです)。

-----|---+---------------|---+---------------|---+---------------|----- ... |p|s|f|b| chunk_X |p|s|f|b| chunk_Y |p|s|f|b| chunk_Z | ... -----|---+---------------|---+---------------|---+---------------|----- |<------------->| ユーザーデータ

  • まず、free(chunk_Y)の呼び出しが3246行以降、3251行以前にSIGALRMによって中断された場合、chunk_Yはすでに解放済みとしてマークされています(3246行でchunk_ZのPREV_INUSEビットがクリアされるため)が、まだ二重リンクリストにリンクされていません(3251行)。つまり、chunk_Yのfdおよびbkポインタには依然としてユーザーデータ(攻撃者制御のデータ)が含まれています。

  • 次に、(SIGALRMハンドラ内で)packet_close()がfree(chunk_X)を呼び出すと、(chunk_Yが解放済みとしてマークされているため)3230-3244行のコードブロックが実行され、chunk_Yがunlink()されます(3241行)。いわゆるaa4bmoプリミティブ(ほぼ任意の4バイトミラーリング上書き)です。なぜなら、chunk_Yのfdおよびbkポインタが依然として攻撃者制御のままだからです。unlink()テクニックとaa4bmoプリミティブの詳細については、以下を参照してください。

    https://www.openwall.com/articles/JPEG-COM-Marker-Vulnerability#exploit http://phrack.org/issues/61/6.html#article

  • 最後に、このaa4bmoプリミティブを使用して、glibcの__free_hook関数ポインタ(この古いDebianバージョンにはASLRもNXもありません)をヒープ内のシェルコードのアドレスで上書きし、packet_close()内の次のfree()呼び出し時にリモートコード実行を達成します。


実践

root@kitploit:~
今や彼らは支配し、完全にコントロールしている
    -- The Interrupters, "Liberty"

sshdに対してこの攻撃を仕掛けるために、sshdのDSA公開鍵解析コード内のfree()呼び出し(つまり、以下の144行目が私たちのfree(chunk_Y)です)を中断し、packet_close()内のfree()呼び出しの1つ(つまり、上記の341-344行のいずれかが私たちのfree(chunk_X)です)で悪用します。


136 buffer_get_bignum2(Buffer *buffer, BIGNUM *value) 137 { 138 u_int len; 139 u_char *bin = buffer_get_string(buffer, &len); ... 143 BN_bin2bn(bin, len, value); 144 xfree(bin); 145 }

しかし、最初はこの競合状態に勝つことができませんでした(つまり、144行目のfree()呼び出しを適切なタイミングで中断できませんでした)。最終的に、競合状態に勝つ確率を大幅に向上させられることに気付きました。DSA公開鍵の解析コードでは、free()を4回呼び出すことができ(以下の704-707行)、さらにsshdでは6回のユーザー認証試行(AUTH_FAIL_MAX)が許可されています。これらの24個のfree()呼び出しのいずれかが適切なタイミングで中断されれば、後でSIGALRMハンドラ内でリモートコード実行を達成できます。


678 key_from_blob(u_char *blob, int blen) 679 { ... 693 switch (type) { ... 702 case KEY_DSA: 703 key = key_new(type); 704 buffer_get_bignum2(&b, key->dsa->p); 705 buffer_get_bignum2(&b, key->dsa->q); 706 buffer_get_bignum2(&b, key->dsa->g); 707 buffer_get_bignum2(&b, key->dsa->pub_key);

この改善により、約1か月後にようやく競合状態に勝ちました。嬉しくて(ルートシェルダンスをしました)、しかしまだ改善の余地があると感じました。


タイミング

root@kitploit:~
心配しないで、待っていればいい
    -- The Interrupters, "Haven't Seen the Last of Me"

そこで、以下の3つのタイミング戦略を実装しました。

  • 最後の瞬間まで(かなり大きい)DSA公開鍵パケットをsshdに送信するのを待たない。代わりに、LoginGraceTimeのずっと前に、パケット全体から1バイト(最後のバイト)を引いたものを送信し、最後の瞬間に最後のバイトを送信することで、ネットワーク遅延の影響を最小限に抑えます(さらに、Nagleアルゴリズムを無効にします)。

  • 中央値の往復時間を追跡し(sshdから応答が返ってくるパケットを定期的に送信することにより)、接続がsshdによって閉じられると予想される瞬間(基本的にsshdのバナー最初のバイトを受信した時刻にLoginGraceTimeを加えたもの)と、実際にsshdによって接続が閉じられた瞬間との差を追跡し、それに応じてタイミング(つまり、DSAパケットの最後のバイトを送信する瞬間)を調整します。

    これらの時間差により、クロックスキューとネットワーク遅延を追跡でき、これらは時間とともに予測可能なパターンを示します。線形回帰とスプライン回帰を試しましたが、結局、最新の測定値を単に再利用すること以上にうまくいくものはありませんでした。おそらく、深層学習によってさらに良い結果が得られるかもしれません。これは興味のある読者のための課題として残します。

  • さらに重要なことに、sshdからの非自発的なフィードバックを通じてタイミングをゆっくりと調整することにより、競合状態に勝つ確率をさらに高めます。

    • DSA公開鍵パケットへの応答(SSH2_MSG_USERAUTH_FAILURE)を受け取った場合、それはパケットを早く送りすぎたことを意味します(sshdは非特権子プロセスでパケットを受信し、解析し、特権子プロセスに送信し、そこで解析し、応答を私たちに返送する時間があった)。
    • DSAパケットの最後のバイトさえ送信できない場合、それは待ちすぎたことを意味します(sshdはすでにSIGALRMを受信し、接続を閉じた)。
    • DSAパケットの最後のバイトを送信でき、sshdが接続を閉じる前に応答がない場合、タイミングはほぼ正確でした。

    このフィードバックにより、私たちが「大きな」競合ウィンドウと呼ぶものを狙うことができます。これに当たっても競合状態に勝つことは保証されませんが、この大きなウィンドウの中には24個の「小さな」競合ウィンドウ(24個のfree()呼び出し内部)が含まれており、これらに当たれば競合状態に勝つことが保証されます。

これらの改善により、この競合状態に勝つには平均で約10,000回の試行が必要です。つまり、600秒(LoginGraceTime)あたり10接続(MaxStartups)が受け入れられる場合、リモートルートシェルを取得するには平均で約1週間かかります。

===================================================================== SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3 (Ubuntu 6.06.1, 2006年)


理論、その1

root@kitploit:~
太陽が昇り始めると私は眠る
    -- The Interrupters, "Alien"

このOpenSSHバージョンのSIGALRMハンドラは、もはやpacket_close()を呼び出しません。さらに、このUbuntuのglibc(2.3.6)は、malloc系関数に入るときに常に必須のロックを取得します(sshdのようにシングルスレッドであっても)。これにより、malloc関数への呼び出しを中断し、後でこれらの関数への別の呼び出し中に悪用することができなくなります(常にデッドロックします)。別の解決策を見つけなければなりません。

CVE-2006-5051はGSSAPIにおけるダブルフリーに言及していますが、GSSAPI(またはKerberos)はデフォルトで有効になっていないため、あまり魅力的ではありません。一方、PAMはデフォルトで有効であり、pam_end()はsshdのSIGALRMハンドラから呼び出されます(そしてもちろん、async-signal-safeではありません)。そこで、適切なタイミングでSIGALRMによって中断された場合にPAMの内部構造を矛盾した状態にし、SIGALRMハンドラ内のpam_end()中に悪用できるPAM関数を探しました。pam_set_data()を見つけました。


33 int pam_set_data( 34 pam_handle_t *pamh, .. 37 void (*cleanup)(pam_handle_t *pamh, void *data, int error_status)) 38 { 39 struct pam_data *data_entry; .. 57 } else if ((data_entry = malloc(sizeof(*data_entry)))) { .. 65 data_entry->next = pamh->data; 66 pamh->data = data_entry; .. 74 data_entry->cleanup = cleanup; -----------------------------------------------------------------------If this function is interrupted by SIGALRM after line 66 but before line 74, then data_entry is already linked into PAM's structures (pamh), but its cleanup field (a function pointer) is not yet initialized (since the malloc() at line 57 does not initialize its memory). If we are able to control cleanup (through leftovers from previous heap allocations), then we can execute arbitrary code when pam_end() (inside the SIGALRM handler) calls _pam_free_data() (at line 118):


104 void _pam_free_data(pam_handle_t *pamh, int status) 105 { 106 struct pam_data *last; 107 struct pam_data *data; ... 112 data = pamh->data; 113 114 while (data) { 115 last = data; 116 data = data->next; 117 if (last->cleanup) { 118 last->cleanup(pamh, last->data, status);

これは非常に単純なエクスプロイトだったでしょうが、残念ながら、私たちはpam_set_data()がPAMモジュールからのみ呼び出せることを完全に見落としていました。もしSIGALRMで割り込むと、pamh->caller_isは依然として_PAM_CALLED_FROM_MODULEのままであり、その場合pam_end()はただちに戻り、_pam_free_data()を呼び出しません。振り出しに戻りました。


Theory, take two

root@kitploit:~
諦めるなんて、私たちのすることじゃない
    -- The Interrupters, "Title Holder"

下記601行目で、sshdがグローバルなsshpam_handleポインタへのポインタを直接pam_start()に渡していることに気づきました(pam_start()は接続ごとに1回呼ばれます)。


202 static pam_handle_t *sshpam_handle = NULL;

584 sshpam_init(Authctxt *authctxt) 585 { ... 600 sshpam_err = 601 pam_start(SSHD_PAM_SERVICE, user, &store_conv, &sshpam_handle);

そこで、pam_start()自体を調べることにしました。SIGALRMで割り込まれると、sshpam_handleが指す構造体が不完全な状態のままになり、その後SIGALRMハンドラ内で"pam_end(sshpam_handle, sshpam_err)"が呼ばれた際に、その状態を悪用できる可能性があります。


18 int pam_start ( .. 22 pam_handle_t **pamh) 23 { .. 32 if ((*pamh = calloc(1, sizeof(**pamh))) == NULL) { ... 110 if ( _pam_init_handlers(*pamh) != PAM_SUCCESS ) {

319 int _pam_init_handlers(pam_handle_t *pamh) 320 { ... 398 retval = _pam_parse_conf_file(pamh, f, pamh->service_name, PAM_T_ANY

66 static int _pam_parse_conf_file(pam_handle_t *pamh, FILE *f .. 73 { ... 252 res = _pam_add_handler(pamh, must_fail, other

581 int _pam_add_handler(pam_handle_t *pamh ... 585 { ... 755 the_handlers = (other) ? &pamh->handlers.other : &pamh->handlers.conf; ... 767 handler_p = &the_handlers->authenticate; ... 874 if ((*handler_p = malloc(sizeof(struct handler))) == NULL) { ... 886 (*handler_p)->next = NULL;

32行目で、pam_start()は直ちにsshdのsshpam_handleをcalloc()で確保したメモリチャンクに設定します。これは安全です。calloc()はメモリをゼロ初期化するからです。一方、_pam_add_handler()(pam_start()によって複数回呼ばれる)が、874行目後、886行目前でSIGALRMに割り込まれた場合、malloc()された構造体がpamhにリンクされますが、そのnextフィールドはまだ初期化されていません。もしnextを(以前のヒープアロケーションの残骸を通じて)制御できるなら、pam_end()の呼び出し中(SIGALRMハンドラ内)、下記1020行目(および1017行目)でfree()に任意のポインタを渡すことができます。


11 int pam_end(pam_handle_t *pamh, int pam_status) 12 { .. 31 if ((ret = _pam_free_handlers(pamh)) != PAM_SUCCESS) {

925 int _pam_free_handlers(pam_handle_t *pamh) 926 { ... 954 _pam_free_handlers_aux(&(pamh->handlers.conf.authenticate));

1009 void _pam_free_handlers_aux(struct handler **hp) 1010 { 1011 struct handler *h = *hp; 1012 struct handler last; .... 1015 while (h) { 1016 last = h; 1017 _pam_drop(h->argv); / This is all alocated in a single chunk */ 1018 h = h->next; 1019 memset(last, 0, sizeof(*last)); 1020 free(last); 1021 }

このUbuntuのglibcのmallocは、古いunlink()テクニックに対してすでに強化されているため、任意のfree()をMalloc MaleficarumのHouse of Mind(fastbin版)に変換することにしました。つまり、自分自身のNON_MAIN_ARENAチャンクをfree()し、偽のアリーナをsshdの.got.plt(このUbuntuのsshdはASLRは有効だがPIEは無効)にポイントさせ、_exit()のエントリをヒープ内のシェルコードのアドレスで上書きします(このUbuntuのヒープはデフォルトでまだ実行可能です)。Malloc Maleficarumの詳細については以下を参照:

https://seclists.org/bugtraq/2005/Oct/118


Practice

root@kitploit:~
すべてを困難な方法で学んだ
    -- The Interrupters, "The Hard Way"

sshdに対してこの攻撃を仕掛けるにあたり、当初は3つの問題に直面しました。

  • House of Mindでは、偽のアリーナへのポインタをヒープのアドレス0x08100000に格納する必要があります。しかし、そのような高位アドレスに攻撃者が制御するデータを格納できるでしょうか? sshdはユーザー認証のごく初期にpam_start()を呼び出すため、ユーザー名以外は何も制御できません。幸い、長さ約128KB(DEFAULT_MMAP_THRESHOLDより短い)のユーザー名を使用すれば、アドレス0x08100000に独自のデータを格納できます。

  • 偽のNON_MAIN_ARENAチャンクのサイズフィールドは大きすぎてはいけません(free()のセキュリティチェックを通過するため)。つまり、NULLバイトを含む必要があります。しかし、長いユーザー名はNULL終端文字列であり、NULLバイトを含めることはできません。幸い、_pam_free_handlers_aux()がfree()する構造体をゼロクリアする(上記1019行目)ことを思い出しました。そこで、まず偽のチャンクのサイズフィールドをそのようなmemset(0)で「パッチ」し、その後でfree()します。

  • 偽のNON_MAIN_ARENAチャンクのfree()の前に、何度かのfree()呼び出し(上記1017行目と1020行目)を生き延びる必要があります。これらのfree()を、偽のIS_MMAPPEDチャンクを指すようにすることでno-opに変換します。free()はmunmap_chunk()を呼び出し、それがmunmap()を呼び出しますが、これらの偽のIS_MMAPPEDチャンクはアライメントがずれているため失敗します。これにより事実上no-opとなります。このUbuntuのglibcではassert()違反は強制されないためです。

最後に、長いユーザー名を使用することで、20個の異なる構造体の初期化されていない可能性のあるnextフィールドを制御することもできます(長いユーザー名の一時的なコピーの残骸を通じて)。pam_start()は_pam_add_handler()を複数回呼び出すため、大きな競合ウィンドウの中に20個の小さな競合ウィンドウが存在することになります。


Timing

root@kitploit:~
彼らが以前使ったのと同じトリック
    -- The Interrupters, "Divide Us"

Ubuntu 6.06.1に対するこの攻撃では、Debian 3.0r6に対して使用したタイミング戦略を単純に再利用しました。競合状態に勝つには平均して約10,000回の試行が必要であり、120秒(LoginGraceTime)あたり10接続(MaxStartups)が許可されるため、リモートルートシェルを取得するには平均して約1〜2日かかります。

注意:このUbuntuのglibcは、mallocファミリーの関数に入るときに常に必須のロックを取得するため、不運な攻撃者はルートシェルを取得する前に、10のMaxStartups接続すべてでデッドロックする可能性があります。私たちはこの問題を回避する試みは行っていません。なぜなら、最終目標はとにかく現代的なOpenSSHバージョンを攻撃することだったからです。

======================================================================== SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2 (Debian 12.5.0, from 2024)


Theory

root@kitploit:~
さあ準備はできた、悪魔に立ち向かえ
    -- The Interrupters, "Be Gone"

このOpenSSHバージョンのSIGALRMハンドラは、packet_close()もpam_end()も呼び出しません。実際には、syslog()という一つの興味深い関数だけを呼び出します。


358 grace_alarm_handler(int sig) 359 { ... 370 sigdie("Timeout before authentication for %s port %d", 371 ssh_remote_ipaddr(the_active_state), 372 ssh_remote_port(the_active_state));

96 #define sigdie(...) sshsigdie(FILE, func, LINE, 0, SYSLOG_LEVEL_ERROR, NULL, VA_ARGS)

451 sshsigdie(const char *file, const char *func, int line, int showfunc, 452 LogLevel level, const char *suffix, const char *fmt, ...) 453 { ... 457 sshlogv(file, func, line, showfunc, SYSLOG_LEVEL_FATAL, 458 suffix, fmt, args);

464 sshlogv(const char *file, const char *func, int line, int showfunc, 465 LogLevel level, const char *suffix, const char *fmt, va_list args) 466 { ... 489 do_log(level, forced, suffix, fmt2, args);

337 do_log(LogLevel level, int force, const char *suffix, const char *fmt, 338 va_list args) 339 { ... 419 syslog(pri, "%.500s", fmtbuf);

そこで、2つの重要な疑問が生じます。このDebianのglibc(2.36)のsyslog()は、malloc()やfree()のような非同期シグナル安全でない関数を呼び出すのでしょうか? そして、もしそうなら、このglibcはmallocファミリーの関数に入るときにまだ必須のロックを取得するのでしょうか?

  • 攻撃者にとって幸運なことに、最初の疑問に対する答えは「はい」です。SIGALRMハンドラ内のsyslog()が最初のsyslog()呼び出しである場合に限り、syslog()によって呼び出される__localtime64_r()が、FILE構造体を確保するためにmalloc(304)(166行目)を、内部読み取りバッファを確保するためにmalloc(4096)を呼び出します(186行目)。

28 __localtime64_r (const __time64_t *t, struct tm *tp) 29 { 30 return __tz_convert (*t, 1, tp);

567 __tz_convert (__time64_t timer, int use_localtime, struct tm *tp) 568 { ... 577 tzset_internal (tp == &_tmbuf && use_localtime);

367 tzset_internal (int always) 368 { ... 405 __tzfile_read (tz, 0, NULL);

105 __tzfile_read (const char *file, size_t extra, char **extrap) 106 { ... 109 FILE *f; ... 166 f = fopen (file, "rce"); ... 186 if (__builtin_expect (__fread_unlocked ((void *) &tzhead, sizeof (tzhead), 187 1, f) != 1, 0)

注:これらのmalloc()について(その順序もサイズも内容も)何も制御できないため、166行目の"rce"を非常に必要な良い兆しとして受け取りました。

  • そして、2番目の疑問に対する答えは「いいえ」です。2017年10月以降、glibcのmalloc関数は、シングルスレッドの場合(sshdのように)ロックを取得しなくなりました。

    https://sourceware.org/git?p=glibc.git;a=commit;h=a15d53e2de4c7d83bda251469d92a3c7b49a90db https://sourceware.org/git?p=glibc.git;a=commit;h=3f6bb8a32e5f5efd78ac08c41e623651cc242a89 https://sourceware.org/git?p=glibc.git;a=commit;h=905a7725e9157ea522d8ab97b4c8b96aeb23df54

さらに、このDebianバージョンは、以下の素晴らしいブログ記事(それぞれJustin Miller氏とMathias Krause氏による)で説明されているASLRの弱点を抱えています。

https://zolutal.github.io/aslrnt/ https://grsecurity.net/toolchain_necromancy_past_mistakes_haunting_aslr

具体的には、i386上のsshdの場合、すべてのメモリマッピングは通常通りランダム化されます(sshdのPIE、ヒープ、ほとんどのライブラリ、スタック)が、glibc自体は常にアドレス0xb7200000または0xb7400000のいずれかにマッピングされます。言い換えれば、半分の確率でglibcのアドレスを正しく推測できます(ASLRを打ち破るための小さな代償です)。私たちのエクスプロイトでは、glibcが0xb7400000にマッピングされていると仮定します。これは0xb7200000よりわずかに一般的だからです。

次の疑問は、glibcのmalloc関数内のどのコードパスが、適切なタイミングでSIGALRMに割り込まれると、ヒープを不完全な状態にし、SIGALRMハンドラ内のmalloc()呼び出しの一つで悪用可能になるか、ということです。

いくつかの興味深い(そして驚くべき!)コードパスを発見しましたが、私たちが選んだものは絶対アドレスではなく相対的なサイズのみを扱うものでした(例えば、unlink_chunk()内の様々なコードパスとは異なります)。この違いは、将来のamd64エクスプロイトにとって決定的に重要になるかもしれません。malloc()内のこのコードパスは、大きな空きチャンク(victim)を2つの小さなチャンクに分割します。最初のチャンクはmalloc()の呼び出し元に返され(4345行目)、2番目のチャンク(remainder)は空きチャンクの未整理リストにリンクされます(4324-4327行目)。


1449 #define set_head(p, s) ((p)->mchunk_size = (s))

3765 _int_malloc (mstate av, size_t bytes) 3766 { .... 3798 nb = checked_request2size (bytes); .... 4295 size = chunksize (victim); .... 4300 remainder_size = size - nb; .... 4316 remainder = chunk_at_offset (victim, nb); .... 4320 bck = unsorted_chunks (av); 4321 fwd = bck->fd; .... 4324 remainder->bk = bck; 4325 remainder->fd = fwd; 4326 bck->fd = remainder; 4327 fwd->bk = remainder; .... 4337 set_head (victim, nb | PREV_INUSE | 4338 (av != &main_arena ? NON_MAIN_ARENA : 0)); 4339 set_head (remainder, remainder_size | PREV_INUSE); .... 4343 void *p = chunk2mem (victim); .... 4345 return p;

  • このコードパスが4327行目後、4339行目前でSIGALRMに割り込まれた場合、この分割のremainderチャンクはすでに空きチャンクの未整理リストにリンクされていますが(4324-4327行目)、そのサイズフィールド(mchunk_size)はまだ初期化されていません(4339行目)。

  • もし以前のヒープアロケーションの残骸を通じてそのサイズフィールドを制御できるなら、このremainderチャンクを本来より大きくし、他のヒープチャンクと重なるようにすることができます。そして、この拡大された重なり合うremainderチャンクが(SIGALRMハンドラ内で)最終的にmalloc()されて書き込まれるときに、ヒープメモリを破壊できる可能性があります。

最後の疑問は、SIGALRMハンドラ内のmalloc()呼び出しについて何も制御できない場合、sshdが(sshsigdie()内で)_exit()を呼び出す前に、任意のコード実行を達成するためにヒープ内で何を上書きできるか、ということです。

__tzfile_read()(SIGALRMハンドラ内)は、ヒープ内にFILE構造体をmalloc()するため(上記166行目)、FILE構造体は任意のコード実行のために長年にわたって悪用されてきた歴史があるため、このFILE構造体を標的にヒープ破壊を仕掛けることにしました。しかし、これは言うは易く行うは難しです。私たちのヒープ破壊は非常に限られており、FILE構造体は長年にわたって大幅に強化されてきたからです(例えば、IO_validate_vtable()やPTR_DEMANGLE()など)。

最終的に、以下の手法を考案しました(これはi386のglibcに特有のようです。amd64のglibcは_vtable_offsetをまったく使っていないように見えます)。

  • 限られたヒープ破壊により、__tzfile_read()のFILE構造体の_vtable_offsetフィールド(単一の符号付きchar)を上書きします。
  • その結果、glibcのlibio関数は、このFILE構造体のvtableポインタ(関数ポインタの配列へのポインタ)を、デフォルトのゼロオフセットではなく、上書きされた_vtable_offsetで指定されたオフセットで探すようになります。
  • 攻撃者(私たち)は、この偽のvtableポインタを(以前のヒープアロケーションの残骸を通じて)簡単に制御できます。なぜなら、このオフセット周辺のFILE構造体のフィールドは、fopen()によって明示的に初期化されないからです。
  • glibcのセキュリティチェックを通過するために、偽のvtableポインタは__libc_IO_vtablesセクション内のどこかを指す必要があります。私たちは、ワイド文字ストリーム用のvtableである_IO_wfile_jumpsを指すことにしました(つまり、glibcがアドレス0xb7400000にマッピングされていると仮定して、0xb761b740を指します)。
  • その結果、__fread_unlocked()(上記186行目)は、_IO_file_underflow()の代わりに_IO_wfile_underflow()を呼び出します。これは、基本的にFILE構造体の別のフィールドである_codecvtから得られる構造体のポインタ経由で、関数ポインタ(__fct)を呼び出します。
  • 攻撃者(私たち)は、この_codecvtポインタを(以前のヒープアロケーションの残骸を通じて)簡単に制御できます。なぜなら、FILE構造体のこのフィールドはfopen()によって明示的に初期化されないからです。これにより、__fct関数ポインタも制御できるようになります。

要約すると、fopen()がmalloc()したFILE構造体の1バイト(_vtable_offset)を上書きするだけで、__fread_unlocked()の中で独自の__fct関数ポインタを呼び出し、任意のコードを実行できます。


Practice

root@kitploit:~
完璧にしたかった、しわひとつなく
    -- The Interrupters, "In the Mirror"

sshdの特権子プロセスに対してこの攻撃を仕掛けるにあたり、まず以下のようなヒープレイアウトを想像してみましょう(「XXX」はヒープに穴を開けるための「バリア」チャンクです。例えば、小さなメモリリークチャンクなど)。

---|----------------------------------------------|---|------------|---

XXXlarge holeXXXsmall holeXXX
~8KB320B
  • sshdがSIGALRMを受信する直前に、約4KBのチャンクをmalloc()して、大きな約8KBの穴を2つの小さなチャンクに分割します。

---|-----------------------|----------------------|---|------------|---

XXXlarge allocated chunkfree remainder chunkXXXsmall holeXXX
~4KB~4KB320B
  • しかし、このmalloc()が4327行目後、4339行目前でSIGALRMに割り込まれた場合、この分割のremainderチャンクはすでに空きチャンクの未整理リストにリンクされていますが、そのサイズフィールドは(以前のヒープアロケーションの残骸を通じて)私たちの制御下にあり、この人為的に拡大されたremainderチャンクは後続の小さな穴と重なります。---|-----------------------|----------------------|---|------------|--- XXX| large allocated chunk | real remainder chunk |XXX| small hole |XXX ---|-----------------------|----------------------|---|------------|--- | ~4KB |<------------------------------------->| artificially enlarged remainder chunk

  • SIGALRMハンドラがsyslog()を呼び出し、次に__tzfile_read()を呼び出すと、 fopen()がFILE構造体のために小さな穴(small hole)をmalloc()し、 __fread_unlocked()が4KBの読み取りバッファをmalloc()し、 その結果、拡大された残りのチャンク(remainder chunk)が2つに分割される (4KB読み取りバッファと小さな残りのチャンク)。

---|-----------------------|----------------------|---|------------|--- XXX| large allocated chunk | |XXX| FILE |XXX ---|-----------------------|----------------------|---|--|---------|--- | ~4KB |<--------------------------->|<------->| 4KB read buffer remainder

  • そのため、この小さな残りのチャンクの内部ヘッダでFILE構造体の一部を上書きする。 より正確には、このヘッダのbkフィールド(空きチャンクの未ソートリストへのポインタ、 0xb761d7f8)の3バイト目でFILEの_vtable_offsetを上書きする (つまり、_vtable_offsetを0x61で上書きする)。

  • 次に、「理論」のサブセクションで説明したように、__fread_unlocked()は _IO_file_underflow()の代わりに_IO_wfile_underflow()を呼び出す。 そして、_IO_wfile_underflow()は(独自の_codecvtポインタを介して) 独自の__fct関数ポインタを呼び出し、任意のコードを実行する。

    注意:制御された_codecvtポインタから制御された__fct関数ポインタへ 確実に遷移する方法はまだ説明していない。これは後で説明するが、 その前に、より差し迫った問題を解決しなければならない。

実際、古いOpenSSHバージョンでの作業から、大きなレースウィンドウに 小さなレースウィンドウが1つしか含まれていない場合、このシグナルハンドラの 競合状態に勝つことは決してできないことを学んだ。そこで、以下のヒープレイアウトに 基づいて、次の戦略を実装した。

---|------------|---|------------|---|------------|---|------------|---

XXXlarge hole 1XXXsmall hole 1XXXlarge hole 2XXXsmall hole 2...

SIGALRMが送信される直前にsshdに送信する最後のパケットは、sshdに 次の一連のmalloc()呼び出しを強制する: malloc(~4KB), malloc(304), malloc(~4KB), malloc(304), など。

1/ 最初のmalloc(~4KB)がlarge hole 1を2つに分割する。

  • この最初の分割が適切なタイミングでSIGALRMによって中断された場合、 SIGALRMハンドラ内のfopen()がFILE構造体のためにsmall hole 1をmalloc()し、 上記のように任意のコード実行が達成される。

  • そうでない場合、最初のmalloc(304)でsmall hole 1を自分でmalloc()し、次のステップへ進む。

2/ 2番目のmalloc(~4KB)がlarge hole 2を2つに分割する。

  • この2番目の分割が適切なタイミングでSIGALRMによって中断された場合、 SIGALRMハンドラ内のfopen()がFILE構造体のためにsmall hole 2をmalloc()し、 上記のように任意のコード実行が達成される。

  • そうでない場合、2番目のmalloc(304)でsmall hole 2を自分でmalloc()し、以下同様。

sshdのヒープ内にこのような大小の穴のペアを27組作ることができた (28組だとPACKET_MAX_SIZEの256KBを超えてしまう)。 これにより、大きなレースウィンドウ内に27個の小さなレースウィンドウが含まれることになる! この複雑なヒープレイアウトを実現するのは非常に困難で時間がかかったが、 ハイライトは以下の2点である。

  • sshdの公開鍵解析コードを悪用して、malloc()とfree()の任意のシーケンスを実行する (1805行目と573行目)。

1754 cert_parse(struct sshbuf *b, struct sshkey *key, struct sshbuf *certbuf) 1755 { .... 1797 while (sshbuf_len(principals) > 0) { .... 1805 if ((ret = sshbuf_get_cstring(principals, &principal, .... 1820 key->cert->principals[key->cert->nprincipals++] = principal; 1821 }

562 cert_free(struct sshkey_cert *cert) 563 { ... 572 for (i = 0; i < cert->nprincipals; i++) 573 free(cert->principals[i]);

  • 小さな「バリア」チャンク用のメモリリークは見つからなかった。 代わりに、tcacheチャンク(inuseビットがクリアされないため実際には解放されない) を即席の「バリア」チャンクとして使用する。

このヒープレイアウトを確実に実現するために、sshdに5つの異なる公開鍵パケットを送信する (パケットa/からd/はSIGALRMのかなり前に送信可能。パケットe/の大部分も SIGALRMのかなり前に送信可能だが、その最後の1バイトは最後の瞬間に送信しなければならない)。

a/ さまざまなtcacheチャンクをmalloc()してfree()し、制御していないヒープ割り当てが これらのtcacheチャンクに割り当てられ、慎重に計画したヒープレイアウトに干渉しないようにする。

b/ さまざまなサイズのチャンクをmalloc()してfree()し、27組の大小の穴と、 それに対応する「バリア」チャンクを作成する。

c/ ~4KBのチャンクと320Bのチャンクをmalloc()してfree()し、次のことを行う。

  • 拡大された可能性のある残りのチャンクの偽のヘッダ(大きなサイズフィールド)を 大きな穴の中央に書き込む。
  • 拡大された可能性のある残りのチャンクの偽のフッタを小さな穴の末尾に書き込む (glibcのセキュリティチェックを通過させるため)。
  • 偽のvtableと_codecvtポインタを小さな穴(FILE構造体の候補)に書き込む。

d/ 非常に大きな文字列(約256KB)をmalloc()してfree()し、大小の穴が 未ソートの空きチャンクリストから削除され、それぞれのmallocビンに 配置されるようにする。

e/ sshdに最後の一連のmalloc()呼び出し(malloc(~4KB), malloc(304), malloc(~4KB), malloc(304), ...)を強制し、27個の小さなレースウィンドウを 開く。

注意深い読者は、_codecvtの問題にまだ対処していないことに気づいたかもしれない (文字通りにも比喩的にも)。実際、_codecvtは構造体(_IO_codecvt)へのポインタであり、 その構造体には構造体(__gconv_step)へのポインタが含まれており、 その中には任意のコードを実行できる__fct関数ポインタが含まれている。 _codecvtを介して__fctを確実に制御するために、_codecvtをglibcの mallocビンの1つにポイントする。mallocビンにはヒープ内の空きチャンクの1つへの ポインタが便利に含まれており、そのチャンクには任意のglibcコードへの 独自の__fct関数ポインタが含まれている (glibcがアドレス0xb7400000にマッピングされていると仮定しているため、 これらのglibcアドレスはすべて既知である)。


タイミング

root@kitploit:~
時間が足りなくなってきた
    -- The Interrupters, "As We Live"

3番目のエクスプロイトを実装するにつれて、以前の2つのOpenSSHバージョンに対して 使用したタイミング戦略を単純に再利用できないことが明らかになった。 新しい競合状態で勝利することは決してできなかった。 最終的に、その理由がわかった。

  • sshdが5番目で最後の公開鍵(上記のパケットe/)を解析するのに時間がかかる (約10ms)。言い換えれば、大きなレースウィンドウが大きすぎる (27個の小さなレースウィンドウは干し草の山の中の針のようなもの)。

  • 最近(OpenSSH 7.8p1)導入されたuser_specific_delay()は、 最後の公開鍵パケットに対するsshdの応答を最大約9ms遅延させるため、 フィードバックベースのタイミング戦略を無効にする。

その結果、まったく異なるタイミング戦略を開発した。

  • 時々、最後の公開鍵パケットに小さな誤りを加えて送信し、 sshkey_from_blob() が公開鍵を解析する直前(下記138-142行目)で エラー応答を発生させる。

  • 時々、最後の公開鍵パケットに別の小さな誤りを加えて送信し、 sshkey_from_blob() が公開鍵を解析した直後(下記151-155行目)で エラー応答を発生させる。

  • これら2つの応答時間の差が、sshdが最後の公開鍵を解析するのにかかる時間であり、 これにより最後のパケットの送信タイミングを正確に計ることができる (sshdが非特権子プロセスで公開鍵を解析し、特権子プロセスに送信し、 そこで解析を開始するのに十分な時間を確保し、その後にSIGALRMが届くようにする)。


88 userauth_pubkey(struct ssh *ssh, const char method) 89 { ... 138 if (pktype == KEY_UNSPEC) { 139 / this is perfectly legal */ 140 verbose_f("unsupported public key algorithm: %s", pkalg); 141 goto done; 142 } 143 if ((r = sshkey_from_blob(pkblob, blen, &key)) != 0) { 144 error_fr(r, "parse key"); 145 goto done; 146 } ... 151 if (key->type != pktype) { 152 error_f("type mismatch for decoded key " 153 "(received %d, expected %d)", key->type, pktype); 154 goto done; 155 }

この戦略の変更により、競合状態に勝つには平均で約10,000回の試行が必要となる。 つまり、120秒(LoginGraceTime)あたり100接続(MaxStartups)が受け入れられる場合、 競合状態に勝つには平均で約3~4時間、リモートルートシェルを取得するには 約6~8時間かかる(ASLRのため)。

======================================================================== amd64エクスプロイトに向けて

root@kitploit:~
明日の計画は何だ?
    -- The Interrupters, "Take Back the Power"

Rocky Linux 9(Red Hat Enterprise Linux 9派生)の"Rocky-9.4-x86_64-minimal.iso"を ターゲットにすることにした。その理由は2つある。

  • そのOpenSSHバージョン(8.7p1)はこのシグナルハンドラの競合状態に対して脆弱であり、 かつglibcは常に2MBの倍数にマッピングされる (前述の「理論」サブセクションで議論したASLRの弱点のため)。 これにより、部分的なポインタ上書きがより強力になる。

  • このglibcバージョン(2.34)のsyslog()関数 (async-signal-unsafeであるが、sshdのSIGALRMハンドラによって呼び出される)は、 内部的に __open_memstream() を呼び出し、ヒープ内にFILE構造体をmalloc()する。 また、calloc()、realloc()、free()も呼び出す (これにより、切望していた自由度が得られる)。

ヒープ破損をプリミティブとし、ヒープ内にmalloc()された2つのFILE構造体、 およびglibcのアドレスの21ビットが固定されていることから、 このシグナルハンドラの競合状態はamd64で悪用可能であると信じている (おそらく6~8時間ではなく、1週間以内には成功するだろう)。 時が経てばわかるだろう。

補足:Ubuntu 24.04ではsshdの子プロセスに対するASLRの再ランダム化が行われない (ブート時に1度だけランダム化される)ことを発見した。これは以下のパッチによるもので、 sshdのrexec_flagをオフにしている。これは一般には悪いアイデアだが、 このシグナルハンドラの競合状態の特定のケースでは、 sshdが悪用されるのを防ぐ効果がある。 SIGALRMハンドラ内のsyslog()は、それが最初のsyslog()呼び出しではないため、 malloc関数を一切呼び出さない。

https://git.launchpad.net/ubuntu/+source/openssh/tree/debian/patches/systemd-socket-activation.patch

======================================================================== パッチと緩和策

root@kitploit:~
嵐は去り、過ぎ去った
    -- The Interrupters, "Good Things"

2024年6月6日、このシグナルハンドラの競合状態は、 コミット81c1099("Add a facility to sshd(8) to penalise particular problematic client behaviours") によって修正された。このコミットでは、async-signal-unsafeなコードを sshdのSIGALRMハンドラからsshdのリスナープロセスに移動し、 同期的に処理できるようにした。

https://github.com/openssh/openssh-portable/commit/81c1099d22b81ebfd20a334ce986c4f753b0db29

この修正は大きなコミット(81c1099)の一部であり、 さらに大きな多層防御コミット(03e3de4 "Start the process of splitting sshd into separate binaries") の上に成り立っているため、バックポートが困難な可能性がある。 その場合、シグナルハンドラの競合状態自体は、 sshsigdie()関数からasync-signal-unsafeなコードを削除またはコメントアウトすることで 修正できる。例えば以下のように。


sshsigdie(const char *file, const char *func, int line, int showfunc, LogLevel level, const char *suffix, const char *fmt, ...) { #if 0 va_list args;

root@kitploit:~
    va_start(args, fmt);
    sshlogv(file, func, line, showfunc, SYSLOG_LEVEL_FATAL,
        suffix, fmt, args);
    va_end(args);

#endif _exit(1); }

最後に、sshdをアップデートまたは再コンパイルできない場合、 設定ファイルでLoginGraceTimeを0に設定するだけで、 このシグナルハンドラの競合状態を修正できる。 これにより、sshdはサービス拒否攻撃(すべてのMaxStartups接続の枯渇)に対して 脆弱になるが、本アドバイザリで提示されたリモートコード実行からは安全になる。

======================================================================== 謝辞

OpenSSHの開発者の皆様の卓越したご尽力と、本リリースにおける緊密な連携に感謝します。 また、distros@openwallにも感謝します。 最後に、本アドバイザリをSophia d'Antoineに捧げます。

======================================================================== タイムライン

2024-05-19: OpenSSHの開発者に連絡。パッチとパッチレビューの反復が続く。

2024-06-20: distros@openwallに連絡。

2024-07-01: 協調公開日。

ツールをダウンロード
~8KB
320B
~8KB
320B