C/C++ランタイムライブラリを改ざんするサプライチェーン攻撃について解説

過去数年間、主要なソフトウェアやサーバコンポーネントはセキュリティ業界において必要不可欠であり重要なものとして認識されています。しかしながら、これらはサイバー犯罪者によってセキュリティ侵害や改ざんなどのさまざまな攻撃の対象とされてきました。そのような攻撃の1つに、ソフトウェアのソースコードや更新サーバ、またはその両方を不正に改ざんするものがあります。いずれの場合も、攻撃者は目立たない方法で対象のネットワークまたはホストに侵入しようとします。これは、サプライチェーン攻撃と呼ばれる攻撃手法です。攻撃者の技術力と活動を隠ぺいしたい度合いに応じて、侵入先のソフトウェアの改ざんに使用される方法は異なります。

以下は、実際の攻撃で確認されたサプライチェーン攻撃の主な4つの方法です。

  • 方法1:ソースコードレベルで不正なコードを注入するソフトウェアの侵害。C/C++のようなネイティブ言語や、Java、.NETなどのインタプリタ方式または実行時コンパイル(JITコンパイラ)方式の言語を対象とする。
  • 方法2:コンパイラのC/C++ランタイムライブラリ(CRT)への不正コードの注入。例としては、特定のCランタイム関数の改ざんが挙げられる。
  • 方法3:更新サーバを侵害して、正規バージョンの代わりに不正プログラムをデプロイ。具体的には、この手法は上の2つとは異なり、ソフトウェアを直接改ざんするのではなく、サーバを侵害して不正なソフトウェアに置き換える間接的な方法をとる。不正プログラムは、攻撃者の管理下にあるまったく別のサーバにホストされている場合もある。
  • 方法4:不正なコードを注入して正規のソフトウェアを再パッケージ化。 このような「トロイの木馬」化されたソフトウェアは、侵害されたソフトウェア企業の公式Webサイトでホストされているか、「BitTorrent」などを使用したファイル共有サービスを通して拡散される。

本記事では、上記4つの手法を通して過去10年間に発生した複数の既知のサプライチェーン攻撃事例について解説します。特に、方法2に焦点を当てて、ポイズニングされたすべてのC / C++ランタイム関数の一覧と、それらを悪用したマルウェアファミリとの対応を表にまとめています。加えて、バックドア型マルウェア「ShadowPad」の事例を取り上げ、そのような改ざんがどのようにして行われるのかを解説します。

■サプライチェーン攻撃におけるさまざまな方法

方法1と2は、他の方法と比べて直接的な攻撃手法であるという特徴を持っています。この2つの手法では、攻撃者が直接ソフトウェアやライブラリを侵害してコードを注入するという意味でため、それらを1つのカテゴリーとして捉えることができます。ただし、この2つの手法のうち最も厄介なのは方法2です。不正なコードがコンパイルまたはリンク時に導入されるため、開発者やソースコードパーサにはコードの改ざんが確認できないからです。

方法3のような攻撃手法を利用した例としては、2013年12月に日本で起きた高速増殖炉「もんじゅ」の事例が挙げられます。この攻撃では、「GOMLab」が提供するメディアプレーヤー「GOM Player」の更新サーバーが侵害され、特定の標的に遠隔操作ツール「Gh0st RAT」の亜種が拡散されました。

方法4のような攻撃手法に関しては、遠隔操作ツール「Havex」を利用した事例が2013年と2014年に確認されています。この事例では、複数の産業用制御システム(ICS)のWebサイトおよびソフトウェアインストーラが侵害されました。

他にも、4つの主な攻撃手法から複数の方法を組み合わせて利用した攻撃手法も確認されています。方法1と方法3を組み合わせた攻撃の例は以下の通りです。

  • BitTorrentクライアント「MediaGet」の更新プログラムが侵害され、トロイの木馬化された事例(2018年2月中旬)。問題の更新プログラムには不正なアップデートコンポーネントとトロイの木馬化されたファイル「mediaget.exe」のコピーが含まれていた。
  • 「Intellect Service」の会計ソフトウェア「M.E.Doc」に対する「Nyetya / NotPetya」を利用した攻撃(2017年4月)。この攻撃では、更新システムを操作されたことによって、破壊的なランサムウェアとして知られる「Nyetya / NotPetya」が拡散された。更新プログラムが改ざんされ、.NETモジュール「ZvitPublishedObjects.dll」がバックドア化された。
  • Windowsイベントログのデータベース「EventID」を狙った攻撃キャンペーン「KingSlayer」。これにより、イベントログ管理ソフトウェアのソースコード(.NETのサービス実行可能ファイル)と更新サーバーが侵害された(2015年3月)。

方法2と3の組み合わせを使用した攻撃の例は以下の通りです。

  • サイバー攻撃「Operation ShadowHammer」。コンピュータベンダの更新サーバが侵害され、ネットワークアダプタのMACアドレスに基づいて識別された標的が攻撃された(2018年6月)。加えられた変更には、不正な更新コンポーネントが含まれていた。
  • サイバー犯罪者集団「Winnti」によるゲーム業界への攻撃。この攻撃では3社のゲーム会社が侵害され、その結果それぞれの主要実行ファイルがバックドア化されました(2019年3月に公表)。Winntiによるゲーム業界を狙った攻撃は2011年にも発生。正規ツール「AheadLib」を使用して作成された不正なライブラリが更新サーバを通して拡散された。
  • システムクリーナーソフト「CCleaner」が改ざんされた事例。ソフトウェア企業「Piriform」の正規ダウンロードサイトが侵害され、「CCleaner」がバックドア化された(2017年8月)

• バックドア型マルウェア「ShadowPad」の事例。「NetSarang Computer,Inc.」が侵害された結果、同社の全製品がバックドア化された(2017年7月)。同社のすべての製品で使用されているライブラリ「nssock2.dll」に不正なコードが注入された。

主な4つの方法以外にも以下のようなサプライチェーン攻撃が確認されています。

  • 2015年9月に起きた「XcodeGhost」の事例。この事例では、Appleが提供する統合開発環境(IDE)「Xcode」のオブジェクトファイル(Mach-O形式)「CoreServices」にマルウェアが含まれていた。これにより、トロイの木馬化されたXcodeで作成されたすべてのiOSアプリはリンカを通してマルウェアに感染。このトロイの木馬化されたバージョンのXcodeは、中国にあると言われている複数のファイル共有サービスでホストされていた。その結果として、「App Store」に何百ものトロイの木馬化されたアプリケーションが公開されていた。
  • 2018年11月には、「event-stream」が改ざんされた事例が発生。この事例において興味深い点は、通常のサプライチェーン攻撃の方法と異なる側面を示している部分。「event-stream」は、サーバサイドJavaScript環境「Node.js」のパッケージマネージャ「npm」によって広く使用されているパッケージの1つである。この事例ではflatmap-streamというパッケージへの依存性が直接追加されていた。攻撃者は、event-streamの元々の開発者または管理者からメンテナンスを引き継ぎ、不正なパッケージflatmap-streamを追加。この不正なパッケージは、ビットコイン用のウォレットアプリ「Copay」の開発者からビットコインを窃取することを目的としている。Copayのビルドスクリプトが実行される際に不正なコードがアプリに書き込まれるため、より一層気付くことが困難となっている。

このほぼ10年間で起こっているほとんどのサプライチェーン攻撃の事例では、最初の感染経路は不明であるか、少なくとも公には裏付けが取れていません。さらに、不正コードが正規ソフトウェアのコードベースにどのように入り込むかの詳細ついても、フォレンジックや「戦略、技術、および手法(Tactics, Techniques, and Procedures、TTP)」の調査からは立証されていません。以下では、上に述べた方法2のサプライチェーン攻撃に焦点を当てて、どのようにして確認が困難な方法で間接的なコードの改ざんが実行されたのか、「ShadowPad」の事例を使用して解説します。

■ケーススタディ:「ShadowPad」を例に方法2を詳しく解説

方法1のように元のソースコードを改ざんする方法と、方法2のようにC / C ++ランタイムライブラリを改ざんする方法には微妙な違いがあります。変更の性質と場所によっては、前者の方が簡単に確認できます。 一方、後者はファイルの監視とインテグリティ(完全性)のチェックが行われていなかった場合、確認を行うのはさらに難しくなります。

C / C ++ランタイムタイムライブラリの改ざんが報告されたすべての事例は、Windowsのバイナリに関連しています。それぞれの事例において、問題のバイナリは、さまざまなバージョンのリンカを使用するMicrosoft Visual C / C ++コンパイラで静的にコンパイルされていました。 加えて、改ざんされた関数は、すべて実際のC / C ++標準ライブラリの一部ではなく、Microsoft Visual C / C ++コンパイラのランタイム初期化ルーチンに固有のものでした。

下の表1は、ランタイム関数が改ざんされた事例の一覧です。

事例 改ざんされたMicrosoft Visual C / C ++ランタイム関数
サイバー攻撃「Operation ShadowHammer」 __crtExitProcess(UINT uExitCode)
// プロセスを抜け、管理されたアプリの一部であるかチェック
// ExitProcess関数のためのCRTラッパー
「Winnti」によるゲーム業界への攻撃 __scrt_common_main_seh(void)
// Cランタイムライブラリ「_mainCRTStartup」のエントリーポイント、プログラムの「main()」関数を呼び出す構造化された例外処理に対応
「CCleaner」が改ざんされた事例 第1段階: __scrt_common_main_seh(void)
第2段階 →32ビット版システムに作成 _security_init_cookie()
第2段階 → 64ビット版システムに作成) _security_init_cookie()
void __security_init_cookie(void);
// グローバルセキュリティクッキーを初期化
// バッファオーバーフローを防ぐために使用
バックドア型マルウェア「ShadowPad」の事例 _initterm(_PVFV * pfbegin, _PVFV * pfend);
// 関数ポインタテーブルのエントリを呼び出す
// 「0x1000E600」が不正なエントリ

表1:サプライチェーン攻撃で改ざんされた「Microsoft Visual C / C ++」のランタイム関数

起動コードを提供するために必要なCRTライブラリを読み込むことがリンカの役割です。リンカフラグを使用して明示的に別のCRTライブラリを指定することもできますが、指定しない場合は、静的にリンクされた既定のCRTライブラリ「libcmt.lib」、または別のライブラリが使用されます。起動コードには、プログラムの「main()」関数を実行する前に、さまざまな環境設定操作を実行する役割があります。この操作には、例外処理、スレッドデータの初期化、プログラムの終了、およびCookieの初期化が含まれます。 ただし、CRTの実装は、コンパイラ、コンパイラオプション、コンパイラのバージョン、プラットフォームによって異なることに留意してください。

Microsoftは、開発者が自分で構築できるVisual Cランタイムライブラリのヘッダファイルとコンパイルファイルを出荷していました。たとえば、「Visual Studio 2010」の場合、そのようなヘッダファイルはは「Microsoft Visual Studio 10.0\VC\crt」配下に存在し、改ざんされた関数「_initterm()」の実際の実装は、ファイル「crt0dat.c」に含まれています。

図1:改ざんされたランタイム関数「_initterm()」
図1:改ざんされたランタイム関数「_initterm()」

(※コメントは読みやすくするために省略)

この内部関数は、nullのエントリをスキップしながら関数ポインタのテーブルをたどって初期化します。 これは、C ++プログラムの初期化中にのみ呼び出されます。この攻撃で改ざんされたDLL「nssock2.dll」はC ++言語で書かれています。

引数「pfbegin」はテーブルの最初の有効なエントリを指し、「pfend」は最後の有効なエントリを指しています。 関数型「_PVFV」は、CRTファイル「internal.h」の中で定義されています。

図2:関数型「_PVFV」の定義
図2:関数型「_PVFV」の定義

関数「_initterm()」は、「crt0dat.c」ファイルに定義されています。これをコンパイルしたオブジェクトファイル「crt0dat.obj」は、ライブラリファイル「libcmt.lib」に含まれています。

以下の図3は、改ざんされた関数「_initterm()」の実行の流れを示しています。

図3:改ざんされたランタイム関数「_initterm()」が呼び出される流れ
図3:改ざんされたランタイム関数「_initterm()」が呼び出される流れ

図4は、「pfbegin」と「pfend」が指すShadowPadの関数「_initterm()」のための関数ポインタテーブルを示しています。このテーブルは、プログラムの開始時にC++のコンストラクタを呼び出してオブジェクトを作成するために使用されます。

図4:改ざんされたランタイム関数「_initterm()」の関数ポインタテーブル
図4:改ざんされたランタイム関数「_initterm()」の関数ポインタテーブル

図4から分かるように、仮想アドレス「0x1000F6A0」において、「malicious_code」というラベルの付いた関数ポインタのエントリは、不正コードのアドレス「0x1000E600」を指しています。このため、改ざんされたのは、関数「_initterm()」ではなく関数ポインタテーブルであると言った方がより正確です。

図5は、コンパイルされた「ShadowPad」のコードが参照するCRT関数「_initterm() 」の参照関係を図示したものです。このグラフは、コードブロックに到達可能なすべての呼び出し経路とコードブロック自身が呼び出すことのできるすべての経路を示しています。関数ポインタテーブルを介してShadowPadのコードが実行される実際の呼び出し経路は次の通りです。

「DllEntryPoint()」→「__DllmainCRTStartup()」→「_CRT_INIT()」→「_initterm()」→「__imp_initterm()」→「malicious_code()」

図5:改ざんされたランタイム関数「_initterm()」が呼び出される参照関係
図5:改ざんされたランタイム関数「_initterm()」が呼び出される参照関係

内部関数「_initterm()」は、C++のDLLを初期化するCRT初期化関数「__CRT_INIT()」内から呼び出されます。「__CRT_INIT()」は図6のようなプロトタイプを持ちます。

図6:CRT初期化関数「__CRT_INIT()」内から呼び出される
図6:CRT初期化関数「__CRT_INIT()」内から呼び出される

内部関数「_initterm()」のプロトタイプ

「__CRT_INIT()」の役割の1つは、「nssock2.dll」内にあるC ++コードのためのC ++コンストラクタを呼び出すことです。この関数は、CRTファイル「crtdll.c」に実装されており、そのオブジェクトファイル「crtdll.obj」はライブラリファイル「msvcrt.lib」に含まれています。

以下の図7では関数「_CRT_INIT()」の実装です。

図7:関数「_CRT_INIT()」の実装
図7:関数「_CRT_INIT()」の実装

それでは、攻撃者はこれらのCRT関数をどのようにして改ざんできるでしょうか?もとの正規ライブラリ「libcmt.lib / msvcrt.lib」を不正なライブラリで上書きするか、不正なライブラリファイルを指すようにリンカフラグを変更することが可能性として挙げられます。 もう1つの可能性としては、リンカがさまざまな関数へのすべての参照を解決するプロセスをハイジャックすることが考えられます。攻撃者は、ツールを使用してリンカのプロセスを監視して乗っ取り、正規の関数の代わりに改ざんした関数を渡します。リンカのバイナリのように、コンパイラの主要な実行可能ファイルをバックドア化することもまた、、気づかれにくい改ざん手法の1つとなる可能性があります。

■被害に遭わないためには

方法2を使用した攻撃の数は非常に少なく予測が難しい上に標的型の攻撃だと考えられ、一度起きてしまえば広い範囲にわたって非常に強い影響をもたらす可能性があります。このため、サプライチェーン攻撃におけるCRTライブラリ関数の改ざんは、特に開発環境とビルド環境の完全性の検証に関して、セキュリティ業界がさらに注意すべき紛れもない脅威です。

サプライチェーン攻撃の被害に遭わないようにするためには、ソフトウェア開発とビルド環境を確実にクリーンに保つための措置を講じることが必要になります。ソースコードやすべてのコンパイラライブラリとバイナリの完全性を維持し、相互検証することは脅威を防ぐための良いスタートポイントです。この措置を講じることに関して、サードパーティ製のライブラリやコードを使用する際は、統合およびデプロイの前に、不正なコードや改ざんの痕跡がないかどうかをスキャンする必要があります。また、ビルド環境と更新サーバのようなおよび配布環境において、重要資産を隔離するために、適切なネットワークセグメンテーションを行うことは必要不可欠です。これと同様に大切なことは、リリースビルドサーバとエンドポイントへのアクセスを多要素認証によって厳しく制限することです。もちろん、これらの措置が講じられたからといって、システムのセキュリティを継続的に監視するという開発者としての責任から除外されたりそれを放棄してもいいということではありません。

参考記事:

翻訳: 下舘 紗耶加(Core Technology Marketing, TrendLabs)