デジタルペンテスト部の北原です。 今回は、Windowsでのツール開発者にとっては重要なハンドルの内部について解説します。
Windowsでは、ファイルやプロセスから始まりすべてのリソースはオブジェクトとして扱われ、ファイルの削除やメモリの確保にはハンドルを取得する必要があります。 ツールやアプリケーションの開発者にとっては、日常的にハンドルを操作する機会がありますが、通常の利用者にとってはあまり意識するものではありません。 本記事では、Windows OSでのハンドルの役割と、OS内部でどの様に管理されているのかについて解説します。 記事の構成は以下の通りです。
本記事は以下の読者層を想定しています。
ハンドルの役割とアクセス権限
Windowsではすべてのリソースがオブジェクトとして扱われ、ハンドルの取得を通じてオブジェクトに対する情報の問い合わせや操作が可能となります。
ハンドルを通じて許可される操作を決定するために、ハンドルには権限情報が設定されます。
アクセス権限は符号なし32ビット整数値で表現されるACCESS_MASK
というフラグ値で決定されます。
ACCESS_MASK
の構造は以下の通りです。
上位16ビットはオブジェクトの種類に依存しない一般的な定義を設定するフィールドです。
GENERIC
で始まる名前の権限(GENERIC_READ
、GENERIC_WRITE
、GENERIC_EXECUTE
、GENERIC_ALL
)は 汎用アクセス権限(Generic Access Right) と呼ばれており、ハンドルを取得する際にのみ用いるものです。
汎用アクセス権限を表すアクセスマスクを指定してオブジェクトのハンドルを要求すると、オブジェクト固有の権限に変換されてハンドルのACCESS_MASK
が更新されます。
また、MAXIMUM_ALLOWED
もハンドルを取得する際にのみ用いるもので、呼び出し元が要求できる最大限のアクセス権限を求めるものです。
ただし、MAXIMUM_ALLOWED
を指定してハンドルを要求する際の挙動は状況によって異なるため、MAXIMUM_ALLOWED
は極力用いずに、目的の動作に必要最小限の権限を把握して適切なACCESS_MASK
を指定してハンドルを取得するのが推奨です。
例えばOpenProcess
APIにMAXIMUM_ALLOWED
を指定して他プロセスのハンドルの取得を試みる場合、一般権限で実行すると許可された最大限のアクセス権限が与えられますが、管理者権限だと全アクセス権限を要求する挙動をするので、何れかの権限が許可されていない場合はハンドルの獲得に失敗してしまいます。
ハンドルに設定されるアクセス権限は、ACCESS_SYSTEM_SECURITY
権限を表すビット以下の25ビットです。
下位16ビットの値はオブジェクトの種類に固有なアクセス権限を定義するためのフィールドなので、オブジェクトの種類に応じて値が意味する権限は異なります。
例えば、プロセスを意味するオブジェクトであるProcess
オブジェクトのACCESS_MASK
は、Windows SDKのヘッダであるwinnt.h
に以下のように定義されています。
#define PROCESS_TERMINATE (0x0001) #define PROCESS_CREATE_THREAD (0x0002) #define PROCESS_SET_SESSIONID (0x0004) #define PROCESS_VM_OPERATION (0x0008) #define PROCESS_VM_READ (0x0010) #define PROCESS_VM_WRITE (0x0020) #define PROCESS_DUP_HANDLE (0x0040) #define PROCESS_CREATE_PROCESS (0x0080) #define PROCESS_SET_QUOTA (0x0100) #define PROCESS_SET_INFORMATION (0x0200) #define PROCESS_QUERY_INFORMATION (0x0400) #define PROCESS_SUSPEND_RESUME (0x0800) #define PROCESS_QUERY_LIMITED_INFORMATION (0x1000) #define PROCESS_SET_LIMITED_INFORMATION (0x2000) // #if (NTDDI_VERSION >= NTDDI_VISTA) #define PROCESS_ALL_ACCESS (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | \ 0xFFFF) #else #define PROCESS_ALL_ACCESS (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | \ 0xFFF) #endif
Windows OS内部で定義されているオブジェクトの種類は、システムコールNtQueryObject
の呼び出しにより確認できます。
このシステムコールの関数シグネチャを以下に示します。
__kernel_entry NTSYSCALLAPI NTSTATUS NtQueryObject( [in, optional] HANDLE Handle, [in] OBJECT_INFORMATION_CLASS ObjectInformationClass, [out, optional] PVOID ObjectInformation, [in] ULONG ObjectInformationLength, [out, optional] PULONG ReturnLength );
ObjectInformationClass
パラメータにObjectTypesInformation
(整数値で3
)を指定すれば、ObjectInformation
パラメータに指定したメモリアドレスに、_OBJECT_TYPES_INFORMATION
構造体から始まる_OBJECT_TYPE_INFORMATION
構造体の配列データとしてオブジェクトの種類の情報が出力されます。
この場合はHandle
パラメータにはNULL
を設定します。
_OBJECT_TYPES_INFORMATION
構造体と_OBJECT_TYPE_INFORMATION
構造体の定義は以下の通りです。
typedef struct _OBJECT_TYPES_INFORMATION { ULONG NumberOfTypes; } OBJECT_TYPES_INFORMATION, *POBJECT_TYPES_INFORMATION; typedef struct _OBJECT_TYPE_INFORMATION { UNICODE_STRING TypeName; ULONG TotalNumberOfObjects; ULONG TotalNumberOfHandles; ULONG TotalPagedPoolUsage; ULONG TotalNonPagedPoolUsage; ULONG TotalNamePoolUsage; ULONG TotalHandleTableUsage; ULONG HighWaterNumberOfObjects; ULONG HighWaterNumberOfHandles; ULONG HighWaterPagedPoolUsage; ULONG HighWaterNonPagedPoolUsage; ULONG HighWaterNamePoolUsage; ULONG HighWaterHandleTableUsage; ULONG InvalidAttributes; GENERIC_MAPPING GenericMapping; ULONG ValidAccessMask; BOOLEAN SecurityRequired; BOOLEAN MaintainHandleCount; UCHAR TypeIndex; // since WINBLUE CHAR ReservedByte; ULONG PoolType; ULONG DefaultPagedPoolCharge; ULONG DefaultNonPagedPoolCharge; } OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;
_OBJECT_TYPE_INFORMATION
構造体の配列要素数は、_OBJECT_TYPES_INFORMATION
構造体のNumberOfTypes
フィールドに出力される値に従います。
本記事にとって重要なフィールドに絞って軽く触れると、まずTypeName
フィールドにオブジェクトの種類名が出力されます。
オブジェクトの種類にはTypeIndex
フィールドに出力される識別番号が割り当てられており、OSのビルドバージョンにより異なる値が設定されます。
文字列の比較による値検証は実装不備につながりやすいため、基本的には識別番号でオブジェクトの種類を識別して、必要な場合にのみ種類名の文字列情報が参照されます。
アクセス権限の面では、GenericMapping
フィールドが重要です。
このフィールドには_GENERIC_MAPPING
構造体の情報として、汎用アクセス権限がどのオブジェクト固有のアクセス権限に変換されるかの定義情報が出力されます。
_GENERIC_MAPPING
構造体の定義は以下の通りです。
typedef struct _GENERIC_MAPPING { ACCESS_MASK GenericRead; ACCESS_MASK GenericWrite; ACCESS_MASK GenericExecute; ACCESS_MASK GenericAll; } GENERIC_MAPPING;
各フィールドに出力される情報は、フィールド名を見れば明らかでしょう。
例えばGenericRead
フィールドには、GENERIC_READ
をアクセスマスクに設定した場合に、オブジェクトに固有なアクセス権限のどれに変換されるかの情報が出力されます。
Process
オブジェクトの_GENERIC_MAPPING
構造体の情報を取得して解釈すると、以下のような結果が得られます。
実際にシステムコールNtQueryObject
を用いてオブジェクトの種類情報を出力するプログラムを作成してWindows 11 23H2で実行すると、オブジェクトの識別番号、種類名、Generic Mappingの情報が以下のように列挙できます。
PS C:\Tools> .\TypeQuery.exe [*] Querying type information. [+] Got 70 types! Index Type Name GenericRead GenericWrite GenericExecute GenericAll ===== ================================ =========== ============ ============== ========== 2 Type 0x00020000 0x00020000 0x00020000 0x000F0001 3 Directory 0x00020003 0x0002000C 0x00020003 0x000F000F 4 SymbolicLink 0x00020001 0x00020000 0x00020001 0x000F0001 5 Token 0x0002001A 0x000201E0 0x00020005 0x000F01FF 6 Job 0x00020004 0x0002000B 0x00120000 0x001F003F 7 Process 0x00020410 0x00020BEA 0x00121001 0x001FFFFF 8 Thread 0x00020048 0x00020437 0x00121800 0x001FFFFF 9 Partition 0x00020001 0x00020002 0x00120001 0x001F0003 10 UserApcReserve 0x00020001 0x00020002 0x00020000 0x000F0003 11 IoCompletionReserve 0x00020001 0x00020002 0x00020000 0x000F0003 12 ActivityReference 0x00020000 0x00020000 0x00020000 0x001F0000 13 ProcessStateChange 0x00020000 0x00020001 0x00020001 0x000F0001 14 ThreadStateChange 0x00020000 0x00020001 0x00020001 0x000F0001 15 CpuPartition 0x00020001 0x00020002 0x00020004 0x000F0007 16 PsSiloContextPaged 0x00020000 0x00020000 0x00020000 0x000F0000 17 PsSiloContextNonPaged 0x00020000 0x00020000 0x00020000 0x000F0000 18 DebugObject 0x00020001 0x00020002 0x00120000 0x001F000F 19 Event 0x00020001 0x00020002 0x00120000 0x001F0003 20 Mutant 0x00020001 0x00020000 0x00120000 0x001F0001 21 Callback 0x00020000 0x00020001 0x00120000 0x001F0001 22 Semaphore 0x00020001 0x00020002 0x00120000 0x001F0003 23 Timer 0x00020001 0x00020002 0x00120000 0x001F0003 24 IRTimer 0x00020001 0x00020002 0x00120000 0x001F0003 25 Profile 0x00020001 0x00020001 0x00020001 0x000F0001 26 KeyedEvent 0x00020001 0x00020002 0x00020000 0x000F0003 27 WindowStation 0x00020303 0x0002001C 0x00020060 0x000F037F 28 Desktop 0x00020041 0x000200BE 0x00020100 0x000F01FF 29 Composition 0x00020001 0x00020002 0x00020000 0x000F0003 30 RawInputManager 0x00020001 0x00020002 0x00020000 0x000F0003 31 CoreMessaging 0x00020000 0x00020000 0x00020000 0x000F0000 32 ActivationObject 0x00020001 0x00020002 0x00020000 0x000F0003 33 TpWorkerFactory 0x00020008 0x00020004 0x00020003 0x000F00FF 34 Adapter 0x00120089 0x00120116 0x001200A0 0x001F01FF 35 Controller 0x00120089 0x00120116 0x001200A0 0x001F01FF 36 Device 0x00120089 0x00120116 0x001200A0 0x001F01FF 37 Driver 0x00120089 0x00120116 0x001200A0 0x001F01FF 38 IoCompletion 0x00020001 0x00020002 0x00120000 0x001F0003 39 WaitCompletionPacket 0x00020001 0x00020001 0x00020001 0x000F0001 40 File 0x00120089 0x00120116 0x001200A0 0x001F01FF 41 IoRing 0x00000000 0x00000000 0x00000000 0x00000000 42 TmTm 0x00020001 0x0002001E 0x00020000 0x000F003F 43 TmTx 0x00120001 0x0012003E 0x00120018 0x001F007F 44 TmRm 0x00120001 0x0012007E 0x0012005C 0x001F007F 45 TmEn 0x00020001 0x0002001E 0x0002001C 0x000F001F 46 Section 0x00020005 0x00020002 0x00020008 0x000F001F 47 Session 0x00020001 0x00020002 0x00120001 0x000F0003 48 Key 0x00020019 0x00020006 0x00020039 0x000F003F 49 RegistryTransaction 0x00120001 0x0012003E 0x00120018 0x001F003F 50 DmaAdapter 0x00020000 0x00020000 0x00020000 0x001F0000 51 ALPC Port 0x00020001 0x00010001 0x00000000 0x001F0001 52 EnergyTracker 0x00000000 0x00000000 0x00000000 0x00000000 53 PowerRequest 0x00020000 0x00020000 0x00020000 0x001F0000 54 WmiGuid 0x00000001 0x00000002 0x00000010 0x00121FFF 55 EtwRegistration 0x0002000D 0x00020062 0x00021E90 0x00021EFF 56 EtwSessionDemuxEntry 0x0002000D 0x00020062 0x00021E90 0x00021EFF 57 EtwConsumer 0x0002000D 0x00020062 0x00021E90 0x00021EFF 58 CoverageSampler 0x00000000 0x00000000 0x00000000 0x00000000 59 PcwObject 0x00000001 0x00000002 0x00000000 0x000F0003 60 FilterConnectionPort 0x00020001 0x00010001 0x00000000 0x001F0001 61 FilterCommunicationPort 0x00020001 0x00010001 0x00000000 0x001F0001 62 NdisCmState 0x00000000 0x00000000 0x00000000 0x00000000 63 DxgkSharedResource 0x00020000 0x00020001 0x00020000 0x000F0001 64 DxgkSharedKeyedMutexObject 0x00020000 0x00020000 0x00020000 0x001F0000 65 DxgkSharedSyncObject 0x00020001 0x00020002 0x00120000 0x001F0003 66 DxgkSharedSwapChainObject 0x00020000 0x00020000 0x00020000 0x001F0000 67 DxgkDisplayManagerObject 0x00020000 0x00020000 0x00020000 0x001F0000 68 DxgkSharedProtectedSessionObject 0x00020000 0x00020000 0x00020000 0x001F0000 69 DxgkSharedBundleObject 0x00020001 0x00020003 0x00120000 0x001F0003 70 DxgkCompositionObject 0x00020001 0x00020002 0x00020000 0x000F0003 71 VRegConfigurationContext 0x00020000 0x00020000 0x00020000 0x000F0000 [*] Completed.
Generic Mapping
のアクセスマスク値をオブジェクトの種類に応じて解釈すれば、汎用アクセス権限を要求した際に割り当てられる権限が分かります。
この列挙結果からProcess
オブジェクトを例にすると、以下の表のように解釈できます。
汎用アクセス権限 | 変換後のアクセスマスク | 対応する権限名 |
---|---|---|
GENERIC_READ |
0x00020410 |
PROCESS_VM_READ PROCESS_QUERY_INFORMATION READ_CONTROL |
GENERIC_WRITE |
0x00020BEA |
PROCESS_CREATE_THREAD PROCESS_VM_OPERATION PROCESS_VM_WRITE PROCESS_DUP_HANDLE PROCESS_CREATE_PROCESS PROCESS_SET_QUOTA PROCESS_SET_INFORMATION PROCESS_SUSPEND_RESUME READ_CONTROL |
GENERIC_EXECUTE |
0x00121001 |
PROCESS_TERMINATE PROCESS_QUERY_LIMITED_INFORMATION READ_CONTROL SYNCHRONIZE |
GENERIC_ALL |
0x001FFFFF |
PROCESS_ALL_ACCESS |
プロセスが確保しているハンドルの一覧情報は、System Informer(旧称Process Hacker)やSysinternals SuiteのProcess Explorerなどのツールで確認できます。
Sysinternals SuiteのProcess Explorerの場合は、[View] > [Lower Pane View] > [Handles]にチェックを入れれば、下部にプロセスが取得しているハンドルの一覧情報を出力できます。
デフォルトだと、オブジェクトの種類名を表示するType
列と、オブジェクト名を表示するName
列しか表示されませんが、列名を表示している部分を右クリックすると[Select Columns...]というメニューが表示できるので、そこからハンドル値やアクセスマスク値を表示できるように設定できます。
NOTE
Sysinternals SuiteはMicrosoft公式ページからダウンロードしてもよいですが、最近はMicrosoft Storeからのインストールが可能です。 Microsoft Storeからインストールすると自動的にPATHが追加されメニューからの起動が可能となるので、Microsoft Storeからのインストールを推奨します。
ハンドルへの情報の問い合わせと操作
ハンドルを経由してオブジェクトの情報を収集するには、オブジェクトの種類に応じたシステムコールを使います。
システムコールが存在しない種類のオブジェクトに関しては、ユーザ空間からは情報の問い合わせや操作が出来ません。
システムコールの名前はNtQuery
で始まり、その後にオブジェクト名が続きます(NtQueryInformationProcess
やNtQueryEvent
など)。
ユーザ空間からは、ntdll.dll
を介してシステムコールが使えます。
Visual Studioをインストールすると付随しているNative Tools Command Promptを起動して、dumpbin
コマンドでntdll.dll
のエクスポート関数を列挙してNtQuery
で始まるものを抽出すれば、ユーザモードから情報の問い合わせが可能なオブジェクトの種類がおおよそ把握できます。
C:\>dumpbin /exports C:\Windows\System32\ntdll.dll | findstr NtQuery 476 1D3 000A0730 NtQueryAttributesFile 477 1D4 000A2860 NtQueryAuxiliaryCounterFrequency 478 1D5 000A2880 NtQueryBootEntryOrder 479 1D6 000A28A0 NtQueryBootOptions 480 1D7 000A28C0 NtQueryDebugFilterState 481 1D8 000A0230 NtQueryDefaultLocale 482 1D9 000A0810 NtQueryDefaultUILanguage 483 1DA 000A0630 NtQueryDirectoryFile 484 1DB 000A28E0 NtQueryDirectoryFileEx 485 1DC 000A2900 NtQueryDirectoryObject 486 1DD 000A2920 NtQueryDriverEntryOrder 487 1DE 000A2940 NtQueryEaFile 488 1DF 000A0A50 NtQueryEvent 489 1E0 000A2960 NtQueryFullAttributesFile 490 1E1 000A2980 NtQueryInformationAtom 491 1E2 000A29A0 NtQueryInformationByName 492 1E3 000A29C0 NtQueryInformationCpuPartition 493 1E4 000A29E0 NtQueryInformationEnlistment 494 1E5 000A01B0 NtQueryInformationFile 495 1E6 000A2A00 NtQueryInformationJobObject 496 1E7 000A2A20 NtQueryInformationPort 497 1E8 000A02B0 NtQueryInformationProcess 498 1E9 000A2A40 NtQueryInformationResourceManager 499 1EA 000A0430 NtQueryInformationThread 500 1EB 000A03B0 NtQueryInformationToken 501 1EC 000A2A60 NtQueryInformationTransaction 502 1ED 000A2A80 NtQueryInformationTransactionManager 503 1EE 000A2AA0 NtQueryInformationWorkerFactory 504 1EF 000A2AC0 NtQueryInstallUILanguage 505 1F0 000A2AE0 NtQueryIntervalProfile 506 1F1 000A2B00 NtQueryIoCompletion 507 1F2 000A2B20 NtQueryIoRingCapabilities 508 1F3 000A0250 NtQueryKey 509 1F4 000A2B40 NtQueryLicenseValue 510 1F5 000A2B60 NtQueryMultipleValueKey 511 1F6 000A2B80 NtQueryMutant 512 1F7 000A0190 NtQueryObject 513 1F8 000A2BA0 NtQueryOpenSubKeys 514 1F9 000A2BC0 NtQueryOpenSubKeysEx 515 1FA 000A05B0 NtQueryPerformanceCounter 516 1FB 000A2BE0 NtQueryPortInformationProcess 517 1FC 000A2C00 NtQueryQuotaInformationFile 518 1FD 000A09B0 NtQuerySection 519 1FE 000A2C20 NtQuerySecurityAttributesToken 520 1FF 000A2C40 NtQuerySecurityObject 521 200 000A2C60 NtQuerySecurityPolicy 522 201 000A2C80 NtQuerySemaphore 523 202 000A2CA0 NtQuerySymbolicLinkObject 524 203 000A2CC0 NtQuerySystemEnvironmentValue 525 204 000A2CE0 NtQuerySystemEnvironmentValueEx 526 205 000A0650 NtQuerySystemInformation 527 206 000A2D00 NtQuerySystemInformationEx 528 207 000A0AD0 NtQuerySystemTime 529 208 000A0690 NtQueryTimer 530 209 000A2D20 NtQueryTimerResolution 531 20A 000A0270 NtQueryValueKey 532 20B 000A03F0 NtQueryVirtualMemory 533 20C 000A08B0 NtQueryVolumeInformationFile 534 20D 000A2D40 NtQueryWnfStateData 535 20E 000A2D60 NtQueryWnfStateNameInformation 701 2B4 00128350 PssNtQuerySnapshot 1701 69C 000804F0 RtlpNtQueryValueKey
カーネルオブジェクトの情報を設定するシステムコール名は、NtQuery
の代わりにNtSet
から始まります。
一部の情報は、カーネル空間でのオブジェクトの作成時点で固定されるので、問い合わせが可能な情報のすべてが上書き可能というわけではありません。
また、Windows APIにもカーネルオブジェクトの情報の問い合わせと操作が可能なものが存在します。
例えばProcess
オブジェクトの場合は、GetProcessInformation
APIとSetProcessInformation
APIです。
オブジェクトの情報の取得や操作をするWindows APIは何れも内部では対応するシステムコールを呼び出していますが、システムコールで可能なすべての情報の取得と操作が可能というわけではなく、機能面では制限があります。
また、システムコールにしてもすべての情報がMicrosoft公式文書として使用方法が公開されているわけではなく、開発者やセキュリティ技術者などがリバースエンジニアリングした情報を基に利用されているものが多いです。
システム上でプロセスが確保しているハンドル情報は、システムコールNtQuerySystemInformation
を用いて取得できます。
特定のプロセスが取得しているハンドルの一覧情報を取得するには、システムコールNtQueryInformationProcess
を用いますが、対象プロセスからPROCESS_QUERY_LIMITED_INFORMATION
権限またはPROCESS_QUERY_INFORMATION
権限のハンドルが入手出来ている必要があります。
他のプロセスが取得しているハンドルの情報を自分のプロセスで解析したり使いたい場合は、システムコールNtDuplicateObject
またはDuplicateHandle
APIで複製してから、専用のシステムコールやWindows APIで情報の取得や操作をします。
ただし、Token
オブジェクトの場合は用途によってはシステムコールNtDuplicateToken
、Windows APIのDuplicateToken
APIまたはDuplicateTokenEx
を使います。
トークンについては本記事の範疇ではないので詳しくは解説しませんが、NtDuplicateObject
(またはDuplicateHandle
)が同じカーネルオブジェクトへの参照を増やしているだけなのに対して、NtDuplicateToken
(またはDuplicateToken
、DuplicateTokenEx
)は同じ情報を持つToken
オブジェクトの複製をカーネル空間に作成するという違いがあります。
ハンドルが存在せず参照されないカーネルオブジェクトは破棄されてしまうので、NtDuplicateObject
(またはDuplicateHandle
)は意図的にカーネルオブジェクトへの参照を増やして、オブジェクトを残存させるという目的でも用いられます(例えば、オブジェクトの操作元のプロセスを切り替えたい場合など)。
カーネル空間でのハンドル管理
ユーザ空間では、ハンドルはプロセス毎に作成され用いられます。
よってカーネル空間では、プロセスの情報を定義する_EPROCESS
構造体の情報から、プロセスが確保しているハンドル情報が参照できるように実装されています。
_EPROCESS
構造体の定義は、Vergilius ProjectやWinDbgのデバッグシンボルなどから確認できます。
以下はVergilius Projectに掲載されているWindows 11 23H2での_EPROCESS
構造体の情報です。
struct _EPROCESS { struct _KPROCESS Pcb; //0x0 struct _EX_PUSH_LOCK ProcessLock; //0x438 VOID* UniqueProcessId; //0x440 struct _LIST_ENTRY ActiveProcessLinks; //0x448 struct _EX_RUNDOWN_REF RundownProtect; //0x458 --snip-- struct _HANDLE_TABLE* ObjectTable; //0x570 --snip-- }
ObjectTable
フィールドには、_HANDLE_TABLE
構造体の情報が格納されているメモリ領域を指すカーネル空間のアドレス情報が格納されています。
Vergilius Projectに掲載されているWindows 11 23H2での_HANDLE_TABLE
構造体の定義は以下の通りです。
struct _HANDLE_TABLE { ULONG NextHandleNeedingPool; //0x0 LONG ExtraInfoPages; //0x4 volatile ULONGLONG TableCode; //0x8 struct _EPROCESS* QuotaProcess; //0x10 struct _LIST_ENTRY HandleTableList; //0x18 ULONG UniqueProcessId; //0x28 union { ULONG Flags; //0x2c struct { UCHAR StrictFIFO:1; //0x2c UCHAR EnableHandleExceptions:1; //0x2c UCHAR Rundown:1; //0x2c UCHAR Duplicated:1; //0x2c UCHAR RaiseUMExceptionOnInvalidHandleClose:1; //0x2c }; }; struct _EX_PUSH_LOCK HandleContentionEvent; //0x30 struct _EX_PUSH_LOCK HandleTableLock; //0x38 union { struct _HANDLE_TABLE_FREE_LIST FreeLists[1]; //0x40 struct { UCHAR ActualEntry[32]; //0x40 struct _HANDLE_TRACE_DEBUG_INFO* DebugInfo; //0x60 }; }; };
QuotaProcess
フィールドには、_HANDLE_TABLE
構造体を参照しているProcess
オブジェクトの_EPROCESS
構造体のアドレスが設定されます。
HandleTableList
フィールドは、他のプロセスが参照している_HANDLE_TABLE
構造体のHandleTableList
フィールドを指す双方向リンクリストの情報です。
双方向リンクリストで各プロセスが参照する_HANDLE_TABLE
構造体を相互参照して、カーネルが意図していない情報改ざんを防いでいます。
ハンドルの一覧情報は、TableCode
フィールドに格納されているカーネルアドレスが指すメモリ領域に格納されています。
TableCode
フィールドが指すメモリ領域は、16バイトの値を統合した以下の_HANDLE_TABLE_ENTRY
共用体の配列として形成されています。
union _HANDLE_TABLE_ENTRY { volatile LONGLONG VolatileLowValue; //0x0 LONGLONG LowValue; //0x0 struct { struct _HANDLE_TABLE_ENTRY_INFO* volatile InfoTable; //0x0 LONGLONG HighValue; //0x8 union _HANDLE_TABLE_ENTRY* NextFreeHandleEntry; //0x8 struct _EXHANDLE LeafHandleValue; //0x8 }; LONGLONG RefCountField; //0x0 ULONGLONG Unlocked:1; //0x0 ULONGLONG RefCnt:16; //0x0 ULONGLONG Attributes:3; //0x0 struct { ULONGLONG ObjectPointerBits:44; //0x0 ULONG GrantedAccessBits:25; //0x8 ULONG NoRightsUpgrade:1; //0x8 ULONG Spare1:6; //0x8 }; ULONG Spare2; //0xc };
共用体なのでやや分かりにくいですが、基本的には16バイトの情報として、最終的なハンドル情報には以下の構造体の定義が用いられます。
struct { ULONGLONG ObjectPointerBits:44; //0x0 ULONG GrantedAccessBits:25; //0x8 ULONG NoRightsUpgrade:1; //0x8 ULONG Spare1:6; //0x8 };
GrantedAccessBits
が実際にハンドルに割り当てられた権限を示すアクセスマスクであり、25ビットなので0x01FFFFFF
の部分のみがハンドルに定義できるアクセスマスクの値です。
先述の通り、無視される7ビットについては最上位の4ビットが汎用アクセス権限のマスク、続く2ビットが予約領域、最後の1ビットはMAXIMUM_ALLOWED
を示すマスクなので、オブジェクトへのアクセス権限を表すには25ビットで十分なのです。
_HANDLE_TABLE
構造体のTableCode
フィールドが指すメモリ領域には、この16バイトの値が順に並んでいます。
ただし、最初の16バイトは予約領域であり、_HANDLE_TABLE_ENTRY
共用体の情報が含まれていません。
また、ハンドルの識別子は4単位で割り当てられるので、先頭アドレスから+16バイトの位置にハンドル値0x4
の情報を示す_HANDLE_TABLE_ENTRY
共用体、+32バイトの位置にハンドル値0x8
の情報を示す_HANDLE_TABLE_ENTRY
共用体という様に整列されています。
ここまでをまとめると、_EPROCESS
構造体を基準としたカーネルオブジェクトの配置は以下の図の様になっています。
こうして_HANDLE_TABLE_ENTRY
共用体からハンドルの値とアクセスマスクの値は分かりますが、Windows OSからするとどのカーネルオブジェクトに対するハンドルなのかを特定する必要があります。
ObjectPointerBits
フィールドにその情報が含まれていますが、64ビットではなく44ビットなので、完全なカーネル空間のアドレスの導出には一手間必要です。
このフィールドの情報は、以下の式でカーネル空間のアドレスに変換できます(4ビット左シフトして、最上位16ビットを1で埋めます)。
(Kernel Address) = 0xFFFF000000000000 | (_HANDLE_TABLE_ENTRY.ObjectPointerBits << 4)
こうして導出されたカーネルアドレスは、_OBJECT_HEADER
構造体から始まるカーネルオブジェクトが保存されているメモリを参照しています。
Vergilius Projectに掲載されているWindows 11 23H2での_OBJECT_HEADER
構造体の定義は以下の通りです。
struct _OBJECT_HEADER { LONGLONG PointerCount; //0x0 union { LONGLONG HandleCount; //0x8 VOID* NextToFree; //0x8 }; struct _EX_PUSH_LOCK Lock; //0x10 UCHAR TypeIndex; //0x18 union { UCHAR TraceFlags; //0x19 struct { UCHAR DbgRefTrace:1; //0x19 UCHAR DbgTracePermanent:1; //0x19 }; }; UCHAR InfoMask; //0x1a union { UCHAR Flags; //0x1b struct { UCHAR NewObject:1; //0x1b UCHAR KernelObject:1; //0x1b UCHAR KernelOnlyAccess:1; //0x1b UCHAR ExclusiveObject:1; //0x1b UCHAR PermanentObject:1; //0x1b UCHAR DefaultSecurityQuota:1; //0x1b UCHAR SingleHandleEntry:1; //0x1b UCHAR DeletedInline:1; //0x1b }; }; ULONG Reserved; //0x1c union { struct _OBJECT_CREATE_INFORMATION* ObjectCreateInfo; //0x20 VOID* QuotaBlockCharged; //0x20 }; VOID* SecurityDescriptor; //0x28 struct _QUAD Body; //0x30 };
ここからの過程はやや複雑なので、一度ここまでの内容を実演し、実例を確認しながら解説を進めます。
まず、PowerShellを起動して、以下のスクリプトをそのまま貼り付けて実行してください。
このスクリプトはOpenProcess
APIとCloseHandle
APIをPowerShellから実行できる様にするためのものです。
$source = @" using System; using System.Runtime.InteropServices; [Flags] public enum ACCESS_MASK : uint { NO_ACCESS = 0x00000000, PROCESS_TERMINATE = 0x00000001, PROCESS_CREATE_THREAD = 0x00000002, PROCESS_VM_OPERATION = 0x00000008, PROCESS_VM_READ = 0x00000010, PROCESS_VM_WRITE = 0x00000020, PROCESS_DUP_HANDLE = 0x00000040, PROCESS_CREATE_PROCESS = 0x000000080, PROCESS_SET_QUOTA = 0x00000100, PROCESS_SET_INFORMATION = 0x00000200, PROCESS_QUERY_INFORMATION = 0x00000400, PROCESS_SUSPEND_RESUME = 0x00000800, PROCESS_QUERY_LIMITED_INFORMATION = 0x00001000, PROCESS_SET_LIMITED_INFORMATION = 0x00002000, PROCESS_ALL_ACCESS = 0x001F0FFF, DELETE = 0x00010000, READ_CONTROL = 0x00020000, WRITE_DAC = 0x00040000, WRITE_OWNER = 0x00080000, SYNCHRONIZE = 0x00100000, ACCESS_SYSTEM_SECURITY = 0x01000000, MAXIMUM_ALLOWED = 0x02000000, GENERIC_ALL = 0x10000000, GENERIC_EXECUTE = 0x20000000, GENERIC_WRITE = 0x40000000, GENERIC_READ = 0x80000000 } public class Kernel32 { [DllImport("kernel32.dll", SetLastError = true)] public static extern bool CloseHandle(IntPtr hObject); [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr OpenProcess( ACCESS_MASK dwDesiredAccess, bool bInheritHandle, int dwProcessId); } "@ Add-Type -TypeDefinition $source
続けて、何かしらのプロセスに対してOpenProcess
APIを実行してハンドルを取得します。
例としてexplorer.exe
のプロセスからGENERIC_READ
でハンドルを取得します。
PS C:\> "PID: 0x$($pid.ToString("X"))" PID: 0x1450 PS C:\> $explorer = Get-Process -Name explorer PS C:\> $explorer Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName ------- ------ ----- ----- ------ -- -- ----------- 3059 100 94952 167628 16.67 1224 1 explorer PS C:\> $hProcess = [Kernel32]::OpenProcess([ACCESS_MASK]::GENERIC_READ, $false, $explorer.Id) PS C:\> "Handle = 0x$($hProcess.ToString("X"))" Handle = 0xD48
値が0xD48
のハンドルが取得できました(権限が無いプロセスの場合は0
が返ってきます)。
Sysinternals SuiteのProcess Explorerで確認すると、以下に図示するような情報が確認できます。
ハンドルが参照しているカーネルオブジェクトのアドレスが確認できます。
こうして取得したハンドル情報を、カーネルデバッガから確認しましょう。
まずはカーネルデバッグしているWinDbgから、explorer.exe
へのハンドルを開いたpowershell.exe
のプロセスの_EPROCESS
構造体のアドレスを調べ、ObjectTable
フィールドから_HANDLE_TABLE
構造体のアドレスを特定します。
0: kd> !process 0 0 powershell.exe PROCESS ffffd7883e8130c0 SessionId: 1 Cid: 1450 Peb: 18b9b7e000 ParentCid: 2238 DirBase: 2e7b4000 ObjectTable: ffffac8ddac4c940 HandleCount: 675. Image: powershell.exe 0: kd> dt nt!_eprocess ffffd7883e8130c0 imagefilename uniqueprocessid objecttable +0x440 UniqueProcessId : 0x00000000`00001450 Void +0x570 ObjectTable : 0xffffac8d`dac4c940 _HANDLE_TABLE +0x5a8 ImageFileName : [15] "powershell.exe" 0: kd> dt nt!_handle_table ffffac8d`dac4c940 +0x000 NextHandleNeedingPool : 0x1000 +0x004 ExtraInfoPages : 0n0 +0x008 TableCode : 0xffffac8d`da7bc001 +0x010 QuotaProcess : 0xffffd788`3e8130c0 _EPROCESS +0x018 HandleTableList : _LIST_ENTRY [ 0xffffac8d`d8403358 - 0xffffac8d`d84181d8 ] +0x028 UniqueProcessId : 0x1450 +0x02c Flags : 0 +0x02c StrictFIFO : 0y0 +0x02c EnableHandleExceptions : 0y0 +0x02c Rundown : 0y0 +0x02c Duplicated : 0y0 +0x02c RaiseUMExceptionOnInvalidHandleClose : 0y0 +0x030 HandleContentionEvent : _EX_PUSH_LOCK +0x038 HandleTableLock : _EX_PUSH_LOCK +0x040 FreeLists : [1] _HANDLE_TABLE_FREE_LIST +0x040 ActualEntry : [32] "" +0x060 DebugInfo : (null)
先述の通り、ObjectTable
から参照されている_HANDLE_TABLE
構造体のQuotaProcess
フィールドの値が、powershell.exe
のプロセスの_EPROCESS
構造体のアドレスと一致しています。
また、_HANDLE_TABLE
構造体のUniqueProcessId
フィールドも、powershell.exe
のプロセスの_EPROCESS
構造体のUniqueProcessId
フィールドと一致しています。
続けて_HANDLE_TABLE
構造体のTableCode
フィールドから_HANDLE_TABLE_ENTRY
共用体の一覧情報が格納されているアドレスを計算しなければいけないのですが、その処理がやや複雑です。
まずはTableCode
フィールドの値の下位2ビット(つまり最大値は10進数で3
)の値を調べる必要があります。
0: kd> dt nt!_handle_table ffffac8d`dac4c940 tablecode +0x008 TableCode : 0xffffac8d`da7bc001 0: kd> ? 0xffffac8d`da7bc001 & 3 Evaluate expression: 1 = 00000000`00000001
この値によって、_HANDLE_TABLE_ENTRY
共用体の一覧情報が格納されているアドレスの計算方法が異なります。
以下の規則に従います。
便宜のため、_HANDLE_TABLE_ENTRY
共用体が格納されているメモリ領域の基準アドレスをpEntryBase
、目的のハンドルの_HANDLE_TABLE_ENTRY
共用体のアドレスをpEntry
、TableCode
フィールドの値をnTableCode
、ハンドルの値をnHandle
とします。
ただし、ハンドルの値は4の倍数として扱うので、元のハンドルの値に0xFFFFFFFFFFFFFFFC
をAND演算したものをnHandle
とします。
0
の場合この場合は、
TableCode
フィールドの値がそのまま_HANDLE_TABLE_ENTRY
共用体の一覧情報が格納されているアドレスとして適用できます。pEntryBase = nTableCode
調べたいハンドルに対応する
_HANDLE_TABLE_ENTRY
共用体のオフセット値の算出には、ハンドルの値をそのまま用います。 ハンドルの値を1/4(2ビット右シフト)して16倍(4ビット左シフト)すればオフセット位置が計算できます。pEntry = pEntryBase + (nHandle << 2)
1
の場合この場合は、以下の式に基づいてアドレス(
pBaseAddress
とします)を算出します。pBaseAddress = nTableCode + ((nHandle >> 10) << 3) - 1
こうして導出した
pBaseAddress
に格納されているアドレス値が、_HANDLE_TABLE_ENTRY
共用体が格納されているメモリ領域の基準アドレスpEntryBase
です。 目的のハンドルの_HANDLE_TABLE_ENTRY
共用体については、ハンドルの値を0x3FF
でマスクしてから、オフセット位置を算出する必要があります。pEntry = pEntryBase + ((nHandle & 0x3FF) << 2)
1
より大きい値の場合この場合は、先頭アドレスの算出がやや複雑です。 まずは以下の式でアドレス(
pNonce
とします)を算出します。pNonce = nTableCode + ((nHandle >> 19) << 3) - 2
算出された
pNonce
からアドレス値(pIntermediate
)を読み取り、以下の式でアドレス(pBaseAddress
)を算出します。pBaseAddress = pIntermediate + (((nHandle >> 10) & 0x1FF) << 3)
こうして導出した
pBaseAddress
に格納されているアドレス値が、_HANDLE_TABLE_ENTRY
共用体が格納されているメモリ領域の基準アドレスpEntryBase
です。 目的のハンドルの_HANDLE_TABLE_ENTRY
共用体については、ハンドルの値を0x3FF
でマスクしてから、オフセット位置を算出する必要があります。pEntry = pEntryBase + ((nHandle & 0x3FF) << 2)
この処理をC言語形式で表現すると、以下のようになります。
nHandle = nHandle & 0xFFFFFFFFFFFFFFFC; if ((nTableCode & 3) == 0) { return (nTableCode + ((nTableCode >> 2) << 4)); } else if ((nTableCode & 3) == 1) { ULONG64 pEntryBase = *(ULONG64*)(nTableCode + ((nHandle >> 10) << 3) - 1); return (pEntryBase + ((nHandle & 0x3FF) << 2)); } else { ULONG64 pIntermediate = *(ULONG64*)(nTableCode + ((nHandle >> 19) << 3) - 2); ULONG64 pEntryBase = *(ULONG64*)(pIntermediate + (((nHandle >> 10) & 0x1FF) << 3)); return (pEntryBase + ((nHandle & 0x3FF) << 2)); }
なぜこのような解決方法なのかははっきりしていませんが、おそらくはメモリ管理の都合であると考えられます。
例示しているTableCode
の値は0xffffbc89`d2ffe001
であり、下位2ビットの値は1
なので、以下のように_HANDLE_TABLE_ENTRY
共用体の一覧情報が格納されているアドレスを導出します(目的のexplorer.exe
を参照しているハンドルの値は0xD48
です)。
0: kd> ? d48 & ffffffff`fffffffc Evaluate expression: 3400 = 00000000`00000d48 0: kd> ? ffffac8d`da7bc001 + ((d48 >> 0n10) << 3) - 1 Evaluate expression: -91749720801256 = ffffac8d`da7bc018 0: kd> dp ffffac8d`da7bc018 L1 ffffac8d`da7bc018 ffffac8d`dd8aa000 0: kd> ? ffffac8d`dd8aa000 + (((d48 & 3ff) >> 2) << 4) Evaluate expression: -91749669493472 = ffffac8d`dd8aa520 0: kd> dd ffffac8d`dd8aa520 L4 ffffac8d`dd8aa520 80500001 d7883d68 00021410 00000000 0: kd> dt nt!_handle_table_entry ffffac8d`dd8aa520 +0x000 VolatileLowValue : 0n-2916013239683776511 +0x000 LowValue : 0n-2916013239683776511 +0x000 InfoTable : 0xd7883d68`80500001 _HANDLE_TABLE_ENTRY_INFO +0x008 HighValue : 0n136208 +0x008 NextFreeHandleEntry : 0x00000000`00021410 _HANDLE_TABLE_ENTRY +0x008 LeafHandleValue : _EXHANDLE +0x000 RefCountField : 0n-2916013239683776511 +0x000 Unlocked : 0y1 +0x000 RefCnt : 0y0000000000000000 (0) +0x000 Attributes : 0y000 +0x000 ObjectPointerBits : 0y11010111100010000011110101101000100000000101 (0xd7883d68805) +0x008 GrantedAccessBits : 0y0000000100001010000010000 (0x21410) +0x008 NoRightsUpgrade : 0y0 +0x008 Spare1 : 0y000000 (0) +0x00c Spare2 : 0
GrantedAccessBits
フィールドが0x21410
というアクセスマスク値としてあり得そうな値に設定されていることから、正しく_HANDLE_TABLE_ENTRY
共用体のアドレスが解決できたと推察できます。
Process
オブジェクトのGenericMapping
情報によれば、GENERIC_READ
でexplorer.exe
プロセスを要求してハンドルの取得に成功した場合はアクセスマスクは0x00020410
(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION | READ_CONTROL
)ですが、PROCESS_QUERY_LIMITED_INFORMATION
権限を表す0x00001000
が1つ余分に見えます。
これは、PROCESS_QUERY_LIMITED_INFORMATION
権限がPROCESS_QUERY_INFORMATION
権限の縮小版であるため、PROCESS_QUERY_INFORMATION
権限に付随して自動的に追加されたものであると考えられます。
64ビットOSの場合は、_HANDLE_TABLE_ENTRY
共用体のObjectPointerBits
フィールドの値を4ビット左シフトして、ffff0000`00000000
をOR演算すれば、対象のカーネルオブジェクトの先頭アドレスが算出できます。
0: kd> ? (0xd7883d68805 << 4) | ffff0000`00000000 Evaluate expression: -44494830927792 = ffffd788`3d688050
こうして算出されたアドレスを、WinDbgの!pool
コマンドで検証してみましょう。
0: kd> !pool ffffd788`3d688050 unable to get nt!PspSessionIdBitmap Pool page ffffd7883d688050 region is Nonpaged pool *ffffd7883d688000 size: e80 previous size: 0 (Allocated) *Proc Pooltag Proc : Process objects, Binary : nt!ps ffffd7883d688e80 size: 160 previous size: 0 (Free) ..!3
算出されたアドレスはNon-Paged Pool領域に位置しており、Process
オブジェクトのメモリ領域に設定されるPool HeaderであるProc
がPool Headerに設定されているので、ハンドルのアクセス対象オブジェクトのアドレスが算出できていそうだと推察できます。
この算出されたアドレスは、カーネルオブジェクトのヘッダ情報である_OBJECT_HEADER
構造体の先頭アドレスを指しています。
0: kd> dt nt!_object_header ffffd788`3d688050 +0x000 PointerCount : 0n1463713 +0x008 HandleCount : 0n56 +0x008 NextToFree : 0x00000000`00000038 Void +0x010 Lock : _EX_PUSH_LOCK +0x018 TypeIndex : 0x78 'x' +0x019 TraceFlags : 0 '' +0x019 DbgRefTrace : 0y0 +0x019 DbgTracePermanent : 0y0 +0x01a InfoMask : 0x88 '' +0x01b Flags : 0 '' +0x01b NewObject : 0y0 +0x01b KernelObject : 0y0 +0x01b KernelOnlyAccess : 0y0 +0x01b ExclusiveObject : 0y0 +0x01b PermanentObject : 0y0 +0x01b DefaultSecurityQuota : 0y0 +0x01b SingleHandleEntry : 0y0 +0x01b DeletedInline : 0y0 +0x01c Reserved : 0 +0x020 ObjectCreateInfo : 0xffffd788`3ba1ccc0 _OBJECT_CREATE_INFORMATION +0x020 QuotaBlockCharged : 0xffffd788`3ba1ccc0 Void +0x028 SecurityDescriptor : 0xffffac8d`d71f5aef Void +0x030 Body : _QUAD
実際のカーネルオブジェクトのデータは、_OBJECT_HEADER
構造体のBody
フィールドの位置から始まります。
Process
オブジェクトなので、_EPROCESS
構造体を適用すれば、ハンドルのアクセス対象であるexplorer.exe
の_EPROCESS
構造体であると分かります。
実際に!process
コマンドから確認できるexplorer.exe
の_EPROCESS
構造体のアドレスとも一致しています。
0: kd> ? ffffd788`3d688050+30 Evaluate expression: -44494830927744 = ffffd788`3d688080 0: kd> dt nt!_eprocess ffffd788`3d688080 imagefilename uniqueprocessid +0x440 UniqueProcessId : 0x00000000`000004c8 Void +0x5a8 ImageFileName : [15] "explorer.exe" 0: kd> !process 0 0 explorer.exe PROCESS ffffd7883d688080 SessionId: 1 Cid: 04c8 Peb: 00995000 ParentCid: 1354 DirBase: 125ad6000 ObjectTable: ffffac8dd8e39a80 HandleCount: 4697. Image: explorer.exe
_OBJECT_HEADER
構造体のTypeIndex
フィールドからは、カーネルオブジェクトの種類も確認できます。
この節の最後の話題として、その方法を解説します。
この記事の冒頭に示したシステムコールNtQueryObject
の結果によればProcess
オブジェクトの識別番号は7
ですが、_OBJECT_HEADER
構造体のTypeIndex
の値は0x78
であり奇妙に見えます。
その原因は、TypeIndex
の値は実際の識別番号をエンコードして算出しているからです。
これはWindows 10から導入された変更であり、それよりも古いOSではオブジェクトの識別番号がそのまま設定されていました。
このエンコードされたTypeIndex
の値をデコードするには、以下2つの値をXOR演算します。
nt!ObHeaderCookie
(1バイト)_OBJECT_HEADER
構造体の2バイト目のバイト値
実際に計算すると以下のようになります。
0: kd> db nt!obheadercookie L1 fffff802`7131ed74 ff . 0: kd> ? (ffffd788`3d688050 >> 8) & ff Evaluate expression: 128 = 00000000`00000080 0: kd> ? ff ^ 80 ^ 78 Evaluate expression: 7 = 00000000`00000007
システムコールNtQueryObject
の結果から得られたProcess
オブジェクトの識別番号に一致する7
が導出できました。
実際のオブジェクト種類情報は、nt!ObTypeIndexTable
に保存されているアドレステーブルから参照できます。
nt!ObTypeIndexTable
はポインタの配列なので、64ビットOSではオブジェクトの識別番号に8
を積算した位置から、そのオブジェクトの種類情報が格納されているアドレスが分かります。
オブジェクトの種類の情報はnt!_OBJECT_TYPE
構造体として格納されています。
0: kd> dp nt!obtypeindextable + (7 * 8) L1 fffff802`7131f668 ffffd788`382a3e80 0: kd> dt nt!_object_type ffffd788`382a3e80 +0x000 TypeList : _LIST_ENTRY [ 0xffffd788`382a3e80 - 0xffffd788`382a3e80 ] +0x010 Name : _UNICODE_STRING "Process" +0x020 DefaultObject : (null) +0x028 Index : 0x7 '' +0x02c TotalNumberOfObjects : 0xac +0x030 TotalNumberOfHandles : 0x6dd +0x034 HighWaterNumberOfObjects : 0xb4 +0x038 HighWaterNumberOfHandles : 0x6fc +0x040 TypeInfo : _OBJECT_TYPE_INITIALIZER +0x0b8 TypeLock : _EX_PUSH_LOCK +0x0c0 Key : 0x636f7250 +0x0c8 CallbackList : _LIST_ENTRY [ 0xffffac8d`d42f5ac0 - 0xffffac8d`d42f5ac0 ]
想定通り、Name
フィールドからProcess
というオブジェクトの種類名が確認できます。
応用例1 - オブジェクトをロックしているプロセスの特定
Windows OSを利用している人の中には、ファイルの削除やプロセスの停止を以下のようなメッセージと共に防がれ、もどかしい思いをした経験がある人が多いでしょう。
この動作は、何かしらのプロセスがディレクトリ中のディレクトリやファイルを開いている(つまりはハンドルを開いている)のが原因ですが、「another program」ではどのプロセスが原因なのかが分かりません。 ロックしているプロセスが重要な操作をしている可能性があるので、余裕があればロックが解除されるまで待つのがシステムの安定性を考えると好ましいでしょうが、PCのデータ整理のためにファイルやフォルダの位置をさっさと移動したいという場合もあるでしょう。
システムプログラミング的には、NtQuerySystemInformation
やNtQueryObject
などをはじめとするいくつかのシステムコールを組み合わせれば、削除したいファイルやプロセスを開いているプロセスが特定するツールが開発できます。
既存のツールでも、Sysinternals SuiteのHandleを使えば同じくハンドルの取得元プロセスの特定が可能なので、本記事ではその方法を紹介します。
その方法は極めて簡単です。
管理者権限でPowerShellを起動し、以下のようにロックされているファイルやディレクトリのパスを-a
オプションに続けて指定して実行するだけです。
以下の例では、C:\Users\admin\Desktop\Temp
というディレクトリパスをオブジェクト名に含むハンドルを探しています。
PS C:\> handle.exe -a C:\Users\admin\Desktop\Temp Nthandle v5.0 - Handle viewer Copyright (C) 1997-2022 Mark Russinovich Sysinternals - www.sysinternals.com FileLocker.exe pid: 18888 type: File 50: C:\Users\admin\Desktop\Temp FileLocker.exe pid: 18888 type: File 2AC: C:\Users\admin\Desktop\Temp\test.txt
この例では、PID 18888
のFileLocker.exe
というプログラムのプロセスが、C:\Users\admin\Desktop\Temp\test.txt
へのハンドルを獲得しているのが原因であると特定できます。
ハンドルを取得しているプロセスが一般権限であれば、一般権限でhandle.exe
を実行しても問題ありませんが、プロセスの動作権限が分からない場合がほとんどなので管理者権限で実行すれば確実です。
また、PowerShellであれば管理者権限での起動時に自動的にSeDebugPrivilege
が有効化されるので、ほぼすべてのプロセスのハンドル情報が取得できます。
応用例2 - カーネルモードルートキット
Windows OSでは、Microsoftから信頼されていない悪意のあるカーネルドライバ(ルートキット)からの感染を防止するために、DSE(Driver Signature Enforcement) という仕組みを実装しています。 しかし、Microsoftから信頼されているドライバやWindows OS自体にメモリ操作につながる脆弱性が存在する場合、DSEによって防がれるべきであるはずの署名無しのカーネルドライバがインストールされてしまう可能性があるため、ルートキットの脅威は完全に消えたわけでありません。 よって、ルートキットでどのように悪用される可能性があるのかについては一考の余地があるでしょう。 知っていれば、類似した手法を悪用するマルウェアに遭遇した際に役立つ可能性があります。
本記事で取り上げている知識を応用すると、カーネルメモリの操作により、低い権限のハンドルを昇格できます。
カーネルデバッガを用いてその挙動を再現してみましょう。
まずは先述の例と同様に、PowerShellでexplorer.exe
のハンドルをGENERIC_READ
権限で開きます。
PS C:\> $explorer = Get-Process -Name explorer PS C:\> $hProcess = [Kernel32]::OpenProcess([ACCESS_MASK]::GENERIC_READ, $false, $explorer.Id) PS C:\> "Handle = 0x$($hProcess.ToString("X"))" Handle = 0xA60
Process Explorerでハンドル情報を確認すると、先ほどの例と同様にアクセスマスクは0x21410
(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION | PROCESS_QUERY_LIMITED_INFORMATION | READ_CONTROL
)です。
まずはpowershell.exe
の_EPROCESS
構造体の情報から、獲得したハンドルの_HANDLE_TABLE_ENTRY
構造体のアドレスを特定します。
0: kd> !process 0 0 powershell.exe PROCESS ffffd7883a1b2080 SessionId: 1 Cid: 089c Peb: 8ccdec8000 ParentCid: 2760 DirBase: 455ce000 ObjectTable: ffffac8ddf12dc00 HandleCount: 597. Image: powershell.exe 0: kd> dt nt!_eprocess ffffd7883a1b2080 objecttable +0x570 ObjectTable : 0xffffac8d`df12dc00 _HANDLE_TABLE 0: kd> dt nt!_handle_table 0xffffac8d`df12dc00 tablecode +0x008 TableCode : 0xffffac8d`daf16001 0: kd> ? a60 & ffffffff`fffffffc Evaluate expression: 2656 = 00000000`00000a60 0: kd> ? ffffac8d`daf16001 + ((a60 >> 0n10) << 3) - 1 Evaluate expression: -91749713092592 = ffffac8d`daf16010 0: kd> dp ffffac8d`daf16010 L1 ffffac8d`daf16010 ffffac8d`e0be7000 0: kd> ? ffffac8d`e0be7000 + (((a60 & 3ff) >> 2) << 4) Evaluate expression: -91749615765120 = ffffac8d`e0be7980 0: kd> dt nt!_handle_table_entry ffffac8d`e0be7980 +0x000 VolatileLowValue : 0n-2916013239683776511 +0x000 LowValue : 0n-2916013239683776511 +0x000 InfoTable : 0xd7883d68`80500001 _HANDLE_TABLE_ENTRY_INFO +0x008 HighValue : 0n136208 +0x008 NextFreeHandleEntry : 0x00000000`00021410 _HANDLE_TABLE_ENTRY +0x008 LeafHandleValue : _EXHANDLE +0x000 RefCountField : 0n-2916013239683776511 +0x000 Unlocked : 0y1 +0x000 RefCnt : 0y0000000000000000 (0) +0x000 Attributes : 0y000 +0x000 ObjectPointerBits : 0y11010111100010000011110101101000100000000101 (0xd7883d68805) +0x008 GrantedAccessBits : 0y0000000100001010000010000 (0x21410) +0x008 NoRightsUpgrade : 0y0 +0x008 Spare1 : 0y000000 (0) +0x00c Spare2 : 0 0: kd> dd ffffac8d`e0be7980 L4 ffffac8d`e0be7980 80500001 d7883d68 00021410 00000000
先述の通り、ここで16バイトの値に適用されているデータ構造は、_HANDLE_TABLE_ENTRY
共用体の以下の構造体です。
struct { ULONGLONG ObjectPointerBits:44; //0x0 ULONG GrantedAccessBits:25; //0x8 ULONG NoRightsUpgrade:1; //0x8 ULONG Spare1:6; //0x8 };
ここで、全アクセス権限を意味するアクセスマスク0x001fffff
を、算出された_HANDLE_TABLE_ENTRY
共用体のアドレスであるffffac8d`e0be7980
から8バイトの位置に設定されている現在のアクセスマスク00021410
にOR演算すれば、GENERIC_READ
権限で獲得したアクセスマスクをPROCESS_ALL_ACCESS
に昇格できます(この例ではNoRightsUpgrad
もSpare1
もすべて0
なのでそのまま上書きしても良いですが、そうでない場合はOR演算により必要以上のデータの改ざんが防げます)。
0: kd> ? 00021410 | 1fffff Evaluate expression: 2097151 = 00000000`001fffff 0: kd> ed ffffac8d`e0be7980+8 001fffff 0: kd> dt nt!_handle_table_entry ffffac8d`e0be7980 +0x000 VolatileLowValue : 0n-2916013239683776511 +0x000 LowValue : 0n-2916013239683776511 +0x000 InfoTable : 0xd7883d68`80500001 _HANDLE_TABLE_ENTRY_INFO +0x008 HighValue : 0n2097151 +0x008 NextFreeHandleEntry : 0x00000000`001fffff _HANDLE_TABLE_ENTRY +0x008 LeafHandleValue : _EXHANDLE +0x000 RefCountField : 0n-2916013239683776511 +0x000 Unlocked : 0y1 +0x000 RefCnt : 0y0000000000000000 (0) +0x000 Attributes : 0y000 +0x000 ObjectPointerBits : 0y11010111100010000011110101101000100000000101 (0xd7883d68805) +0x008 GrantedAccessBits : 0y0000111111111111111111111 (0x1fffff) +0x008 NoRightsUpgrade : 0y0 +0x008 Spare1 : 0y000000 (0) +0x00c Spare2 : 0
WinDbgによるブレークを解除して、再びProcess Explorerでアクセスマスクを確認すると、explorer.exe
へのハンドルの権限が00021410
(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION | PROCESS_QUERY_LIMITED_INFORMATION | READ_CONTROL
)から001fffff
(PROCESS_ALL_ACCESS
)に更新されているのが確認できます。
DSEを回避して、この実装したルートキットとして作用するカーネルドライバのインストールに成功すれば、こうして任意のハンドルを全アクセス権限を持つハンドルに昇格させられます。 ただし前提条件として、この機能を持つルートキットの悪用には何かしらの権限で対象オブジェクトのハンドルが得られている必要があります。 ほとんどの場合は低権限のプロセスから特権プロセスのハンドルは獲得出来ないので、プロセスに対してこの手法を用いるのはあまり効果的ではありません。
この手法が効果を発揮するオブジェクトは、ファイルやレジストリです。 ほとんどのファイルやレジストリは、整合性レベルがLowであっても、低権限のプロセスからでも読み取りはできるので、少なくとも読み取り用のハンドルを獲得できます。 よって、システムにとって重要なファイルやレジストリから読み取り用のハンドル獲得した後で、この機能を持つルートキットでハンドルの権限を昇格すれば上書きや削除が可能となり、SYSTEM権限の奪取などが可能になります。
まとめ
本記事では、Windows OSのオブジェクトからの情報の取得や操作に必要なハンドルについて、カーネル空間でのデータ構造を中心に解説しました。 解説した知識の活用方法としては、日頃のWindowsの利用に活用できる例と、マルウェア解析に役立つだろう例を紹介しました。 他にも、マルウェア対策製品やメモリフォレンジックツールなどのセキュリティ製品の開発に、本記事で解説した知識が役に立つと考えられます。