std::basic_string<CharT> CheckedReadString(Process const& process, PeFile const& pe_file, void* address) { if (pe_file.GetType() == PeFileType::Image) { return ReadString<CharT>(process, address); } else if (pe_file.GetType() == PeFileType::Data) { void* const file_end = static_cast<std::uint8_t*>(pe_file.GetBase()) + pe_file.GetSize(); if (address >= file_end) { HADESMEM_DETAIL_THROW_EXCEPTION(Error{} << ErrorString{"Invalid VA."}); } // Handle EOF termination. // Sample: maxsecXP.exe (Corkami PE Corpus) return ReadStringBounded<CharT>(process, address, file_end); } else { HADESMEM_DETAIL_ASSERT(false); HADESMEM_DETAIL_THROW_EXCEPTION(Error{} << ErrorString{"Unknown PE file type."}); } }
explicit RelocationBlockIterator(Process const& process, PeFile const& pe_file) { try { NtHeaders const nt_headers{process, pe_file}; DWORD const data_dir_va = nt_headers.GetDataDirectoryVirtualAddress(PeDataDir::BaseReloc); DWORD const size = nt_headers.GetDataDirectorySize(PeDataDir::BaseReloc); if (!data_dir_va || !size) { return; } auto base = static_cast<std::uint8_t*>(RvaToVa(process, pe_file, data_dir_va)); if (!base) { return; } // Cast to integer and back to avoid pointer overflow UB. auto const reloc_dir_end = reinterpret_cast<void const*>( reinterpret_cast<std::uintptr_t>(base) + size); auto const file_end = static_cast<std::uint8_t*>(pe_file.GetBase()) + pe_file.GetSize(); // Sample: virtrelocXP.exe if (pe_file.GetType() == PeFileType::Data && (reloc_dir_end < base || reloc_dir_end > file_end)) { return; } // TODO: Dump should warn for this. RelocationBlock const relocation_block{ process, pe_file, reinterpret_cast<IMAGE_BASE_RELOCATION*>(base), reloc_dir_end}; if (relocation_block.IsInvalid()) { return; } impl_ = std::make_shared<Impl>( process, pe_file, relocation_block, reloc_dir_end); } catch (std::exception const& /*e*/) { // Nothing to do here. } }
explicit DosHeader(Process const& process, PeFile const& pe_file) : process_{&process}, base_{static_cast<std::uint8_t*>(pe_file.GetBase())} { UpdateRead(); EnsureValid(); }
// TODO: 'Harden' this function against malicious/malformed PE files like is // done for RvaToVa. inline DWORD FileOffsetToRva(Process const& process, PeFile const& pe_file, DWORD file_offset) { PeFileType const type = pe_file.GetType(); PBYTE base = static_cast<PBYTE>(pe_file.GetBase()); if (type == PeFileType::Data) { IMAGE_DOS_HEADER dos_header = Read<IMAGE_DOS_HEADER>(process, base); if (dos_header.e_magic != IMAGE_DOS_SIGNATURE) { HADESMEM_DETAIL_THROW_EXCEPTION(Error{} << ErrorString{"Invalid DOS header."}); } BYTE* ptr_nt_headers = base + dos_header.e_lfanew; if (Read<DWORD>(process, ptr_nt_headers) != IMAGE_NT_SIGNATURE) { HADESMEM_DETAIL_THROW_EXCEPTION(Error{} << ErrorString{"Invalid NT headers."}); } auto const file_header = Read<IMAGE_FILE_HEADER>(process, ptr_nt_headers + sizeof(DWORD)); auto ptr_section_header = reinterpret_cast<PIMAGE_SECTION_HEADER>( ptr_nt_headers + offsetof(IMAGE_NT_HEADERS, OptionalHeader) + file_header.SizeOfOptionalHeader); WORD num_sections = file_header.NumberOfSections; for (WORD i = 0; i < num_sections; ++i) { auto const section_header = Read<IMAGE_SECTION_HEADER>(process, ptr_section_header); DWORD const raw_beg = section_header.PointerToRawData; DWORD const raw_size = section_header.SizeOfRawData; DWORD const raw_end = raw_beg + raw_size; if (raw_beg <= file_offset && file_offset < raw_end) { file_offset -= raw_beg; file_offset += section_header.VirtualAddress; return file_offset; } ++ptr_section_header; } return 0; } else if (type == PeFileType::Image) { return file_offset; } else { HADESMEM_DETAIL_THROW_EXCEPTION(Error{} << ErrorString{"Unhandled file type."}); } }
inline PVOID RvaToVa(Process const& process, PeFile const& pe_file, DWORD rva, bool* virtual_va = nullptr) { if (virtual_va) { *virtual_va = false; } PeFileType const type = pe_file.GetType(); PBYTE base = static_cast<PBYTE>(pe_file.GetBase()); if (type == PeFileType::Data) { if (!rva) { return nullptr; } IMAGE_DOS_HEADER dos_header = Read<IMAGE_DOS_HEADER>(process, base); if (dos_header.e_magic != IMAGE_DOS_SIGNATURE) { HADESMEM_DETAIL_THROW_EXCEPTION(Error{} << ErrorString{"Invalid DOS header."}); } BYTE* ptr_nt_headers = base + dos_header.e_lfanew; if (Read<DWORD>(process, ptr_nt_headers) != IMAGE_NT_SIGNATURE) { HADESMEM_DETAIL_THROW_EXCEPTION(Error{} << ErrorString{"Invalid NT headers."}); } auto const file_header = Read<IMAGE_FILE_HEADER>(process, ptr_nt_headers + sizeof(DWORD)); auto const optional_header_32 = pe_file.Is64() ? IMAGE_OPTIONAL_HEADER32{} : Read<IMAGE_OPTIONAL_HEADER32>(process, ptr_nt_headers + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER)); auto const optional_header_64 = pe_file.Is64() ? Read<IMAGE_OPTIONAL_HEADER64>( process, ptr_nt_headers + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER)) : IMAGE_OPTIONAL_HEADER64{}; DWORD const size_of_headers = pe_file.Is64() ? optional_header_64.SizeOfHeaders : optional_header_32.SizeOfHeaders; DWORD const file_alignment = pe_file.Is64() ? optional_header_64.FileAlignment : optional_header_32.FileAlignment; DWORD const size_of_image = pe_file.Is64() ? optional_header_64.SizeOfImage : optional_header_32.SizeOfImage; // Windows will load specially crafted images with no sections. WORD num_sections = file_header.NumberOfSections; if (!num_sections) { // In cases where the PE file has no sections it can apparently also have // all sorts of messed up RVAs for data dirs etc... Make sure that none of // them lie outside the file, because otherwise simply returning a direct // offset from the base wouldn't work anyway... if (rva > pe_file.GetSize()) { return nullptr; } else { return base + rva; } } // SizeOfHeaders can be arbitrarily large, including the size of the // entire file. RVAs inside the headers are treated as an offset from // zero, rather than finding the 'true' location in a section. if (rva < size_of_headers) { // Only applies in low alignment, otherwise it's invalid? if (file_alignment < 200) { return base + rva; } // Also only applies if the RVA is smaller than file alignment? else if (rva < file_alignment) { return base + rva; } else { return nullptr; } } if (rva > size_of_image) { return nullptr; } auto ptr_section_header = reinterpret_cast<PIMAGE_SECTION_HEADER>( ptr_nt_headers + offsetof(IMAGE_NT_HEADERS, OptionalHeader) + file_header.SizeOfOptionalHeader); void const* const file_end = static_cast<std::uint8_t*>(pe_file.GetBase()) + pe_file.GetSize(); // Virtual section table. if (ptr_section_header >= file_end) { if (rva > pe_file.GetSize()) { return nullptr; } else { return base + rva; } } bool in_header = true; for (WORD i = 0; i < num_sections; ++i) { // For a virtual section header, simply return nullptr. (Similar to above, // except this time only the Nth entry onwards is virtual, rather than all // the headers.) if (ptr_section_header + 1 > file_end) { return nullptr; } auto const section_header = Read<IMAGE_SECTION_HEADER>(process, ptr_section_header); DWORD const virtual_beg = section_header.VirtualAddress; DWORD const virtual_size = section_header.Misc.VirtualSize; DWORD const raw_size = section_header.SizeOfRawData; // If VirtualSize is zero then SizeOfRawData is used. DWORD const virtual_end = virtual_beg + (virtual_size ? virtual_size : raw_size); if (virtual_beg <= rva && rva < virtual_end) { rva -= virtual_beg; // If the RVA is outside the raw data (which would put it in the // zero-fill of the virtual data) just return nullptr because it's // invalid. Technically files like this will work when loaded by the // PE loader due to the sections being mapped differention in memory // to on disk, but if you want to inspect the file in that manner you // should just use LoadLibrary with the appropriate flags for your // scenario and then use PeFileType::Image. if (rva > raw_size) { // It's useful to be able to detect this case as a user for things // like exports, where typically a failure to resolve an RVA would be // an error/suspicious, but not in the case of a data export where it // is normal for the RVA to be in the zero fill of a data segment. // TODO: Find other places in this function where we need to set this // flag. if (rva < virtual_size && virtual_va) { *virtual_va = true; } return nullptr; } // If PointerToRawData is less than 0x200 it is rounded // down to 0. if (section_header.PointerToRawData >= 0x200) { // TODO: Check whether we actually need/want to force alignment here. rva += section_header.PointerToRawData & ~(file_alignment - 1); } // If the RVA now lies outside the actual file just return nullptr // because it's invalid. if (rva >= pe_file.GetSize()) { return nullptr; } return base + rva; } // This should be the 'normal' case. However sometimes the RVA is at a // lower address than any of the sections, so we want to detect this so we // can just treat the RVA as an offset from the module base (similar to // when the image is loaded). if (virtual_beg <= rva) { in_header = false; } ++ptr_section_header; } // Doing the same thing as in the SizeOfHeaders check above because we're // not sure of better criteria to base it off. Perhaps it's correct now? if (in_header && rva < pe_file.GetSize()) { // Only applies in low alignment, otherwise it's invalid? if (file_alignment < 200) { return base + rva; } // Also only applies if the RVA is smaller than file alignment? else if (rva < file_alignment) { return base + rva; } else { return nullptr; } } // Sample: nullSOH-XP (Corkami PE Corpus) if (rva < size_of_image && rva < pe_file.GetSize()) { return base + rva; } return nullptr; } else if (type == PeFileType::Image) { return rva ? (base + rva) : nullptr; } else { HADESMEM_DETAIL_THROW_EXCEPTION(Error{} << ErrorString{"Unhandled file type."}); } }
inline bool operator>=(PeFile const& lhs, PeFile const& rhs) noexcept { return lhs.GetBase() >= rhs.GetBase(); }