From bed7a543c058b29bae1f24ab49799ff3d7198c73 Mon Sep 17 00:00:00 2001 From: Scott Graham Date: Thu, 5 Mar 2015 22:07:38 -0800 Subject: [PATCH] win: Add implementation of ProcessInfo This is as a precursor to ProcessReader. Some basic functionality is included for now, with more to be added later as necessary. The PEB code is pretty icky -- walking the doubly-linked list in the target's address space is cumbersome. The alternative is to use EnumProcessModules. That would work but: 1) needs different APIs for XP and Vista 64+ 2) retrieves modules in memory-location order, rather than initialization order. I felt retrieving them in initialization order might be useful when detecting third party DLL injections. In the end, we may want to make both orders available. R=mark@chromium.org BUG=crashpad:1 Review URL: https://codereview.chromium.org/977003003 --- util/util.gyp | 34 ++++ util/win/process_info.cc | 287 ++++++++++++++++++++++++++++ util/win/process_info.h | 99 ++++++++++ util/win/process_info_test.cc | 134 +++++++++++++ util/win/process_info_test_child.cc | 45 +++++ 5 files changed, 599 insertions(+) create mode 100644 util/win/process_info.cc create mode 100644 util/win/process_info.h create mode 100644 util/win/process_info_test.cc create mode 100644 util/win/process_info_test_child.cc diff --git a/util/util.gyp b/util/util.gyp index 083a92d..ba72b15 100644 --- a/util/util.gyp +++ b/util/util.gyp @@ -129,6 +129,8 @@ 'synchronization/semaphore_posix.cc', 'synchronization/semaphore_win.cc', 'synchronization/semaphore.h', + 'win/process_info.cc', + 'win/process_info.h', 'win/scoped_handle.cc', 'win/scoped_handle.h', 'win/time.cc', @@ -308,6 +310,7 @@ 'test/multiprocess_exec_test.cc', 'test/multiprocess_posix_test.cc', 'test/scoped_temp_dir_test.cc', + 'win/process_info_test.cc', 'win/time_test.cc', ], 'conditions': [ @@ -318,6 +321,13 @@ ], }, }], + ['OS=="win"', { + 'link_settings': { + 'libraries': [ + '-lrpcrt4.lib', + ], + }, + }], ], }, { @@ -328,4 +338,28 @@ ], }, ], + 'conditions': [ + ['OS=="win"', { + 'targets': [ + { + 'target_name': 'util_test_process_info_test_child', + 'type': 'executable', + 'sources': [ + 'win/process_info_test_child.cc', + ], + # Set an unusually high load address to make sure that the main + # executable still appears as the first element in + # ProcessInfo::Modules(). + 'msvs_settings': { + 'VCLinkerTool': { + 'AdditionalOptions': [ + '/BASE:0x78000000', + '/FIXED', + ], + }, + }, + }, + ] + }], + ], } diff --git a/util/win/process_info.cc b/util/win/process_info.cc new file mode 100644 index 0000000..66157aa --- /dev/null +++ b/util/win/process_info.cc @@ -0,0 +1,287 @@ +// Copyright 2015 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/win/process_info.h" + +#include "base/logging.h" + +namespace crashpad { + +namespace { + +NTSTATUS NtQueryInformationProcess(HANDLE process_handle, + PROCESSINFOCLASS process_information_class, + PVOID process_information, + ULONG process_information_length, + PULONG return_length) { + static decltype(::NtQueryInformationProcess)* nt_query_information_process = + reinterpret_cast(GetProcAddress( + LoadLibrary(L"ntdll.dll"), "NtQueryInformationProcess")); + DCHECK(nt_query_information_process); + return nt_query_information_process(process_handle, + process_information_class, + process_information, + process_information_length, + return_length); +} + +bool IsProcessWow64(HANDLE process_handle) { + static decltype(IsWow64Process)* is_wow64_process = + reinterpret_cast( + GetProcAddress(LoadLibrary(L"kernel32.dll"), "IsWow64Process")); + if (!is_wow64_process) + return false; + BOOL is_wow64; + if (!is_wow64_process(process_handle, &is_wow64)) { + PLOG(ERROR) << "IsWow64Process"; + return false; + } + return is_wow64; +} + +bool ReadUnicodeString(HANDLE process, + const UNICODE_STRING& us, + std::wstring* result) { + if (us.Length == 0) { + result->clear(); + return true; + } + DCHECK_EQ(us.Length % sizeof(wchar_t), 0u); + result->resize(us.Length / sizeof(wchar_t)); + SIZE_T bytes_read; + if (!ReadProcessMemory( + process, us.Buffer, &result->operator[](0), us.Length, &bytes_read)) { + PLOG(ERROR) << "ReadProcessMemory UNICODE_STRING"; + return false; + } + if (bytes_read != us.Length) { + LOG(ERROR) << "ReadProcessMemory UNICODE_STRING incorrect size"; + return false; + } + return true; +} + +template bool ReadStruct(HANDLE process, uintptr_t at, T* into) { + SIZE_T bytes_read; + if (!ReadProcessMemory(process, + reinterpret_cast(at), + into, + sizeof(T), + &bytes_read)) { + // We don't have a name for the type we're reading, so include the signature + // to get the type of T. + PLOG(ERROR) << "ReadProcessMemory " << __FUNCSIG__; + return false; + } + if (bytes_read != sizeof(T)) { + LOG(ERROR) << "ReadProcessMemory " << __FUNCSIG__ << " incorrect size"; + return false; + } + return true; +} + +// PEB_LDR_DATA in winternl.h doesn't document the trailing +// InInitializationOrderModuleList field. See `dt ntdll!PEB_LDR_DATA`. +struct FULL_PEB_LDR_DATA : public PEB_LDR_DATA { + LIST_ENTRY InInitializationOrderModuleList; +}; + +// LDR_DATA_TABLE_ENTRY doesn't include InInitializationOrderLinks, define a +// complete version here. See `dt ntdll!_LDR_DATA_TABLE_ENTRY`. +struct FULL_LDR_DATA_TABLE_ENTRY { + LIST_ENTRY InLoadOrderLinks; + LIST_ENTRY InMemoryOrderLinks; + LIST_ENTRY InInitializationOrderLinks; + PVOID DllBase; + PVOID EntryPoint; + ULONG SizeOfImage; + UNICODE_STRING FullDllName; + UNICODE_STRING BaseDllName; + ULONG Flags; + WORD LoadCount; + WORD TlsIndex; + LIST_ENTRY HashLinks; + ULONG TimeDateStamp; + _ACTIVATION_CONTEXT* EntryPointActivationContext; +}; + +} // namespace + +ProcessInfo::ProcessInfo() + : process_basic_information_(), + command_line_(), + modules_(), + is_64_bit_(false), + is_wow64_(false), + initialized_() { +} + +ProcessInfo::~ProcessInfo() { +} + +bool ProcessInfo::Initialize(HANDLE process) { + INITIALIZATION_STATE_SET_INITIALIZING(initialized_); + + is_wow64_ = IsProcessWow64(process); + + if (is_wow64_) { + // If it's WoW64, then it's 32-on-64. + is_64_bit_ = false; + } else { + // Otherwise, it's either 32 on 32, or 64 on 64. Use GetSystemInfo() to + // distinguish between these two cases. + SYSTEM_INFO system_info; + GetSystemInfo(&system_info); + is_64_bit_ = + system_info.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64; + } + +#if ARCH_CPU_64_BITS + if (!is_64_bit_) { + LOG(ERROR) << "Reading different bitness not yet supported"; + return false; + } +#else + if (is_64_bit_) { + LOG(ERROR) << "Reading x64 process from x86 process not supported"; + return false; + } +#endif + + ULONG bytes_returned; + NTSTATUS status = + crashpad::NtQueryInformationProcess(process, + ProcessBasicInformation, + &process_basic_information_, + sizeof(process_basic_information_), + &bytes_returned); + if (status < 0) { + LOG(ERROR) << "NtQueryInformationProcess: status=" << status; + return false; + } + if (bytes_returned != sizeof(process_basic_information_)) { + LOG(ERROR) << "NtQueryInformationProcess incorrect size"; + return false; + } + + // Try to read the process environment block. + PEB peb; + if (!ReadStruct(process, + reinterpret_cast( + process_basic_information_.PebBaseAddress), + &peb)) { + return false; + } + + RTL_USER_PROCESS_PARAMETERS process_parameters; + if (!ReadStruct(process, + reinterpret_cast(peb.ProcessParameters), + &process_parameters)) { + return false; + } + + if (!ReadUnicodeString( + process, process_parameters.CommandLine, &command_line_)) { + return false; + } + + FULL_PEB_LDR_DATA peb_ldr_data; + if (!ReadStruct(process, reinterpret_cast(peb.Ldr), &peb_ldr_data)) + return false; + + // Include the first module in the memory order list to get our own name as + // it's not included in initialization order below. + std::wstring self_module; + FULL_LDR_DATA_TABLE_ENTRY self_ldr_data_table_entry; + if (!ReadStruct(process, + reinterpret_cast( + reinterpret_cast( + peb_ldr_data.InMemoryOrderModuleList.Flink) - + offsetof(FULL_LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks)), + &self_ldr_data_table_entry)) { + return false; + } + if (!ReadUnicodeString( + process, self_ldr_data_table_entry.FullDllName, &self_module)) { + return false; + } + modules_.push_back(self_module); + + // Walk the PEB LDR structure (doubly-linked list) to get the list of loaded + // modules. We use this method rather than EnumProcessModules to get the + // modules in initialization order rather than memory order. + const LIST_ENTRY* last = peb_ldr_data.InInitializationOrderModuleList.Blink; + FULL_LDR_DATA_TABLE_ENTRY ldr_data_table_entry; + for (const LIST_ENTRY* cur = + peb_ldr_data.InInitializationOrderModuleList.Flink; + ; + cur = ldr_data_table_entry.InInitializationOrderLinks.Flink) { + // |cur| is the pointer to the LIST_ENTRY embedded in the + // FULL_LDR_DATA_TABLE_ENTRY, in the target process's address space. So we + // need to read from the target, and also offset back to the beginning of + // the structure. + if (!ReadStruct( + process, + reinterpret_cast(reinterpret_cast(cur) - + offsetof(FULL_LDR_DATA_TABLE_ENTRY, + InInitializationOrderLinks)), + &ldr_data_table_entry)) { + break; + } + // TODO(scottmg): Capture TimeDateStamp, Checksum, etc. too? + std::wstring module; + if (!ReadUnicodeString(process, ldr_data_table_entry.FullDllName, &module)) + break; + modules_.push_back(module); + if (cur == last) + break; + } + + INITIALIZATION_STATE_SET_VALID(initialized_); + return true; +} + +bool ProcessInfo::Is64Bit() const { + INITIALIZATION_STATE_DCHECK_VALID(initialized_); + return is_64_bit_; +} + +bool ProcessInfo::IsWow64() const { + INITIALIZATION_STATE_DCHECK_VALID(initialized_); + return is_wow64_; +} + +pid_t ProcessInfo::ProcessID() const { + INITIALIZATION_STATE_DCHECK_VALID(initialized_); + return process_basic_information_.UniqueProcessId; +} + +pid_t ProcessInfo::ParentProcessID() const { + INITIALIZATION_STATE_DCHECK_VALID(initialized_); + return process_basic_information_.InheritedFromUniqueProcessId; +} + +bool ProcessInfo::CommandLine(std::wstring* command_line) const { + INITIALIZATION_STATE_DCHECK_VALID(initialized_); + *command_line = command_line_; + return true; +} + +bool ProcessInfo::Modules(std::vector* modules) const { + INITIALIZATION_STATE_DCHECK_VALID(initialized_); + *modules = modules_; + return true; +} + +} // namespace crashpad diff --git a/util/win/process_info.h b/util/win/process_info.h new file mode 100644 index 0000000..3bfb7d4 --- /dev/null +++ b/util/win/process_info.h @@ -0,0 +1,99 @@ +// Copyright 2015 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef CRASHPAD_UTIL_WIN_PROCESS_INFO_H_ +#define CRASHPAD_UTIL_WIN_PROCESS_INFO_H_ + +#include +#include +#include +#include + +#include +#include + +#include "base/basictypes.h" +#include "util/misc/initialization_state_dcheck.h" + +namespace crashpad { + +namespace internal { + +//! \brief This structure matches PROCESS_BASIC_INFORMATION in winternl.h but +//! gives names to the Reserved fields (matching the WDK's ntddk.h). +struct FULL_PROCESS_BASIC_INFORMATION { + NTSTATUS ExitStatus; + PPEB PebBaseAddress; + KAFFINITY AffinityMask; + PVOID BasePriority; + ULONG UniqueProcessId; + ULONG InheritedFromUniqueProcessId; +}; + +} // namespace internal + +//! \brief Gathers information about a process given its `HANDLE`. This consists +//! primarily of information stored in the Process Environment Block. +class ProcessInfo { + public: + ProcessInfo(); + ~ProcessInfo(); + + //! \brief Initializes this object with information about the given + //! \a process. + //! + //! This method must be called successfully prior to calling any other + //! method in this class. This method may only be called once. + //! + //! \return `true` on success, `false` on failure with a message logged. + bool Initialize(HANDLE process); + + //! \return `true` if the target process is a 64-bit process. + bool Is64Bit() const; + + //! \return `true` if the target process is running on the Win32-on-Win64 + //! subsystem. + bool IsWow64() const; + + //! \return The target process's process ID. + pid_t ProcessID() const; + + //! \return The target process's parent process ID. + pid_t ParentProcessID() const; + + //! \return The command line from the target process's Process Environment + //! Block. + bool CommandLine(std::wstring* command_line) const; + + //! \brief Retrieves the modules loaded into the target process. + //! + //! The modules are enumerated in initialization order as detailed in the + //! Process Environment Block. The main executable will always be the + //! first element. + bool Modules(std::vector* modules) const; + + private: + internal::FULL_PROCESS_BASIC_INFORMATION process_basic_information_; + std::wstring command_line_; + std::vector modules_; + bool is_64_bit_; + bool is_wow64_; + InitializationStateDcheck initialized_; + + DISALLOW_COPY_AND_ASSIGN(ProcessInfo); +}; + +} // namespace crashpad + +#endif // CRASHPAD_UTIL_WIN_PROCESS_INFO_H_ diff --git a/util/win/process_info_test.cc b/util/win/process_info_test.cc new file mode 100644 index 0000000..386bd2f --- /dev/null +++ b/util/win/process_info_test.cc @@ -0,0 +1,134 @@ +// Copyright 2015 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/win/process_info.h" + +#include +#include + +#include "base/files/file_path.h" +#include "build/build_config.h" +#include "gtest/gtest.h" +#include "util/misc/uuid.h" +#include "util/test/executable_path.h" +#include "util/win/scoped_handle.h" + +namespace crashpad { +namespace test { +namespace { + +const wchar_t kNtdllName[] = L"\\ntdll.dll"; + +TEST(ProcessInfo, Self) { + ProcessInfo process_info; + ASSERT_TRUE(process_info.Initialize(GetCurrentProcess())); + EXPECT_EQ(GetCurrentProcessId(), process_info.ProcessID()); + EXPECT_GT(process_info.ParentProcessID(), 0u); + +#if defined(ARCH_CPU_64_BITS) + EXPECT_TRUE(process_info.Is64Bit()); + EXPECT_FALSE(process_info.IsWow64()); +#else + EXPECT_FALSE(process_info.Is64Bit()); + // Assume we won't be running these tests on a 32 bit host OS. + EXPECT_TRUE(process_info.IsWow64()); +#endif + + std::wstring command_line; + EXPECT_TRUE(process_info.CommandLine(&command_line)); + EXPECT_EQ(std::wstring(GetCommandLine()), command_line); + + std::vector modules; + EXPECT_TRUE(process_info.Modules(&modules)); + ASSERT_GE(modules.size(), 2u); + const wchar_t kSelfName[] = L"\\util_test.exe"; + ASSERT_GE(modules[0].size(), wcslen(kSelfName)); + EXPECT_EQ(kSelfName, + modules[0].substr(modules[0].size() - wcslen(kSelfName))); + ASSERT_GE(modules[1].size(), wcslen(kNtdllName)); + EXPECT_EQ(kNtdllName, + modules[1].substr(modules[1].size() - wcslen(kNtdllName))); +} + +TEST(ProcessInfo, SomeOtherProcess) { + ProcessInfo process_info; + + ::UUID system_uuid; + ASSERT_EQ(RPC_S_OK, UuidCreate(&system_uuid)); + UUID started_uuid(reinterpret_cast(&system_uuid.Data1)); + ASSERT_EQ(RPC_S_OK, UuidCreate(&system_uuid)); + UUID done_uuid(reinterpret_cast(&system_uuid.Data1)); + + ScopedKernelHANDLE started( + CreateEvent(nullptr, true, false, started_uuid.ToString16().c_str())); + ASSERT_TRUE(started.get()); + ScopedKernelHANDLE done( + CreateEvent(nullptr, true, false, done_uuid.ToString16().c_str())); + ASSERT_TRUE(done.get()); + + base::FilePath test_executable = ExecutablePath(); + std::wstring child_test_executable = + test_executable.RemoveFinalExtension().value() + + L"_process_info_test_child.exe"; + // TODO(scottmg): Command line escaping utility. + std::wstring command_line = child_test_executable + L" " + + started_uuid.ToString16() + L" " + + done_uuid.ToString16(); + STARTUPINFO startup_info = {0}; + startup_info.cb = sizeof(startup_info); + PROCESS_INFORMATION process_information; + ASSERT_TRUE(CreateProcess(child_test_executable.c_str(), + &command_line[0], + nullptr, + nullptr, + false, + 0, + nullptr, + nullptr, + &startup_info, + &process_information)); + // Take ownership of the two process handles returned. + ScopedKernelHANDLE process_main_thread_handle(process_information.hThread); + ScopedKernelHANDLE process_handle(process_information.hProcess); + + // Wait until the test has completed initialization. + ASSERT_EQ(WaitForSingleObject(started.get(), INFINITE), WAIT_OBJECT_0); + + ASSERT_TRUE(process_info.Initialize(process_information.hProcess)); + + // Tell the test it's OK to shut down now that we've read our data. + SetEvent(done.get()); + + std::vector modules; + EXPECT_TRUE(process_info.Modules(&modules)); + ASSERT_GE(modules.size(), 3u); + const wchar_t kChildName[] = L"\\util_test_process_info_test_child.exe"; + ASSERT_GE(modules[0].size(), wcslen(kChildName)); + EXPECT_EQ(kChildName, + modules[0].substr(modules[0].size() - wcslen(kChildName))); + ASSERT_GE(modules[1].size(), wcslen(kNtdllName)); + EXPECT_EQ(kNtdllName, + modules[1].substr(modules[1].size() - wcslen(kNtdllName))); + // lz32.dll is an uncommonly-used-but-always-available module that the test + // binary manually loads. + const wchar_t kLz32dllName[] = L"\\lz32.dll"; + ASSERT_GE(modules.back().size(), wcslen(kLz32dllName)); + EXPECT_EQ( + kLz32dllName, + modules.back().substr(modules.back().size() - wcslen(kLz32dllName))); +} + +} // namespace +} // namespace test +} // namespace crashpad diff --git a/util/win/process_info_test_child.cc b/util/win/process_info_test_child.cc new file mode 100644 index 0000000..a0cf5c4 --- /dev/null +++ b/util/win/process_info_test_child.cc @@ -0,0 +1,45 @@ +// Copyright 2015 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +// A simple binary to be loaded and inspected by ProcessInfo. +int wmain(int argc, wchar_t** argv) { + if (argc != 3) + abort(); + + // Get handles to the events we use to communicate with our parent. + HANDLE started_event = CreateEvent(nullptr, true, false, argv[1]); + HANDLE done_event = CreateEvent(nullptr, true, false, argv[2]); + if (!started_event || !done_event) + abort(); + + // Load an unusual module (that we don't depend upon) so we can do an + // existence check. + if (!LoadLibrary(L"lz32.dll")) + abort(); + + if (!SetEvent(started_event)) + abort(); + + if (WaitForSingleObject(done_event, INFINITE) != WAIT_OBJECT_0) + abort(); + + CloseHandle(done_event); + CloseHandle(started_event); + + return EXIT_SUCCESS; +}