リモートプロセスのコマンドラインパラメータを取得する
自分のプロセスのコマンドラインを取得するのは非常に簡単だ。main関数が存在するならargc, argvで取得できるし、そうでなければ、__argv, __argv, (__wargv/__targv)で取得することもできる。また、APIとしては、GetCommandLineというものもある。好んで使う理由は見あたらないけど。
しかしながら、リモートプロセスのコマンドラインとなると、とたんに敷居が高くなる。プロセスハンドルを持っていても、GetRemoteCommandLineというAPIは見あたらないのでどうしようもない。となると、考えられる方法としては、
- CreateRemoteThreadなどで相手プロセス内でGetCommandLineを呼び出す
- 相手のプロセスのメモリを直接読み込む
という2つのうちのいずれかを選択することになる。一つ目の案は、大げさすぎるのと、相手プロセスに送り込むスタブコードを考えるのが面倒という2つの問題があるので、現実的には、二つ目の奴を選択すべきだろう。ということで、ググって適当なところからパクってきたコードを修正したコードが次の通り。
#include <locale.h> #include <stdio.h> #include <windows.h> #include <winternl.h> #include <tlhelp32.h> #ifndef NT_ERROR #define NT_ERROR(Status) ((((ULONG)(Status)) >> 30) == 3) #endif DWORD GetRemoteCommandLineW(HANDLE hProcess, LPWSTR pszBuffer, UINT bufferLength) { struct RTL_USER_PROCESS_PARAMETERS_I { BYTE Reserved1[16]; PVOID Reserved2[10]; UNICODE_STRING ImagePathName; UNICODE_STRING CommandLine; }; struct PEB_INTERNAL { BYTE Reserved1[2]; BYTE BeingDebugged; BYTE Reserved2[1]; PVOID Reserved3[2]; struct PEB_LDR_DATA* Ldr; RTL_USER_PROCESS_PARAMETERS_I* ProcessParameters; BYTE Reserved4[104]; PVOID Reserved5[52]; struct PS_POST_PROCESS_INIT_ROUTINE* PostProcessInitRoutine; BYTE Reserved6[128]; PVOID Reserved7[1]; ULONG SessionId; }; typedef NTSTATUS (NTAPI* NtQueryInformationProcessPtr)( IN HANDLE ProcessHandle, IN PROCESSINFOCLASS ProcessInformationClass, OUT PVOID ProcessInformation, IN ULONG ProcessInformationLength, OUT PULONG ReturnLength OPTIONAL); typedef ULONG (NTAPI* RtlNtStatusToDosErrorPtr)(NTSTATUS Status); // Locating functions HINSTANCE hNtDll = GetModuleHandleW(L"ntdll.dll"); NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hNtDll, "NtQueryInformationProcess"); RtlNtStatusToDosErrorPtr RtlNtStatusToDosError = (RtlNtStatusToDosErrorPtr)GetProcAddress(hNtDll, "RtlNtStatusToDosError"); if(!NtQueryInformationProcess || !RtlNtStatusToDosError) { printf("Functions cannot be located.\n"); return 0; } // Get PROCESS_BASIC_INFORMATION PROCESS_BASIC_INFORMATION pbi; ULONG len; NTSTATUS status = NtQueryInformationProcess( hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), &len); SetLastError(RtlNtStatusToDosError(status)); if(NT_ERROR(status) || !pbi.PebBaseAddress) { printf("NtQueryInformationProcess(ProcessBasicInformation) failed.\n"); return 0; } // Read PEB memory block SIZE_T bytesRead = 0; PEB_INTERNAL peb; if(!ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), &bytesRead)) { printf("Reading PEB failed.\n"); return 0; } // Obtain size of commandline string RTL_USER_PROCESS_PARAMETERS_I upp; if(!ReadProcessMemory(hProcess, peb.ProcessParameters, &upp, sizeof(upp), &bytesRead)) { printf("Reading USER_PROCESS_PARAMETERS failed.\n"); return 0; } if(!upp.CommandLine.Length) { printf("Command line length is 0.\n"); return 0; } // Check the buffer size DWORD dwNeedLength = (upp.CommandLine.Length+1) / sizeof(wchar_t) +1; if(bufferLength < dwNeedLength) { printf("Not enough buffer.\n"); return dwNeedLength; } // Get the actual command line pszBuffer[dwNeedLength - 1] = L'\0'; if(!ReadProcessMemory(hProcess, upp.CommandLine.Buffer, pszBuffer, upp.CommandLine.Length, &bytesRead)) { printf("Reading command line failed.\n"); return 0; } return bytesRead / sizeof(wchar_t); } int wmain() { setlocale(LC_ALL, ""); HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if(hSnapshot == INVALID_HANDLE_VALUE) return 0; PROCESSENTRY32 pe; pe.dwSize = sizeof(pe); if(!Process32First(hSnapshot, &pe)) { CloseHandle(hSnapshot); return 0; } do { HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pe.th32ProcessID); if(hProcess) { WCHAR buf[MAX_PATH]; buf[0] = 0; if(GetRemoteCommandLineW(hProcess, buf, MAX_PATH)) wprintf(L"%08X: %s\n", pe.th32ProcessID, buf); else wprintf(L"%08X: ????\n", pe.th32ProcessID); CloseHandle(hProcess); } } while(Process32Next(hSnapshot, &pe)); CloseHandle(hSnapshot); }
このコードで現在起動している全プロセスのコマンドラインがとれる。
WOW64上での挙動
実はこのコードは、WOW64上の32bitバイナリでは、WOW64のプロセスのコマンドラインしかとれない。というのも、相手プロセスが64bitだった場合、NtQueryInformationProcess(ProcessBasicInformation)が失敗する。冷静に考えれば分かるけど、PVOIDとかを含んでいる構造体である以上、sizeof(PROCESS_BASIC_INFORMATION)が変わってしまうのだから当たり前だ。逆に言えば、WOW64のプロセスでは正常に動作していることが不思議なぐらいだ。
そう思っていたのだけれども、現実はもうちょっと複雑らしい。実は、NtQueryInformationProcessは失敗していない。PebBaseAddressのゼロチェックをやっているのが裏目に出ているらしい。その証拠に、GetLastErrorは0を返すし、UniqueProcessIdには正しいプロセス番号が入っている。どうやら、NtQueryInformationProcessには、WOW64用の対策がなされているようだ。ただし、64bitのプロセスでは、PEBは、かなり上位のアドレスにあり(今回調べた感じでは、7FFFFFDB000h)、正直なところ、32bitのプロセスではこのアドレスがとれないし、とれたところで、ReadProcessMemoryできない。結局、どうしようもない。