All Articles

Win32 API でシステム内のプロセス情報を列挙してみるやつ

ちょうど 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

もくじ

まえがき

本記事の内容はすべて一般に公開されている情報や、出版された書籍、または個人の検証環境で動作確認を実施した結果のみを元に作成しています。

関連記事はこちら。

今回使用する API

CreateToolhelp32Snapshot

CreateToolhelp32Snapshot関数は、指定したプロセスのヒープ、モジュール、スレッドに関するスナップショットを取得できる関数です。

この関数はth32ProcessIDで指定した PID を持つプロセスの情報を取得しますが、dFlagsTH32CS_SNAPHEAPLISTTH32CS_SNAPMODULETH32CS_SNAPMODULE32TH32CS_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の呼び出し後に引数として与えたポインタのアドレスに格納されていました。

image-20230503221321369

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 でも有効に動作しました。

image-20230503222907883

まとめ

Windows でプロセス情報取得する時は一旦スナップショットオブジェクトを取得してからプログラムに渡す必要があるようですね。

タスクマネージャとかも同じ実装なのかな?

今度デバッガかけて調べてみたいと思います。