ちょうど Malware のコードインジェクション周りの挙動を勉強していたところ、OpenProcess API で参照する PID を Malware はどうやって特定しているんだろうかという疑問に至りました。
MalAPI.ioの Injection のセクションにも以下の 3 つの API が記載されていますが、一つの実装例としてCreateToolhelp32Snapshot
を使用する方法があるようです。
- CreateToolhelp32Snapshot
- Process32First
- Process32Next
そこで、今回は Win32 API のCreateToolhelp32Snapshot
を使用してシステム内のプロセス情報を列挙する方法をまとめてみました。
参考:Taking a Snapshot and Viewing Processes - Win32 apps | Microsoft Learn
参考:Snapshots of the System - Win32 apps | Microsoft Learn
もくじ
まえがき
本記事の内容はすべて一般に公開されている情報や、出版された書籍、または個人の検証環境で動作確認を実施した結果のみを元に作成しています。
関連記事はこちら。
- WinDbg でダンプ解析、ライブデバッグを行う時のチートシート
- WinDbg で Windows のプロセス情報を読むためのメモ書き
- GFlags でグローバルフラグを設定して詳細デバッグを行うためのナレッジ集
今回使用する API
CreateToolhelp32Snapshot
CreateToolhelp32Snapshot
関数は、指定したプロセスのヒープ、モジュール、スレッドに関するスナップショットを取得できる関数です。
この関数はth32ProcessID
で指定した PID を持つプロセスの情報を取得しますが、dFlags
にTH32CS_SNAPHEAPLIST
、TH32CS_SNAPMODULE
、TH32CS_SNAPMODULE32
、TH32CS_SNAPALL
のいずれかが与えられた場合には無視され、すべてのプロセスの情報を返すようになります。
HANDLE CreateToolhelp32Snapshot(
[in] DWORD dwFlags,
[in] DWORD th32ProcessID
);
この関数の実行に成功すると戻り値として取得したスナップショットへのハンドルが返されます。
後述するProcess32First
などのヘルプ関数を使用することでスナップショット内の情報を参照できます。
参考:CreateToolhelp32Snapshot function (tlhelp32.h) - Win32 apps | Microsoft Learn
実際のところこの関数がどのような動作をしているのか、WinDbg でCreateToolhelp32Snapshot
の呼び出す関するを列挙することで調査をしてみました。
KERNEL32!CreateToolhelp32Snapshot
===>
call to KERNELBASE!GetCurrentProcessId (00007fff`3ae04080)
call to KERNEL32!ThpCreateRawSnap (00007fff`3d3fe480)
call to KERNEL32!BaseSetLastNTError (00007fff`3d3f30e0)
call to KERNEL32!BaseSetLastNTError (00007fff`3d3f30e0)
call to KERNEL32!ULongMult (00007fff`3d3ffd84)
call to KERNEL32!BaseSetLastNTError (00007fff`3d3f30e0)
call to KERNEL32!BaseSetLastNTError (00007fff`3d3f30e0)
call to KERNEL32!BaseSetLastNTError (00007fff`3d3f30e0)
call to KERNEL32!ULongMult (00007fff`3d3ffd84)
call to KERNEL32!BaseSetLastNTError (00007fff`3d3f30e0)
call to KERNEL32!BaseSetLastNTError (00007fff`3d3f30e0)
call to KERNEL32!ThpAllocateSnapshotSection (00007fff`3d3fd8c4)
call to KERNEL32!BaseSetLastNTError (00007fff`3d3f30e0)
call to KERNEL32!ThpProcessToSnap (00007fff`3d3fb274)
call to KERNEL32!BaseSetLastNTError (00007fff`3d3f30e0)
call to ntdll!NtClose (00007fff`3d66d230)
call to ntdll!NtUnmapViewOfSection (00007fff`3d66d590)
call to ntdll!NtFreeVirtualMemory (00007fff`3d66d410)
call to ntdll!RtlDestroyQueryDebugBuffer (00007fff`3d6a76d0)
call to ntdll!RtlDestroyQueryDebugBuffer (00007fff`3d6a76d0)
これだけだといまいちよくわからないですが、ThpCreateRawSnap
関数、やThpAllocateSnapshotSection
関数がスナップショットを取得する処理に大きく関連していそうです。
ThpCreateRawSnap
関数は、呼び出している関数の名前を見る限りシステム情報とプロセスの情報を取得しているようです。
KERNEL32!ThpCreateRawSnap
===>
call to ntdll!NtAllocateVirtualMemory (00007fff`3d66d350)
call to ntdll!NtQuerySystemInformation (00007fff`3d66d710)
call to ntdll!RtlDestroyQueryDebugBuffer (00007fff`3d6a76d0)
call to ntdll!RtlCreateQueryDebugBuffer (00007fff`3d6a7420)
call to ntdll!RtlQueryProcessDebugInformation (00007fff`3d6a78a0)
call to ntdll!NtFreeVirtualMemory (00007fff`3d66d410)
call to ntdll!RtlCreateQueryDebugBuffer (00007fff`3d6a7420)
call to ntdll!RtlQueryProcessDebugInformation (00007fff`3d6a78a0)
call to ntdll!NtFreeVirtualMemory (00007fff`3d66d410)
call to ntdll!RtlDestroyQueryDebugBuffer (00007fff`3d6a76d0)
参考:NtQuerySystemInformation function (winternl.h) - Win32 apps | Microsoft Learn
参考:NtQueryInformationProcess function (winternl.h) - Win32 apps | Microsoft Learn
また、ThpAllocateSnapshotSection
関数は、以下の関数を呼び出していました。
ここで実際にハンドルが格納する領域を確保しているのかな?
KERNEL32!ThpAllocateSnapshotSection
===>
call to KERNELBASE!BaseFormatObjectAttributes (00007fff`3addf5f0)
call to ntdll!NtCreateSection (00007fff`3d66d990)
call to ntdll!NtMapViewOfSection (00007fff`3d66d550)
参考:NtCreateSection 関数 (ntifs.h) - Windows drivers | Microsoft Learn
参考:NtCreateSection + NtMapViewOfSection Code Injection - Red Team Notes
参考:MalAPI.io NtMapViewOfSection
Process32First
スナップショット内で最初のプロセスに関する情報を取得する関数です。
関数の実行に成功すると True、失敗すると False を返します。(プロセスが存在しない場合などはエラー値を返します。)
取得したプロセスの情報は、第 2 引数に与えたPROCESSENTRY32 (tlhelp32.h)構造体のポインタアドレスの指す領域に格納されます。
BOOL Process32First(
[in] HANDLE hSnapshot,
[in, out] LPPROCESSENTRY32 lppe
);
PROCESSENTRY32 (tlhelp32.h)構造体は以下の情報を持ちます。
typedef struct tagPROCESSENTRY32 {
DWORD dwSize;
DWORD cntUsage;
DWORD th32ProcessID;
ULONG_PTR th32DefaultHeapID;
DWORD th32ModuleID;
DWORD cntThreads;
DWORD th32ParentProcessID;
LONG pcPriClassBase;
DWORD dwFlags;
CHAR szExeFile[MAX_PATH];
} PROCESSENTRY32;
参考:Process32First function (tlhelp32.h) - Win32 apps | Microsoft Learn
呼び出している関数としてはこんな感じでした。
KERNEL32!Process32FirstW
===>
call to ntdll!NtMapViewOfSection (00007fff`3d66d550)
call to ntdll!NtUnmapViewOfSection (00007fff`3d66d590)
call to KERNEL32!memset (00007fff`3d408147)
call to ntdll!RtlSetLastWin32Error (00007fff`3d6207c0)
call to KERNEL32!BaseSetLastNTError (00007fff`3d3f30e0)
デバッガで見てみると以下のようにPROCESSENTRY32->szExeFile
の情報と見られるデータがntdll!NtMapViewOfSection
の呼び出し後に引数として与えたポインタのアドレスに格納されていました。
Process32Next
システム スナップショットに記録された次のプロセスに関する情報を取得します。
この関数では、Process32FirstW
関数に与えたものと同じスナップショットハンドルを与えることで、次のプロセスのPROCESSENTRY32
を取得できます。
BOOL Process32Next(
[in] HANDLE hSnapshot,
[out] LPPROCESSENTRY32 lppe
);
参考:PROCESSENTRY32 (tlhelp32.h) - Win32 apps | Microsoft Learn
取得したスナップショットのハンドルには現在までに参照しているプロセス情報を特定するためのバッファがあるようで、Process32First
関数を実行すると 1 つ目を指し、Process32Next
関数を実行すると 1 つずつ順番に次のプロセスを指すようになります。
そのため、Process32Next
関数を実行しているループの途中でProcess32First
関数を実行すると、参照先のバッファが 1 つ目に戻るため、処理をループし続けることになります。
サンプルプログラム
実際にこれらの API を使用してプロセス情報を列挙するプログラムを作成してみました。
このプログラムでは、CreateToolhelp32Snapshot
関数の引数にTH32CS_SNAPPROCESS
を指定することですべてのプロセスのスナップショットを取得し、ヘルパー関数をループさせて先頭から順番に各プロセスのPROCESSENTRY32
構造体からプロセス名と PID、そして実行中のスレッド数の情報を出力しています。
#include <windows.h>
#include <tlhelp32.h>
#include <tchar.h>
#pragma comment(lib, "advapi32.lib")
int main() {
//HANDLE CreateToolhelp32Snapshot(
// [in] DWORD dwFlags,
// [in] DWORD th32ProcessID
//);
HANDLE hToolhelp32Snapshot = NULL;
hToolhelp32Snapshot = CreateToolhelp32Snapshot(
TH32CS_SNAPPROCESS,
NULL
);
if (hToolhelp32Snapshot == INVALID_HANDLE_VALUE) {
wprintf(L"ERROR: Could not get a Toolhelp32Snapshot\n");
wprintf(L"Faild with %u.\n", GetLastError());
return 1;
}
//typedef struct tagPROCESSENTRY32
//{
// DWORD dwSize;
// DWORD cntUsage;
// DWORD th32ProcessID;
// ULONG_PTR th32DefaultHeapID;
// DWORD th32ModuleID;
// DWORD cntThreads;
// DWORD th32ParentProcessID;
// LONG pcPriClassBase;
// DWORD dwFlags;
// CHAR szExeFile[MAX_PATH];
//} PROCESSENTRY32;
/*BOOL Process32First(
[in] HANDLE hSnapshot,
[in, out] LPPROCESSENTRY32 lppe
);*/
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hToolhelp32Snapshot, &pe32)) {
do {
wprintf(L"Process name is : %s\n", pe32.szExeFile);
wprintf(L"=====> PID : %d\n", pe32.th32ProcessID);
wprintf(L"=====> Threads count : %d\n", pe32.cntThreads);
} while (Process32Next(hToolhelp32Snapshot, &pe32));
wprintf(L"Got all process entries.\n");
}
else {
wprintf(L"ERROR: Could not get the first PROCESSENTRY32.\n");
wprintf(L"Faild with %u.\n", GetLastError());
CloseHandle(hToolhelp32Snapshot);
return 1;
}
return 0;
}
実行結果はこんな感じでした。
管理者権限で起動したコマンドプロンプトから実行していますが、Medium Integrity でも有効に動作しました。
まとめ
Windows でプロセス情報取得する時は一旦スナップショットオブジェクトを取得してからプログラムに渡す必要があるようですね。
タスクマネージャとかも同じ実装なのかな?
今度デバッガかけて調べてみたいと思います。