このページの本文へ

McAfee Blog

McAfee Labs、ビル管理にも使われる産業用制御システムの脆弱性を発見(前編)

2019年08月13日 15時30分更新

文● McAfee

  • この記事をはてなブックマークに追加
  • 本文印刷

 McAfee LabsのAdvanced Threat Research Team(ATR)は、より安全な製品を開発者が事業や個人に提供するのをサポートするため、ソフトウェアとハードウェア双方のセキュリティーに関する問題の発見に取り組んでいます。私たちはこのほどDelta Controls社製の産業用制御システム(ICS)について調べました。「enteliBUS Manager」と呼ばれるこの製品は、ビル管理など複数のアプリケーションに使用されています。Deltaのコントローラを調べた結果、「main.so」ライブラリで未報告のバッファーオーバーフローが発見されました。CVE-2019-9569によって識別されたこの欠陥は、最終的にリモートコード実行を可能とし、悪意のある攻撃者によってアクセスコントロール、ボイラー室、冷暖房空調設備などが操作される可能性があります。私たちは2018年12月7日にこの調査結果をDelta Controlsに報告しました。Deltaは数週間のうちに対応し、私たちは継続的な対話を開始し、セキュリティー修復の構築とテストを経て、2019年6月下旬に公開しました。その全体のプロセスにおけるDeltaの努力と協力を私たちは高く評価します。今回の調査で確認された脆弱性とその潜在的な影響について詳細の技術的分析をお伝えします。

攻撃対象範囲の調査

 新しいデバイスを調査するにあたっての最初の課題は、ソフトウェアとハードウェアの両面から、それがどのように機能するかを理解することです。産業用制御システム領域の多くのデバイスと同様に、このデバイスには3つの主要なソフトウェアコンポーネントがあります。ブートローダー、システムアプリケーション、そしてユーザー定義プログラミングです。攻撃の経路についてソフトウェアを調べることは重要ですが、ユーザーが定義する対象範囲についてはインストールごとに変更される可能性があるため重点を置いていません。したがって、私たちが重視したいのは、ボートローダーとシステムアプリケーションです。オペレーティング・システムについては、ユーザー固有のプログラミングに関わらず、デバイスを運用するためにメーカーがカスタムコードを実装するのが一般的です。このカスタムコードは脆弱性が最も多く存在する場所にあることが多く、プロダクトインストール基盤の全体に広がっています。それにもかかわらず、私たちはどうやってこのコードにアクセスすればいいのでしょうか?重要なシステムであるがゆえに、ファームウェアとソフトウェアは公開されておらず、資料も限定されています。従って、基礎となるシステムソフトウェアの周辺を調査するしか手はありません。最も重大な脆弱性がリモートであることを考えると、デバイスの単純なネットワークスキャンから始めることは理にかなっています。TCPスキャンではポートが開かれず、UDPスキャンによってのみポート47808と47809が開かれたことを確認しました。資料を参照して、これがおそらくBuilding Automation Control Network(BACnet)と呼ばれるプロトコルに使用されていることを特定しました。BACnet固有のネットワーク列挙スクリプトを使用して、もう少し情報を見つけ出しました。

root@kali:~# nmap –script bacnet-info -sU -p 47808 192.168.7.15

Starting Nmap 7.60 ( https://nmap.org ) at 2018-10-01 11:03 EDT
Nmap scan report for 192.168.7.15
Host is up (0.00032s latency).

PORT STATE SERVICE
47808/udp open bacnet
| bacnet-info:
| Vendor ID: Delta Controls (8)
| Vendor Name: Delta Controls
| Object-identifier: 29000
| Firmware: 571848
| Application Software: V3.40
| Model Name: eBMGR-TCH

 次に問題となるのは、ハードウェアからは何を学べるのか、ということです。この問題の答えを出すため、図1に示すように、最初にデバイスを慎重に分解しました。

図1

 コントローラーには、ディスプレーを管理する1つのボードと、プロセッサーモジュールとフラッシュモジュールの両方を含むシステムオンモジュール(SOM)チップを保持するメイン基板があります。その基盤をよく観察し、私たちはいくつかの重要な所見を述べました。まず、プロセッサーはARM926EJコアプロセッサー、フラッシュモジュールはボールグリッドアレイ(BGA)チップで、基盤には複数の未実装ヘッダーがありました。

図2

 ソフトウェアをより効果的に調べるには、ファームウェアを引き出す方法を決めなければなりませんでした。フラッシュメモリーのシステムに使用されるBGAチップはほぼ間違いなくファームウェアを保持しているとみられますが、ここで新たな課題が生じます。BGAチップはほかのチップと違って、付着可能なピンを対外的に提供していません。つまり、チップに直接アクセスするため、はんだ付けされたチップを基盤から除去しなければなりません。これはシステムを損壊する恐れがあり、良い考えとは言えません。

 また、基盤に複数の未実装ヘッダーがあることも判明しました。これらのヘッダーの1つを使用してファームウェアを取り出す別の方法を見つけることができるので、これは有望でした。それぞれの未実装ヘッダーにピンをはんだ付けし、ロジックアナライザーを使用することで、基盤の中央にある4ピンヘッダーは115200の通信速度で実行中の汎用非同期送受信回路(UART)ヘッダーであると判断しました。

図3

 Exodus XI Breakoutボード(@Logan_BrownとExodusチームに敬意を)を使用してUARTヘッダーに接続すると、保護されてないルートプロンプトがシステムに表示されます。今やシステムにフルアクセスすれば、システムがどのように機能するかより深い理解を得て、ファームウェアを抽出できるようになります。

図4 

ファームウェア抽出とシステム分析

 UARTインターフェイスを使用すると、リアルタイムでシステムを調査できますが、オフライン分析のためのファームウェアをどのように取り出せばいいのでしょうか?デバイスには2つのUSBポートがあり、ともにUSBドライブを実装できます。これにより、ddを使用してメモリーで実行中のものをフラッシュドライブにコピーできるようになり、ファームウェアを効率的に抽出します。次なる課題は、何をコピーするか、でした。

 「/proc/mtd」を使用してメモリーがどのように分割されているかの情報を得ると、mtd4とmtd5にあるファイルシステムを見ることができます。ddを使用してmtd4とmtd5の両方のパーティションからコピーしました。私たちは後に、もしも永続的な問題が検出されたら、画像の1つがシステムフォールバックとして使用されるバックアップであることを発見しました。プロジェクトが続くにつれて、このファイルシステムのコピーはますます有益になりました。

 アクティブなUART接続を使用することでシステムがどのように実行しているかをより詳細に調べられるようになりました。私たちはこのデバイスが 47808と47809のポートだけをリッスンすることを、前もって決定することができるので、これらのポートをリッスンするいかなるアプリケーションも、リモートエクスプロイトの唯一の攻撃ポイントになります。これは、UARTコンソールから「netstat-nap」を使用するとすぐに確認されました。

 47808ポートは「dactetra」と呼ばれるアプリケーションによって使用されていることがわかりました。最低限の追加調査の結果、これはデバイスの主要機能を担うDeltaコントローラー特有のバイナリであると判断しました。

脆弱性を確認

 オープンポートを介したネットワークをリッスンするデバイス固有のバイナリーを使用すれば、脆弱性の探索を開始するのにふさわしい場所がわかります。調査を始めるにあたって、私たちは一般的なネットワークファジングのアプローチを用いました。BACnetのネットワークファジングを実行するため、BACnetサーバー用に設計されたモジュールを持つSynopsys製のDefensicsと呼ばれるツールを用いました。このデバイスはBACnetサーバーではなく、ルーターとしての機能を持ちますが、このテストスイートは、着手するのに最適な場所を示す複数のユニバーサルテストケースを提供します。BACnetは通信するために何種類かのブロードキャストパケットを活用します。「Who-Is」と「I-Am」の2つの通信パケットはすべてのBACnetデバイスに対応し、Defensics はそれらと連動するモジュールを提供します。Defensicsファズツールを使用してこれらのパケットの変異を作成することで、図5に示すように、デバイスが障害ポイントに遭遇してコアダンプを生成し、すぐに再起動するのを観察することができました。

図5

 クラッシュを引き起こしたテストケースはその後切り分けられ、クラッシュに反復性があることを確認するため、何度か実行させました。このプロセスの過程で、クラッシュを引き起こすオリジナルの不正形式のパケットの後、さらにに96パケットが送信されたことを確認しました。一連の不正形式のパケットは、以下にあるように「I-Am」パケットでした。フルパケットは容量が大きいため表示されません。

図6

 さらに調査を進めると、ファズツールが「0x22」を使用して8216バイトのBACnetのレイヤーサイズを持つパケットを生成したことがすぐにわかりました。また、ファズツールがBACnetアプリケーションレイヤーの最大受容サイズをわずか1476バイトと認識したこともわかりました。追加テストによって、このパケットを送信するだけでは同じ結果とはならないことがわかりました。97のすべてのパケットが送信された場合のみ、クラッシュが発生しました。

クラッシュの分析

 システムはクラッシュにコアダンプを提供するため、さらなる情報を得るために分析することは理にかなっています。コアダンプ(図7で再現)により、デバイスがセグメンテーションの欠陥に直面したことが確認できました。また、レジスターR0が破損しているおそれがあるバックトレースとともに、不正形式のパケットをコピーしたデータと似通ったものを含んでいることがわかりました。

図7

 コアダンプによってクラッシュの正確な場所も判明しました。デバイスのメモリーマップを使用することで、アドレス0x4026e580がmemcpyに位置していると判断することができました。デバイスは、Address Space Layout Randomization(ASLR)を導入していないため、メモリーアドレスはテストを通じて変更はありませんでした。ファームウェアの抽出に成功したので、IDA Proを使用してこのクラッシュの原因についてさらなる調査を試みました。開発者がコンパイル時にバイナリーを削除していなかったことで、IDAでの反転処理が容易になりました。

図8

 分解によって、memcpyがR0に格納されている「アドレス」にR3内にあるものを書き込もうとしていたことがわかりました。ところが、このケースではそのアドレスを破損したため、セグメンテーションの欠陥を引き起こしてしまいました。いくつかの他レジスターのコンテンツも新たな情報を提供しました。R3内の0x81値は、BACnet Virtual Link Control (BVLC)レイヤーからの最初のBACnetパケットのバイトである可能性があり、そのパケットをBACnetとして識別しました。R3と、R5にあるアドレス値を一緒に調べることで、これがまぎれもなくBVLCレイヤーであるとの確信を高めました。これは、コピーされたデータが最後に送信されたパケットからきたもので、コピーデータの送信先が最初の不正形式のパケットから得られたものであることを示唆しています。レジスターR8とR10はソースと送信先ポート番号をそれぞれ持っており、このケースでは0xBAC0(エンディアンを構成)、標準的なBACnetポートの47808でした。調べてみると、R4には上書きの痕跡があるメモリーのセクションを示すメモリーアドレスがありました。ここに示すのが不正形式のパケットからのデータ(0x22)で、いくつかの領域では、メモリーはパケットデータで一部上書きされていました。memcpyの送信先の値はこの領域のメモリーに由来しているようでした。ASLRが有効ではないので、いつもこれが同じ位置にくると予測することができます。

図9

 コアダンプ、パケット、IDAから得られた情報によって、この時点では、発見されたクラッシュはバッファー・オーバーフローでほぼ間違いないと考えました。ただし、memcpyはとても一般的な関数なので、このクラッシュの原因をより正確に究明する必要がありました。memcpyの送信先アドレスが破損しているとすれば、memcpyでのクラッシュはバッファー・オーバーフローによる単なる二次的被害ですが、それではどんなコードが実際にバッファー・オーバーフローを引き起こしたのでしょうか?この分析を始めるのにふさわしいのは、バックトレースでしょう。ただし、上記にあるように、バックトレースは入力データにより破損しています。このデバイスはARMプロセッサーを使用しているため、どんなコードがmemcpyを呼び出しているかのヒントをLRレジスターから探すことができます。処理のメモリーマップを参照すると、ここでは、LRは「main.so」に落ち込んだ0x401e68a8を指しています。統計分析に使用するため補正値を計算すると、図10にあるコードにたどり着きました。

図10

 LRレジスターはmemcpyが戻った後に呼び出された指示を指しています。この場合、補正値0x15C8A4でLRが指しているアドレスの直前の指示に興味を持ちました。最初は予期していたmemcpyの呼び出しがなかったことに驚きましたが、scNetMove関数を少々掘り下げてみると、scNetMoveは単にmemcpyのラッパーだったことがわかりました。

図11 

 それでは、どうやって間違った送信先アドレスがmemcpyに渡されたのでしょうか?これに答えるためには、memcpyに送信されるバッファーの設定にどんなコードが対応しているのかとともに、システムがどうやって着信するパケットを処理しているのかについて理解をより深める必要があります。私たちはメインプロセスが19個のスレッドを生成しているのを見るため、psを使用して実行中のシステムを評価することができます。

 「scNetMove」で発見した関数は「scBIPRxTask」と呼ばれ、メインバイナリーの外部の一か所のみで、つまりアプリケーションのネットワークの初期化関数において参照されました(図12参照)。

図12

 scBIPRxTaskを分解したところ、新しいスレッド、つまり、ポート47808とポート47809上の両方のBACnet IPインターフェースのために作成された「タスク」を発見しました。生成されたこれらのスレッドはそれぞれのポートに着信するすべてのパケットを処理します。パケットがシステムによって受信されると、scBIPRxTaskに対応するスレッドがそれぞれのパケットをトリガーします。IDA Proデコンパイラーを使用すると、それぞれのパケットで何が起きるかを確認できます。まず、この関数はmemsetを使用してスタック上に割り当てられたバッファーを消去し、ネットワークソケットからこのバッファーに読み込みます。このバッファーは次のmemcpy呼び出しのソースになります。新しいバッファーは1732バイトの静的サイズで作成され、ソケットから1732バイトのみが適切に読み取られます。

図13

 ソケットからデータを読み込んだ後、この関数は受信したばかりのパケットを格納する場所を設定します。ここでは「pk_alloc」と呼ばれる関数を使用します。これは作成するパケットのサイズを唯一の引数とします。サイズは新たな静的な値となり、ソケット読み取り機能から受信したサイズではないことがわかりました。今回渡される新たな静的な値は1476バイトです。この割り当てられたバッファーはmemcpyの送信先になるものです。

図14

 ソースと送信先の両方のバッファーが割り当てられると、「scNetMove」が呼び出され、続いてmemcpyが呼び出され、ソケット読み取りの戻り値から取得したサイズパラメータとともに両方のバッファーが渡されます。

図15

 このコードパスでは、脆弱性がなぜ、どのように起こるかを説明しています。送信されたパケットごとに、スタックからメモリーにコピーされます。 ただし、パケットが1476バイトより長い場合、1476より大きく1732以下のバイトでは、送信先バッファーの最後を過ぎたメモリー内の多くのバイトが上書きされます。上書きされたメモリー内には、その後のmemcpy呼び出し送信先へのアドレスがあります。これは任意の書き込み条件につながるバッファー・オーバーフローの脆弱性があることを意味します。最初の不正な形式のパケットは、攻撃者が定義したデータ(このケースでは攻撃者が書き込みを希望する場所のアドレス)でメモリの一部を上書きします。追加の95パケットがシステムによって読み取られた後、攻撃者によってコントロールされたアドレスは、送信先バッファーとしてmemcpyに入力されます。不正な形式である不要な最後のパケットのデータは、以前の不正な形式のパケットで設定された場所に書き込まれる内容となります。最後のパケットも攻撃者によってコントロールされていると仮定すると、これはwrite-what-where状態になります。

ウォッチドッグタイマーの無効化

 発見した脆弱性をしっかりと把握した上で、次の論理的なステップは実用的なエクスプロイトを作成することです。エクスプロイトを開発するとき、ターゲットを動的にデバッグする機能は非常に重要です。そのために、チームは最初にgdbserverなどのデバッグツールをデバイス固有のカーネルとアーキテクチャー用にクロスコンパイルする必要がありました。このデバイスは古いバージョンのLinuxカーネルを実行しているので、古いバージョンのBuildrootを使用してgdbserverとそれ以降の他のアプリケーションを構築しました。

 USBドライブを使用してgdbserverをデバイスに転送し、実行中のアプリケーションをデバッグする最初の試みが行なわれました。図16に示すように、デバッガーをアプリケーションに接続して数秒後に、デバイスは再起動を開始しました。

図16

 エラーメッセージは、なぜクラッシュが発生したのかについての手がかりを与えてくれます。それは、ウォッチドッグタイマーの不具合を示しています。ウォッチドッグタイマーは、重要な組み込みデバイスでは一般的で、システムが一定時間応答しない場合、問題を解決しようと対処します。このケースでは、開発者が選択した操作はシステムを再起動することです。このエラーメッセージのシステムバイナリーを検索すると、図17に示すコードのセクションが明らかになりました。実際のエラーメッセージはベンダーの要求により編集されています。

図17

 この関数は、3つのカウンターをデクリメントします。いずれかのカウンターがゼロになると、エラーがスローされ、それからシステムが再起動されます。コードをさらに調べると、複数のプロセスがこの関数を呼び出してカウンターをひっきりなしにチェックしていることがわかります。つまり、このソフトウェアのウォッチドッグを無効にする方法を見つけ出さなければ、システムを動的にデバッグすることはできません。

 この問題に対する一般的なアプローチの1つは、バイナリーにパッチを当てることです。バイナリ―へのパッチ適用を検討する際に重要なのは、使用しているパッチが意図しない副作用を引き起こさないようにすることです。これは一般的に、できる限り変更を最小限にとどめたいことを意味します。このケースでチームが考え出した最小限の有意義な変更は、「5を引く」を「0を引く」に変更することでした。これによってプログラム全体がどのように機能するのかが変わることはありませんが、カウンターをデクリメントするために関数を呼び出しても、必ずしもカウンターが単純に小さくなるとは限りません。パッチされたコードは図18に示してあります。IDAデコンパイラーが、もはや意味がなくなったコードから減算ステートメントを完全に削除したことに留意してください。

図18

 ソフトウェアのウォッチドッグがパッチされた状態で、チームはアプリケーションを再び動的にデバッグしようとしました。gdbserverに接続してアプリケーションのデバッグを開始することができたため、当初はテストは成功したとみられていました。ところが、3分後にシステムがまたもや再起動しました。図19は、同じ結果をもたらす実験が何度か繰り返された後、再起動時にチームが検出したメッセージを示しています。

図19

 これは、開始時の起動フェーズでのハードウェアのウォッチドッグが180秒(3分)に設定されていることを示します。システムにはハードウェアとソフトウェアの2つのウォッチドッグタイマーがあります。そこでタイマーを1つだけ無効にしました。バイナリ―にパッチを当てる方法はソフトウェアのウォッチドッグタイマーを無効にするために使用した方法と同じで、ハードウェアのウォッチドッグタイマーには機能しません。つまり再起動を防ぐためにアプリケーションもウォッチドッグを無効にする必要があります。この知識を武器に、ハードウェアのウォッチドッグを「追い出す」のに役立つかもしれないコードのためにデバイス上にあるDeltaバイナリーに目を向けました。デバッグの特徴が残っているので、ハードウェアのウォッチドッグ管理に対応する関数を比較的簡単に見つけることができます。

 ハードウェアのウォッチドッグを無効にしようとする際に使用する可能性がある複数のアプローチがあります。このシナリオでは、ハードウェアウォッチドッグを処理したコードが共有ライブラリーにあり、エクスポートされたという事実を利用することにしました。これにより、既存のウォッチドッグ無効化コードを使用して新しいプログラムの作成が可能になりました。ハードウェアウォッチドッグを無効にする2番目のプログラムを作成することで、システムをリセットせずにDeltaアプリケーションをデバッグできました。

 このプログラムはシステムのinitスクリプトに組み込まれていたため、起動時に実行され、継続的に「ドッグを追い出し」てハードウェアウォッチドッグを効果的に無効にしていました。注:このエクスプロイトの調査または作成において実際の犬が害を受けることはありません。それどころか、特別なご褒美をもらって、ウォッチドッグパッチのコーディングに貢献しました。その証拠に、これは今回の調査のリサーチャーが飼っている犬の最近の写真です。

図20

 ハードウェアとソフトウェアの両方のウォッチドッグタイマーが収束したので、以前発見した脆弱性が悪用可能かどうかを引き続き判断することができます。

エクスプロイトの作成

 エクスプロイトの実行を試してみる前に、まず私たちが気づいておく必要があるエクスプロイトの軽減または制限がシステムにあるかどうかを調べたいと考えました。まず、「checksec.sh」というオープンソースのスクリプトを実行しました。バイナリーで実行すると、このスクリプトは一般的なエクスプロイトの軽減が適用されているかどうかを報告します。図21は、「dactetra」という名前の主要なDeltaバイナリーで実行したときのスクリプトの出力を示しています。

図21

 チェックの結果、NXのみが有効になりました。これは、脆弱なコードが存在する各共有ライブラリーにも当てはまります。

 前述のように、この脆弱性によりwrite-what-where状態が可能になり、どこに何を書き込みたいのですか?という最も重要な質問につながります。最終的には、シェルコードをメモリーのどこかに書いて、そのシェルコードにジャンプしたいのです。攻撃者は最後に送信されたパケットをコントロールするため、攻撃者が自分のシェルコードをスタックに保有するのはもっともです。シェルコードをスタックに追加すると、checksecツールを使用して発見されたNo eXecute(NX)保護を回避する必要があります。これも可能ではあるのですが、もっと簡単な方法があるのではないかと思いました。

 大きな不正な形式のパケットによって上書きされたメモリー位置でクラッシュダンプを再調査すると、攻撃者がコントロールできる合計32バイトの小さな連続したヒープメモリーのセクションが見つかりました。不正な形式のパケットのペイロードの内容である0x22バイトが存在するため、私たちはこの結論に至りました。オーバーフローが発生するとき、この領域の多くが0x22でいっぱいになりますが、write-what-where状態がトリガーされるまでに、これらのバイトの多くが上書きされてしまい、図22に示す32バイトのセクションが残ります。

図22

 ヒープメモリーであるため、この領域も実行可能で、詳細はすぐに重要になります。不正な形式のパケットの0x22を繰り返しのないパターンに置き換えたことで、ペイロードのどこにシェルコードを配置するかが明らかになり、またこの領域のバイトがすべて固有のものであることが確認されました。

 シェルコードを置く潜在的な場所で、次に対処するべき主要コンポーネントは実行をコントロールすることでした。write-what-where状態により、メモリ内の任意の場所に書き込むことができましたが、実行コントロールは付与されませんでした。この問題に対処する1つの技術が、Global Offset Table (GOT)を活用することです。Linuxでは、GOTは関数ポインターを絶対位置にリダイレクトし、ELF実行ファイルまたは共有オブジェクトの.gotセクションに配置されます。.gotセクションは実行時に書き込まれるため、通常はこの後の実行中でも書き込み可能です。再配置読み取り専用(RELRO)はエクスプロイトの軽減で、ロードされた.gotセクションがマッピングされると読み取り専用としてマークします。しかし、すでに見たように、この保護は都合がよいことに有効ではありません。これはwrite-what-were条件を使用してメモリー内のシェルコードのアドレスをGOTに書き込むことが可能であることを意味し、将来の関数呼び出しの関数ポインターを置き換えます。置き換えられた関数ポインターが呼び出されると、シェルコードが実行されます。

 しかし、どの関数ポインターを置き換えるべきでしょうか?確実に成功確率を最高にするためには、可能な限り上書きに近いところで呼び出される関数に、ポインターを置き換えることが最善だと判断しました。これは、プログラムの実行中にメモリーレイアウトへの変更を最小限に抑えたいからです。「scNetMove」関数の戻りからコードをもう一度調べると、わずかな指示で「scDecodeBACnetUDP」が呼び出されているのがわかります。したがって、これはGOTで上書きするポインターの理想的な選択になります。

図23

 何をどこに書くべきかがわかったので、次に脆弱性をトリガーするためにとられた正しいコードパスに適合するために必要なあらゆる条件を検討しました。バッファー・オーバーフローを引き起こすmemcpyのコードをもう一度見てみると、図24に示すように、上書きには確かに条件があることがわかりました。

図24

 即値3でビット演算の論理積をしたとき、メモリー内で上書きを生成するコードはR0の値が0ではない場合にのみ使用されます。クラッシュダンプから、R0の値がコピーしたい送信先のアドレスであることがわかりました。これは問題を引き起こす可能性があります。書き込みをしたいアドレスが4バイトで揃えられた場合、(この可能性はとても高いのですが)、脆弱性のコードパスは採用されません。GOTで書き込みたいアドレスから1を引き、前のエントリーの最後のバイトを修復することで、コードパスを確実に取得できます。これにより、正しいコードパスが取得され、誤って2番目の関数ポインターを破損することはありません。

シェルコード

 シェルコードを配置する場所は見つかりましたが、図25にあるように、ペイロードを書き込むスペースはごくわずか、具体的には32バイトしかみつけられませんでした。このような小さなスペースで何ができるでしょうか?大規模なシェルコードを必要としない1つの方法は、「return to libc」攻撃を使用してシステムコマンドを実行することです。エクスプロイトがそのまま使えるようにするには、システムで実行するコマンドであれプログラムであれ、デフォルトでデバイスに存在している必要があります。さらに、コマンド文字列自体は、処理しなければならない限られたバイト数を収容するために、かなり短くする必要があります。

 理想的なシナリオは、デバイスへのリモートシェルアクセスを許可するコードを実行することです。幸いなことに、Netcatはデバイス上に存在し、このバージョンのNetcatは、接続のために持続的にポートをリッスンする「-ll」フラグと、接続時にコマンドを実行するための「-e」フラグの両方をサポートします。したがって、システムを使用して、いくつかのポートをリッスンするためにNetcatを実行し、接続が確立されたときにシェルを実行することができます。このコマンドでシステムを実行するためのシェルコードを書く前に、最初にデバイス上でさまざまなNetcatコマンドを直接テストして、それでもシェルを生成する最短のNetcatコマンドを究明しました。何度か反復した後、Netcatコマンドを13バイトに短縮することができました。

nc -llp9 -esh

 命令は4バイトで揃える必要があり、対処すべき32バイトがあるので、4の倍数に最も近くなるように切り上げる文字列の長さにのみ関心があります。このケースでは16バイトです。これを合計32バイトから引くので、残りは16バイト、あるいは合計4つの命令があり、システムの引数を設定してそこにジャンプします。ARMでメモリー内の小さなスペースに、より多くの命令を収める一般的な方法は、Thumbモードに切り替えることです。というのも、ARMのThumbモードは、通常の32ビット(4バイト)ARM命令ではなく、16ビット(2バイト)命令を活用しているからです。残念ながら、このデバイスのプロセッサーはThumbモードをサポートしていなかったため、これは使えませんでした。

 わずか4つのARM命令でタスクを達成するにあたっての課題は、ARMが即値に置く制限です。システムにジャンプするには、ジャンプ先アドレスとして即値を使用する必要がありますが、メモリーアドレスは一般的に小さい値ではありません。ARMの即値は12ビットに制限されています。 これらのビットのうちの8つは値自体のためであり、他の4つはビットシフトに使われます。つまり、即値の長さは1バイト(2桁の16進数)に限られますが、そのバイトには好きなやり方でゼロを埋め込むことができます。したがって、即値を使用して4バイトのフルメモリーアドレスを読み込むと、MOVを使用してもADDを使用しても4つの命令すべてが必要になります。処理すべき4つの命令に加え、システムの最初のパラメーターとして使用するレジスターであるR0にコマンド文字列のアドレスを読み込むために少なくとも1つ、そしてアドレスに分岐するためにも少なくとも1つの命令が必要です。 要求される命令は合計6つになります。

 必要な命令数を減らす1つの方法は、シェルコードの実行時に必要なアドレスに近い値をすでに含んでいるレジスターをコピーすることから始めます。これが実現可能かどうかは、ジャンプしたいアドレスの値にかかっており、シェルコードが実行される直前にレジスターで利用可能なアドレスと比較します。

 呼び出す必要があるアドレスから始めることで、ジャンプできる3つのアドレスがシステムを呼び出すことがわかりました。

1. 0x4006425C – the address of a BL system (branch to system) instruction in boot.so.
2. 0x40054510 – the address of the system entry in “so”’s GOT.
3. 0x402874A4 – the direct address of system in libuClibc-0.9.30.so.

 次に、図25に示すように、シェルコードがGDBを使用して実行されようとしているときに、これらのオプションとレジスター内の値を比較しました。

図25 

 シェルコードの実行時にアクセスできるレジスターのうち、その内容と、システムを呼び出すことができるこの3つのアドレスのうちの1つとの間で、最小デルタを与えるのはR4です。R4には0x40235CB4が含まれており、システムへの直接呼び出しのアドレスと比較すると、0x517F0のデルタが得られます。最後のニブルが0であることは理想的です。というのも最後のビットを考慮する必要がないからで、ARMの即値固有のローテーション・メカニズムによるものです。つまり、R4の内容を私たちが必要とするアドレスに変換するために必要なのは、2つの即値だけということです。1つは0x51000用、もう1つは0x7F0用です。あるレジスターを別のレジスターにMOVするときに即時オフセットを適用できるので、システムのアドレスを用いてレジスターを読み込むことが、2つの命令だけでできるはずです。分岐を実行するための1つの命令とコマンド文字列のための16バイトによって、32バイトですべてのシェルコードを取得できることになり、1つの命令でR0を文字列のアドレスに読み込むことができるとみなします。

 4番目と最後の命令の直後にコマンドのASCII文字列を開始することで、文字列にそれを認識させる適切なオフセットでPCをR0にコピーできます。このアプローチによって加わる利点は、PCに関連するため、文字列のアドレスをメモリー内に配置されたシェルコードの場所から独立させることです。図26は、すべての制限を考慮したシェルコードの外観を示しています。

図26

 重要なのは、nullで終わるASCII文字列リテラルをメモリーに配置するための「.asciz」アセンブラー・ディレクティブの使用に注意することです。R12 はARMアーキテクチャー上のIntra Procedural(IP)スクラッチ空間レジスターなので、分岐のアドレスを含むレジスターとして選択されました。これは、R12がサブルーチン内の汎用レジスターとしてしばしば使用されることを意味しており、想定外の悪影響を経験することなく、私たちの目的に合わせて上書きすることがほぼ間違いなく安全であることを示しています。

すべてをつなぎ合わせる

 脆弱性、エクスプロイト、必要なシェルコードをしっかりと理解すれば、エクスプロイトの実行を試してみることができます。この攻撃を引き起こすために使用されたパケットのシーケンスを見ると、それは単一のパケット攻撃ではなく、複数のパケット攻撃でした。最初のバッファー・オーバーフローは大きな不正な形式のパケットに含まれていますが、そこにどんなデータを構築すればいいでしょうか?このパケットはメモリーを上書きしていますが、実行のコントロールを提供していません。 したがって、これは「セットアップ」または「ステージング」パケットと見ることができます。これは、memcpyが最後のパケットの送信先バッファーのアドレスを探す場所です。このパケットには上書きしたいアドレスが入り、その後にシェルコードが続きます。先に説明したように、上書きしようとしているアドレスは、GOT内のscDecodeBACnetUDP関数ポインターのアドレスから1を引いたもので、アドレスが4バイトで揃えられていないことを確認します。前の関数ポインターの最後のバイトを修復し、このアドレスを上書きすることで、実行をコントロールできるようになります。

 大きな不正な形式のパケットには、「書き込み」先の「場所」が含まれており、シェルコードをメモリーに配置しますが、書き込む「内容」は含まれていません。この場合、その「内容」はシェルコードのアドレスなので、最後のパケットにこのアドレスが含まれる必要があります。最後の課題は、最後のパケットのどこにアドレスが属しているのかを判断することです。

 不良アドレスに値0x81を書き込もうとしてmemcpyでクラッシュが起きた以前のコアダンプを思い出してください。0x81はBVLCレイヤーの最初のバイトで、必要なアドレスだけが上書きされるようにするために、アドレスが最後のパケット内に配置される必要があることを示しています。また、確実にアドレスの後にバイトがないようにする必要があり、そうでなければ、ターゲットアドレスを超えてGOTを上書きし続けます。このアプリケーションはマルチスレッドアプリケーションなので、シェルコードが実行する機会を得る前にアプリケーションがクラッシュするかもしれません。BVLCレイヤーは通常、パケットをBACnetパケットとして識別する方法であるため、このレイヤーを変更する際の潜在的な問題は、最後のパケットがもはやBACnetパケットのようには見えなくなることです。このようなケースでは、アプリケーションはなおパケットを取り込むのでしょうか?チームがこれをテストした結果、脆弱なコードはパケットタイプを検証するコードに先立って実行されるため、アプリケーションはタイプに関係なくすべてのブロードキャストパケットを取り込むことがわかりました。

 すべてを考慮して一連の97個のパケットを送信することで、バインドシェルを作成してビル管理者をエクスプロイトすることに成功しました。以下はこの攻撃を示すビデオです。

 それでは現実にこの脆弱性を突いた攻撃としてはどのようなものがあり得るのか、「McAfee Labs、ビル管理にも使われる産業用制御システムの脆弱性を発見(後編) 」で解説する。

※本ページの内容は、2019年8月9日(US時間)更新の以下のMcAfee Blogの内容です。
原文:HVACking: Understanding the Delta Between Security and Reality
著者: Douglas McKee and Mark Bereza

カテゴリートップへ