src/internal/image_identity.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /** | ||
| 2 | * @file internal/image_identity.cpp | ||
| 3 | * @brief Guarded PE-header identity read shared by scan::image_identity and the RTTI generation token. | ||
| 4 | */ | ||
| 5 | |||
| 6 | #include "internal/image_identity.hpp" | ||
| 7 | |||
| 8 | #include "internal/memory_guarded.hpp" | ||
| 9 | #include "internal/memory_representation_win32.hpp" | ||
| 10 | |||
| 11 | #include <windows.h> | ||
| 12 | |||
| 13 | #include <cstddef> | ||
| 14 | #include <cstring> | ||
| 15 | #include <optional> | ||
| 16 | |||
| 17 | namespace DetourModKit | ||
| 18 | { | ||
| 19 | namespace detail | ||
| 20 | { | ||
| 21 | namespace | ||
| 22 | { | ||
| 23 | // The Windows loader caps a PE at 96 sections; a larger count is corrupt and would otherwise let the | ||
| 24 | // section walk below run away. | ||
| 25 | constexpr std::uint32_t MAX_SECTIONS = 96; | ||
| 26 | |||
| 27 | // A wild e_lfanew is the signature of a forged or truncated header. The NT headers are re-bounded against | ||
| 28 | // SizeOfImage once that field has been read; this cap only keeps the first read off a wild address. | ||
| 29 | constexpr std::uint32_t MAX_NT_OFFSET = 0x100000; | ||
| 30 | |||
| 31 | 41696 | [[nodiscard]] constexpr std::uint64_t mix(std::uint64_t seed, std::uint64_t value) noexcept | |
| 32 | { | ||
| 33 | 41696 | seed ^= value + 0x9E3779B97F4A7C15ULL + (seed << 6) + (seed >> 2); | |
| 34 | 41696 | return seed; | |
| 35 | } | ||
| 36 | |||
| 37 | // True when [offset, offset + bytes) lies wholly inside an image of image_size bytes, with an explicit | ||
| 38 | // wrap guard so a hostile offset/size cannot alias a low offset. | ||
| 39 | [[nodiscard]] constexpr bool | ||
| 40 | 1200 | fits_image(std::uint64_t offset, std::uint64_t bytes, std::uint32_t image_size) noexcept | |
| 41 | { | ||
| 42 |
3/4✓ Branch 2 → 3 taken 1200 times.
✗ Branch 2 → 5 not taken.
✓ Branch 3 → 4 taken 1199 times.
✓ Branch 3 → 5 taken 1 time.
|
1200 | return bytes <= image_size && offset <= static_cast<std::uint64_t>(image_size) - bytes; |
| 43 | } | ||
| 44 | } // namespace | ||
| 45 | |||
| 46 | 606 | ImageIdentityFields image_identity_at(std::uintptr_t module_base) noexcept | |
| 47 | { | ||
| 48 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 606 times.
|
606 | if (!is_plausible_ptr(module_base)) |
| 49 | { | ||
| 50 | ✗ | return ImageIdentityFields{}; | |
| 51 | } | ||
| 52 | |||
| 53 | 606 | const std::optional<IMAGE_DOS_HEADER> dos = guarded_read<IMAGE_DOS_HEADER>(module_base); | |
| 54 |
4/8✓ Branch 7 → 8 taken 606 times.
✗ Branch 7 → 14 not taken.
✓ Branch 9 → 10 taken 606 times.
✗ Branch 9 → 14 not taken.
✓ Branch 11 → 12 taken 606 times.
✗ Branch 11 → 14 not taken.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 606 times.
|
1212 | if (!dos || dos->e_magic != IMAGE_DOS_SIGNATURE || dos->e_lfanew <= 0 || |
| 55 |
1/2✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 606 times.
|
606 | static_cast<std::uint32_t>(dos->e_lfanew) > MAX_NT_OFFSET) |
| 56 | { | ||
| 57 | ✗ | return ImageIdentityFields{}; | |
| 58 | } | ||
| 59 | |||
| 60 | 606 | const auto nt_offset = static_cast<std::uint32_t>(dos->e_lfanew); | |
| 61 | 606 | const std::optional<IMAGE_NT_HEADERS64> nt = guarded_read<IMAGE_NT_HEADERS64>(module_base + nt_offset); | |
| 62 |
1/2✓ Branch 23 → 24 taken 606 times.
✗ Branch 23 → 30 not taken.
|
1212 | if (!nt || nt->Signature != IMAGE_NT_SIGNATURE || |
| 63 |
1/2✓ Branch 25 → 26 taken 606 times.
✗ Branch 25 → 30 not taken.
|
606 | nt->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC || |
| 64 |
4/6✓ Branch 21 → 22 taken 606 times.
✗ Branch 21 → 30 not taken.
✓ Branch 27 → 28 taken 606 times.
✗ Branch 27 → 30 not taken.
✓ Branch 32 → 33 taken 3 times.
✓ Branch 32 → 34 taken 603 times.
|
1818 | nt->FileHeader.SizeOfOptionalHeader < sizeof(IMAGE_OPTIONAL_HEADER64) || |
| 65 |
2/2✓ Branch 29 → 30 taken 3 times.
✓ Branch 29 → 31 taken 603 times.
|
606 | nt->OptionalHeader.SizeOfImage == 0) |
| 66 | { | ||
| 67 | 3 | return ImageIdentityFields{}; | |
| 68 | } | ||
| 69 | |||
| 70 | // Every later address is bounded by the image size this header declares. Guarding contains a concurrent | ||
| 71 | // unmap fault, but does not pin the mapping; callers that authorize work across a transition revalidate. | ||
| 72 | 603 | const std::uint32_t image_size = nt->OptionalHeader.SizeOfImage; | |
| 73 |
1/2✗ Branch 36 → 37 not taken.
✓ Branch 36 → 38 taken 603 times.
|
603 | if (!fits_image(nt_offset, sizeof(IMAGE_NT_HEADERS64), image_size)) |
| 74 | { | ||
| 75 | ✗ | return ImageIdentityFields{}; | |
| 76 | } | ||
| 77 | |||
| 78 | 603 | const std::uint32_t num_sections = nt->FileHeader.NumberOfSections; | |
| 79 |
4/4✓ Branch 39 → 40 taken 598 times.
✓ Branch 39 → 41 taken 5 times.
✓ Branch 40 → 41 taken 1 time.
✓ Branch 40 → 42 taken 597 times.
|
603 | if (num_sections == 0 || num_sections > MAX_SECTIONS) |
| 80 | { | ||
| 81 | 6 | return ImageIdentityFields{}; | |
| 82 | } | ||
| 83 | |||
| 84 | // IMAGE_FIRST_SECTION: the section table starts immediately after the optional header, whose length is | ||
| 85 | // SizeOfOptionalHeader. Using sizeof(IMAGE_NT_HEADERS64) would misplace the table whenever that size | ||
| 86 | // differs from the compile-time struct size. | ||
| 87 | 597 | const std::uint64_t table_offset = static_cast<std::uint64_t>(nt_offset) + | |
| 88 | 597 | offsetof(IMAGE_NT_HEADERS64, OptionalHeader) + | |
| 89 | 597 | nt->FileHeader.SizeOfOptionalHeader; | |
| 90 | 597 | const std::uint64_t table_bytes = static_cast<std::uint64_t>(num_sections) * sizeof(IMAGE_SECTION_HEADER); | |
| 91 |
2/2✓ Branch 44 → 45 taken 1 time.
✓ Branch 44 → 46 taken 596 times.
|
597 | if (!fits_image(table_offset, table_bytes, image_size)) |
| 92 | { | ||
| 93 | 1 | return ImageIdentityFields{}; | |
| 94 | } | ||
| 95 | |||
| 96 | // One guarded read for the whole table rather than one per header keeps the fault-guard entry count bounded | ||
| 97 | // on a path that a warm TypeIdentity revalidates on every call. The buffer is the loader's own section cap, | ||
| 98 | // so it needs no heap. | ||
| 99 | alignas(alignof(IMAGE_SECTION_HEADER)) std::byte section_table[MAX_SECTIONS * sizeof(IMAGE_SECTION_HEADER)]; | ||
| 100 |
2/2✓ Branch 47 → 48 taken 1 time.
✓ Branch 47 → 49 taken 595 times.
|
596 | if (!guarded_read_bytes( |
| 101 | module_base + static_cast<std::uintptr_t>(table_offset), | ||
| 102 | section_table, | ||
| 103 | static_cast<std::size_t>(table_bytes) | ||
| 104 | )) | ||
| 105 | { | ||
| 106 | 1 | return ImageIdentityFields{}; | |
| 107 | } | ||
| 108 | |||
| 109 | 595 | std::uint64_t digest = mix(0x0DDC0FFEEULL, static_cast<std::uint64_t>(num_sections)); | |
| 110 |
2/2✓ Branch 59 → 51 taken 10237 times.
✓ Branch 59 → 60 taken 595 times.
|
10832 | for (std::uint32_t i = 0; i < num_sections; ++i) |
| 111 | { | ||
| 112 | 10237 | IMAGE_SECTION_HEADER section{}; | |
| 113 | 10237 | std::memcpy( | |
| 114 | §ion, | ||
| 115 | 10237 | section_table + static_cast<std::size_t>(i) * sizeof(IMAGE_SECTION_HEADER), | |
| 116 | sizeof(section) | ||
| 117 | ); | ||
| 118 | 10237 | std::uint64_t name = 0; | |
| 119 |
2/2✓ Branch 53 → 52 taken 81896 times.
✓ Branch 53 → 54 taken 10237 times.
|
92133 | for (std::size_t byte = 0; byte < IMAGE_SIZEOF_SHORT_NAME; ++byte) |
| 120 | { | ||
| 121 | 81896 | name |= static_cast<std::uint64_t>(section.Name[byte]) << (byte * 8); | |
| 122 | } | ||
| 123 | 10237 | digest = mix(digest, name); | |
| 124 | 10237 | digest = mix(digest, static_cast<std::uint64_t>(section.VirtualAddress)); | |
| 125 | 10237 | digest = mix(digest, static_cast<std::uint64_t>(section.Misc.VirtualSize)); | |
| 126 | 10237 | digest = mix(digest, static_cast<std::uint64_t>(section.Characteristics)); | |
| 127 | } | ||
| 128 | |||
| 129 | return ImageIdentityFields{ | ||
| 130 | 595 | .timestamp = nt->FileHeader.TimeDateStamp, | |
| 131 | .size_of_image = image_size, | ||
| 132 | .section_digest = digest, | ||
| 133 | .valid = true, | ||
| 134 | 595 | }; | |
| 135 | } | ||
| 136 | |||
| 137 | 51 | std::uint64_t image_generation_token(std::uintptr_t module_base) noexcept | |
| 138 | { | ||
| 139 | 51 | const ImageIdentityFields fields = image_identity_at(module_base); | |
| 140 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 51 times.
|
51 | if (!fields.valid) |
| 141 | { | ||
| 142 | ✗ | return 0; | |
| 143 | } | ||
| 144 | // The base separates two modules that carry byte-identical headers; the section digest separates | ||
| 145 | // layout-differing images the loader mapped at one base across an unload/reload. | ||
| 146 | 51 | std::uint64_t token = static_cast<std::uint64_t>(module_base); | |
| 147 | 51 | token = mix(token, static_cast<std::uint64_t>(fields.size_of_image)); | |
| 148 | 51 | token = mix(token, static_cast<std::uint64_t>(fields.timestamp)); | |
| 149 | 51 | token = mix(token, fields.section_digest); | |
| 150 |
1/2✓ Branch 8 → 9 taken 51 times.
✗ Branch 8 → 10 not taken.
|
51 | return token == 0 ? 1 : token; |
| 151 | } | ||
| 152 | } // namespace detail | ||
| 153 | } // namespace DetourModKit | ||
| 154 |