src/internal/hook_fault_boundary.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "internal/hook_fault_boundary.hpp" | ||
| 2 | |||
| 3 | #include "internal/memory_guarded.hpp" | ||
| 4 | #include "internal/scan_pages.hpp" | ||
| 5 | |||
| 6 | #include <windows.h> | ||
| 7 | |||
| 8 | #include <array> | ||
| 9 | #include <cstddef> | ||
| 10 | #include <cstdint> | ||
| 11 | #include <optional> | ||
| 12 | #include <string_view> | ||
| 13 | |||
| 14 | namespace DetourModKit | ||
| 15 | { | ||
| 16 | 21340 | std::optional<detail::FunctionBound> detail::unwind_bound(std::uintptr_t target) noexcept | |
| 17 | { | ||
| 18 | 21340 | DWORD64 image_base = 0; | |
| 19 | 21340 | const PRUNTIME_FUNCTION entry = RtlLookupFunctionEntry(target, &image_base, nullptr); | |
| 20 | 21340 | if (entry == nullptr || image_base == 0) | |
| 21 | { | ||
| 22 | 9308 | return std::nullopt; | |
| 23 | } | ||
| 24 | |||
| 25 | 12032 | RUNTIME_FUNCTION record{}; | |
| 26 | 12032 | if (!guarded_read_bytes(reinterpret_cast<std::uintptr_t>(entry), &record, sizeof(record))) | |
| 27 | { | ||
| 28 | ✗ | return std::nullopt; | |
| 29 | } | ||
| 30 | 12032 | if (record.EndAddress <= record.BeginAddress) | |
| 31 | { | ||
| 32 | ✗ | return std::nullopt; | |
| 33 | } | ||
| 34 | 12032 | const auto base = static_cast<std::uintptr_t>(image_base); | |
| 35 | 12032 | if (record.BeginAddress > UINTPTR_MAX - base || record.EndAddress > UINTPTR_MAX - base) | |
| 36 | { | ||
| 37 | ✗ | return std::nullopt; | |
| 38 | } | ||
| 39 | 12032 | const FunctionBound bound{base + record.BeginAddress, base + record.EndAddress}; | |
| 40 | 12032 | if (target < bound.lo || target >= bound.hi) | |
| 41 | { | ||
| 42 | ✗ | return std::nullopt; | |
| 43 | } | ||
| 44 | 12032 | return bound; | |
| 45 | } | ||
| 46 | |||
| 47 | namespace | ||
| 48 | { | ||
| 49 | /// The protection granularity of a mapped section on x86-64 Windows. | ||
| 50 | constexpr std::uintptr_t SECTION_PAGE_BYTES = 0x1000; | ||
| 51 | |||
| 52 | /** | ||
| 53 | * @brief Reports whether every page of [address, address + size) is committed image memory that is readable | ||
| 54 | * and either executable or writable. | ||
| 55 | * @details A patch window writes its page, so it leaves the page writable. A guard, no-access, read-only, or | ||
| 56 | * execute-only page keeps that protection until its owner changes it, so it is never a patch window. | ||
| 57 | */ | ||
| 58 | 318 | [[nodiscard]] bool image_pages_patchable(std::uintptr_t address, std::size_t size) noexcept | |
| 59 | { | ||
| 60 | 318 | constexpr DWORD PATCHABLE_PAGE_FLAGS = | |
| 61 | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY | PAGE_READWRITE | PAGE_WRITECOPY; | ||
| 62 | 318 | if (size > UINTPTR_MAX - address) | |
| 63 | { | ||
| 64 | ✗ | return false; | |
| 65 | } | ||
| 66 | 318 | const std::uintptr_t end = address + size; | |
| 67 | 623 | for (std::uintptr_t cursor = address; cursor < end;) | |
| 68 | { | ||
| 69 | 318 | MEMORY_BASIC_INFORMATION region{}; | |
| 70 | 318 | if (VirtualQuery(reinterpret_cast<LPCVOID>(cursor), ®ion, sizeof(region)) != sizeof(region) || | |
| 71 | 318 | region.State != MEM_COMMIT || region.Type != MEM_IMAGE || | |
| 72 | 943 | (region.Protect & (PAGE_GUARD | PAGE_NOACCESS)) != 0 || | |
| 73 | 307 | (region.Protect & PATCHABLE_PAGE_FLAGS) == 0) | |
| 74 | { | ||
| 75 | 13 | return false; | |
| 76 | } | ||
| 77 | 305 | const std::uintptr_t region_end = | |
| 78 | 305 | reinterpret_cast<std::uintptr_t>(region.BaseAddress) + region.RegionSize; | |
| 79 | 305 | if (region_end <= cursor) | |
| 80 | { | ||
| 81 | ✗ | return false; | |
| 82 | } | ||
| 83 | 305 | cursor = region_end; | |
| 84 | } | ||
| 85 | 305 | return true; | |
| 86 | } | ||
| 87 | |||
| 88 | /// Reports whether every byte of [address, address + size) lies in an executable section of one mapped image. | ||
| 89 | 305 | [[nodiscard]] bool image_sections_execute(std::uintptr_t address, std::size_t size) noexcept | |
| 90 | { | ||
| 91 | 305 | MEMORY_BASIC_INFORMATION region{}; | |
| 92 | 305 | if (VirtualQuery(reinterpret_cast<LPCVOID>(address), ®ion, sizeof(region)) != sizeof(region) || | |
| 93 | 305 | region.State != MEM_COMMIT || region.Type != MEM_IMAGE) | |
| 94 | { | ||
| 95 | ✗ | return false; | |
| 96 | } | ||
| 97 | 305 | const auto base = reinterpret_cast<std::uintptr_t>(region.AllocationBase); | |
| 98 | 305 | IMAGE_DOS_HEADER dos{}; | |
| 99 | 610 | if (!detail::guarded_read_bytes(base, &dos, sizeof(dos)) || dos.e_magic != IMAGE_DOS_SIGNATURE || | |
| 100 | 305 | dos.e_lfanew <= 0) | |
| 101 | { | ||
| 102 | ✗ | return false; | |
| 103 | } | ||
| 104 | 305 | const std::uintptr_t nt = base + static_cast<std::uintptr_t>(dos.e_lfanew); | |
| 105 | 305 | DWORD signature = 0; | |
| 106 | 305 | IMAGE_FILE_HEADER file{}; | |
| 107 | 610 | if (!detail::guarded_read_bytes(nt, &signature, sizeof(signature)) || signature != IMAGE_NT_SIGNATURE || | |
| 108 | 305 | !detail::guarded_read_bytes(nt + sizeof(signature), &file, sizeof(file))) | |
| 109 | { | ||
| 110 | ✗ | return false; | |
| 111 | } | ||
| 112 | 305 | const std::uintptr_t table = nt + offsetof(IMAGE_NT_HEADERS64, OptionalHeader) + file.SizeOfOptionalHeader; | |
| 113 | // Section headers ascend by address, so one pass covers a window that spans two adjacent sections. | ||
| 114 | 305 | std::uintptr_t cursor = address - base; | |
| 115 | 305 | const std::uintptr_t end = cursor + size; | |
| 116 | 915 | for (WORD index = 0; index < file.NumberOfSections && cursor < end; ++index) | |
| 117 | { | ||
| 118 | 618 | IMAGE_SECTION_HEADER section{}; | |
| 119 | 618 | if (!detail::guarded_read_bytes(table + index * sizeof(section), §ion, sizeof(section))) | |
| 120 | { | ||
| 121 | 8 | return false; | |
| 122 | } | ||
| 123 | 618 | const std::uintptr_t begin = section.VirtualAddress; | |
| 124 | 618 | const std::uintptr_t extent = | |
| 125 | 618 | (static_cast<std::uintptr_t>(section.Misc.VirtualSize) + SECTION_PAGE_BYTES - 1) & | |
| 126 | ~(SECTION_PAGE_BYTES - 1); | ||
| 127 | 618 | if (cursor < begin || cursor - begin >= extent) | |
| 128 | { | ||
| 129 | 313 | continue; | |
| 130 | } | ||
| 131 | 305 | if ((section.Characteristics & IMAGE_SCN_MEM_EXECUTE) == 0) | |
| 132 | { | ||
| 133 | 8 | return false; | |
| 134 | } | ||
| 135 | 297 | cursor = begin + extent; | |
| 136 | } | ||
| 137 | 297 | return cursor >= end; | |
| 138 | } | ||
| 139 | } // namespace | ||
| 140 | |||
| 141 | 17236 | detail::TargetWindowResult detail::validate_backend_steal_window(std::uintptr_t target) noexcept | |
| 142 | { | ||
| 143 | // The backend decodes forward with no region bound, so the whole window must be executable and committed. | ||
| 144 | 17236 | if (!is_executable_range(target, BACKEND_MAX_STEAL_WINDOW)) | |
| 145 | { | ||
| 146 | // Code runs only from executable pages, so a patcher restores execute access to an image section that | ||
| 147 | // carries it before that code runs again. | ||
| 148 | 623 | const TargetWindowVerdict verdict = image_pages_patchable(target, BACKEND_MAX_STEAL_WINDOW) && | |
| 149 | 305 | image_sections_execute(target, BACKEND_MAX_STEAL_WINDOW) | |
| 150 | 318 | ? TargetWindowVerdict::PatchInFlight | |
| 151 | 318 | : TargetWindowVerdict::NotExecutable; | |
| 152 | 318 | return TargetWindowResult{verdict, target}; | |
| 153 | } | ||
| 154 | |||
| 155 | // Only a guarded touch finds a guard page or a backing store that fails to fault in. | ||
| 156 | 16918 | std::array<std::uint8_t, BACKEND_MAX_STEAL_WINDOW> window{}; | |
| 157 | 16918 | volatile std::uintptr_t fault_address = target; | |
| 158 | 33836 | if (!guarded_read_bytes(target, window.data(), window.size(), &fault_address)) | |
| 159 | { | ||
| 160 | ✗ | return TargetWindowResult{TargetWindowVerdict::Unreadable, fault_address}; | |
| 161 | } | ||
| 162 | |||
| 163 | // The fallback can write more bytes than the near jump, and the backend picks the form after this check. | ||
| 164 | 16918 | const std::optional<FunctionBound> bound = unwind_bound(target); | |
| 165 | 16918 | if (bound && target + BACKEND_FALLBACK_MIN_PATCH > bound->hi) | |
| 166 | { | ||
| 167 | 2 | return TargetWindowResult{TargetWindowVerdict::BoundOverrun, bound->hi}; | |
| 168 | } | ||
| 169 | |||
| 170 | 16916 | return TargetWindowResult{TargetWindowVerdict::Ok, target}; | |
| 171 | } | ||
| 172 | |||
| 173 | 22 | std::string_view detail::target_window_description(TargetWindowVerdict verdict) noexcept | |
| 174 | { | ||
| 175 | 22 | switch (verdict) | |
| 176 | { | ||
| 177 | 21 | case TargetWindowVerdict::NotExecutable: | |
| 178 | 21 | return "the bytes the backend decodes are not all executable committed memory"; | |
| 179 | ✗ | case TargetWindowVerdict::Unreadable: | |
| 180 | ✗ | return "the bytes the backend decodes are not all readable"; | |
| 181 | 1 | case TargetWindowVerdict::BoundOverrun: | |
| 182 | 1 | return "the target function is shorter than the largest patch minimum the backend may select"; | |
| 183 | ✗ | case TargetWindowVerdict::PatchInFlight: | |
| 184 | ✗ | return "a patch window outside the process coordinator holds the target page without execute access"; | |
| 185 | ✗ | case TargetWindowVerdict::Ok: | |
| 186 | ✗ | return "the target is hookable"; | |
| 187 | } | ||
| 188 | ✗ | return "the target is hookable"; | |
| 189 | } | ||
| 190 | |||
| 191 | 295 | detail::ObjectWordResult detail::validate_vmt_object_word(std::uintptr_t object) noexcept | |
| 192 | { | ||
| 193 | // Readability first, and by touching rather than querying: VirtualQuery cannot report that a guard page will | ||
| 194 | // trap the first access. | ||
| 195 | 295 | volatile std::uintptr_t fault_address = object; | |
| 196 | 295 | std::uintptr_t vptr = 0; | |
| 197 | 295 | if (!guarded_read_bytes(object, &vptr, sizeof(vptr), &fault_address)) | |
| 198 | { | ||
| 199 | 24 | return ObjectWordResult{ObjectWordVerdict::Unreadable, fault_address, 0}; | |
| 200 | } | ||
| 201 | |||
| 202 | // Readable does not imply writable. There is no non-mutating write probe, so ask the OS and let the later | ||
| 203 | // fault-contained publication compare-exchange close a displacement/protection/unmap race. | ||
| 204 | 271 | MEMORY_BASIC_INFORMATION info{}; | |
| 205 | 271 | if (VirtualQuery(reinterpret_cast<LPCVOID>(object), &info, sizeof(info)) != sizeof(info)) | |
| 206 | { | ||
| 207 | ✗ | return ObjectWordResult{ObjectWordVerdict::NotWritable, object, vptr}; | |
| 208 | } | ||
| 209 | 271 | if (info.State != MEM_COMMIT) | |
| 210 | { | ||
| 211 | ✗ | return ObjectWordResult{ObjectWordVerdict::NotWritable, object, vptr}; | |
| 212 | } | ||
| 213 | // A guard armed before the read already failed it closed above. This catches one armed since: PAGE_GUARD traps | ||
| 214 | // the first access to a page whose protection otherwise reads as writable, so the word is not publishable even | ||
| 215 | // though its protection bits say it is. | ||
| 216 | 271 | if ((info.Protect & PAGE_GUARD) != 0) | |
| 217 | { | ||
| 218 | ✗ | return ObjectWordResult{ObjectWordVerdict::NotWritable, object, vptr}; | |
| 219 | } | ||
| 220 | 271 | constexpr DWORD WRITABLE_PROTECTIONS = | |
| 221 | PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY; | ||
| 222 | 271 | if ((info.Protect & WRITABLE_PROTECTIONS) == 0) | |
| 223 | { | ||
| 224 | 12 | return ObjectWordResult{ObjectWordVerdict::NotWritable, object, vptr}; | |
| 225 | } | ||
| 226 | |||
| 227 | // The word must not straddle two regions with different protections: VirtualQuery reports the region containing | ||
| 228 | // the first byte, so a pointer-sized word ending in a read-only neighbour would pass on its first byte alone. | ||
| 229 | // VirtualQuery places @p object inside the region it reports, so measuring the remainder as an offset from the | ||
| 230 | // region base keeps this arithmetic wrap-free without trusting the reported bounds not to overflow. | ||
| 231 | 259 | const std::uintptr_t offset_in_region = object - reinterpret_cast<std::uintptr_t>(info.BaseAddress); | |
| 232 | 259 | if (static_cast<std::uintptr_t>(info.RegionSize) - offset_in_region < sizeof(std::uintptr_t)) | |
| 233 | { | ||
| 234 | ✗ | return ObjectWordResult{ObjectWordVerdict::NotWritable, object, vptr}; | |
| 235 | } | ||
| 236 | |||
| 237 | 259 | return ObjectWordResult{ObjectWordVerdict::Ok, object, vptr}; | |
| 238 | } | ||
| 239 | } // namespace DetourModKit | ||
| 240 |