このブログは、広く使用されているアクセス制御システムの脆弱性発見に焦点を当てたマルチパート シリーズの 第2回目です。ターゲットの取得からエクスプロイトに至るプロセスの調査過程について紹介する内容です。ベンダーと製品を選択から始め、ハードウェアハッキングの技術まで掘り下げていきます。対象のデバイスは、HID Mercuryのアクセス コントロール パネル LNL-4420です。このシリーズでは、発見された最も影響力のある脆弱性に焦点を当てて、取り上げます。このブログシリーズの第1回目はこちらです。
目次
はじめに
重要インフラは、グローバルなインフラ全体の基幹です。国家主体の攻撃者にとって紛れもなく魅力的なターゲット領域であり、時として許しがたいほど脆弱です。ここ数年だけを振り返っても、多くの事例がそれを証明しています。パイプライン、エネルギー網、水処理システム、通信事業者などに対するサイバー攻撃が大きく報道され、大胆不敵な攻撃者が世界中で増えていることが強調されています。
アクセス制御システムはデジタルと物理の領域を隔てる数少ない障壁の1つですが、機密性の高い資産の保護にあたっては過信されがちで、独特な攻撃経路になっています。産業用制御システム(ICS)やビルディングオートメーションシステム(BAS)に通じるこの攻撃経路を、研究者も敵対者も見落としてきました。このような隙があったので、このエリアの調査に取り組むことを決めました。
ソフトウェアハッキング
このセクションで参照されているツールとソフトウェアの一部は、自由にダウンロードまたは購入できます。以下は、私たちが使用したツールやソフトウェアのリストです。
ソフトウェア ツール ショッピング リスト
ここまで、ソフトウェア分析を可能にするために遭遇したさまざまな障害と回避策について説明するのに多くの時間を費やしてきました。明らかに、この調査の最終目標は、ターゲットの脆弱性を明らかにすることでした。あまりネタバレすることなく、最終的に 9 つの HID Mercury パネルで 8 つの固有の脆弱性を発見することができました。LenelS2 は、影響を受けるすべてのハードウェアとソフトウェアのバージョンの概要を図 1 に示しました。
図1. 影響を受ける HID Mercury アクセス コントロール パネル
詳細に入る前に、調査結果の要約表を提供することが賢明であると考えました。これらの脆弱性の中で最も重大なのは、認証なしのリモート コード実行を可能にするものでした。この記事でこれらのバグの分析に多くの時間を費やすことはあまり重要ではないと考えました。
発見された脆弱性
CVE | 詳細概要 | Mercury ファームウェアのバージョン | CVSSスコア |
---|---|---|---|
CVE-2022-31479 |
未認証のコマンドインジェクション |
<=1.291 | ベース 9.0、テンポラル 8.1 |
CVE-2022-31480 |
未認証のDoS(Denial-of-Service) |
<=1.291 | ベース 7.5、テンポラル 6.7 |
CVE-2022-31481 |
未認証のリモートコード実行 |
<=1.291 | ベース 10.0、テンポラル 9.0 |
CVE-2022-31486 |
認証付きコマンドインジェクション |
<=1.291 | ベース 9.1、テンポラル 8.2 |
CVE-2022-31482 |
未認証のDoS(Denial-of-Service) |
<=1.265 | ベース 7.5、テンポラル 6.7 |
CVE-2022-31483 |
認証された任意のファイルの書き込み |
<=1.265 | ベース 9.1、テンポラル 8.2 |
CVE-2022-31484 |
未認証のユーザーによる改ざん |
<=1.265 | ベース 7.5、テンポラル 6.7 |
CVE-2022-31485 |
未認証の情報詐称 |
<=1.265 | ベース 5.3、テンポラル 4.8 |
シリーズをここまで読んでいただいた方は、これらの発見の中で最も重要な発見と活用に関する技術的な詳細に興味をお持ちいただいていることでしょう。
CVE-2022-31479: Web インターフェイスを介したコマンド インジェクション
脆弱な機能
ブログ シリーズの第1回目で説明したように、Webインターフェイスにはユーザー入力を消費する領域がいくつかありました。私たちはそれらの調査を行いました。OSコマンド インジェクションの脆弱性を見つける最も簡単な方法の 1 つは、「system()」への呼び出しのバイナリを検索することです。これは通常、図 2 のように、プログラムがコンパイル済みプログラムから Linux シェル コマンドを呼び出す場所です。
図2. 引数が渡されたシステム コール
ただし、システムへのすべての呼び出しがコマンド インジェクションの機会を探すときに役立つわけではありません。たとえば、図 2 では、渡された引数は静的です。つまり、何かを挿入して実行することはできません。ただし、図 3 に示すように、ユーザー提供のデータを利用するシステム関数は、はるかに魅力的です。ここでは、snprintf() 呼び出しには、ユーザー提供の変数を含むフォーマット文字列があります。
図3. システム コールへのパラメーター内のユーザーデータ
これと、この関数呼び出しのホスト名がどのように消化されているかを調査することに着手しました。ホスト名は「ネットワーク」設定タブ内のフィールドであることが判明しましたが、有効な文字は 0 ~ 9、az、AZ、「.」、および「-」のみであることが明確に示されていました (図 4)。これは私たちにとって「ビールを飲む」瞬間であり、この制限を回避できるかどうかの調査を開始しました。
図4. ホスト名の制限文字
スペースなどの無効な文字を追加しようとすると、エラー メッセージが表示されました。これはクライアント側のチェックであり、サーバー自体では行われませんでした。図 5 に示すように、エラーをトリガーする前に POST 要求を送信しなかったため、これを特定できました。
図5. ホスト名に「スペース」を含めようとしたときのエラー メッセージ
クライアント側の JavaScript をバイパスする簡単な方法は、Linux ユーティリティの curl を使用することです。Firefox と Chrome の両方の開発者ツールで、ネットワーク モニター セクションでネットワーク リクエストを右クリックし、「cURL としてコピー」オプションを使用して (図 6)、コマンド ラインを使用してネットワーク リクエストを再生するのは簡単です (図 7) )。
図6. Chrome の curl オプションとしてコピー
図7.「cURL オプションとしてコピー」からのポスト リクエスト
copy as cURL オプションを使用すると、結果のコマンドにはすべての Cookie と HTTP ヘッダー データが含まれ、元の要求と区別がつかなくなります (図 7)。ただし、POST リクエストのみを送信し、JavaScript を実行していなかったため、「ホスト名」フィールドに JavaScript 制限文字を追加して入力をテストしました。curl コマンドを実行した後、ホスト名が切り捨てられ、スペース文字が含まれていないことに気付きましたが、最終的にはバックエンドに POST しました (図 8)。これは、サーバー側の検証も行っていたことを意味します。
図8.「スペース」を含まない切り捨てられたホスト名
図9. ホスト名フィールドを解析した後の XssStringTest
「network.cgi」CGI バイナリを逆コンパイルすると、「parseFormsData」(図 9 #1) および「XssStringTest」(図 9 #2) 内でも有効な文字がサーバー側でフィルタリングされていることがわかりました。「parseFormsData」関数には、通常の空白文字を使用できないことを意味する「=」または「 」で POST データを分割する strtok 呼び出しがありました。さらに、この「XssStringTest」の内部では、「\”/%&=\\<>;」という追加文字のフィルタリングが行われていました (図 10)。
図10. 制限されたホスト名の文字
「&& 挿入コマンド」や「; 注入されたコマンド」。広範な試行錯誤の末、図 11 に示すように完全なコマンド インジェクション コマンドを許可する回避策を発見しました。これは最終的に CVE-2022-31479 として報告されました。このコマンド インジェクションをよりよく理解するために、以下で詳しく説明します。
図11. コマンド インジェクションを含むホスト名
図 11 は、スペース (\s または 0x20) 文字を機能させることができたことを示している可能性がありますが、表示されている空白は実際にはタブ文字 (\t または 0x09) です。Linux は、各引数を区切るスペースの数や空白の種類を気にしないため、シェルによって同じように解釈されます。「system()」の呼び出しのためにホスト名に興味をそそられましたが、図 12 に示すように、起動時に udhcpc 呼び出し中にコマンド インジェクションが行われました。Udhcpc は、ルーターから IP アドレスを取得するために使用される DHCP クライアントです。これは、デバイスのネットワーク機能がセットアップされる前に、コマンド インジェクションが行われていることを意味します。この機能をコマンド インジェクションに利用するために、ホスト名フィールド内に「udhcpc」への別のネストされた呼び出しを追加して、DHCP が適切に IP アドレスを返せるようにしました。制御するローカル マシンからより多くのコマンドをダウンロードできるようになります。「/」文字は XssStringTest によって除外されたため使用できなかったため、何かが接続されたときにシェル コマンドを提供するデバイスと同じネットワーク上に基本的なファイル サーバーをセットアップしました。ファイルへのフル パスがなくても、Web サーバーは常に応答します。これにより、「wget | wget」を挿入できました。ash” プリミティブを使用して、実行中の XssStringTest フィルターの制限なしでコマンドをリモートで実行できます。これにより、「wget | wget」を挿入できました。ash” プリミティブを使用して、実行中の XssStringTest フィルターの制限なしでコマンドをリモートで実行できます。これにより、「wget | wget」を挿入できました。ash” プリミティブを使用して、実行中の XssStringTest フィルターの制限なしでコマンドをリモートで実行できます。
完全なコマンド インジェクションは、次の図で簡単に理解できます。
図12. –H を介したホスト名への 2 部構成のコマンド インジェクション
このコマンド インジェクションの 1 つの注意点は、起動時またはシャットダウン時にのみ発生し、コア ダンプが作成されたときにファームウェアの以前のバージョンでのみ発生することです。そのため、コマンド インジェクションの脆弱性は機能していましたが、インジェクトしたコマンドは起動またはシャットダウンするまで実行されませんでした。これは最終的に、認証されていない強制的な再起動のために追加の CGI バイナリを調査することを意味しました。これについては後で詳しく説明します。
HTTP Cookie 検証について
私たちは当初、このコマンド インジェクションの脆弱性は、認証されたセッションを介してのみアクセスできると考えていました。Web インターフェイスにアクセスするたびに、ログイン ページにリダイレクトされます。ログインに成功した場合にのみ、特定の Cookie「session_id」に一意の乱数が入力され、サーバー側でチェックされます。そのため、Web ブラウザーまたは cURL などのコマンドライン ユーティリティを使用すると、Cookie が検証されます。セキュリティ研究で見過ごされがちな特性の 1 つは、失敗が予想される場合でも、常に試してみることが最善であるということです。これはまさに、ホスト名コマンド インジェクションで行ったことです。「session_id」Cookie 変数を架空の番号「13371337」に設定し、cURL 経由で送信しました (図 13 #1)。
図13. 偽の「session_id」を使用してコマンド インジェクションを送信しても、エラー応答が返される
一見すると、コマンドは失敗しており、HTTP タイムアウトが観察されました (図 13 #2)。「session_id」Cookie が明らかに有効でないため、これは予想どおりでした。ホスト名が、コマンド ライン経由で送信したコマンド インジェクションに変更されたことに気付いたのは、通常どおりデバイスに再度ログインした後でした。では、ログインしていないことを示すエラー メッセージが表示され、「time_out.htm」ページにリダイレクトされたのはなぜでしょうか。図 13 #2。
これは、セッション Cookie の管理と、これがまぐれなのか一貫して誤った処理なのかを調査し始めたときです。無効な Cookie データを Web サーバーのネットワーク ページに送信した後、最初にこの問題に気付いたのはコンソールでした。適切なセッション認証がなければ、ネットワーク設定が更新されることはありませんでしたが、すぐにコンソールに「DHCP IP を使用してください」および「ネットワーク データを適用してください」というメッセージが表示されました (図 14)。この知識を武器に、「network.cgi」バイナリ ファイルに戻ってさらに調査しました。
図14. 偽の「session_id」がまだネットワーク データを適用している
ここで、ユーザー入力が適用されるまで「session_id」Cookie がチェックされていないことがわかりました。これは、POST リクエストに含まれるすべての設定が認証なしで処理され、ログイン ページにリダイレクトされることを意味していました。明らかに、これは明らかなセキュリティ問題であり、認証バイパスは私たちが発見した多くの脆弱性で一般的でした.
さらに調査した結果、根本的な問題は、「network.cgi」バイナリが GET リクエストの「session_id」Cookie のみを適切にチェックし (図 15)、POST リクエストの「session_id」が存在するかどうかのみをチェックすることであることが判明しました (図 15)。 16)。
図15. GET 要求中にチェックされるセッション ID
図16. 値ではなく Cookie が存在するかどうかのみをチェックする POST 要求
これは、開発者が最初に GET 要求を発行せずに POST 要求が送信されるとは予想していなかったためである可能性があり、これは重大な見落としです。これを発見した後、他のどの CGI バイナリ ファイルに同様の問題があるかを確認することにしました。
残りの CGI バイナリの分析
LNL-4420 Web インターフェイスへの最初の偵察から、各 CGI バイナリがユーザー Cookie の存在を個別にチェックしていることに気付きました。これは、各 CGI バイナリが呼び出すことができる Cookie をチェックするための標準的な操作がないことを示していました。このため、これらの CGI バイナリのいくつかの操作の一部は、以前に行われました。Cookie チェックまたは GET リクエストのみの場合、認証されていないコード パスにヒットする可能性があります。ホスト名を介してコマンド インジェクションを行っても、ホスト名は起動時の DHCP プロセスで安全に使用されなかったため、適切な再起動なしでは提供されたコードをトリガーできませんでした。これにより、デバイスをオンデマンドで再起動する方法に焦点を当てるようになりました。デバイスで再起動プリミティブを検索しているときに、開発者がカスタム コア ダンプ ハンドラーをセットアップしていることがわかりました。LNL-4420 上のいずれかのバイナリでセグメンテーション フォールトが発生し、そのコアがダンプされた場合、このスクリプトはそれを処理します (図 17)。
図17. カスタム コア ダンプ ハンドラー
このファイルで最も重要なのは、コードの最後の行である「再起動」コマンドです。これは、CGI バイナリを含むデバイス上の実行可能ファイルを体系的にセグメンテーション違反にすることができれば、必要に応じて LNL-4420 を再起動できることを意味していました。
メモリ破損の新しい目標を念頭に置いて、メモリ破損の脆弱性に関連することが多い memcpy、strcpy、およびその他の既知の関数を含む「危険な」関数を検索することにしました。合計で 30 以上の CGI バイナリがあるため、「session_id」Cookie 値がチェックされた場所とその前にどの関数があったかを検索するプロセスを自動化することにしました。その結果が、図 18 に示す IDA Python スクリプトです。
図18. 「getSessionId」コードの前にあるすべての「危険な」関数を検出する IDA Python スクリプト
このスクリプトを使用してすべての CGI バイナリを実行すると、どれにもっと時間を費やすべきかが特定されました。さらに、「session_id」チェックの前に表示された「危険な」関数のそれぞれを手動で調べ、ユーザー提供のデータを利用してオーバーフローやメモリ破損を引き起こす可能性があるかどうかを判断しました (図 19)。
図19. IDA Python の危険な関数スクリプトからの出力
バイナリは、1 つを除いて、これらの関数を使用するセキュリティ リスクを軽減する方法でコーディングされています。これは「advanced_networking.cgi」であり、認証されていないセグメンテーション違反を体系的に引き起こすことができました。これが 2 番目の脆弱性となりました。その時点では、コマンド インジェクションのエンド ツー エンドのエクスプロイトを強制的な再起動で完了するのに必要なことはこれだけだと考えていました。
CVE-2022-31482: strcpy による非認証の強制再起動
ここでは、最初の脆弱性である CVE-2022-31479 に焦点を当てることに多くの時間を費やしました。ただし、コマンド インジェクションの実現は比較的単純であるにもかかわらず、これらのコマンドの実際の実行は再起動時にのみ行われ、任意に制御することはできませんでした。この脆弱性の発見は欠落していた部分であることが判明し、これら 2 つの脆弱性の連鎖を使用して制御された再起動で RCE を有効にしました。ただし、このクラッシュの悪用可能性の調査を開始したため、デバッグの取り組みにより、解決すべき追加の問題が発生しました。この問題の概要を簡単に説明した後、この脆弱性に戻ります。
デバッグとウォッチドッグ タイマーのバイパス
デバイスのソフトウェアをデバッグしようとしたときに、ウォッチドッグ タイマーで問題が発生しました。ウォッチドッグ タイマーは、ハングしているプロセスを処理するために CPU に組み込まれている低レベルのモニターです。これは、アクセス制御などの重要なシステムに役立ち、CPU がハングしたり、管理するシステムに問題を引き起こす可能性のあるデッドロック状態になったりした場合の解決策を提供します。この場合、これはアクセスを制御していた施設になります。ウォッチドッグは、一定時間 (LNL-4420 では 15 秒) が経過するとデッドロックを修正します。デバイスがまだ応答しない場合は、自動的にリセットされます。
通常の使用ではウォッチドッグがトリガーされないため、これが問題になることはめったにありません。ただし、プロセスをデバッグする場合、CPU はオペレーターが再び続行するまで命令を停止します。デバイスが再起動するまでブレークポイントで 15 秒間しか一時停止できなかったため、これは非常にイライラしました。15 秒で多くのことを成し遂げることはできません。そのため、デバッグ作業のペースを制御できるように、それを無効にすることにしました。LNL-4420 CPU のデータシートを見つけることができました。このデータシートには、ウォッチドッグ タイマーを無効にする方法が記載されています (図 20)。
図20. ウォッチドッグ タイマー モード レジスタを説明する Atmel データシート
これは紛らわしいかもしれませんが、ウォッチドッグ タイマー モード レジスタは 32 ビット (DWORD) であり、各「設定」は個々のビットです。図 20 のドキュメントには、15 番目のビットにある WDDIS ビットが 1 に設定されている場合、OS がウォッチドッグ タイマーを無効にすることが明確に記載されています。 Uboot の bootdelay 値を変更するために使用されます。デフォルト値は 0x3FFF2FFF (図 21) で、「WDDIS」ビットを 0 から 1 に上書きする必要がありました。
図21. ウォッチドッグ タイマー モード レジスタの有効化
完全な DWORD アドレスで変更する必要がある唯一のバイトは 0x2F でした。これには、反転する必要がある 15 番目のビットが含まれているためです。次に、メモリの内容を出力して、モード レジスタのメモリ アドレスが正しいことを再確認しました。これを確認したら、値を変更してウォッチドッグ タイマーを無効にしました (図 22)。
図22. J-Link を使用してウォッチドッグ タイマーを無効に
このプロセスで困惑したことの 1 つは、書き込みコマンドの結果です。WDDIS ビットを 0 から 1 に反転するために、「w1」を使用して 2 番目のバイト 0x2F だけを 0xAF に書き込みました。CPU が停止していても、1 バイトの上書きを実行しただけで、4 バイトのウォッチドッグ タイマー全体が変更されました。すぐに 0x2FAFAFAF にします。これについて簡単に調査しましたが、最終的に問題のバイトが 0xAF に変更されたままであり、目的には十分だったので先に進むことにしました。これらのコマンドが実行されると、変更が機能し、ウォッチドッグ タイマーが無効になっていることが確認できました。
図23. ウォッチドッグ タイマーの無効化
脆弱性に戻ります。セグメンテーション違反またはクラッシュとその後の再起動は、HTTP 要求のパラメーターで渡された長すぎる文字列によってトリガーされます。具体的には、非表示の「AcctStr」パラメーターです。ユーザー入力は安全でない関数 strcpy() によって解析され、null バイトに遭遇するまで文字列を読み込み続けます。これは、Mercury Web サーバーで使用されるさまざまな CGI バイナリ全体に存在するいくつかの安全でない関数の静的な反転と分析によって発見されました。前述のように、特定のバイナリは advanced_networking.cgi であり、後で確認するためにマークした追加の安全でないプリミティブが多数含まれていました。次のリクエスト (図 24) がクラッシュを引き起こし、100% の確率で確実に機能しました。
図24. クラッシュにつながる HTTP AcctStr 要求
以前のコマンド インジェクションの脆弱性とこの強制再起動の脆弱性を組み合わせて、優れた Python pwntoolsを使用して、エンド ツー エンドの認証されていない RCE エクスプロイト チェーンを作成しました。
システムのアップグレード
この時点で、システムを最新のファームウェアに更新する適切な方法があることを確認して、シミュレートされた実稼働システムを構築する時が来たと判断しました。これを行うために、私たちは Lenel システムのローカル サードパーティ インストーラーを雇って機能するドアを構築し、アクセス コントロール カード リーダーと配線し、Lenel の OnGuard 管理ソフトウェアと統合しました。このプロセスを通じて、最新のファームウェア バージョンが strcpy オーバーフローの脆弱性にパッチを適用していることがわかりました。コマンド インジェクションのエクスプロイトをトリガーするには、通常の再起動を待つ必要があります。これにより、再起動を呼び出すかコードを直接実行する認証されていない RCE、または再起動と関連するコマンド インジェクションをトリガーする新しい方法のいずれかである代替の脆弱性が必要になりました。
CVE-2022-31481: 認証されていないファームウェアのアップロードと再起動
CVE-2022-31482 の再起動プリミティブを失った理由は、以前は利用していたカスタム コアダンプ ハンドラがセグメンテーション エラーで再起動しなくなったためです。新しいロジックは、CGI バイナリが含まれていない重要なバイナリの 1 つがクラッシュしたときにのみ再起動するというものでした。これにより、以前と同じポイントに戻りました。認証されていないコマンド インジェクションの脆弱性がありましたが、再起動するまでコマンド インジェクションを利用する方法がありませんでした。
新しいファームウェアの更新により、一部のバイナリが変更されたことがわかり、新しい再起動プリミティブの検索に着手しました。ここで、ファームウェアの新しいバージョンでは、管理者が Web インターフェイスを介してファームウェアの更新ファイルを直接アップロードできることに気付きました。このファームウェア更新プロセスは、以前には存在しなかった新しい CGI バイナリによって処理され、「診断」タブと同時にロードされました (図 25)。
図25. 診断ペインの新しいファームウェア更新機能
ファームウェア更新機能を含む診断ウィンドウは、認証 Cookie の存在を正しくチェックしました。しかし、このページにサイレント モードで読み込まれた CGI バイナリ (view_FwUpdate.cgi) に直接アクセスしたところ、このページで Cookie チェックがまったく実行されていないことにショックを受けました。認証 (図 26)。
図26. 埋め込まれていないファームウェア更新ウィンドウ
これに関する最も魅力的なことは、「ファイルのロード」ボタンが押されるとボードが再起動することが明確に示されていることです. 最初にランダムなファイルをアップロードして、その方法で再起動をトリガーできるかどうかを確認しました. バックエンドが暗号化されたファームウェア更新の署名をチェックしていたため、これは失敗しました (図 27)。
図27. 無効なパッケージ署名エラー
OnGuard ファイル システムから有効なファームウェア ファイルにアクセスできたので、最初のアプローチはそれをアップロードすることでした。新しいファームウェアをデバイスに書き込むときにホスト名を保持することをお勧めします。
適切に署名され、暗号化されたバイナリ更新ファイルは、OnGuard 管理サーバー (後で詳しく説明します) にバンドルされており、インストール時からローカルの C: ドライブ (Lnl4420.bin) に配置されていました。さらに、ファームウェア ファイルのファイル サイズをチェックし、最大 15 MB に制限するために使用されるクライアント側の JavaScript がありました。ファイル サイズは、次のように JavaScript でチェックされます (図 28)。
図28. クライアント側の JavaScript ファイル サイズの検証
このサイズ チェックは、15MB のサイズ制限 (実際には 15728640 バイト) に明確に合わせられていないため、恣意的であるように見えます。実際、最新のファームウェア ファイル サイズは 15102734 バイトで、クライアント側のファイル サイズ検証に課された 15000000 バイトの制限よりも大きくなっています。これを回避するために、JavaScript をローカルで変更し、サイズ チェックをより大きなもの (1600000 バイトなど) に変更しました。この単純なクライアント側の変更手法により、署名および暗号化されたファームウェアをデバイスにロードし、最終的に再起動をトリガーすることができました。この認証されていない強制的な再起動は、最終的にサービス拒否として CVE-2022-31480 が割り当てられました。
正当なファームウェアをアップロードすると再起動が発生し、エクスプロイト チェーンの欠落した部分が再び埋められましたが、ファイルのアップロードと再起動プロセスの完了には長い時間がかかりました。また、署名チェックの設定ミスがないか、または悪意のあるファームウェア アップデートを自分で暗号化して、選択したファイルでデバイスをフラッシュできるかどうかにも関心がありました。
最初のステップは、署名チェックがいつ行われたかを確認することでした。ユーザー提供のデータを抽出または復号化せずに署名がチェックされていることを発見しました。これは、この種の検証を処理するための正しく安全な方法です。つまり、署名は平文で、データを復号化することなく読み取り可能である必要があります。正規のファームウェア アップデート ファイルを 16 進エディタで見ると、ファームウェア アップデート パッケージの最後の数百バイトが暗号化されておらず、base64 でエンコードされたデータと非常によく似ていることが明らかになりました(図 29)。
図29. ファームウェア更新ファイルの下部
図29 で、ファイル コンテンツの下部にはすべて ASCII の印刷可能な文字が含まれていることに注意してください。一方、上部には、記号が混在している非常に読みにくい文字表現があります。私たちの目を引いたもう 1 つの点は、末尾の数字 (158) でした。単純に見てみると、base64 でエンコードされたセクションのサイズを表しているのではないかと考えたので、データをコピーして base64 であるかどうかをテストしました (図 30)。
図30. 0x158 バイトまでは base64 コードの範囲のように見える
次に、このエンコードされた文字列を base64 デコーダーに渡し、さらにバイナリ データが残りました (図 31)。
図31. base64 文字列をデコーダーに渡す
この時点で、ファームウェア アップデート ファイルの最後の 3 バイトが、その前にあるファイル署名であると推定されるサイズとして使用されていると理論付けました (図 33)。サイズ値は、攻撃者によって指定され、任意の値に変更される可能性があります。さらに調査した後、openssl を使用して、これがファイル署名であるという仮定を確認し、検証しました (図 32)。
図32. 署名を使用して更新バイナリを検証
図33. 赤でハイライトされた署名サイズ
ターゲットを調査する場合、ユーザーが制御可能なサイズ フィールドは、多くの場合、調査に余分な時間を費やすのに適した場所です。これは、ほとんどのメモリ破損バグが可変サイズの問題によるものであるためです。私たちは、ファームウェア更新のために新しく導入された機能を処理する「mpl_icd_ep4592」バイナリ内のコードを調べ始めました。
ファームウェア アップデートから署名を抽出するコード内で、ハードコーディングされたサイズ 0x190 の「malloc()」(図 34 #1) が、署名の宛先バッファとして使用されました。ただし、サイズ フィールドは更新ファイル自体から読み取られ、文字列を long に変換する「strtol()」に渡されます (図 34 #2)。これは、サイズ パラメータとして「fread()」に渡され、「署名」の内容を固定サイズのバッファに保存します (図 34 #3)。渡されたサイズ パラメータが0x190。
図34. バッファ オーバーフローの静的な識別
認証されていないバッファ オーバーフローが特定されたため、この脆弱性を有利に利用するためのペイロードを構築するだけで済みました。検証または署名チェックの前に脆弱性に到達できるため、オーバーフローをトリガーするために正当なファイルを送信する必要がなくなりました。代わりに、テキスト ファイルに一意のパターンを入力して、オーバーラン バッファーが原因でユーザーが指定したデータ試行が実行されたかどうかを確認しました。最後の手順は、署名サイズ フィールドを 0x190 よりもかなり大きい値に変更することでした。値は 0x999 を使用しました (図 35)。
図35. 一意のパターンと 0x999 の署名サイズ
案の定、バッファをオーバーランした「fread()」関数のすぐ下の関数呼び出しで、「fclose()」から戻るときに、独自のパターンの値が実行されました (図 36)。
図36. 制御レジスタ r3 への分岐によるコード実行
これを悪用するための取り組みについては、次のセクションで説明します。さらに簡単にするために、「mpl_icd_ep4502」バイナリは「no RELRO」でコンパイルされました。これは、このバイナリ内のすべてのアドレスが静的であることを意味し、攻撃者がこのバッファ オーバーフローを介してコード実行を制御しやすくしました。
私たちの当初の目標は、新しい再起動プリミティブを見つけることだったので、「再起動」を呼び出すバイナリ内の既知の場所のアドレスを使用して、実行しようとした一意のパターンの一部を単純に変更することができました (図 37)。
図37. リブートが呼び出される mpl_icd_ep4502 内のアドレス
少なくともこれは私たちが考えていたことですが、さらに調査した結果、ヒープ操作がコード実行でアドレスの不整合を引き起こし、デバイスを再起動するバイナリの 1 つでセグメンテーション違反を引き起こしていることがわかりました。つまり、実際には再起動を実行していませんでした。現時点では。
再起動を直接呼び出すか、デバイスを segfault して再起動させるかに関係なく、この時点で、LNL-4420 で root として任意のコードを実行できる、完全にリモートで認証されていないエクスプロイト チェーンが再び存在しました。研究者は通常、抵抗が最も少ない方法を選択する傾向があり、再起動の原因となったオンデマンドのセグメンテーション エラーには満足していました。最終的に、このバグには CVE-20022-31481 が割り当てられ、ベース CVSS スコア 10.0 で Carrier に提出された最も影響の大きい脆弱性でした。
ただし、私たち研究者も粘り強く、ボードをエミュレートした後 (詳細は次で説明します)、Return Oriented Programming (ROP) を介してコードを直接実行できる可能性を探りました。ここではそのテクニックについて説明します。
リターン指向プログラミング
デバイス上のすべての Mercury バイナリは ASLR なしでコンパイルされましたが、ヒープとスタックは NX (実行不可) とマークされていました。ファイル アップロード ヒープ オーバーフローの脆弱性を悪用します。ROP は、アプリケーションまたはそのインポートされたライブラリによって利用されるバイナリから取得されたアセンブリ命令を連鎖させる手段を提供します。ROP「ガジェット」またはこれらの命令のセットを生成する最も一般的なツールの 1 つは、Ropperです。. 図 38 は、実行した 2 つの短いコマンドの結果を示しています。1 つ目は Ropper を使用し、検索にすべてのバイナリを含めて、可能性のある ROP 候補をテキスト ファイルに生成します。後者は、制御できるレジスターの条件に一致するガジェットを返すために定式化した単純な正規表現を実行します。
図38. Ropper ガジェットとガジェットの正規表現
ARM レジスタ r0、r1、r2、r3、r5、r6 を含むガジェットを選択したのは、実行時にこれらのレジスタに影響を与える可能性があるためです。私たちの偽のファームウェア ファイル (固有の ASCII パターン データで満たされた) はバッファ オーバーフローを引き起こすため、その過程で多くのレジスタが破壊されます。ファイルを閉じる操作の直前にブレークポイントを設定すると、r3 レジスタへの無条件の分岐が見られます (図 39 の blx r3 として示されています)。さらに、パターン データで直接 (r0、r1、r3、および r6)、またはパターン データへのポインターを介して (r4) 上書きされる特定のレジスターを確認できます。
図39. コード実行時に制御されるレジスタ
リバース シェルへの引数を指定して、「system()」を直接呼び出すことにしました。さまざまな ROP ガジェットで少し遊んだ後、(図 40) のガジェットがまさに目的を果たしていることがわかりました。
図40. 効果的な ROP ガジェット
このガジェットは完璧です!図 39 の分岐を介して r3 が「呼び出された」時点で、r3 を制御できることを思い出してください。このガジェットのアドレス (0xb6e43d2c) をパターン ファイル内のパターン内の「uvad」となるオフセットに単純に配置するとします。 、次にそのアドレスに分岐し、そこでガジェットの実行を開始します。
ガジェットの最初の部分は、mov r3、r0 です。最後のステップは blx r3 で、2 つの間に r3 への書き込みはありません。これは、system() の呼び出しにジャンプする理想的な場所です。したがって、バイナリで system() のアドレス (図 41) を見つけ、それをパターン ファイルの r0 – ‘utaa’ に配置します。これで、ガジェットが実行されると、r0 のシステム アドレスが r3 に移動され、その直後に分岐で実行されます。
図41. system() のアドレス
ただし、system() へのすべての呼び出しには、実行する対象を指定するアドレスへのポインターである引数が必要です。この関数呼び出しを呼び出すと、カーネルはレジスタ r0 で始まる引数を見つけることを期待しています。ガジェットの 2 番目のステップで r4 を r0 に移動し、r0 を再度上書きしないため、これも完璧です。r4 はパターン データへのポインターであり、system() も同様にポインター値を想定しているため (図 41)、すべてが揃っています。これで、ガジェットを解析して最適な候補を見つけるために作成した正規表現について理解が深まったかもしれません。system の引数に対する最初の試みは、制御している C2 サーバーからリバース シェルをダウンロードするために使用した wget コマンドでした。これは、後で必要になるレジスタ値を上書きする前に、パターンに約 28 の連続したバイトしかなかったからです。いくつかのヒープ操作が原因で、そのエクスプロイト文字列の一部がコピーされ、無効になってしまいました。ただし、明るい兆しは、同じクラッシュの亜種が発生したことです。同じレジスタはすべて制御可能でしたが、さらに重要なことは、r4 レジスタ パターンがファイルのはるかに上位にあり、真逆のシェル引数。
かかったのはこれだけです。これらの手順を実行すると、パターン ファイルは図 42 のようになります (パターン データは、前に示した例と一致しない場合があることに注意してください)。
図42. 正しいレジスタ オフセットを含む最終的なパターン ファイル
ガジェットの最終命令にブレークポイントを設定することで、すべてリバース シェルの引数を持つシステム コールに配置されます。
図43. システム コールとリバース シェルを使用した最終的なブレークポイント
これにより、エミュレートされたシステム内でデバイスをクラッシュまたは再起動することなく、認証されていない完全なリモート コード実行が可能になりました。しかし、これを物理ハードウェアに転送しようとすると、競合するプロセス、スレッド、およびヒープ操作がエクスプロイトの信頼性を低下させることがわかりました。時間に余裕があったので、以前のコマンド インジェクションに連鎖したこの脆弱性を使用して、強制的なセグメンテーション フォールトを使用することを選択しました。
残りの脆弱性
strcpy を適切に使用していない CGI バイナリは 1 つしか見つかりませんでしたが、さらに見つけることを諦めませんでした。私たちが探していた他の種類の脆弱性は、ロジック エラーとコマンド インジェクションでした。CGI バイナリのどこで「session_id」Cookie がチェックされているかを知ることで、認証されていない CGI バイナリ コマンド インジェクションを絞り込むことができました。
このセクションの冒頭の表は、これらの調査結果の概要を示しています。再起動によって認証されていない RCE をすでに達成しているため、詳細には触れません。
ベンダーに提出されたその他の脆弱性には、ユーザーを削除する機能 (CVE-2022-31484) と、認証されていないホームページの「メモ」セクションを更新する機能 (CVE-2022-31485) が含まれていました。また、認証された場合、ファイル アップロード フォームのディレクトリ トラバーサル バグを介してシステム上の任意のファイルを上書きし (CVE-2022-31483)、特別に細工されたネットワーク ルートを介してコマンドを挿入できることも示しました (CVE-2022-31486)。 . これらの他の脆弱性はどれも、認証されていない RCE のプロセスにおいて、ホスト名とバッファー オーバーフローによる再起動よりも役に立ちませんでした。発見されたすべての脆弱性は、パッチ適用のために (Carrier を通じて) HID Mercury に報告されました。
エミュレーション
メモリ破損の脆弱性がこれ以上ないことを徹底的に確認し、CGI バイナリを介して他のクラッシュが発生する可能性があるかどうかを確認するために、基本的なファジングを実行しました。これは、移植可能なファイルにきちんと配置されたすべての実行可能ファイルとサービス構成とともに、既にダンプした各 MTD パーティションによって支援されました。LNL-4420 をエミュレートすることは、ボードが 1 つしかなかったため、リスク許容度の観点からだけでなく、ファジングと動的デバッグの容易さからも優先されました。
LNL-4420 ボードから取得したファイルシステム ダンプに chroot するためのホストとして、32 ビット ARM Debian Jessie VM (図 44) を作成しました。
図44. ARM 32 ビット Debian VM の作成と実行
動的ライブラリが使用されている場合、システム エミュレーションはユーザー/バイナリ エミュレーションよりもわずかに使いやすいため、このために VM 全体を作成しました。Debian VM 内に入ると、ダンプしたイメージから MTD パーティションをエミュレートし、LNL-4420 のファイル システムに chroot して調査を続けることができました。
図45. MTD デバイスをエミュレートし、MTD ファイルシステム ダンプに chroot
図 45 に示すスクリプトは、フラッシュをダンプした時点のターゲット パネルと同じ元のファイル システムを作成します。デバイスの完全なシステム エミュレーションを実行しようとせず、Qemu にカーネルと MTD パーティションを起動させようとはしませんでした。単純な理由は、CGI バイナリに焦点を当てていて、すべてのハードウェアをエミュレートする必要がなかったからです。もう 1 つの理由は、LNL-4420 には LED、リレー、およびカード リーダーを含む多数のデバイスがあり、機能にパッチを当てたり、エミュレートを成功させるのは困難だったからです。
デフォルトでは、CGI-bin は STDIN からすべての POST データを読み取り、図 46 #1 に示す関数「getQspFromPOST」関数でデータを検証します。
図46. STDIN から読み取る CGI バイナリ
CGI-bin ファイルのエミュレートは、「echo 「post data」と同じくらい簡単でした。./file.cgi」. この設定により、デバイスや実行中の Web サーバーにアクセスすることなく、GDB および GDB サーバーを使用して各 CGI バイナリを動的にデバッグすることもできました。
エミュレートされたセットアップと GDB を使用して、CGI バイナリをステップ実行するときに、実行がユーザー提供の POST データを消費する解析コードに到達しないことに気付きました。これは、関数がローカル通信のために内部の名前付きパイプを開こうとしたことが原因でした (図 47)。
図47. エミュレーションに失敗したブランチ
CGI バイナリからこのブランチにバイナリ パッチを適用して、解析コードをヒットさせることができました。簡単にするために、 Cutterを使用してバイナリにパッチを適用しました (図 48)。「BL OpenCcreqDomainSocket」命令は「mov r0, 1」に変更され、「OpenCcreqDomainSocket」がまったく実行されなかったにもかかわらず、「正常に」実行されたことを結果のチェックに伝えました (図 49)。
図48. 「OpenCcreqDomainSocket」への呼び出しにパッチを適用
図49. CGI バイナリにパッチを適用した結果
これらのバイナリを手動で実行し、デバッガーを使用してステップ実行できる機能は便利ですが、31 個の CGI バイナリを手動で分析するには時間がかかりすぎます。したがって、ファジングをさらに深く掘り下げることにしました。
ファジング
古典的なプログラマーのメンタリティーで、おそらく手動で数時間かかっていたであろうバグの検索を自動化するのに数日を費やしました。でも、楽しかったし、新しいことを学びました!
最初に、単純なファジング ミューテーターRadamsaを使用しました。Radamsa は非常に使いやすく、非常にうまく機能します。図 50 からわかるように、ミューテーションは「インテリジェント」であり、ランダムではありません。Radamsa は、フィールドが文字列であることを認識して文字列を変更しようとします。または、IP アドレスの場合 (図 50 の赤いテキストで強調表示)、IP を負の数に変更することを決定しました。
図50. Radamsa ファジング文字列の例
エミュレートされた ARM 環境で Radamsa を使用するために、ソースからコンパイルし、「—static」を GCC に渡して静的にしました。このようにして、chroot 環境内から Radamsa ライブラリを使用でき、依存関係の問題について心配する必要はありません。Radamsa はミューテーターであるため、テスト中のアプリケーションを監視して、ミューテーションがクラッシュを引き起こしたかどうかを確認する方法はありません。このタスクは、ユーザーが処理する必要があります。私たちの場合、「0」以外のリターン コード (正常終了) を監視することにしました。「0」以外のリターン コードが返されるたびに、GDB を呼び出してテスト ケースを再実行し、プログラムがクラッシュした理由と場所のスタック トレースをキャプチャします (図 51 参照)。クラッシュの結果を次に示します。図 52.
図51. Radamsa を使用して CGI バイナリをファジングする簡単なスクリプト
図52. Radamsa ファザーの結果
新しいファジング プラットフォームを学習するために、Honggfuzzをセットアップして、CGI バイナリをさらにファジングしました。Honggfuzz は、Google が開発したフィードバック駆動型のファザーであり、Qemu ブラックボックス ファジングをサポートしており、このシナリオに最適です。Docker のファンとして、私たちは Docker に Honggfuzz をセットアップし (図 53)、すべての依存関係を保持し、100% (60% の時間) TM を「正常に動作」させるのに役立ちます。
図53. Docker コンテナー内での Honggfuzz のビルド
Honggfuzz が Docker イメージにインストールされているので、Qemu ブラックボックス インストルメンテーション モードを有効にする必要がありました。これを実現するには、Honggfuzz Docker コンテナーに入り、カスタム Honggfuzz Qemu バイナリーをビルドする必要がありました (図 54 を参照)。
図54. ブラックボックス ファジング用の Honggfuzz Qemu のビルド
デフォルトでは、Honggfuzz Qemu モードは x64 および x86 用の Qemu をビルドします。ARM バイナリをファジングしたかったので、図 55 に示すように変更する必要がありました。すべての警告がエラーとして処理されたためにビルドも失敗したため、「—disable-werror」を追加することでこの問題を回避しました。
図55. Honggfuzz Qemu モードの Makefile の編集
Honggfuzz の「Qemu-arm」バイナリをコンパイルしたら、Honggfuzz を通常どおり呼び出し、「Qemu-arm」バイナリをテスト対象として、CGI バイナリを引数として渡すことで、CGI バイナリのファジング プロセスを開始できます (図 56)。
図56. 「advanced_networking.cgi」ファイルをファジングする Honggfuzz コマンド
Honggfuzz が起動すると、巧妙に作成されたダッシュボードにステータスが表示されます (図 57)。図 57 には、「advanced_networking.cgi」ファイルの 2 つのクラッシュも示されています。これらは、わずか 9 秒の実行時間後に検出されました。
図57. 2 つのクラッシュが発生した Honggfuzz ダッシュボード
図 58 に示すように、Honggfuzz を 3 時間だけ実行しただけで、750,000 を超えるクラッシュが発生しました。
図58. 3 時間以内にさらに多くのクラッシュが発生
これを判断するには、膨大な数のクラッシュを絞り込んで、どれが新しいもので、どれが別の方法でトリガーされた同じバグの亜種であるかを理解する必要がありました. 元のファジング スクリプトを GDB で再利用して、各クラッシュのスタック トレースを取得し、md5sum ハッシュを使用して他のクラッシュと比較しました (図 59)。
図59. GDB を使用して「固有の」クラッシュを識別する単純なスクリプト
最終的に、「advanced_networking.cgi」の 2 つ以外にクラッシュは見つかりませんでした。これらのバイナリに対してファジングを機能させるのに多くの時間がかかったように見えるかもしれませんが、それはセキュリティ研究の重要な部分です. 新しいツールを学び、スキルを練習することで、たとえ望ましい結果が得られなくても、将来の他のプロジェクトでチームの専門知識が常に向上します。
ただし、前述のファームウェアの以前のバージョンでは、これら 2 つのセグメンテーション違反を再起動プリミティブとして引き続き使用できました。ホスト名コマンド インジェクションと組み合わせると、完全な RCE、リモート、認証なしの結果が得られました。
2022年8月25日(木)には、シリーズの最終パートである第3回目の記事を公開しています。このパートでは、エクスプロイトとエンドツーエンドのデモの様子をお伝えし、締めくくります。
本記事およびここに含まれる情報は、啓蒙目的およびTrellixの顧客の利便性のみを目的としてコンピュータ セキュリティの研究について説明しています。Trellixは、脆弱性合理的開示ポリシーに基づいて調査を実施しています。記載されている活動の一部または全部を再現する試みについては、ユーザーの責任において行われるものとし、Trellixおよびその関連会社はいかなる責任も負わないものとします。
Trellixは、米国およびその他の国におけるMusarubra US LLCまたはその関連会社の商標または登録商標です。その他の名称やブランドは、該当各社の商標または登録商標です。
※本ページの内容は2022年8月11日(US時間)更新の以下のTrellix Storiesの内容です。
原文:A Door Isn’t a Door When It’s Ajar – Part 1
著者:Sam Quinn、Steve Povolny