
Windows PrintNightmare(CVE-2021-1675/34527)の詳細な分析とエクスプロイト実装。RPCベースの権限昇格と、悪意のあるプリンタードライバーインストールによるリモートコード実行を備えています。
= Print Nightmare 分析レポート :imagesdir: Figures :toc: :icons: font :figure-caption: 図 :xrefstyle: short :pdf-theme: basic-theme.yml
2021 年 6 月 29 日、非常に深刻な Windows プリントサービス脆弱性が 0day として公開され、基本スコア 8.8 で GitHub に投稿(現在は削除)されました。 この脆弱性が有名な PrintNightmare: CVE-2021-34527 であり、その危険度は EternalBlue をも上回ります。
== 脆弱性基本情報
34527 脆弱性は Windows 7、Windows Server 2008 以降のほぼすべてのバージョンに影響します。詳細は <> を参照してください。
被害の観点から見ると、攻撃者は一般ユーザー認証を用いて、管理者権限でリモートから任意のコードを実行できます。 脆弱性の悪用難易度は非常に低いため、その被害は甚大です。
脆弱性の特徴として、34527 脆弱性は CVE-2021-1675 脆弱性に基づいています。 1675 脆弱性はローカル権限昇格とリモートコード実行の脆弱性であり、34527 脆弱性と非常に似た性質を持っています。
脆弱性の動作原理を理解する前に、Windows プリントバックグラウンド処理プログラムのアーキテクチャを大まかに把握しておくことで、脆弱性に関わる各モジュール間の関係を整理しやすくなります。
== CVE-2021-1675 呼び出しフロー
=== Windows プリントバックグラウンド処理プログラムのアーキテクチャ
spooler アーキテクチャは <<spooler_arch>> のように表されます:
[[spooler_arch]] .Print Spooler Architecture image::Print Spooler Architecture.png[]
具体的には、バックグラウンド印刷プログラムは印刷タスクを管理し、以下のコンポーネントから構成されています:
winspool.drv:: ユーザーに提供される Dynamic Link Library ファイル。 このファイルは spooler 関連の Win32 API を定義し、ユーザーが呼び出すためのものです。 その中の API はすべてリモートプロシージャコールの形式でサービスを取得します。
spoolsv.exe:: spoolsv.exe はシステム内でサーバーの役割を担い、API 呼び出しを最初に処理するプログラムです。 この設計により、print spooler はローカルの印刷ジョブとリモートの印刷ジョブを区別なく処理できます。
spoolsv.dll:: ルーティングプログラム。 spoolsv.exe が受信した印刷要求を各印刷プロバイダに振り分け、最終的にどの印刷プロバイダがその要求を処理するかを決定します。 その役割は、印刷タスクがリモートタスクかローカルタスクかを区別することです。 リモートマシンでは、そのタスクをローカルの印刷プロバイダに固定的に割り当てます。
localspl.dll:: ローカル印刷プロバイダ。 印刷プロバイダの主な役割は印刷タスク管理のニーズを満たすことであり、ほとんどの API はこのモジュール内で実装されています。
上記の理論に基づいてさらに研究を進めると、例えば AddPrinterDriverEx 関数(CVE-2021-1675)を呼び出すと、以下のフローを経由します:
=== 関数バージョンの選択
まずこの関数は実際にはマクロであり、ローカルのコンパイル環境に応じて Unicode バージョン(W)または Ansi バージョン(A)が選択されます(<>):
[[AddPrinterDriverEx]] .AddPrinterDriverEx image::AddPrinterDriverEx.png[]
しかし、ワイド文字版でもナロー文字版でも、結果に違いはありません。Windows カーネルの文字列は Unicode でエンコードされているため、最終的に Ansi 版の呼び出しは Unicode 版の呼び出しに変換されます(<>):
[[AnsiToUnicode]] .AnsiToUnicode image::AnsiToUnicode.png[]
Ansi 関数のパラメータがすべて Unicode 版に変換されると、ある関数が呼び出されます(<>):
[[AnsiCallUnicode]] .AnsiCallUnicode image::AnsiCallUnicode.png[]
そしてその関数は実際には AddPrinterDriverEx の Unicode 版です(<>):
[[GetUnicodeProcAddress]] .GetUnicodeProcAddress image::GetUnicodeProcAddress.png[]
=== API 関数が RPC リクエストを spooler サーバーに送信
Unicode 版の関数の内部に入ると、まず Level の値に基づいて関数パラメータの型を選択します:
image::pDriverInfo.png[]
この脆弱性では Level を 2 に設定します。つまり、パラメータ pDriverInfo の型として DRIVER_INFO_2 構造体を選択します。 次に Windows は関数パラメータを処理し、処理が完了するとリモートプロシージャコールを介してこの API の処理を継続します:
image::set arguments.png[]
image::NdrClientCall3.png[]
=== MSRPC メカニズム
Microsoft のリモートプロシージャコールメカニズムは DCE 標準に基づいて構築されています。 簡単に説明すると、リモートプロシージャコールはリモートシステム上でプロセスを実行することで、これらのプロセスはプログラマまたはシステムによって事前に定義されています。
RPC の具体的な方法は、リモートで呼び出したい関数をシリアル化し、ネットワークを介してリモートシステムに転送し、リモートシステムでそれをデシリアライズして実行することです。 Microsoft の構築したシステムでは、TCP/IP と SMB が通常 RPC 呼び出しを転送するために選ばれるプロトコルです。
MSRPC を使用するには、まず呼び出す関数の IDL インターフェース記述を定義し、MIDL ツールを使用してクライアントおよびサーバー側の対応するシリアル化プログラム stub を生成する必要があります。 一部の Win32 API では、サーバー側 stub があらかじめ定義されているため、クライアント側 stub だけを生成して使用すれば十分です。
MSRPC は UUID を使用して特定のタイプのプロトコルを識別します。例えば、MS-RPRN はリモート印刷プロトコルを記述するために使用され、リモート印刷に関連するすべての関数はこのプロトコルの一部です。MSRPC は UUID 12345678-1234-ABCD-EF00-0123456789AB を使用してこのプロトコルを識別します(<<rprn_uuid>>):
[[rprn_uuid]] .MS-RPRN UUID image::spoolss uuid.png[]
次に、この接続に基づいて、プロトコル内の関数を識別するために operator number を使用してリモート呼び出しを行います。例えば、AddPrinterDriverEx は 89 を使用して自身を識別します(<<addPrinterDriverEx_opnum>>):
[[addPrinterDriverEx_opnum]] .AddPrinterDriverEx Opnum image::AddPrinterDriverEx Opnum.png[]
MSRPC の使用において、2 点注意すべき点があります:
"As you can see in your output, the scripts are trying to connect to port 135 (endpoint mapper) in order to get the TCP/IP port where the DCOM endpoint is listening (that is a dynamic port)." -- SecureAuthCorp/impacket issue #412
=== spoolsv.exe が API リクエストを処理
[[call_flow]] .RpcAddPrinterDriverEx Call Flow image::Function Calls.png[]
<<call_flow>> から明らかなように、spoolsv.exe はこれらの関数を呼び出しますが、関数内部の分析によれば、このモジュールでは初期化以外の操作は行われていません。 最後に、このモジュールは pLocalProvidor が指す関数、つまり localspl.dll モジュール内の関数 LocalAddPrinterDriverEx を呼び出します。 localspl はローカル印刷プロバイダとして、確かに API 機能を実装するモジュールです。
=== ローカル印刷プロバイダの関数実装ロジック
[[LocalAddPrinterDriverEx]] .LocalAddPrinterDriverEx image::LocalAddPrinterDriverEx.png[]
まず <> は、このモジュールが spooler が正常に動作しているかを確認し、その後関数 SplAddPrinterDriverEx にジャンプします。
[[SplAddPrinterDriverEx]] .SplAddPrinterDriverEx image::SplAddPrinterDriverEx.png[]
<> 関数内部は、AddPrinterDriverEx 関数が正常に実行できるかどうかを判断する重要な場所です。 前半部分は無視して構いません。WPP はログ関連の技術であり、今は飛ばします。
後半部分では、Microsoft は変数 v12 を定義しています。これは関数を続行するか直接終了するかを示すフラグです。
[[bittest]] .bittest dwFileCopyFlags image::bittest in spl.png[]
<> からわかるように、続行の条件は 2 つあります:1 つは v12 が 0、つまり bittest の判断が成功すること、もう 1 つは Validate が成功することです。 Validate は権限のチェックであり、簡単に回避できるものではありません。 その中に OpenProcessToken という API があり、これは後続のプロセスで権限を昇格する必要があることを示しており、管理者でなければこれを行うことはできません。
そのため続行するには bittest の判断をバイパスするしかありません。判断対象の a4——関数の第 4 パラメータは、公式ドキュメントで説明されているパラメータです <>:
|=== |名前/値 |説明
|APD_STRICT_UPGRADE
0x00000001
|置き換えるプリンタドライバの全ファイルが現在インストールされているドライバの対応ファイルよりも古くない場合のみ、置き換え用プリンタドライバを追加します。
|APD_STRICT_DOWNGRADE
0x00000002
|現在インストールされているドライバの全ファイルが置き換え用プリンタドライバの対応ファイルよりも古くない場合のみ、置き換え用プリンタドライバを追加します。
|APD_COPY_ALL_FILES
0x00000004
|プリンタドライバを追加し、ドライバディレクトリ内の全ファイルをコピーします。ファイルタイムスタンプは無視されます。
|APD_COPY_NEW_FILES
0x00000008
|プリンタドライバを追加し、現在使用中の対応ファイルより新しいドライバディレクトリ内のファイルをコピーします。
|APD_COPY_FROM_DIRECTORY
0x00000010
|_DRIVER_INFO_6 構造体で指定された完全修飾ファイル名を使用してプリンタドライバを追加します。このフラグが指定された場合、このビットフィールド内の他のコピーフラグのいずれかを指定する必要があります。
|APD_DONT_COPY_FILES_TO_CLUSTER
0x00001000
|プリントサーバークラスタにプリンタドライバを追加する際、共有クラスタディスクにドライバファイルをコピーしません。
|APD_COPY_TO_ALL_SPOOLERS
0x00002000
|クラスタスプーラサーバにプリンタドライバを追加します。
|APD_INSTALL_WARNED_DRIVER
0x00008000
|サーバーの警告プリンタドライバリストに含まれていても、プリンタドライバを追加します。
|APD_RETURN_BLOCKING_STATUS_CODE
0x00010000
|サーバーポリシーによりプリンタドライバがインストールをブロックされた場合に返す実装固有のエラーコードを指定します。
|===
bittest 16 は変数の第 16 ビットが 1 かどうかをチェックし、対応するパラメータ値は 0x8000(APD_INSTALL_WARNED_DRIVER)です。 説明からもわかるように、このパラメータは検証なしでプリンタドライバをサーバーに追加することを意味します。
1675 が修正される前は、このパラメータは公式ドキュメントにまだ掲載されていなかったとされ、これが脆弱性の所在を示しています。
=== 脆弱性の利用方法
プリンタドライバ追加メソッドが実際に実行される際、DRIVER_INFO_2 構造体を選択した場合、以下のことが発生します:
. DriverFile、ConfigFile、DataFile をそれぞれ開き、これら 3 つのファイルが存在することを確認します。 そのうち DataFile のみ UNC パスが許可されています。
. 3 つのファイルがすべて存在する場合、それらを C:\Windows\System32\spool\drivers\x64\3\New ディレクトリにコピーします(<<cp_conf_file>>、<<cp_data_file>> を参照): + [[cp_conf_file]] .設定ファイルのコピー image::copy config file.png[] + [[cp_data_file]] .データファイルのコピー image::copy data file.png[]
. このディレクトリにコピーする理由は、対応するファイルを実行するためです。3 はこのプリンタドライバが v3 タイプであることを示します。 まず新しいファイルを New ディレクトリにコピーし、3 ディレクトリ内のファイルが上書きされるのを防ぎます。 もし 3 ディレクトリに同名ファイルが存在する場合、その同名ファイルを Old ディレクトリにバックアップとして移動し、その後 New ディレクトリ内のファイルを 3 ディレクトリにコピーして上書きします。 この点は、RpcAddPrinterDriverEx メソッドを 2 回目に実行した際に観察できます: + .2 回目の RPC 呼び出し image::second time call.png[] + .ファイルを Old ディレクトリにバックアップ image::backup file.png[] + .新しいファイルをコピー image::copy file.png[] + この仕組みにより、リモートパスのファイルをローカルパスのファイルとして保存できます。関数パラメータでは、ドライバファイルと設定ファイルパラメータはローカルパスしか許可されず、データファイルパラメータのみリモートパスが許可されるためです。
. <> によると、pConfigFile はデバイスドライバの設定用 Dynamic Link Library であるため、初期化のために一度ロードする必要があります。 この点は実際の実行状況からも確認されています。 + .pConfigFile のロード image::Load Image.png[] + これにより、悪意のある dll を作成し、その dll のエントリポイントに悪意のあるコードを配置して実行すれば、任意のコードを管理者権限で実行できます。 + .spoolsv.exe は管理者権限で動作 image::spoolsv user.png[]
. CreateInternalDriverFileArray() 関数は、ファイル操作フラグに基づいて spool ドライバディレクトリをチェックするかどうかを決定します。 a5 フラグが False に設定されている場合、ドライバ読み込み関数は、コピー対象のドライバファイルがユーザーディレクトリに含まれているかどうかのみチェックします。 そうでない場合、関数は spool ドライバディレクトリ内で対象のドライバを検索しようとします。 そのため、dwFileCopyFlags には同時に APD_COPY_FROM_DIRECTORY パラメータを設定する必要があります。 + image::APD.png[] + image::APD_1.png[]