// Copyright 2015 The Crashpad Authors // // 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 #include #include #include #include #include "base/logging.h" #include "base/memory/free_deleter.h" #include "base/process/memory.h" #include "base/strings/stringprintf.h" #include "build/build_config.h" #include "util/misc/from_pointer_cast.h" #include "util/numeric/safe_assignment.h" #include "util/win/get_function.h" #include "util/win/handle.h" #include "util/win/nt_internals.h" #include "util/win/ntstatus_logging.h" #include "util/win/process_structs.h" #include "util/win/scoped_handle.h" namespace crashpad { namespace { using UniqueMallocPtr = std::unique_ptr; UniqueMallocPtr UncheckedAllocate(size_t size) { void* raw_ptr = nullptr; if (!base::UncheckedMalloc(size, &raw_ptr)) return UniqueMallocPtr(); return UniqueMallocPtr(new (raw_ptr) uint8_t[size]); } NTSTATUS NtQueryInformationProcess(HANDLE process_handle, PROCESSINFOCLASS process_information_class, PVOID process_information, ULONG process_information_length, PULONG return_length) { static const auto nt_query_information_process = GET_FUNCTION_REQUIRED(L"ntdll.dll", ::NtQueryInformationProcess); return nt_query_information_process(process_handle, process_information_class, process_information, process_information_length, return_length); } bool IsProcessWow64(HANDLE process_handle) { static const auto is_wow64_process = GET_FUNCTION(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; } template bool ReadUnicodeString(HANDLE process, const process_types::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, reinterpret_cast(static_cast(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, WinVMAddress 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 " << FUNCTION_SIGNATURE; return false; } if (bytes_read != sizeof(T)) { LOG(ERROR) << "ReadProcessMemory " << FUNCTION_SIGNATURE << " incorrect size"; return false; } return true; } bool RegionIsAccessible(const MEMORY_BASIC_INFORMATION64& memory_info) { return memory_info.State == MEM_COMMIT && (memory_info.Protect & PAGE_NOACCESS) == 0 && (memory_info.Protect & PAGE_GUARD) == 0; } MEMORY_BASIC_INFORMATION64 MemoryBasicInformationToMemoryBasicInformation64( const MEMORY_BASIC_INFORMATION& mbi) { MEMORY_BASIC_INFORMATION64 mbi64 = {0}; mbi64.BaseAddress = FromPointerCast(mbi.BaseAddress); mbi64.AllocationBase = reinterpret_cast(mbi.AllocationBase); mbi64.AllocationProtect = mbi.AllocationProtect; mbi64.RegionSize = mbi.RegionSize; mbi64.State = mbi.State; mbi64.Protect = mbi.Protect; mbi64.Type = mbi.Type; return mbi64; } // NtQueryObject with a retry for size mismatch as well as a minimum size to // retrieve (and expect). std::unique_ptr QueryObject( HANDLE handle, OBJECT_INFORMATION_CLASS object_information_class, ULONG minimum_size) { ULONG size = minimum_size; ULONG return_length; std::unique_ptr buffer(new uint8_t[size]); NTSTATUS status = crashpad::NtQueryObject( handle, object_information_class, buffer.get(), size, &return_length); if (status == STATUS_INFO_LENGTH_MISMATCH) { DCHECK_GT(return_length, size); size = return_length; // Free the old buffer before attempting to allocate a new one. buffer.reset(); buffer.reset(new uint8_t[size]); status = crashpad::NtQueryObject( handle, object_information_class, buffer.get(), size, &return_length); } if (!NT_SUCCESS(status)) { NTSTATUS_LOG(ERROR, status) << "NtQueryObject"; return nullptr; } DCHECK_LE(return_length, size); DCHECK_GE(return_length, minimum_size); return buffer; } } // namespace template bool GetProcessBasicInformation(HANDLE process, bool is_wow64, ProcessInfo* process_info, WinVMAddress* peb_address, WinVMSize* peb_size) { ULONG bytes_returned; process_types::PROCESS_BASIC_INFORMATION process_basic_information; NTSTATUS status = crashpad::NtQueryInformationProcess(process, ProcessBasicInformation, &process_basic_information, sizeof(process_basic_information), &bytes_returned); if (!NT_SUCCESS(status)) { NTSTATUS_LOG(ERROR, status) << "NtQueryInformationProcess"; return false; } if (bytes_returned != sizeof(process_basic_information)) { LOG(ERROR) << "NtQueryInformationProcess incorrect size"; return false; } // API functions (e.g. OpenProcess) take only a DWORD, so there's no sense in // maintaining the top bits. process_info->process_id_ = static_cast(process_basic_information.UniqueProcessId); process_info->inherited_from_process_id_ = static_cast( process_basic_information.InheritedFromUniqueProcessId); // We now want to read the PEB to gather the rest of our information. The // PebBaseAddress as returned above is what we want for 64-on-64 and 32-on-32, // but for Wow64, we want to read the 32 bit PEB (a Wow64 process has both). // The address of this is found by a second call to NtQueryInformationProcess. if (!is_wow64) { *peb_address = process_basic_information.PebBaseAddress; *peb_size = sizeof(process_types::PEB); } else { ULONG_PTR wow64_peb_address; status = crashpad::NtQueryInformationProcess(process, ProcessWow64Information, &wow64_peb_address, sizeof(wow64_peb_address), &bytes_returned); if (!NT_SUCCESS(status)) { NTSTATUS_LOG(ERROR, status) << "NtQueryInformationProcess"; return false; } if (bytes_returned != sizeof(wow64_peb_address)) { LOG(ERROR) << "NtQueryInformationProcess incorrect size"; return false; } *peb_address = wow64_peb_address; *peb_size = sizeof(process_types::PEB); } return true; } template bool ReadProcessData(HANDLE process, WinVMAddress peb_address_vmaddr, ProcessInfo* process_info) { typename Traits::Pointer peb_address; if (!AssignIfInRange(&peb_address, peb_address_vmaddr)) { LOG(ERROR) << base::StringPrintf("peb address 0x%llx out of range", peb_address_vmaddr); return false; } // Try to read the process environment block. process_types::PEB peb; if (!ReadStruct(process, peb_address, &peb)) return false; process_types::RTL_USER_PROCESS_PARAMETERS process_parameters; if (!ReadStruct(process, peb.ProcessParameters, &process_parameters)) return false; if (!ReadUnicodeString(process, process_parameters.CommandLine, &process_info->command_line_)) { return false; } process_types::PEB_LDR_DATA peb_ldr_data; if (!ReadStruct(process, peb.Ldr, &peb_ldr_data)) return false; process_types::LDR_DATA_TABLE_ENTRY ldr_data_table_entry; ProcessInfo::Module 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 load order rather than memory order. Notably, this includes the // main executable as the first element. typename Traits::Pointer last = peb_ldr_data.InLoadOrderModuleList.Blink; for (typename Traits::Pointer cur = peb_ldr_data.InLoadOrderModuleList.Flink;; cur = ldr_data_table_entry.InLoadOrderLinks.Flink) { // |cur| is the pointer to the LIST_ENTRY embedded in the // 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, static_cast(cur) - offsetof(process_types::LDR_DATA_TABLE_ENTRY, InLoadOrderLinks), &ldr_data_table_entry)) { break; } // TODO(scottmg): Capture Checksum, etc. too? if (!ReadUnicodeString( process, ldr_data_table_entry.FullDllName, &module.name)) { module.name = L"???"; } module.dll_base = ldr_data_table_entry.DllBase; module.size = ldr_data_table_entry.SizeOfImage; module.timestamp = ldr_data_table_entry.TimeDateStamp; process_info->modules_.push_back(module); if (cur == last) break; } return true; } bool ReadMemoryInfo(HANDLE process, bool is_64_bit, ProcessInfo* process_info) { DCHECK(process_info->memory_info_.empty()); constexpr WinVMAddress min_address = 0; // We can't use GetSystemInfo() to get the address space range for another // process. VirtualQueryEx() will fail with ERROR_INVALID_PARAMETER if the // address is above the highest memory address accessible to the process, so // we just probe the entire potential range (2^32 for x86, or 2^64 for x64). const WinVMAddress max_address = is_64_bit ? std::numeric_limits::max() : std::numeric_limits::max(); MEMORY_BASIC_INFORMATION memory_basic_information; for (WinVMAddress address = min_address; address <= max_address; address += memory_basic_information.RegionSize) { size_t result = VirtualQueryEx(process, reinterpret_cast(address), &memory_basic_information, sizeof(memory_basic_information)); if (result == 0) { if (GetLastError() == ERROR_INVALID_PARAMETER) break; PLOG(ERROR) << "VirtualQueryEx"; return false; } process_info->memory_info_.push_back( MemoryBasicInformationToMemoryBasicInformation64( memory_basic_information)); if (memory_basic_information.RegionSize == 0) { LOG(ERROR) << "RegionSize == 0"; return false; } } return true; } std::vector ProcessInfo::BuildHandleVector( HANDLE process) const { ULONG buffer_size = 2 * 1024 * 1024; // Typically if the buffer were too small, STATUS_INFO_LENGTH_MISMATCH would // return the correct size in the final argument, but it does not for // SystemExtendedHandleInformation, so we loop and attempt larger sizes. NTSTATUS status; ULONG returned_length; UniqueMallocPtr buffer; for (int tries = 0; tries < 5; ++tries) { buffer.reset(); buffer = UncheckedAllocate(buffer_size); if (!buffer) { LOG(ERROR) << "UncheckedAllocate"; return std::vector(); } status = crashpad::NtQuerySystemInformation( static_cast(SystemExtendedHandleInformation), buffer.get(), buffer_size, &returned_length); if (NT_SUCCESS(status) || status != STATUS_INFO_LENGTH_MISMATCH) break; buffer_size *= 2; } if (!NT_SUCCESS(status)) { NTSTATUS_LOG(ERROR, status) << "NtQuerySystemInformation SystemExtendedHandleInformation"; return std::vector(); } const auto& system_handle_information_ex = *reinterpret_cast( buffer.get()); DCHECK_LE(offsetof(process_types::SYSTEM_HANDLE_INFORMATION_EX, Handles) + system_handle_information_ex.NumberOfHandles * sizeof(system_handle_information_ex.Handles[0]), returned_length); std::vector handles; for (size_t i = 0; i < system_handle_information_ex.NumberOfHandles; ++i) { const auto& handle = system_handle_information_ex.Handles[i]; if (handle.UniqueProcessId != process_id_) continue; Handle result_handle; result_handle.handle = HandleToInt(handle.HandleValue); result_handle.attributes = handle.HandleAttributes; result_handle.granted_access = handle.GrantedAccess; // TODO(scottmg): Could special case for self. HANDLE dup_handle; if (DuplicateHandle(process, handle.HandleValue, GetCurrentProcess(), &dup_handle, 0, false, DUPLICATE_SAME_ACCESS)) { // Some handles cannot be duplicated, for example, handles of type // EtwRegistration. If we fail to duplicate, then we can't gather any more // information, but include the information that we do have already. ScopedKernelHANDLE scoped_dup_handle(dup_handle); std::unique_ptr object_basic_information_buffer = QueryObject(dup_handle, ObjectBasicInformation, sizeof(PUBLIC_OBJECT_BASIC_INFORMATION)); if (object_basic_information_buffer) { PUBLIC_OBJECT_BASIC_INFORMATION* object_basic_information = reinterpret_cast( object_basic_information_buffer.get()); // The Attributes and GrantedAccess sometimes differ slightly between // the data retrieved in SYSTEM_HANDLE_INFORMATION_EX and // PUBLIC_OBJECT_TYPE_INFORMATION. We prefer the values in // SYSTEM_HANDLE_INFORMATION_EX because they were retrieved from the // target process, rather than on the duplicated handle, so don't use // them here. // Subtract one to account for our DuplicateHandle() and another for // NtQueryObject() while the query was being executed. DCHECK_GT(object_basic_information->PointerCount, 2u); result_handle.pointer_count = object_basic_information->PointerCount - 2; // Subtract one to account for our DuplicateHandle(). DCHECK_GT(object_basic_information->HandleCount, 1u); result_handle.handle_count = object_basic_information->HandleCount - 1; } std::unique_ptr object_type_information_buffer = QueryObject(dup_handle, ObjectTypeInformation, sizeof(PUBLIC_OBJECT_TYPE_INFORMATION)); if (object_type_information_buffer) { PUBLIC_OBJECT_TYPE_INFORMATION* object_type_information = reinterpret_cast( object_type_information_buffer.get()); DCHECK_EQ(object_type_information->TypeName.Length % sizeof(result_handle.type_name[0]), 0u); result_handle.type_name = std::wstring(object_type_information->TypeName.Buffer, object_type_information->TypeName.Length / sizeof(result_handle.type_name[0])); } } handles.push_back(result_handle); } return handles; } ProcessInfo::Module::Module() : name(), dll_base(0), size(0), timestamp() { } ProcessInfo::Module::~Module() { } ProcessInfo::Handle::Handle() : type_name(), handle(0), attributes(0), granted_access(0), pointer_count(0), handle_count(0) { } ProcessInfo::Handle::~Handle() { } ProcessInfo::ProcessInfo() : process_id_(), inherited_from_process_id_(), process_(), command_line_(), peb_address_(0), peb_size_(0), modules_(), memory_info_(), handles_(), is_64_bit_(false), is_wow64_(false), initialized_() { } ProcessInfo::~ProcessInfo() { } bool ProcessInfo::Initialize(HANDLE process) { INITIALIZATION_STATE_SET_INITIALIZING(initialized_); process_ = process; 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); #if defined(ARCH_CPU_X86_FAMILY) constexpr uint16_t kNative64BitArchitecture = PROCESSOR_ARCHITECTURE_AMD64; #elif defined(ARCH_CPU_ARM_FAMILY) constexpr uint16_t kNative64BitArchitecture = PROCESSOR_ARCHITECTURE_ARM64; #endif is_64_bit_ = system_info.wProcessorArchitecture == kNative64BitArchitecture; } #if defined(ARCH_CPU_32_BITS) if (is_64_bit_) { LOG(ERROR) << "Reading x64 process from x86 process not supported"; return false; } #endif // ARCH_CPU_32_BITS #if defined(ARCH_CPU_64_BITS) bool result = GetProcessBasicInformation( process, is_wow64_, this, &peb_address_, &peb_size_); #else bool result = GetProcessBasicInformation( process, false, this, &peb_address_, &peb_size_); #endif // ARCH_CPU_64_BITS if (!result) { LOG(ERROR) << "GetProcessBasicInformation failed"; return false; } result = is_64_bit_ ? ReadProcessData( process, peb_address_, this) : ReadProcessData( process, peb_address_, this); if (!result) { LOG(ERROR) << "ReadProcessData failed"; return false; } if (!ReadMemoryInfo(process, is_64_bit_, this)) { LOG(ERROR) << "ReadMemoryInfo failed"; return false; } 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_; } crashpad::ProcessID ProcessInfo::ProcessID() const { INITIALIZATION_STATE_DCHECK_VALID(initialized_); return process_id_; } crashpad::ProcessID ProcessInfo::ParentProcessID() const { INITIALIZATION_STATE_DCHECK_VALID(initialized_); return inherited_from_process_id_; } bool ProcessInfo::CommandLine(std::wstring* command_line) const { INITIALIZATION_STATE_DCHECK_VALID(initialized_); *command_line = command_line_; return true; } void ProcessInfo::Peb(WinVMAddress* peb_address, WinVMSize* peb_size) const { *peb_address = peb_address_; *peb_size = peb_size_; } bool ProcessInfo::Modules(std::vector* modules) const { INITIALIZATION_STATE_DCHECK_VALID(initialized_); *modules = modules_; return true; } const ProcessInfo::MemoryBasicInformation64Vector& ProcessInfo::MemoryInfo() const { INITIALIZATION_STATE_DCHECK_VALID(initialized_); return memory_info_; } std::vector> ProcessInfo::GetReadableRanges( const CheckedRange& range) const { return GetReadableRangesOfMemoryMap(range, MemoryInfo()); } bool ProcessInfo::LoggingRangeIsFullyReadable( const CheckedRange& range) const { const auto ranges = GetReadableRanges(range); if (ranges.empty()) { LOG(ERROR) << base::StringPrintf( "range at 0x%llx, size 0x%llx fully unreadable", range.base(), range.size()); return false; } if (ranges.size() != 1 || ranges[0].base() != range.base() || ranges[0].size() != range.size()) { LOG(ERROR) << base::StringPrintf( "range at 0x%llx, size 0x%llx partially unreadable", range.base(), range.size()); return false; } return true; } const std::vector& ProcessInfo::Handles() const { INITIALIZATION_STATE_DCHECK_VALID(initialized_); if (handles_.empty()) handles_ = BuildHandleVector(process_); return handles_; } std::vector> GetReadableRangesOfMemoryMap( const CheckedRange& range, const ProcessInfo::MemoryBasicInformation64Vector& memory_info) { using Range = CheckedRange; // Constructing Ranges and using OverlapsRange() is very, very slow in Debug // builds, so do a manual check in this loop. The ranges are still validated // by a CheckedRange before being returned. WinVMAddress range_base = range.base(); WinVMAddress range_end = range.end(); // Find all the ranges that overlap the target range, maintaining their order. ProcessInfo::MemoryBasicInformation64Vector overlapping; const size_t size = memory_info.size(); // This loop is written in an ugly fashion to make Debug performance // reasonable. const MEMORY_BASIC_INFORMATION64* begin = &memory_info[0]; for (size_t i = 0; i < size; ++i) { const MEMORY_BASIC_INFORMATION64& mi = *(begin + i); static_assert(std::is_same::value, "expected range address to be WinVMAddress"); static_assert(std::is_same::value, "expected range size to be WinVMSize"); WinVMAddress mi_end = mi.BaseAddress + mi.RegionSize; if (range_base < mi_end && mi.BaseAddress < range_end) overlapping.push_back(mi); } if (overlapping.empty()) return std::vector(); // For the first and last, trim to the boundary of the incoming range. MEMORY_BASIC_INFORMATION64& front = overlapping.front(); WinVMAddress original_front_base_address = front.BaseAddress; front.BaseAddress = std::max(front.BaseAddress, range.base()); front.RegionSize = (original_front_base_address + front.RegionSize) - front.BaseAddress; MEMORY_BASIC_INFORMATION64& back = overlapping.back(); WinVMAddress back_end = back.BaseAddress + back.RegionSize; back.RegionSize = std::min(range.end(), back_end) - back.BaseAddress; // Discard all non-accessible. overlapping.erase(std::remove_if(overlapping.begin(), overlapping.end(), [](const MEMORY_BASIC_INFORMATION64& mbi) { return !RegionIsAccessible(mbi); }), overlapping.end()); if (overlapping.empty()) return std::vector(); // Convert to return type. std::vector as_ranges; for (const auto& mi : overlapping) { as_ranges.push_back(Range(mi.BaseAddress, mi.RegionSize)); DCHECK(as_ranges.back().IsValid()); } // Coalesce remaining regions. std::vector result; result.push_back(as_ranges[0]); for (size_t i = 1; i < as_ranges.size(); ++i) { if (result.back().end() == as_ranges[i].base()) { result.back().SetRange(result.back().base(), result.back().size() + as_ranges[i].size()); } else { result.push_back(as_ranges[i]); } DCHECK(result.back().IsValid()); } return result; } } // namespace crashpad