ラック・セキュリティごった煮ブログ

セキュリティエンジニアがエンジニアの方に向けて、 セキュリティやIT技術に関する情報を発信していくアカウントです。

【お知らせ】2021年5月10日~リニューアルオープン!今後はこちらで新しい記事を公開します。

株式会社ラックのセキュリティエンジニアが、 エンジニアの方向けにセキュリティやIT技術に関する情報を発信するブログです。(編集:株式会社ラック・デジタルペンテスト部)
当ウェブサイトをご利用の際には、こちらの「サイトのご利用条件」をご確認ください。

デジタルペンテスト部提供サービス:ペネトレーションテスト

Windowsカーネルから見るオブジェクトハンドル

デジタルペンテスト部の北原です。 今回は、Windowsでのツール開発者にとっては重要なハンドルの内部について解説します。

Windowsでは、ファイルやプロセスから始まりすべてのリソースはオブジェクトとして扱われ、ファイルの削除やメモリの確保にはハンドルを取得する必要があります。 ツールやアプリケーションの開発者にとっては、日常的にハンドルを操作する機会がありますが、通常の利用者にとってはあまり意識するものではありません。 本記事では、Windows OSでのハンドルの役割と、OS内部でどの様に管理されているのかについて解説します。 記事の構成は以下の通りです。

  • ハンドルの役割とアクセス権限
  • ハンドルへの情報の問い合わせと操作
  • カーネル空間でのハンドル管理
  • 応用例1 - オブジェクトをロックしているプロセスの特定
  • 応用例2 - カーネルモードルートキット

本記事は以下の読者層を想定しています。

ハンドルの役割とアクセス権限

Windowsではすべてのリソースがオブジェクトとして扱われ、ハンドルの取得を通じてオブジェクトに対する情報の問い合わせや操作が可能となります。 ハンドルを通じて許可される操作を決定するために、ハンドルには権限情報が設定されます。 アクセス権限は符号なし32ビット整数値で表現されるACCESS_MASKというフラグ値で決定されます。 ACCESS_MASKの構造は以下の通りです。

上位16ビットはオブジェクトの種類に依存しない一般的な定義を設定するフィールドです。 GENERICで始まる名前の権限(GENERIC_READGENERIC_WRITEGENERIC_EXECUTEGENERIC_ALL)は 汎用アクセス権限(Generic Access Right) と呼ばれており、ハンドルを取得する際にのみ用いるものです。 汎用アクセス権限を表すアクセスマスクを指定してオブジェクトのハンドルを要求すると、オブジェクト固有の権限に変換されてハンドルのACCESS_MASKが更新されます。 また、MAXIMUM_ALLOWEDもハンドルを取得する際にのみ用いるもので、呼び出し元が要求できる最大限のアクセス権限を求めるものです。 ただし、MAXIMUM_ALLOWEDを指定してハンドルを要求する際の挙動は状況によって異なるため、MAXIMUM_ALLOWEDは極力用いずに、目的の動作に必要最小限の権限を把握して適切なACCESS_MASKを指定してハンドルを取得するのが推奨です。 例えばOpenProcess APIMAXIMUM_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で始まり、その後にオブジェクト名が続きます(NtQueryInformationProcessNtQueryEventなど)。 ユーザ空間からは、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 APISetProcessInformation APIです。 オブジェクトの情報の取得や操作をするWindows APIは何れも内部では対応するシステムコールを呼び出していますが、システムコールで可能なすべての情報の取得と操作が可能というわけではなく、機能面では制限があります。 また、システムコールにしてもすべての情報がMicrosoft公式文書として使用方法が公開されているわけではなく、開発者やセキュリティ技術者などがリバースエンジニアリングした情報を基に利用されているものが多いです。

システム上でプロセスが確保しているハンドル情報は、システムコールNtQuerySystemInformationを用いて取得できます。 特定のプロセスが取得しているハンドルの一覧情報を取得するには、システムコールNtQueryInformationProcessを用いますが、対象プロセスからPROCESS_QUERY_LIMITED_INFORMATION権限またはPROCESS_QUERY_INFORMATION権限のハンドルが入手出来ている必要があります。

他のプロセスが取得しているハンドルの情報を自分のプロセスで解析したり使いたい場合は、システムコールNtDuplicateObjectまたはDuplicateHandle APIで複製してから、専用のシステムコールWindows APIで情報の取得や操作をします。 ただし、Tokenオブジェクトの場合は用途によってはシステムコールNtDuplicateTokenWindows APIDuplicateToken APIまたはDuplicateTokenExを使います。 トークンについては本記事の範疇ではないので詳しくは解説しませんが、NtDuplicateObject(またはDuplicateHandle)が同じカーネルオブジェクトへの参照を増やしているだけなのに対して、NtDuplicateToken(またはDuplicateTokenDuplicateTokenEx)は同じ情報を持つTokenオブジェクトの複製をカーネル空間に作成するという違いがあります。 ハンドルが存在せず参照されないカーネルオブジェクトは破棄されてしまうので、NtDuplicateObject(またはDuplicateHandle)は意図的にカーネルオブジェクトへの参照を増やして、オブジェクトを残存させるという目的でも用いられます(例えば、オブジェクトの操作元のプロセスを切り替えたい場合など)。

カーネル空間でのハンドル管理

ユーザ空間では、ハンドルはプロセス毎に作成され用いられます。 よってカーネル空間では、プロセスの情報を定義する_EPROCESS構造体の情報から、プロセスが確保しているハンドル情報が参照できるように実装されています。 _EPROCESS構造体の定義は、Vergilius ProjectWinDbgデバッグシンボルなどから確認できます。 以下は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 APICloseHandle APIPowerShellから実行できる様にするためのものです。

$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共用体のアドレスをpEntryTableCodeフィールドの値をnTableCode、ハンドルの値をnHandleとします。 ただし、ハンドルの値は4の倍数として扱うので、元のハンドルの値に0xFFFFFFFFFFFFFFFCをAND演算したものをnHandleとします。

  1. 0の場合

    この場合は、TableCodeフィールドの値がそのまま_HANDLE_TABLE_ENTRY共用体の一覧情報が格納されているアドレスとして適用できます。

     pEntryBase = nTableCode
    

    調べたいハンドルに対応する_HANDLE_TABLE_ENTRY共用体のオフセット値の算出には、ハンドルの値をそのまま用います。 ハンドルの値を1/4(2ビット右シフト)して16倍(4ビット左シフト)すればオフセット位置が計算できます。

     pEntry = pEntryBase + (nHandle << 2)
    
  2. 1の場合

    この場合は、以下の式に基づいてアドレス(pBaseAddressとします)を算出します。

     pBaseAddress = nTableCode + ((nHandle >> 10) << 3) - 1
    

    こうして導出したpBaseAddressに格納されているアドレス値が、_HANDLE_TABLE_ENTRY共用体が格納されているメモリ領域の基準アドレスpEntryBaseです。 目的のハンドルの_HANDLE_TABLE_ENTRY共用体については、ハンドルの値を0x3FFでマスクしてから、オフセット位置を算出する必要があります。

     pEntry = pEntryBase + ((nHandle & 0x3FF) << 2)
    
  3. 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_READexplorer.exeプロセスを要求してハンドルの取得に成功した場合はアクセスマスクは0x00020410PROCESS_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のデータ整理のためにファイルやフォルダの位置をさっさと移動したいという場合もあるでしょう。

システムプログラミング的には、NtQuerySystemInformationNtQueryObjectなどをはじめとするいくつかのシステムコールを組み合わせれば、削除したいファイルやプロセスを開いているプロセスが特定するツールが開発できます。 既存のツールでも、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 18888FileLocker.exeというプログラムのプロセスが、C:\Users\admin\Desktop\Temp\test.txtへのハンドルを獲得しているのが原因であると特定できます。

ハンドルを取得しているプロセスが一般権限であれば、一般権限でhandle.exeを実行しても問題ありませんが、プロセスの動作権限が分からない場合がほとんどなので管理者権限で実行すれば確実です。 また、PowerShellであれば管理者権限での起動時に自動的にSeDebugPrivilegeが有効化されるので、ほぼすべてのプロセスのハンドル情報が取得できます。

応用例2 - カーネルモードルートキット

Windows OSでは、Microsoftから信頼されていない悪意のあるカーネルドライバ(ルートキット)からの感染を防止するために、DSE(Driver Signature Enforcement) という仕組みを実装しています。 しかし、Microsoftから信頼されているドライバやWindows OS自体にメモリ操作につながる脆弱性が存在する場合、DSEによって防がれるべきであるはずの署名無しのカーネルドライバがインストールされてしまう可能性があるため、ルートキットの脅威は完全に消えたわけでありません。 よって、ルートキットでどのように悪用される可能性があるのかについては一考の余地があるでしょう。 知っていれば、類似した手法を悪用するマルウェアに遭遇した際に役立つ可能性があります。

本記事で取り上げている知識を応用すると、カーネルメモリの操作により、低い権限のハンドルを昇格できます。 カーネルデバッガを用いてその挙動を再現してみましょう。 まずは先述の例と同様に、PowerShellexplorer.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でハンドル情報を確認すると、先ほどの例と同様にアクセスマスクは0x21410PROCESS_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に昇格できます(この例ではNoRightsUpgradSpare1もすべて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へのハンドルの権限が00021410PROCESS_VM_READ | PROCESS_QUERY_INFORMATION | PROCESS_QUERY_LIMITED_INFORMATION | READ_CONTROL)から001fffffPROCESS_ALL_ACCESS)に更新されているのが確認できます。

DSEを回避して、この実装したルートキットとして作用するカーネルドライバのインストールに成功すれば、こうして任意のハンドルを全アクセス権限を持つハンドルに昇格させられます。 ただし前提条件として、この機能を持つルートキットの悪用には何かしらの権限で対象オブジェクトのハンドルが得られている必要があります。 ほとんどの場合は低権限のプロセスから特権プロセスのハンドルは獲得出来ないので、プロセスに対してこの手法を用いるのはあまり効果的ではありません。

この手法が効果を発揮するオブジェクトは、ファイルやレジストリです。 ほとんどのファイルやレジストリは、整合性レベルがLowであっても、低権限のプロセスからでも読み取りはできるので、少なくとも読み取り用のハンドルを獲得できます。 よって、システムにとって重要なファイルやレジストリから読み取り用のハンドル獲得した後で、この機能を持つルートキットでハンドルの権限を昇格すれば上書きや削除が可能となり、SYSTEM権限の奪取などが可能になります。

まとめ

本記事では、Windows OSのオブジェクトからの情報の取得や操作に必要なハンドルについて、カーネル空間でのデータ構造を中心に解説しました。 解説した知識の活用方法としては、日頃のWindowsの利用に活用できる例と、マルウェア解析に役立つだろう例を紹介しました。 他にも、マルウェア対策製品やメモリフォレンジックツールなどのセキュリティ製品の開発に、本記事で解説した知識が役に立つと考えられます。