tlhelp32.h によるプロセス名の列挙

前々回の復元コードのの中でひとつ気になった個所。

#include <windows.h>
#include <tchar.h>
#include <tlhelp32.h>

HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot != INVALID_HANDLE_VALUE) {
	PROCESSENTRY32 processEntry;
	BOOL missingPatchMixDSP = TRUE;
	ZeroMemory(&processEntry, sizeof(processEntry));
	processEntry.dwSize = sizeof(processEntry);
	if (Process32First(snapshot, &processEntry) == TRUE) {
		do {
			if (!_tcscmp(processEntry.szExeFile, procname)) {
				missingPatchMixDSP = FALSE;
				break;
			}
		} while (Process32Next(snapshot, &processEntry) == TRUE);
	}
	CloseHandle(snapshot);
...
}

通常の Win32 API の命名規約であれば

  • PROCESSENTRY32, Process32First, Process32Next はマクロ
  • コンパイルオプションで Character Set を Multibyte にした場合は PROCESSENTRY32A Process32FirstA Process32NextA のエイリアス
  • 〃 Unicode 〃 PROCESSENTRY32W Process32FirstW Process32NextW 〃

になってるはずである、ところが tlhelp32.h の Multibyte API の 末尾には A がつかずそのまんま PROCESSENTRY32, Process32First, Process32Next として実装されてんのよね。

/***** Process walking *************************************************/

typedef struct tagPROCESSENTRY32W
{
...
    WCHAR   szExeFile[MAX_PATH];    // Path
} PROCESSENTRY32W;
typedef PROCESSENTRY32W *  PPROCESSENTRY32W;
typedef PROCESSENTRY32W *  LPPROCESSENTRY32W;

BOOL
WINAPI
Process32FirstW(
    HANDLE hSnapshot,
    LPPROCESSENTRY32W lppe
    );

BOOL
WINAPI
Process32NextW(
    HANDLE hSnapshot,
    LPPROCESSENTRY32W lppe
    );

typedef struct tagPROCESSENTRY32
{
...
    CHAR    szExeFile[MAX_PATH];    // Path
} PROCESSENTRY32;
typedef PROCESSENTRY32 *  PPROCESSENTRY32;
typedef PROCESSENTRY32 *  LPPROCESSENTRY32;

BOOL
WINAPI
Process32First(
    HANDLE hSnapshot,
    LPPROCESSENTRY32 lppe
    );

BOOL
WINAPI
Process32Next(
    HANDLE hSnapshot,
    LPPROCESSENTRY32 lppe
    );

#ifdef UNICODE
#define Process32First Process32FirstW
#define Process32Next Process32NextW
#define PROCESSENTRY32 PROCESSENTRY32W
#define PPROCESSENTRY32 PPROCESSENTRY32W
#define LPPROCESSENTRY32 LPPROCESSENTRY32W
#endif  // !UNICODE

なんで命名規約に沿ってないのかというとおそらくは文字列扱う API には A or W をつけるルールが徹底される前に実装されたからと思われる。 名前に 32 が入ってるから 32bit な 95/NT 4.0 以降に実装されたっぽいけどその時期でもまだそんなんだったっけ?

ただまぁこれでも実用上はなんら問題ないはずである普通のコード書いてれば、ところがちょっと邪悪なプログラミングをしようと考えてしまったぼくは罠に嵌ってしまったのだった。

#include <windows.h>
#include <tchar.h>
#include <tlhelp32.h>

#define CONCAT(a,b)	a##b
#if defined(__cplusplus)
#define BEGIN_DECLS	extern "C" {
#define END_DECLS	}
#else
#define BEGIN_DECLS
#define END_DECLS
#endif

#define RUNNING_PROCESS_PROTO_DECL(suffix) \
typedef PROC(*CONCAT(ConsumeRunningProcess,suffix))(PROC, PVOID, CONCAT(PPROCESSENTRY32,suffix), LSTATUS*); \
BEGIN_DECLS \
LSTATUS CONCAT(RunningProcessProvider,suffix)(PVOID, CONCAT(ConsumeRunningProcess,suffix)); \
END_DECLS

RUNNING_PROCESS_PROTO_DECL(A, PCSTR, PSTR)
RUNNING_PROCESS_PROTO_DECL(W, PCWSTR, PWSTR)

#if defined(UNICODE)
#define ConsumeRunningProcess	ConsumeRunningProcessW
#define RunningProcessProvider	RunningProcessProviderW
#else
#define ConsumeRunningProcess	ConsumeRunningProcessA
#define RunningProcessProvider	RunningProcessProviderA
#endif

#define RUNNING_PROCESS_IMPL_DEF(suffix) \
LSTATUS \
CONCAT(RunningProcessProvider,suffix)(PVOID param, CONCAT(ConsumeRunningProcess,suffix) consume) \
{ \
	LSTATUS ret = ERROR_SUCCESS; \
	HANDLE snapshot; \
	CONCAT(PROCESSENTRY32,suffix) processEntry; \
 \
	/* param may be NULL */ \
	/* consume may be NULL*/ \
 \
	if (consume != NULL) { \
		snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); \
		if (snapshot == INVALID_HANDLE_VALUE) { \
			ret = GetLastError(); \
		} else { \
			if (CONCAT(Process32First,suffix)(snapshot, &processEntry) == TRUE) { \
				do { \
					consume = (CONCAT(ConsumeRunningProcess,suffix))(*consume)((PROC)consume, param, &processEntry, &ret); \
					if (consume == NULL) \
						break; \
				} while (CONCAT(Process32Next,suffix)(snapshot, &processEntry) == TRUE); \
			} \
			CloseHandle(snapshot); \
		} \
	} \
	return ERROR_SUCCESS; \
}

RUNNING_PROCESS_IMPL_DEF(A)
RUNNING_PROCESS_IMPL_DEF(W)

これは tlhelp32.h の設計が古くて使いにくいので、ナウいヤングにバカウケの古臭い関数型プログラミング風に Provider/Consumer デザインとしてラップしたもの。

そんでさらに Multibyte と Unicode で同じコードを二度書きたくないからプリプロセッサ使ってジェネリック!テンプレートメタプログラミング!みたいな邪悪マクロによって実装している。

本当はこういうのインテリセンスや静的解析ツールが壊滅するので、C++ のテンプレート使って書くか C 縛りするにしても何らかのテンプレートエンジン使ってコード生成すべきなのは重々承知であるけど俺こういうの大好きなのよ。 つか C++ テンプレートだってデバッガーのデバッグポイント壊滅するやろがい、いやちょっとこのへんうろ覚え、なんせ俺エディタは vi で デバッガーは printf おじさんだし(最低野郎)。

C++ 好きな人が俺の書く邪悪マクロを読んだら殺意沸くと思うが Unix C がちょっと書けるだけのおじさんなので勘弁してほしい、つうか最初期の C++ だってスッポスッポ先生はマクロとダンスってたはずである。

つーか tchar.h そのものがプリプロセッサ使った邪悪なテンプレートメタプログラミングであって、インテリセンスでマクロをインライン展開しないと引数も見れないんだから、俺だけやっちゃダメな道理がねえよなぁ!? やはり Win32 API 自体滅ぼさねばならぬ、その結果が C++/WinRT における winrt::hstring つまり Unicode への一本化ですって?あっ C から使えないので結構です。

まぁいいや、このコードはこんな感じで使う。

static PROC
PrintRunningProcess(PROC self, PVOID param, PPROCESSENTRY32 processEntry, LSTATUS* result)
{
	_tprintf(_T("%s"), processEntry->szExeFile);
	return self;
}

int
_tmain(int argc, _TCHAR *argv[])
{
	RunningProcessProvider(NULL, &PrintRunningProcess);
}

C++ ならラムダ式も使えるゾ。

int
_tmain(int argc, _TCHAR *argv[])
{
	RunningProcessProvider(NULL, [](PROC self, PVOID param, PPROCESSENTRY32 processEntry, LSTATUS* result) -> PROC {
		_tprintf(_T("%s"), processEntry->szExeFile);
		return self;
	});
}

ただしキャプチャとの併用は不可、これはキャプチャ使うとラムダ式は無名関数のポインタではなくキャプチャした変数をメンバに持つ無名クラスのメソッドポインタになるので互換性が無いからしゃーない。

両者を別にするよう注意深く設計されてればキャプチャもラムダ式自身を渡す self 同様に C API に引数として渡せて相互運用性も高まったのにね。 どうせ C++ は ABI なんて無いも同然だし(偏見)今からでも仕様変えてくれねえかな。

ただ上記のコードのままだと Multibyte 系の構造体や関数に末尾 A 存在しないのでビルド通らんのだ、まさか命名規則に沿ってない API があるとは思わず 15 分ほど悩んでしまった。

PROCESS_PROTO_DECL()
PROCESS_PROTO_DECL(W)

PROCESS_IMPL_DEF()
PROCESS_IMPL_DEF(W)

と空にするのも癪だしな、かといって以下のように再定義してあたかも A があるようにすると副作用出てくるし。

#if UNICODE
#undef Process32First
#undef Process32Next
#undef PROCESSENTRY32
#undef PPROCESSENTRY32
#undef LPPROCESSENTRY32
#endif
#define Process32FirstA Process32First
#define Process32NextA Process32Next
#define PROCESSENTRY32A PROCESSENTRY32
#define PPROCESSENTRY32A PPROCESSENTRY32
#define LPPROCESSENTRY32A LPPROCESSENTRY32

N でこんなんあったらとっくに A つきに修正コミットいれてたであろう(過去の話です)、バイナリ互換性については weak alias 使えばいいだけだし。

ちなみにこれら tlhelp32.h の関数は今となっては非推奨だから、正気なプログラマであれば使わないはずなので本当にどうでもいい話である。

非推奨の理由は命名規則に沿ってないからではなく CreateToolhelp32Snapshot でスナップショットをとるコストが高いから。 より軽量な psapi.h の EnumProcess を使ってプロセス ID の一覧を取得することが推奨されるなどと MSDN は供述しており…

つーわけで当該コードは書き直しますかね…

psapi.h によるプロセス名の列挙

ただし EnumProcess が返すのは プロセス ID だけなのでプロセス名まで調べたい今回のケースではさらに以下の処理も必要と MSDN の この記事 に書かれている。

  • OpenProcess でプロセスハンドルを取得
  • EnumProcessModules あるいは EnumProcessModulesEx でモジュールハンドルを取得
  • GetModuleBaseName でモジュール名を取得

うーんめんどくせえ、そんで EnumProcess と EnumProcessModules スナップショット取らないから全てのプロセス ID を格納したけりゃできるだけ大きな配列をご用意くださいってのがもうデザインとして終わっとる。 それってスナップショットのコストをこっちに押し付けただけじゃねーかこのカスゥ!

それにそもそも tlhelp32.h の置き換えなら .exe 名だけでよくて .dll 名までは不要だし EnumProcessModules 呼ぶ必要ねえなこれ。 OpenProcess して QueryFullProcessImageName すりゃいいだけじゃねえのこれ、余計な記述まで入れるからそこらじゅうのブログで自分で考える頭を持たないコピペマンが無駄な処理までそのまんまコピーしてて笑う。

そんで 32bit プロセスから 64bit プロセスを EnumProcessModules しようとするとビット数の壁で ERROR_PARTIAL_COPY 返ってくる問題も踏むじゃんこれ。 その結果 32bit プロセスからは 64bit プロセスのリストは取得できないなんていう間違ったいかがでしたか記事を鵜呑みにしたおバカ AI が誇らしげに嘘を真実かのように答えるのである。 32bit プロセスからは 64bit プロセスがロードした DLL までは取得できないの間違いやでそれ、名前だけなら問題ないです。

ということで tlhelp32.h でなく psapi.h 使って上のコードを書き直した結果がこちら、クソみてえに行数増えた。

#include <windows.h>
#include <psapi.h>
#include <tchar.h>

#define CONCAT(a,b)	a##b
#if defined(__cplusplus)
#define BEGIN_DECLS	extern "C" {
#define END_DECLS	}
#else
#define BEGIN_DECLS
#define END_DECLS
#endif

#define PAGESIZE (4096)

#define RUNNING_PROCESS_PROTO_DECL(suffix, cstrtype) \
typedef PROC(*CONCAT(RunningProcessIdConsumer,suffix))(PROC, PVOID, DWORD, LSTATUS*); \
typedef PROC(*CONCAT(RunningProcessHandleConsumer,suffix))(PROC, PVOID, DWORD, HANDLE, LSTATUS*); \
typedef PROC(*CONCAT(RunningProcessImageFileNameConsumer,suffix))(PROC, PVOID, DWORD, HANDLE, cstrtype, LSTATUS*); \
BEGIN_DECLS \
LSTATUS CONCAT(RunningProcessIdProvider,suffix)(PVOID, CONCAT(RunningProcessIdConsumer,suffix)); \
LSTATUS CONCAT(RunningProcessHandleProvider,suffix)(PVOID, CONCAT(RunningProcessHandleConsumer,suffix)); \
LSTATUS CONCAT(RunningProcessImageFileNameProvider,suffix)(PVOID, CONCAT(RunningProcessImageFileNameConsumer,suffix)); \
END_DECLS

#if defined(UNICODE)
#define RunningProcessIdConsumer RunningProcessIdConsumerW
#define RunningProcessIdProvider RunningProcessIdProviderW
#define RunningProcessHandleConsumer RunningProcessHandleConsumerW
#define RunningProcessHandleProvider RunningProcessHandleProviderW
#define RunningProcessImageFileNameConsumer RunningProcessImageFileNameConsumerW
#define RunningProcessImageFileNameProvider RunningProcessImageFileNameProviderW
#else
#define RunningProcessIdConsumer RunningProcessIdConsumerA
#define RunningProcessIdProvider RunningProcessIdProviderA
#define RunningProcessHandleConsumer RunningProcessHandleConsumerA
#define RunningProcessHandleProvider RunningProcessHandleProviderA
#define RunningProcessImageFileNameConsumer RunningProcessImageFileNameConsumerA
#define RunningProcessImageFileNameProvider RunningProcessImageFileNameProviderA
#endif

RUNNING_PROCESS_PROTO_DECL(A, PCSTR)
RUNNING_PROCESS_PROTO_DECL(W, PCWSTR)

#define RUNNING_PROCESS_IMPL_DEF(suffix, strtype, cstrtype) \
LSTATUS \
CONCAT(RunningProcessIdProvider,suffix)(PVOID param, CONCAT(RunningProcessIdConsumer,suffix) consume) \
{ \
	LSTATUS ret; \
	HANDLE heap; \
	PBYTE processId; \
	SIZE_T processIdAllocated; \
	DWORD processIdSize; \
	PDWORD q, p; \
	LPVOID tmp; \
 \
	heap = GetProcessHeap(); \
	if (heap == NULL) \
		return GetLastError(); \
	processIdAllocated = PAGESIZE; \
	processId = (PBYTE)HeapAlloc(heap, 0, processIdAllocated); \
	if (processId == NULL) \
		return GetLastError(); \
	ret = ERROR_SUCCESS; \
	for (;;) { \
		if (EnumProcesses((DWORD *)processId, (DWORD)processIdAllocated, &processIdSize) == FALSE) { \
			ret = GetLastError(); \
			break; \
		} \
		if ((SIZE_T)processIdSize <= processIdAllocated) { \
			q = (PDWORD)(processId + processIdSize); \
			for (p = (PDWORD)processId; p < q; ++p) { \
				consume = (CONCAT(RunningProcessIdConsumer,suffix))(*consume)((PROC)consume, param, *p, &ret); \
				if (consume == NULL) \
					break; \
			} \
			break; \
		} \
		if (processIdAllocated >= SIZE_MAX / 2) { \
			ret = ERROR_ARITHMETIC_OVERFLOW; \
			break; \
		} \
		processIdAllocated *= 2; \
		tmp = HeapReAlloc(heap, 0, processId, processIdAllocated); \
		if (tmp == NULL) { \
			ret = GetLastError(); \
			break; \
		} \
		processId = (PBYTE)tmp; \
	} \
	HeapFree(heap, 0, processId); \
	return ret; \
} \
 \
struct CONCAT(RunningProcessHandleDelegateCapture,suffix) { \
	PVOID param; \
	CONCAT(RunningProcessHandleConsumer,suffix)* consume; \
}; \
 \
static PROC \
CONCAT(RunningProcessHandleDelegate,suffix)(PROC self, PVOID param, DWORD processId, LSTATUS* result) \
{ \
	struct CONCAT(RunningProcessHandleDelegateCapture,suffix)* capture; \
	HANDLE processHandle; \
	DWORD ret; \
 \
	capture = (struct CONCAT(RunningProcessHandleDelegateCapture,suffix)*)param; \
	processHandle = OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_READ, FALSE, processId); \
	if (processHandle == NULL) { \
		ret = GetLastError(); \
		switch (ret) { \
		case ERROR_ACCESS_DENIED: /* process is Client/Server Runtime Subsystem(CSRSS) */ \
		case ERROR_INVALID_PARAMETER: /* process is SystemIdleProcess(id=0) */ \
			break; \
		default: \
			*result = (LSTATUS)ret; \
			self = NULL; \
		} \
	} else { \
		*capture->consume = (CONCAT(RunningProcessHandleConsumer,suffix))(*(*capture->consume))((PROC)*capture->consume, capture->param, processId, processHandle, result); \
		if (*capture->consume == NULL) \
			self = NULL; \
	} \
	return self; \
} \
 \
LSTATUS \
CONCAT(RunningProcessHandleProvider,suffix)(PVOID param, CONCAT(RunningProcessHandleConsumer,suffix) consume) \
{ \
	struct CONCAT(RunningProcessHandleDelegateCapture,suffix) capture = { \
		param, \
		&consume \
	}; \
	return CONCAT(RunningProcessIdProvider,suffix)(&capture, &CONCAT(RunningProcessHandleDelegate,suffix)); \
} \
 \
struct CONCAT(RunningProcessImageFileNameDelegateCapture,suffix) { \
	PVOID param; \
	CONCAT(RunningProcessImageFileNameConsumer,suffix)* consume; \
}; \
 \
static PROC \
CONCAT(RunningProcessImageFileNameDelegate,suffix)(PROC self, PVOID param, DWORD processId, HANDLE processHandle, LSTATUS* result) \
{ \
	DWORD ret; \
	struct CONCAT(RunningProcessImageFileNameDelegateCapture,suffix)* capture; \
	HANDLE heap; \
	strtype path; \
	SIZE_T pathAllocated; \
	DWORD pathSize; \
	LPVOID tmp; \
 \
	ret = ERROR_SUCCESS; \
	capture = (struct CONCAT(RunningProcessImageFileNameDelegateCapture,suffix)*)param; \
	heap = GetProcessHeap(); \
	if (heap == NULL) { \
		ret = GetLastError(); \
	} else { \
		pathAllocated = PAGESIZE; \
		path = (strtype)HeapAlloc(heap, 0, pathAllocated); \
		if (path == NULL) {\
			ret = GetLastError(); \
		} else { \
			for (;;) { \
				pathSize = (DWORD)(pathAllocated / sizeof(*path)); \
				if (CONCAT(QueryFullProcessImageName,suffix)(processHandle, 0, path, &pathSize) == FALSE) { \
					ret = GetLastError(); \
					if (ret != ERROR_INSUFFICIENT_BUFFER) \
						break; \
					ret = ERROR_SUCCESS; \
				} else { \
					*capture->consume = (CONCAT(RunningProcessImageFileNameConsumer,suffix))(*(*capture->consume))((PROC)*capture->consume, capture->param, processId, processHandle, (cstrtype)path, result); \
					if (*capture->consume == NULL) \
						self = NULL; \
					break; \
				} \
				if (pathAllocated >= SIZE_MAX / 2) { \
					ret = ERROR_ARITHMETIC_OVERFLOW; \
					break; \
				} \
				pathAllocated *= 2; \
				tmp = HeapReAlloc(heap, 0, path, pathAllocated); \
				if (tmp == NULL) { \
					ret = GetLastError(); \
					break; \
				} \
				path = (strtype)tmp; \
			} \
			HeapFree(heap, 0, path); \
		} \
	} \
	if (ret != ERROR_SUCCESS) { \
		*result = ret; \
		self = NULL; \
	} \
	return self; \
} \
 \
LSTATUS \
CONCAT(RunningProcessImageFileNameProvider,suffix)(PVOID param, CONCAT(RunningProcessImageFileNameConsumer,suffix) consume) \
{ \
	struct CONCAT(RunningProcessImageFileNameDelegateCapture,suffix) capture = { \
		param, \
		&consume \
	}; \
	return CONCAT(RunningProcessHandleProvider,suffix)(&capture, &CONCAT(RunningProcessImageFileNameDelegate,suffix)); \
}

RUNNING_PROCESS_IMPL_DEF(A, PSTR, PCSTR)
RUNNING_PROCESS_IMPL_DEF(W, PWSTR, PCWSTR)

static PROC
PrintRunningProcessImageFileName(PROC self, PVOID param, DWORD processId, HANDLE processHandle, PCTSTR processImageFileName, LSTATUS* result)
{
	_tprintf(_T("%s\n"), processImageFileName);
	return self;
}

int _tmain(void)
{
	RunningProcessImageFileNameProvider(NULL, &PrintRunningProcessImageFileName);
}

とりあえず Win32 API プログラミングなのでヒープメモリ操作に HeapAlloc 使ってるけど GetProcessHeap との組み合わせなら malloc 使っていいんじゃねえの(鼻ホジ)。 CRT 使ったら負け!ニンニクマシマシ Win32 API のみで!という謎の風潮なんなんだろうね…

それにしても C++ 使いにナイフでメッタ刺しにされそうなコードである、俺の個人情報はとっくの昔にダークウェブに流出済なので怖いですねブルブル。

そんで今日もまた佐川急便が個人情報漏らしたそうだから、そのうち俺が買ったあれやこれやの情報も見れるようになると思うので見つけたらぜひ活用して頂きたい。 こいつハードオフですら引き取らなそうなジャンクしか購入履歴無え!ってドン引きされそうである、そいや別会社だけどブックオフもお漏らししたな…

根本的に個人情報流出を防ぐにはそもそも個人情報を集めないことが鉄則なのだけど、まあ今の狂ったマーケティング至上主義では無理やろね。 個人情報漏らして何が悪いに法改正されていくだけよ胸糞悪いことに、これを止めたいなら 20 年前に Yahoo! が 500 円配って終わらせたのを許してはならなかったのだ(民事でも 6000 円)。 そもそも政府が率先してパランティアに個人情報売ってるんやでこの馬鹿アメポチ国家は。

まぁ IT 業界も政治家も幼児がそのまま大人になり幼児的万能感振り回して、倫理観皆無の海賊気取りのカスだらけだからさっさと戦争やって人類滅んでどうぞ。

ちなみに Windows 10 1607 からは MAX_PATH の 260 字制限が撤廃されているので(つうかこれまでもシェルが勝手に制限してただけで NTFS と UNC で 32768 字まで使えたし)、QueryFullProcessImageName で取得するパス名のバッファサイズは 固定長でなく動的に確保しないとダメ。

そもそも Unix だってファイルシステム次第では PATH_MAX でなく pathconf(_PC_PATH_MAX) つかわんとならんのである。 やむを得ず MAX_PATH や PATH_MAX のサイズで確保した固定長バッファを使う場合、パスが切り詰められた場合はエラーとせんとセキュリティリスクになるからほんと注意ね。

ディレクトリトラバーサル系の脆弱性で原因がパスの切り詰めってのはよくあるパターン、コピペしかできない認知的降伏したプログラマのせいで俺の個人情報が今日も漏れるのであった。

今回もまた無駄なコードであった

そんでここまで実装してようやく気づいたんだけど、そもそも CtHelper.exe が PatchMix DSP プロセスが実行されてるか調べたければ、ウインドウタイトルで FindWindow すりゃいいだけなんよな。

なんでプロセス一覧から苦労して調べる必要があるねんこのカスボケタニシ!

まぁこれはレジストリから実行ファイル名を取得してるので、ウィンドタイトルはプログラムにハードコードしたくないからと思われる。 ならレジストリにウィンドタイトルいれとけよ、それにそもそも PatchMix DSP 側だって多重起動チェックしてんだろうし問答無用で起動すりゃいいはず。 なんせプロセス間通信する気が無いのは前回のコードで判明してるし。

また馬鹿な設計のピタゴラスイッチに振り回されてしまった…もう終わりだ…