src/scan_string_xref.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /** | ||
| 2 | * @file scan_string_xref.cpp | ||
| 3 | * @brief String-reference (xref) anchor resolver: locate an immutable string literal in a module image, then resolve | ||
| 4 | * the unique instruction that references it. | ||
| 5 | * @details Two fail-closed phases. Phase 1 locates the single occurrence of the query string in the image's readable | ||
| 6 | * pages (the page-gated readable scan). Phase 2 finds the single RIP-relative reference to that string: a | ||
| 7 | * fast, desync-immune shape scan for the dominant lea/mov forms by default, plus Zydis-verified candidate | ||
| 8 | * discovery for opt-in broad matches and derived-return uniqueness confirmation. Both phases resolve through | ||
| 9 | * the private engine page primitives. Zydis is confined to this TU: no public header exposes a Zydis type. | ||
| 10 | */ | ||
| 11 | |||
| 12 | #include "DetourModKit/scan.hpp" | ||
| 13 | |||
| 14 | #include "internal/memory_fault.hpp" | ||
| 15 | #include "internal/memory_guarded.hpp" | ||
| 16 | #include "internal/scan_engine.hpp" | ||
| 17 | #include "internal/scan_exclusions.hpp" | ||
| 18 | #include "internal/scan_fault_seam.hpp" | ||
| 19 | #include "internal/scan_pages.hpp" | ||
| 20 | #include "internal/scan_shared.hpp" | ||
| 21 | |||
| 22 | #include "DetourModKit/logger.hpp" | ||
| 23 | #include "DetourModKit/memory.hpp" | ||
| 24 | |||
| 25 | #include <windows.h> | ||
| 26 | |||
| 27 | #include <Zydis/Zydis.h> | ||
| 28 | |||
| 29 | #include <array> | ||
| 30 | #include <cstddef> | ||
| 31 | #include <cstdint> | ||
| 32 | #include <cstring> | ||
| 33 | #include <new> | ||
| 34 | #include <optional> | ||
| 35 | #include <span> | ||
| 36 | #include <string> | ||
| 37 | #include <vector> | ||
| 38 | |||
| 39 | namespace DetourModKit | ||
| 40 | { | ||
| 41 | namespace scan | ||
| 42 | { | ||
| 43 | namespace | ||
| 44 | { | ||
| 45 | struct ReferenceScanResult | ||
| 46 | { | ||
| 47 | std::uintptr_t site = 0; | ||
| 48 | // Address of the reference's disp32 field. This, not the instruction start, is what identifies a | ||
| 49 | // reference across the two phases: the field address is a property of the reference itself and is | ||
| 50 | // identical in both, while the instruction start is a framing verdict the leaf/JIT probe may place | ||
| 51 | // earlier than the narrow shape scan does when a byte the decoder absorbs precedes the instruction. | ||
| 52 | std::uintptr_t key = 0; | ||
| 53 | // One past the last byte of the reference instruction @ref site frames. The whole instruction is the | ||
| 54 | // evidence a resolved value rides on, so a quorum member that witnesses any byte of it shares this | ||
| 55 | // reference's failure domain; publishing only the first byte would let a selector matching the rest of | ||
| 56 | // the same instruction abut the span instead of overlapping it. | ||
| 57 | std::uintptr_t site_end = 0; | ||
| 58 | std::size_t count = 0; | ||
| 59 | // This flag is true when the TOCTOU guard skipped an execute-readable window that faulted mid-sweep. | ||
| 60 | // The reference count is then only a lower bound, because a second reference to the string can hide | ||
| 61 | // in that window. A uniqueness verdict must then fail closed rather than report the lone observed | ||
| 62 | // reference as the unique site. merge_reference_scan accumulates the flag across the narrow and broad | ||
| 63 | // sweeps. | ||
| 64 | bool incomplete = false; | ||
| 65 | }; | ||
| 66 | |||
| 67 | // The matched narrow reference's lea destination register and instruction length, recovered alongside the | ||
| 68 | // unique-site search so the store-xref forward scan needs no second decode of the reference. Valid only | ||
| 69 | // when the unique reference is a REX.W `lea reg, [rip+string]` (is_lea == true); a `mov reg, [rip+string]` | ||
| 70 | // load already delivered the value to a register, so there is no store to model from the lea. reg is the | ||
| 71 | // 4-bit x86-64 register number (REX.R << 3 | ModRM.reg). | ||
| 72 | struct LeaReferenceInfo | ||
| 73 | { | ||
| 74 | std::uint8_t reg = 0; | ||
| 75 | std::size_t instr_len = 0; | ||
| 76 | std::uintptr_t window_end = 0; | ||
| 77 | bool is_lea = false; | ||
| 78 | }; | ||
| 79 | |||
| 80 | 854 | void merge_reference_scan( | |
| 81 | ReferenceScanResult &result, | ||
| 82 | std::uintptr_t site, | ||
| 83 | std::uintptr_t site_end, | ||
| 84 | std::uintptr_t key, | ||
| 85 | std::size_t count, | ||
| 86 | bool incomplete | ||
| 87 | ) noexcept | ||
| 88 | { | ||
| 89 | // Incompleteness is monotonic: once either sweep skipped a faulted window, the merged count is a lower | ||
| 90 | // bound regardless of what the other sweep found. | ||
| 91 | 854 | result.incomplete = result.incomplete || incomplete; | |
| 92 | 854 | if (count == 0) | |
| 93 | { | ||
| 94 | 23 | return; | |
| 95 | } | ||
| 96 | 831 | if (count >= 2) | |
| 97 | { | ||
| 98 | 7 | result.count = 2; | |
| 99 | 7 | result.site = 0; | |
| 100 | 7 | result.site_end = 0; | |
| 101 | 7 | result.key = 0; | |
| 102 | 7 | return; | |
| 103 | } | ||
| 104 | 824 | if (result.count == 0) | |
| 105 | { | ||
| 106 | 438 | result.site = site; | |
| 107 | 438 | result.site_end = site_end; | |
| 108 | 438 | result.key = key; | |
| 109 | 438 | result.count = 1; | |
| 110 | 438 | return; | |
| 111 | } | ||
| 112 | 386 | if (result.key != key) | |
| 113 | { | ||
| 114 | ✗ | result.site = 0; | |
| 115 | ✗ | result.site_end = 0; | |
| 116 | ✗ | result.key = 0; | |
| 117 | ✗ | result.count = 2; | |
| 118 | } | ||
| 119 | } | ||
| 120 | |||
| 121 | // Builds a literal-byte EnginePattern from a string query plus an optional trailing NUL, emitted as a hex | ||
| 122 | // AOB and run through parse_aob so the string scan reuses the exact same compiled-pattern path as every | ||
| 123 | // other AOB. | ||
| 124 | // | ||
| 125 | // StringEncoding::Utf8 searches for query.text byte for byte, so a caller may anchor on any byte sequence. | ||
| 126 | // StringEncoding::Utf16le searches for the UTF-16LE encoding of that text, which requires it to be | ||
| 127 | // well-formed UTF-8: each code point is transcoded, and a supplementary one becomes a surrogate pair. | ||
| 128 | // Byte-wise widening is correct only for ASCII and would search for a different literal otherwise. | ||
| 129 | // | ||
| 130 | // An embedded NUL is rejected on both routes: it contradicts require_terminator, cannot appear in the C | ||
| 131 | // string literals these anchors name, and would otherwise make the compiled pattern's terminator ambiguous. | ||
| 132 | // Every allocation failure throws std::bad_alloc, the allocation contract of both public entry points. | ||
| 133 | 786 | [[nodiscard]] Result<detail::EnginePattern> compile_string_pattern(const StringRefQuery &query) | |
| 134 | { | ||
| 135 | 15 | const auto malformed = []() -> Result<detail::EnginePattern> | |
| 136 | 15 | { return std::unexpected(Error{ErrorCode::MalformedQueryText, "scan::find_string_xref"}); }; | |
| 137 | 786 | if (query.text.find('\0') != std::string_view::npos) | |
| 138 | { | ||
| 139 | 4 | return malformed(); | |
| 140 | } | ||
| 141 | |||
| 142 | 782 | const bool wide = (query.encoding == StringEncoding::Utf16le); | |
| 143 | 782 | std::string aob; | |
| 144 | 782 | aob.reserve(query.text.size() * (wide ? 6 : 3) + 6); | |
| 145 | 15736 | const auto emit = [&aob](std::uint8_t byte) | |
| 146 | { | ||
| 147 | static constexpr char hex_digits[] = "0123456789ABCDEF"; | ||
| 148 | 15736 | aob.push_back(hex_digits[byte >> 4]); | |
| 149 | 15736 | aob.push_back(hex_digits[byte & 0x0F]); | |
| 150 | 15736 | aob.push_back(' '); | |
| 151 | 16515 | }; | |
| 152 | 23 | const auto emit_utf16_unit = [&emit](char16_t unit) | |
| 153 | { | ||
| 154 | 23 | emit(static_cast<std::uint8_t>(unit & 0xFFU)); | |
| 155 | 23 | emit(static_cast<std::uint8_t>((unit >> 8) & 0xFFU)); | |
| 156 | 802 | }; | |
| 157 | |||
| 158 | 779 | if (!wide) | |
| 159 | { | ||
| 160 | 15681 | for (const char ch : query.text) | |
| 161 | { | ||
| 162 | 14918 | emit(static_cast<std::uint8_t>(ch)); | |
| 163 | } | ||
| 164 | } | ||
| 165 | else | ||
| 166 | { | ||
| 167 | 16 | std::size_t pos = 0; | |
| 168 | 38 | while (pos < query.text.size()) | |
| 169 | { | ||
| 170 | 33 | char32_t code_point = 0; | |
| 171 | 33 | if (!detail::decode_utf8(query.text, pos, code_point)) | |
| 172 | { | ||
| 173 | 11 | return malformed(); | |
| 174 | } | ||
| 175 | 22 | if (code_point < 0x10000) | |
| 176 | { | ||
| 177 | 21 | emit_utf16_unit(static_cast<char16_t>(code_point)); | |
| 178 | } | ||
| 179 | else | ||
| 180 | { | ||
| 181 | 1 | const char32_t offset = code_point - 0x10000; | |
| 182 | 1 | emit_utf16_unit(static_cast<char16_t>(0xD800 + (offset >> 10))); | |
| 183 | 1 | emit_utf16_unit(static_cast<char16_t>(0xDC00 + (offset & 0x3FF))); | |
| 184 | } | ||
| 185 | } | ||
| 186 | } | ||
| 187 | |||
| 188 | 768 | if (query.require_terminator) | |
| 189 | { | ||
| 190 | 767 | emit(0x00); | |
| 191 | 767 | if (wide) | |
| 192 | { | ||
| 193 | 5 | emit(0x00); | |
| 194 | } | ||
| 195 | } | ||
| 196 | 768 | Result<detail::EnginePattern> pattern = detail::parse_aob(aob); | |
| 197 | 768 | if (pattern) | |
| 198 | { | ||
| 199 | 732 | return pattern; | |
| 200 | } | ||
| 201 | 36 | if (pattern.error().code == ErrorCode::OutOfMemory) | |
| 202 | { | ||
| 203 | 36 | throw std::bad_alloc{}; | |
| 204 | } | ||
| 205 | ✗ | return malformed(); | |
| 206 | 818 | } | |
| 207 | |||
| 208 | // Best-effort diagnosis for executable windows skipped because they faulted mid-scan. A module image is | ||
| 209 | // rarely decommitted under a live resolve, but collect_executable_windows only proves readability at gate | ||
| 210 | // time, so the window scans below guard their reads and a faulted window is skipped, not fatal. try_log is | ||
| 211 | // level-gated and no-throw. | ||
| 212 | 854 | void log_faulted_windows(std::size_t faulted_windows) noexcept | |
| 213 | { | ||
| 214 | 854 | if (faulted_windows == 0) | |
| 215 | { | ||
| 216 | 715 | return; | |
| 217 | } | ||
| 218 | 139 | (void)log().try_log( | |
| 219 | LogLevel::Debug, | ||
| 220 | "scan::find_string_xref: skipped {} executable window(s) that faulted mid-scan (concurrent " | ||
| 221 | "decommit/reprotect).", | ||
| 222 | faulted_windows | ||
| 223 | ); | ||
| 224 | } | ||
| 225 | |||
| 226 | // Inner narrow scan of one already-gated executable window (no fault guard). Mutates found_count / | ||
| 227 | // first_site, returning once a second referencing site is seen (found_count == 2) so the caller fails | ||
| 228 | // closed on ambiguity. The recognized instruction shape is documented on scan_string_ref_narrow. | ||
| 229 | 547 | void scan_window_narrow_body( | |
| 230 | const detail::ExecutableWindow &window, | ||
| 231 | std::uintptr_t string_addr, | ||
| 232 | std::size_t instr_len, | ||
| 233 | std::size_t &found_count, | ||
| 234 | std::uintptr_t &first_site, | ||
| 235 | LeaReferenceInfo *info | ||
| 236 | ) noexcept | ||
| 237 | { | ||
| 238 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 239 | // Inside the guard's frame, so an armed address the gated window does not cover proves the guard | ||
| 240 | // screens the faulting address rather than only the exception class. | ||
| 241 | 547 | detail::fire_scan_fault_seam_for_test( | |
| 242 | detail::g_scan_window_fault_for_test, | ||
| 243 | &detail::g_scan_window_fault_preparation_for_test | ||
| 244 | ); | ||
| 245 | #endif | ||
| 246 | 546 | const auto *bytes = reinterpret_cast<const std::uint8_t *>(window.base); | |
| 247 | 2071692 | for (std::size_t i = 0; i + instr_len <= window.span; ++i) | |
| 248 | { | ||
| 249 | 2071150 | const std::uint8_t rex = bytes[i]; | |
| 250 | 2071150 | const std::uint8_t opcode = bytes[i + 1]; | |
| 251 | 2071150 | const std::uint8_t modrm = bytes[i + 2]; | |
| 252 | 2071150 | if (rex < 0x48 || rex > 0x4F || (opcode != 0x8D && opcode != 0x8B) || (modrm & 0xC7) != 0x05) | |
| 253 | { | ||
| 254 | 2070712 | continue; | |
| 255 | } | ||
| 256 | 439 | std::int32_t disp = 0; | |
| 257 | 439 | std::memcpy(&disp, &bytes[i + 3], sizeof(disp)); | |
| 258 | 439 | const std::uintptr_t instr_addr = window.base + i; | |
| 259 | 439 | const std::uintptr_t target = | |
| 260 | 439 | instr_addr + instr_len + static_cast<std::uintptr_t>(static_cast<std::int64_t>(disp)); | |
| 261 | 439 | if (target != string_addr) | |
| 262 | { | ||
| 263 | 1 | continue; | |
| 264 | } | ||
| 265 | 438 | ++found_count; | |
| 266 | 438 | if (found_count == 1) | |
| 267 | { | ||
| 268 | 434 | first_site = instr_addr; | |
| 269 | 434 | if (info != nullptr) | |
| 270 | { | ||
| 271 | // REX.R (bit 2 of the REX byte) is the high bit of the ModRM.reg field; the narrow shape | ||
| 272 | // accepts REX in 0x48..0x4F, so REX.R may be set for an r8..r15 destination. opcode 0x8D is | ||
| 273 | // lea (a load whose pointer a following store can cache); 0x8B is a mov load with no such | ||
| 274 | // store to model. | ||
| 275 | 434 | const std::uint8_t reg = | |
| 276 | 434 | static_cast<std::uint8_t>(((rex & 0x04) << 1) | ((modrm >> 3) & 0x07)); | |
| 277 | 434 | *info = LeaReferenceInfo{reg, instr_len, window.base + window.span, opcode == 0x8D}; | |
| 278 | } | ||
| 279 | } | ||
| 280 | else | ||
| 281 | { | ||
| 282 | // Ambiguous; caller maps found_count >= 2 to AmbiguousReference. | ||
| 283 | 4 | return; | |
| 284 | } | ||
| 285 | } | ||
| 286 | } | ||
| 287 | |||
| 288 | // Window-granular TOCTOU fault guard around scan_window_narrow_body. collect_executable_windows gated each | ||
| 289 | // window with one VirtualQuery; a concurrent decommit / reprotect before these unguarded byte reads | ||
| 290 | // complete would otherwise fault the host. Both arms claim a fault only inside the gated window bytes, | ||
| 291 | // [window.base, window.base + window.span): on MSVC through detail::guarded_range_fault_filter, on MinGW | ||
| 292 | // x64 through the process-wide vectored read guard the guarded_read paths use (detail::run_guarded_region), | ||
| 293 | // armed over the same span. A fault outside it did not come from this window's reads, so it reaches the | ||
| 294 | // host's handlers rather than being recorded as a faulted window, exactly as the broad sibling below | ||
| 295 | // already does. A 32-bit build is rejected by the defines.hpp architecture gate, so only the two x64 arms | ||
| 296 | // exist. Returns true when a fault was swallowed. | ||
| 297 | 547 | bool scan_window_narrow_guarded( | |
| 298 | const detail::ExecutableWindow &window, | ||
| 299 | std::uintptr_t string_addr, | ||
| 300 | std::size_t instr_len, | ||
| 301 | std::size_t &found_count, | ||
| 302 | std::uintptr_t &first_site, | ||
| 303 | LeaReferenceInfo *info | ||
| 304 | ) noexcept | ||
| 305 | { | ||
| 306 | #ifdef _MSC_VER | ||
| 307 | const std::size_t original_found_count = found_count; | ||
| 308 | const std::uintptr_t original_first_site = first_site; | ||
| 309 | const LeaReferenceInfo original_info = (info != nullptr) ? *info : LeaReferenceInfo{}; | ||
| 310 | __try | ||
| 311 | { | ||
| 312 | scan_window_narrow_body(window, string_addr, instr_len, found_count, first_site, info); | ||
| 313 | return false; | ||
| 314 | } | ||
| 315 | __except ( | ||
| 316 | detail::guarded_range_fault_filter( | ||
| 317 | GetExceptionInformation(), | ||
| 318 | window.base, | ||
| 319 | window.base + window.span | ||
| 320 | ) | ||
| 321 | ) | ||
| 322 | { | ||
| 323 | // The caller skips faulted windows, so discard any reference count (and recovered lea info) | ||
| 324 | // collected before the fault, or a partially-scanned window could leak a stale site/register. | ||
| 325 | found_count = original_found_count; | ||
| 326 | first_site = original_first_site; | ||
| 327 | if (info != nullptr) | ||
| 328 | { | ||
| 329 | *info = original_info; | ||
| 330 | } | ||
| 331 | return true; | ||
| 332 | } | ||
| 333 | #elif defined(_WIN64) | ||
| 334 | // MinGW x64: arm the process-wide vectored read guard over exactly the bytes the window gate proved | ||
| 335 | // readable, so a concurrent decommit / reprotect that faults the scan is swallowed and the window | ||
| 336 | // reported faulted. That is the same skip-the-window contract that the MSVC __except arm follows. | ||
| 337 | 547 | const std::size_t original_found_count = found_count; | |
| 338 | 547 | const std::uintptr_t original_first_site = first_site; | |
| 339 | 547 | const LeaReferenceInfo original_info = (info != nullptr) ? *info : LeaReferenceInfo{}; | |
| 340 | struct NarrowScanContext | ||
| 341 | { | ||
| 342 | const detail::ExecutableWindow *window; | ||
| 343 | std::uintptr_t string_addr; | ||
| 344 | std::size_t instr_len; | ||
| 345 | std::size_t *found_count; | ||
| 346 | std::uintptr_t *first_site; | ||
| 347 | LeaReferenceInfo *info; | ||
| 348 | 547 | } scan_ctx{&window, string_addr, instr_len, &found_count, &first_site, info}; | |
| 349 | |||
| 350 | 547 | const auto run_scan = [](void *opaque) noexcept -> void | |
| 351 | { | ||
| 352 | 547 | auto *context = static_cast<NarrowScanContext *>(opaque); | |
| 353 | 547 | scan_window_narrow_body( | |
| 354 | 547 | *context->window, | |
| 355 | context->string_addr, | ||
| 356 | context->instr_len, | ||
| 357 | 547 | *context->found_count, | |
| 358 | 547 | *context->first_site, | |
| 359 | context->info | ||
| 360 | ); | ||
| 361 | 478 | }; | |
| 362 | |||
| 363 | 547 | if (detail::run_guarded_region(window.base, window.base + window.span, run_scan, &scan_ctx)) | |
| 364 | { | ||
| 365 | 478 | return false; | |
| 366 | } | ||
| 367 | // Faulted: discard any partial count / site / lea info so a partially-scanned window cannot leak a | ||
| 368 | // stale site or register, exactly as the MSVC arm does. | ||
| 369 | 69 | found_count = original_found_count; | |
| 370 | 69 | first_site = original_first_site; | |
| 371 | 69 | if (info != nullptr) | |
| 372 | { | ||
| 373 | 69 | *info = original_info; | |
| 374 | } | ||
| 375 | 69 | return true; | |
| 376 | #endif | ||
| 377 | } | ||
| 378 | |||
| 379 | // The narrow phase sweeps execute-readable windows for dominant REX.W RIP-relative loads that resolve to | ||
| 380 | // string_addr. | ||
| 381 | // | ||
| 382 | // The accepted seven-byte shape uses REX.W LEA/MOV with a RIP-relative ModRM byte and disp32. The disp32 | ||
| 383 | // starts at offset 3. Exact target equality accepts a reference. The second hit marks ambiguity. `[B-63]` | ||
| 384 | // owns shapes outside this tier. | ||
| 385 | 453 | std::uintptr_t scan_string_ref_narrow( | |
| 386 | std::uintptr_t string_addr, | ||
| 387 | std::span<const detail::ExecutableWindow> windows, | ||
| 388 | std::size_t &found_count, | ||
| 389 | LeaReferenceInfo &info, | ||
| 390 | bool &incomplete | ||
| 391 | ) | ||
| 392 | { | ||
| 393 | 453 | found_count = 0; | |
| 394 | 453 | incomplete = false; | |
| 395 | 453 | std::uintptr_t first_site = 0; | |
| 396 | 453 | info = LeaReferenceInfo{}; | |
| 397 | // REX.W + opcode + ModRM + disp32. | ||
| 398 | 453 | constexpr std::size_t instr_len = 7; | |
| 399 | // scan_window_narrow_body reads bytes[i], bytes[i+1], bytes[i+2] and a disp32 at bytes[i+3..i+6], so | ||
| 400 | // the highest index it touches is i+6. The per-window loop only bounds i + instr_len <= span, so | ||
| 401 | // instr_len must cover that widest read or the disp32 fetch could run up to four bytes past the window. | ||
| 402 | // Pin the coupling here, beside the shape's byte count, so a future instr_len change cannot silently | ||
| 403 | // reopen it. | ||
| 404 | 453 | constexpr std::size_t narrow_max_read_index = 6; | |
| 405 | static_assert( | ||
| 406 | narrow_max_read_index < instr_len, | ||
| 407 | "instr_len must span scan_window_narrow_body's disp32 tail read at bytes[i+3..i+6]" | ||
| 408 | ); | ||
| 409 | 453 | std::size_t faulted_windows = 0; | |
| 410 | |||
| 411 | // Adjacent execute windows require back-carry from the prior window. Without it, a split instruction | ||
| 412 | // fits neither window and can create false uniqueness. When windows abut, the next scan includes at | ||
| 413 | // most instr_len - 1 prior bytes. The earlier window collection applied the readability gate to those | ||
| 414 | // bytes. A carried start never fits the prior sweep, so no duplicate count arises. The | ||
| 415 | // StringXrefTest.NarrowReferenceStraddlingProtectionSplitIsFound and | ||
| 416 | // NarrowTwoReferencesOneStraddlingSplitAreAmbiguous cases pin both outcomes. | ||
| 417 | 453 | std::uintptr_t prev_end = 0; | |
| 418 | 453 | std::size_t prev_span = 0; | |
| 419 | 453 | bool have_prev = false; | |
| 420 | 1449 | for (const detail::ExecutableWindow &window : windows) | |
| 421 | { | ||
| 422 | 547 | detail::ExecutableWindow effective = window; | |
| 423 | 547 | if (have_prev && window.base == prev_end) | |
| 424 | { | ||
| 425 | 4 | const std::size_t carry = (instr_len - 1 < prev_span) ? instr_len - 1 : prev_span; | |
| 426 | 4 | effective.base = window.base - carry; | |
| 427 | 4 | effective.span = window.span + carry; | |
| 428 | } | ||
| 429 | 547 | prev_end = window.base + window.span; | |
| 430 | 547 | prev_span = window.span; | |
| 431 | 547 | have_prev = true; | |
| 432 | |||
| 433 | 547 | if (effective.span < instr_len) | |
| 434 | { | ||
| 435 | 69 | continue; | |
| 436 | } | ||
| 437 | 547 | if (scan_window_narrow_guarded(effective, string_addr, instr_len, found_count, first_site, &info)) | |
| 438 | { | ||
| 439 | 69 | ++faulted_windows; | |
| 440 | 69 | continue; | |
| 441 | } | ||
| 442 | 478 | if (found_count >= 2) | |
| 443 | { | ||
| 444 | // resolve_string_xref maps a merged count of 2 or more to AmbiguousReference. | ||
| 445 | 4 | incomplete = faulted_windows > 0; | |
| 446 | 4 | log_faulted_windows(faulted_windows); | |
| 447 | 4 | return 0; | |
| 448 | } | ||
| 449 | } | ||
| 450 | // A skipped faulted window makes the count a lower bound. Surface it so that resolve_string_xref never | ||
| 451 | // commits a lone surviving reference as unique. It fails closed on the truncation itself. | ||
| 452 | 449 | incomplete = faulted_windows > 0; | |
| 453 | 449 | log_faulted_windows(faulted_windows); | |
| 454 | 449 | return (found_count == 1) ? first_site : 0; | |
| 455 | } | ||
| 456 | |||
| 457 | // `[B-76]` defines the terminal predicate for forward value-attribution scans. RET, unconditional branches, | ||
| 458 | // and the SYSRET category stop by category. INT3, UD2, and UIRET stop by mnemonic because Zydis places them | ||
| 459 | // elsewhere. None has a fall-through path. A conditional branch keeps its fall-through path. The | ||
| 460 | // StringXrefTest.StringPointerSlotStopsAt* and StringPointerSlotContinuesPastConditionalBranch cases pin | ||
| 461 | // these choices. | ||
| 462 | 270 | [[nodiscard]] bool is_non_fall_through_terminal(const ZydisDecodedInstruction &insn) noexcept | |
| 463 | { | ||
| 464 | 269 | return insn.meta.category == ZYDIS_CATEGORY_RET || insn.meta.category == ZYDIS_CATEGORY_UNCOND_BR || | |
| 465 | 268 | insn.meta.category == ZYDIS_CATEGORY_SYSRET || insn.mnemonic == ZYDIS_MNEMONIC_INT3 || | |
| 466 | 539 | insn.mnemonic == ZYDIS_MNEMONIC_UD2 || insn.mnemonic == ZYDIS_MNEMONIC_UIRET; | |
| 467 | } | ||
| 468 | |||
| 469 | // The store-slot scan decodes forward after `lea reg, [rip+string]`. | ||
| 470 | // It accepts the first `REX.W MOV [rip+disp32], reg64` store within the caller's tight window. | ||
| 471 | // | ||
| 472 | // store_end marks one past the accepted store. The complete decoded span forms the evidence for selector | ||
| 473 | // failure-domain overlap. | ||
| 474 | 17 | std::uintptr_t scan_store_slot_after_lea( | |
| 475 | std::uintptr_t lea_site, | ||
| 476 | std::size_t lea_len, | ||
| 477 | std::uintptr_t window_end, | ||
| 478 | std::uint8_t lea_reg, | ||
| 479 | detail::ModuleSpan range, | ||
| 480 | std::uintptr_t &store_end | ||
| 481 | ) noexcept | ||
| 482 | { | ||
| 483 | 17 | store_end = 0; | |
| 484 | // A cached pointer is stored very close to its load; bound the forward scan so a pathological region | ||
| 485 | // cannot scan unboundedly and the store cannot be attributed to a distant, unrelated reuse of the same | ||
| 486 | // register. | ||
| 487 | 17 | constexpr std::size_t forward_window = 0x80; // 128 bytes. | |
| 488 | 17 | const std::uintptr_t scan_lo = lea_site + lea_len; | |
| 489 | 17 | if (scan_lo < lea_site) | |
| 490 | { | ||
| 491 | ✗ | return 0; | |
| 492 | } | ||
| 493 | 17 | const std::uintptr_t scan_end = (window_end < range.end) ? window_end : range.end; | |
| 494 | 17 | if (scan_lo >= scan_end) | |
| 495 | { | ||
| 496 | ✗ | return 0; | |
| 497 | } | ||
| 498 | 17 | const std::uintptr_t scan_hi = | |
| 499 | 17 | (scan_end - scan_lo < forward_window) ? scan_end : scan_lo + forward_window; | |
| 500 | |||
| 501 | ZydisDecoder decoder; | ||
| 502 | 17 | if (!ZYAN_SUCCESS(ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64))) | |
| 503 | { | ||
| 504 | ✗ | return 0; | |
| 505 | } | ||
| 506 | // The lea wrote a 64-bit pointer, so the store source and any clobber are tested against the full | ||
| 507 | // 64-bit register. ZydisRegisterEncode maps the x86 register number (REX.R << 3 | ModRM.reg) to the | ||
| 508 | // GPR64 register. | ||
| 509 | 17 | const ZydisRegister target_reg = ZydisRegisterEncode(ZYDIS_REGCLASS_GPR64, lea_reg); | |
| 510 | 17 | if (target_reg == ZYDIS_REGISTER_NONE) | |
| 511 | { | ||
| 512 | ✗ | return 0; | |
| 513 | } | ||
| 514 | |||
| 515 | 17 | std::uintptr_t p = scan_lo; | |
| 516 | 279 | while (p < scan_hi) | |
| 517 | { | ||
| 518 | 276 | std::array<std::uint8_t, ZYDIS_MAX_INSTRUCTION_LENGTH> code{}; | |
| 519 | 276 | const std::size_t avail = | |
| 520 | 276 | (scan_hi - p < code.size()) ? static_cast<std::size_t>(scan_hi - p) : code.size(); | |
| 521 | 276 | if (!detail::guarded_read_bytes(p, code.data(), avail)) | |
| 522 | { | ||
| 523 | // Unreadable byte: the store sits in the same execute-readable window as the lea, so a fault | ||
| 524 | // here means it is not present in mapped code. | ||
| 525 | 14 | return 0; | |
| 526 | } | ||
| 527 | |||
| 528 | ZydisDecodedInstruction insn; | ||
| 529 | ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT]; | ||
| 530 | 276 | if (!ZYAN_SUCCESS(ZydisDecoderDecodeFull(&decoder, code.data(), avail, &insn, operands))) | |
| 531 | { | ||
| 532 | // The store must be instruction-aligned with the lea; a decode failure means alignment was lost | ||
| 533 | // or the bytes are not code. Fail closed rather than accept a misaligned match. | ||
| 534 | 2 | return 0; | |
| 535 | } | ||
| 536 | |||
| 537 | // The store: MOV with a RIP-relative memory destination (operand 0) and the matching 64-bit source | ||
| 538 | // register (operand 1). ZydisCalcAbsoluteAddress turns the destination into the slot's effective | ||
| 539 | // address (next-instruction address + sign-extended disp32). | ||
| 540 | 274 | if (insn.mnemonic == ZYDIS_MNEMONIC_MOV && insn.operand_count_visible >= 2 && | |
| 541 | 7 | operands[0].type == ZYDIS_OPERAND_TYPE_MEMORY && operands[0].mem.base == ZYDIS_REGISTER_RIP && | |
| 542 | 5 | operands[1].type == ZYDIS_OPERAND_TYPE_REGISTER && operands[1].reg.value == target_reg) | |
| 543 | { | ||
| 544 | 4 | ZyanU64 absolute = 0; | |
| 545 | 4 | if (ZYAN_SUCCESS( | |
| 546 | ZydisCalcAbsoluteAddress(&insn, &operands[0], static_cast<ZyanU64>(p), &absolute) | ||
| 547 | )) | ||
| 548 | { | ||
| 549 | 4 | store_end = p + insn.length; | |
| 550 | 4 | return static_cast<std::uintptr_t>(absolute); | |
| 551 | } | ||
| 552 | ✗ | return 0; | |
| 553 | } | ||
| 554 | |||
| 555 | // A CALL clobbers the caller-saved registers; a store after it cannot be trusted to cache this | ||
| 556 | // lea's pointer, so stop conservatively rather than attribute a post-call store to the wrong value. | ||
| 557 | 270 | if (insn.meta.category == ZYDIS_CATEGORY_CALL) | |
| 558 | { | ||
| 559 | ✗ | return 0; | |
| 560 | } | ||
| 561 | // A store past a non-fall-through terminal belongs to a different function or an unreachable | ||
| 562 | // path, so fail closed rather than attribute it to this lea. | ||
| 563 | 270 | if (is_non_fall_through_terminal(insn)) | |
| 564 | { | ||
| 565 | 7 | return 0; | |
| 566 | } | ||
| 567 | // Any write to the loaded register (at any width: a 32-bit write zeroes the upper half) means a | ||
| 568 | // later store would cache a different value. Check every operand, including implicit ones. | ||
| 569 | 1047 | for (std::size_t op = 0; op < insn.operand_count; ++op) | |
| 570 | { | ||
| 571 | 785 | const ZydisDecodedOperand &operand = operands[op]; | |
| 572 | 2093 | if (operand.type == ZYDIS_OPERAND_TYPE_REGISTER && | |
| 573 | 1047 | (operand.actions & ZYDIS_OPERAND_ACTION_MASK_WRITE) != 0 && | |
| 574 | 262 | ZydisRegisterGetLargestEnclosing(ZYDIS_MACHINE_MODE_LONG_64, operand.reg.value) == | |
| 575 | target_reg) | ||
| 576 | { | ||
| 577 | 1 | return 0; | |
| 578 | } | ||
| 579 | } | ||
| 580 | |||
| 581 | 262 | p += insn.length; | |
| 582 | } | ||
| 583 | 3 | return 0; | |
| 584 | } | ||
| 585 | |||
| 586 | // Most bytes an x86-64 instruction can place before its disp32 field: up to four legacy prefixes, a REX | ||
| 587 | // byte, a three-byte opcode escape, and the ModRM byte the displacement directly follows. Bounding the | ||
| 588 | // leaf/JIT backward probe by this is what keeps candidate verification constant-cost. | ||
| 589 | constexpr std::size_t MAX_BYTES_BEFORE_DISP32 = 9; | ||
| 590 | |||
| 591 | // Instructions decoded while walking a trusted function stream forward to reach one candidate. A real | ||
| 592 | // function cannot exceed this before the walk is worth abandoning, and the bound stops a corrupt or hostile | ||
| 593 | // .pdata record from turning one candidate into an unbounded decode. | ||
| 594 | constexpr std::size_t MAX_TRUSTED_STREAM_STEPS = 8192; | ||
| 595 | |||
| 596 | // True when @p insn has a visible memory operand based on RIP whose absolute target is @p string_addr, and | ||
| 597 | // that operand's displacement field sits exactly at @p disp_field. Visible operands are ordered first in | ||
| 598 | // the array, so iterating the visible count covers every explicit operand a disassembler would show. The | ||
| 599 | // displacement-position check is what makes this a verification of a specific candidate rather than a | ||
| 600 | // second, independent search: a decode that happens to reference the string through some other field does | ||
| 601 | // not confirm the framing under test. | ||
| 602 | 3891 | bool instruction_references_at( | |
| 603 | const ZydisDecodedInstruction &insn, | ||
| 604 | const ZydisDecodedOperand *operands, | ||
| 605 | std::uintptr_t instr_addr, | ||
| 606 | std::uintptr_t disp_field, | ||
| 607 | std::uintptr_t string_addr | ||
| 608 | ) noexcept | ||
| 609 | { | ||
| 610 | 3891 | if (insn.raw.disp.size != 32 || instr_addr + insn.raw.disp.offset != disp_field) | |
| 611 | { | ||
| 612 | 3490 | return false; | |
| 613 | } | ||
| 614 | 794 | for (std::size_t op = 0; op < insn.operand_count_visible; ++op) | |
| 615 | { | ||
| 616 | 794 | const ZydisDecodedOperand &operand = operands[op]; | |
| 617 | 794 | if (operand.type != ZYDIS_OPERAND_TYPE_MEMORY || operand.mem.base != ZYDIS_REGISTER_RIP) | |
| 618 | { | ||
| 619 | 393 | continue; | |
| 620 | } | ||
| 621 | 401 | ZyanU64 absolute = 0; | |
| 622 | 401 | if (ZYAN_SUCCESS( | |
| 623 | ZydisCalcAbsoluteAddress(&insn, &operand, static_cast<ZyanU64>(instr_addr), &absolute) | ||
| 624 | 802 | ) && | |
| 625 | 401 | static_cast<std::uintptr_t>(absolute) == string_addr) | |
| 626 | { | ||
| 627 | 401 | return true; | |
| 628 | } | ||
| 629 | } | ||
| 630 | ✗ | return false; | |
| 631 | } | ||
| 632 | |||
| 633 | // Resolves the candidate at @p disp_field against a decode stream that starts at a trusted boundary. The | ||
| 634 | // innermost .pdata RUNTIME_FUNCTION's BeginAddress is a real instruction boundary by construction, so | ||
| 635 | // decoding forward from it reaches @p disp_field synchronized with the compiler's own framing. | ||
| 636 | // | ||
| 637 | // Three outcomes, and the difference between the last two is load-bearing. A nonzero value is the | ||
| 638 | // referencing instruction's address. Zero means the stream REACHED the candidate and rejected it, so | ||
| 639 | // probing a shorter inner framing would contradict a boundary the compiler itself declared. No value means | ||
| 640 | // the stream produced no verdict at all: no record covers the candidate, the record is unreadable, the | ||
| 641 | // function begins outside the bytes this window proved readable, or the decode broke or ran out of budget | ||
| 642 | // before arriving. Treating no verdict as a rejection is what would let one undecodable byte (an embedded | ||
| 643 | // jump table is the common case) suppress every reference after it in the same function, which is the | ||
| 644 | // failure mode this whole discovery path exists to prevent, so those cases fall through to the probe. | ||
| 645 | 543 | std::optional<std::uintptr_t> resolve_candidate_from_trusted_origin( | |
| 646 | const ZydisDecoder &decoder, | ||
| 647 | const detail::ExecutableWindow &window, | ||
| 648 | std::uintptr_t disp_field, | ||
| 649 | std::uintptr_t string_addr | ||
| 650 | ) noexcept | ||
| 651 | { | ||
| 652 | 543 | DWORD64 image_base = 0; | |
| 653 | 543 | const PRUNTIME_FUNCTION entry = RtlLookupFunctionEntry(disp_field, &image_base, nullptr); | |
| 654 | 543 | if (entry == nullptr || image_base == 0) | |
| 655 | { | ||
| 656 | 516 | return std::nullopt; | |
| 657 | } | ||
| 658 | 27 | RUNTIME_FUNCTION record{}; | |
| 659 | 27 | if (!detail::guarded_read_bytes(reinterpret_cast<std::uintptr_t>(entry), &record, sizeof(record))) | |
| 660 | { | ||
| 661 | // The record exists but could not be read, so it adjudicates nothing. | ||
| 662 | ✗ | return std::nullopt; | |
| 663 | } | ||
| 664 | 27 | const std::uintptr_t origin = static_cast<std::uintptr_t>(image_base) + record.BeginAddress; | |
| 665 | 27 | if (origin < window.base || origin > disp_field) | |
| 666 | { | ||
| 667 | // Decoding must start inside bytes this window already proved readable, and a record that does not | ||
| 668 | // actually precede the candidate cannot describe its stream. A function whose entry lies in an | ||
| 669 | // earlier window is ordinary, not suspicious, so this yields no verdict rather than a rejection. | ||
| 670 | ✗ | return std::nullopt; | |
| 671 | } | ||
| 672 | |||
| 673 | 27 | const auto *bytes = reinterpret_cast<const std::uint8_t *>(window.base); | |
| 674 | 27 | std::uintptr_t cursor = origin; | |
| 675 | 53567 | for (std::size_t step = 0; step < MAX_TRUSTED_STREAM_STEPS; ++step) | |
| 676 | { | ||
| 677 | 53567 | if (cursor >= window.base + window.span) | |
| 678 | { | ||
| 679 | // The stream left the readable window before arriving, so it never saw the candidate. | ||
| 680 | 27 | return std::nullopt; | |
| 681 | } | ||
| 682 | ZydisDecodedInstruction insn; | ||
| 683 | ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT]; | ||
| 684 | 53567 | const std::size_t offset = static_cast<std::size_t>(cursor - window.base); | |
| 685 | 53567 | if (!ZYAN_SUCCESS( | |
| 686 | ZydisDecoderDecodeFull(&decoder, bytes + offset, window.span - offset, &insn, operands) | ||
| 687 | )) | ||
| 688 | { | ||
| 689 | // Real functions carry undecodable bytes: an embedded jump table, compiler padding, or data a | ||
| 690 | // switch lowered into .text. The stream stops there and adjudicates nothing beyond it. | ||
| 691 | 1 | return std::nullopt; | |
| 692 | } | ||
| 693 | 53566 | if (cursor + insn.length > disp_field) | |
| 694 | { | ||
| 695 | // Arrived. This instruction is the compiler's own framing of the candidate's bytes, so its | ||
| 696 | // verdict is final in both directions. | ||
| 697 | 26 | return instruction_references_at(insn, operands, cursor, disp_field, string_addr) ? cursor : 0; | |
| 698 | } | ||
| 699 | 53540 | cursor += insn.length; | |
| 700 | } | ||
| 701 | ✗ | return std::nullopt; | |
| 702 | } | ||
| 703 | |||
| 704 | // Resolves the candidate at @p disp_field without a trusted origin: leaf functions carry no unwind data, | ||
| 705 | // and JIT or raw code buffers have no exception table at all. Every instruction start that could place a | ||
| 706 | // disp32 at the candidate is probed, so no single framing can suppress the reference. | ||
| 707 | // | ||
| 708 | // Probing runs from the earliest possible start toward the candidate, so the LONGEST accepted framing wins. | ||
| 709 | // That is the disambiguation rule, not a preference: an instruction's encoding includes its legacy prefixes | ||
| 710 | // and REX byte, so a start that skips one decodes a different instruction that merely happens to share the | ||
| 711 | // displacement. Reporting the shorter framing would put the site one byte past the instruction and disagree | ||
| 712 | // with the exact narrow shape scan for the very shapes both can see. | ||
| 713 | // | ||
| 714 | // The converse is possible too and is not a defect here: an unrelated byte before the true start that the | ||
| 715 | // decoder absorbs as a legacy prefix or a superseded REX yields an earlier accepted framing than the narrow | ||
| 716 | // shape scan reports. The two phases therefore identify a reference by its displacement FIELD, not by the | ||
| 717 | // instruction start, so a framing disagreement over one genuine reference cannot manufacture ambiguity. | ||
| 718 | 517 | std::uintptr_t resolve_candidate_by_probe( | |
| 719 | const ZydisDecoder &decoder, | ||
| 720 | const detail::ExecutableWindow &window, | ||
| 721 | std::uintptr_t disp_field, | ||
| 722 | std::uintptr_t string_addr | ||
| 723 | ) noexcept | ||
| 724 | { | ||
| 725 | 517 | const auto *bytes = reinterpret_cast<const std::uint8_t *>(window.base); | |
| 726 | 517 | const std::uintptr_t floor = (disp_field - window.base >= MAX_BYTES_BEFORE_DISP32) | |
| 727 | 517 | ? disp_field - MAX_BYTES_BEFORE_DISP32 | |
| 728 | : window.base; | ||
| 729 | 3992 | for (std::uintptr_t start = floor; start < disp_field; ++start) | |
| 730 | { | ||
| 731 | ZydisDecodedInstruction insn; | ||
| 732 | ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT]; | ||
| 733 | 3868 | const std::size_t offset = static_cast<std::size_t>(start - window.base); | |
| 734 | 3868 | if (!ZYAN_SUCCESS( | |
| 735 | ZydisDecoderDecodeFull(&decoder, bytes + offset, window.span - offset, &insn, operands) | ||
| 736 | )) | ||
| 737 | { | ||
| 738 | 3 | continue; | |
| 739 | } | ||
| 740 | 3865 | if (instruction_references_at(insn, operands, start, disp_field, string_addr)) | |
| 741 | { | ||
| 742 | 393 | return start; | |
| 743 | } | ||
| 744 | } | ||
| 745 | 124 | return 0; | |
| 746 | } | ||
| 747 | |||
| 748 | // Inner broad scan of one already-gated executable window (no fault guard). Mutates found_count / | ||
| 749 | // first_site and returns once a second referencing site is seen. The discovery/verification contract is | ||
| 750 | // documented on scan_string_ref_broad. | ||
| 751 | 490 | void scan_window_broad_body( | |
| 752 | const ZydisDecoder &decoder, | ||
| 753 | const detail::ExecutableWindow &window, | ||
| 754 | std::uintptr_t string_addr, | ||
| 755 | std::uintptr_t count_floor, | ||
| 756 | std::size_t &found_count, | ||
| 757 | std::uintptr_t &first_site, | ||
| 758 | std::uintptr_t &first_end, | ||
| 759 | std::uintptr_t &first_key | ||
| 760 | ) noexcept | ||
| 761 | { | ||
| 762 | 490 | if (window.span < sizeof(std::int32_t)) | |
| 763 | { | ||
| 764 | ✗ | return; | |
| 765 | } | ||
| 766 | 490 | const auto *bytes = reinterpret_cast<const std::uint8_t *>(window.base); | |
| 767 | 490 | const std::size_t limit = window.span - sizeof(std::int32_t); | |
| 768 | 1846649 | for (std::size_t i = 0; i <= limit; ++i) | |
| 769 | { | ||
| 770 | // Candidate discovery, at every byte offset and independent of any decode. A RIP-relative operand | ||
| 771 | // computes target = end_of_instruction + disp32, and the instruction ends at the displacement field | ||
| 772 | // plus four plus whatever immediate follows, so a field can only reference the string when | ||
| 773 | // string_addr - (field + 4) - disp is one of the immediate widths an x86-64 instruction may place | ||
| 774 | // after a disp32. This is the property that makes the sweep desync-immune: a false boundary can | ||
| 775 | // mis-frame the bytes around a reference, but it cannot make this arithmetic stop holding. | ||
| 776 | 1846232 | std::int32_t disp = 0; | |
| 777 | 1846232 | std::memcpy(&disp, bytes + i, sizeof(disp)); | |
| 778 | 1846232 | const std::uintptr_t disp_field = window.base + i; | |
| 779 | 1846232 | const std::int64_t immediate_width = static_cast<std::int64_t>(string_addr) - | |
| 780 | 1846232 | static_cast<std::int64_t>(disp_field + sizeof(disp)) - disp; | |
| 781 | 1846232 | if (immediate_width != 0 && immediate_width != 1 && immediate_width != 2 && immediate_width != 4) | |
| 782 | { | ||
| 783 | 1845762 | continue; | |
| 784 | } | ||
| 785 | |||
| 786 | // Verification. The trusted .pdata stream decides wherever an exception record covers the | ||
| 787 | // candidate; otherwise the bounded probe covers leaf functions and code with no registered table. | ||
| 788 | const std::optional<std::uintptr_t> trusted_site = | ||
| 789 | 613 | resolve_candidate_from_trusted_origin(decoder, window, disp_field, string_addr); | |
| 790 | const std::uintptr_t site = | ||
| 791 | 543 | trusted_site.has_value() ? *trusted_site | |
| 792 | 543 | : resolve_candidate_by_probe(decoder, window, disp_field, string_addr); | |
| 793 | 543 | if (site == 0) | |
| 794 | { | ||
| 795 | 142 | continue; | |
| 796 | } | ||
| 797 | |||
| 798 | // Cross-window back-carry de-duplication: this window may have been extended backward into the | ||
| 799 | // previous window's tail so a boundary-straddling instruction is discovered whole (see the loop in | ||
| 800 | // scan_string_ref_broad). An instruction that ENDS at or before count_floor lies wholly in the | ||
| 801 | // previous window and was already counted there. count_floor equals this window's real base, so an | ||
| 802 | // un-extended window counts every reference it finds. | ||
| 803 | 401 | const std::uintptr_t site_end = | |
| 804 | 401 | disp_field + sizeof(disp) + static_cast<std::uintptr_t>(immediate_width); | |
| 805 | 401 | if (site_end <= count_floor) | |
| 806 | { | ||
| 807 | 1 | continue; | |
| 808 | } | ||
| 809 | |||
| 810 | 400 | ++found_count; | |
| 811 | 400 | if (found_count == 1) | |
| 812 | { | ||
| 813 | 397 | first_site = site; | |
| 814 | 397 | first_end = site_end; | |
| 815 | 397 | first_key = disp_field; | |
| 816 | } | ||
| 817 | else | ||
| 818 | { | ||
| 819 | // Ambiguous; caller maps found_count >= 2 to AmbiguousReference. | ||
| 820 | 3 | return; | |
| 821 | } | ||
| 822 | } | ||
| 823 | } | ||
| 824 | |||
| 825 | // Window-granular TOCTOU fault guard around scan_window_broad_body; the narrow sibling | ||
| 826 | // scan_window_narrow_guarded documents the shared rationale. This body also leaves the window: | ||
| 827 | // resolve_candidate_from_trusted_origin calls RtlLookupFunctionEntry, whose dynamic-function-table walk is | ||
| 828 | // not the scanned range. A fault raised there is a defect or hostile registration, not the concurrent | ||
| 829 | // unmap this guard exists to absorb, so it must reach the host's handlers instead of being recorded as a | ||
| 830 | // faulted window. The nested guarded_read_bytes of the .pdata record carries its own inner range filter, so | ||
| 831 | // its faults never arrive here. Returns true when a fault was swallowed. | ||
| 832 | 490 | bool scan_window_broad_guarded( | |
| 833 | const ZydisDecoder &decoder, | ||
| 834 | const detail::ExecutableWindow &window, | ||
| 835 | std::uintptr_t string_addr, | ||
| 836 | std::uintptr_t count_floor, | ||
| 837 | std::size_t &found_count, | ||
| 838 | std::uintptr_t &first_site, | ||
| 839 | std::uintptr_t &first_end, | ||
| 840 | std::uintptr_t &first_key | ||
| 841 | ) noexcept | ||
| 842 | { | ||
| 843 | #ifdef _MSC_VER | ||
| 844 | const std::size_t original_found_count = found_count; | ||
| 845 | const std::uintptr_t original_first_site = first_site; | ||
| 846 | const std::uintptr_t original_first_end = first_end; | ||
| 847 | const std::uintptr_t original_first_key = first_key; | ||
| 848 | __try | ||
| 849 | { | ||
| 850 | scan_window_broad_body( | ||
| 851 | decoder, | ||
| 852 | window, | ||
| 853 | string_addr, | ||
| 854 | count_floor, | ||
| 855 | found_count, | ||
| 856 | first_site, | ||
| 857 | first_end, | ||
| 858 | first_key | ||
| 859 | ); | ||
| 860 | return false; | ||
| 861 | } | ||
| 862 | __except ( | ||
| 863 | detail::guarded_range_fault_filter( | ||
| 864 | GetExceptionInformation(), | ||
| 865 | window.base, | ||
| 866 | window.base + window.span | ||
| 867 | ) | ||
| 868 | ) | ||
| 869 | { | ||
| 870 | // The caller skips faulted windows, so discard any reference count collected before the fault. | ||
| 871 | found_count = original_found_count; | ||
| 872 | first_site = original_first_site; | ||
| 873 | first_end = original_first_end; | ||
| 874 | first_key = original_first_key; | ||
| 875 | return true; | ||
| 876 | } | ||
| 877 | #elif defined(_WIN64) | ||
| 878 | // MinGW x64: same vectored read guard as the narrow sibling, armed over the gated window bytes; a fault | ||
| 879 | // is swallowed and the window reported faulted. A 32-bit build is rejected by the defines.hpp | ||
| 880 | // architecture gate, so only the two x64 arms exist. | ||
| 881 | 490 | const std::size_t original_found_count = found_count; | |
| 882 | 490 | const std::uintptr_t original_first_site = first_site; | |
| 883 | 490 | const std::uintptr_t original_first_end = first_end; | |
| 884 | 490 | const std::uintptr_t original_first_key = first_key; | |
| 885 | struct BroadScanContext | ||
| 886 | { | ||
| 887 | const ZydisDecoder *decoder; | ||
| 888 | const detail::ExecutableWindow *window; | ||
| 889 | std::uintptr_t string_addr; | ||
| 890 | std::uintptr_t count_floor; | ||
| 891 | std::size_t *found_count; | ||
| 892 | std::uintptr_t *first_site; | ||
| 893 | std::uintptr_t *first_end; | ||
| 894 | std::uintptr_t *first_key; | ||
| 895 | } scan_ctx{ | ||
| 896 | &decoder, | ||
| 897 | &window, | ||
| 898 | string_addr, | ||
| 899 | count_floor, | ||
| 900 | &found_count, | ||
| 901 | &first_site, | ||
| 902 | &first_end, | ||
| 903 | &first_key | ||
| 904 | 490 | }; | |
| 905 | |||
| 906 | 490 | const auto run_scan = [](void *opaque) noexcept -> void | |
| 907 | { | ||
| 908 | 490 | auto *context = static_cast<BroadScanContext *>(opaque); | |
| 909 | 490 | scan_window_broad_body( | |
| 910 | 490 | *context->decoder, | |
| 911 | 490 | *context->window, | |
| 912 | context->string_addr, | ||
| 913 | context->count_floor, | ||
| 914 | 490 | *context->found_count, | |
| 915 | 490 | *context->first_site, | |
| 916 | 490 | *context->first_end, | |
| 917 | 490 | *context->first_key | |
| 918 | ); | ||
| 919 | 420 | }; | |
| 920 | |||
| 921 | 490 | if (detail::run_guarded_region(window.base, window.base + window.span, run_scan, &scan_ctx)) | |
| 922 | { | ||
| 923 | 420 | return false; | |
| 924 | } | ||
| 925 | // Faulted: discard any partial count / site so a partially-scanned window cannot leak a stale site. | ||
| 926 | 70 | found_count = original_found_count; | |
| 927 | 70 | first_site = original_first_site; | |
| 928 | 70 | first_end = original_first_end; | |
| 929 | 70 | first_key = original_first_key; | |
| 930 | 70 | return true; | |
| 931 | #endif | ||
| 932 | } | ||
| 933 | |||
| 934 | // The broad phase uses Zydis to verify every RIP-relative memory reference that resolves to string_addr. | ||
| 935 | // `[B-63]` owns when a derived return must use this phase. | ||
| 936 | // | ||
| 937 | // An arbitrary byte does not identify an x86-64 instruction boundary. Discovery therefore tests target | ||
| 938 | // arithmetic at every byte offset. Only survivors pay for decode and boundary validation. The .pdata entry | ||
| 939 | // supplies a registered boundary, while a bounded probe covers fallback code. The second hit marks | ||
| 940 | // ambiguity. StringXrefBoundaryProof.FalseBoundaryCannotSuppressBroadOnlyReference pins this order. | ||
| 941 | 401 | std::uintptr_t scan_string_ref_broad( | |
| 942 | std::uintptr_t string_addr, | ||
| 943 | std::span<const detail::ExecutableWindow> windows, | ||
| 944 | std::size_t &found_count, | ||
| 945 | std::uintptr_t &out_end, | ||
| 946 | std::uintptr_t &out_key, | ||
| 947 | bool &incomplete | ||
| 948 | ) | ||
| 949 | { | ||
| 950 | 401 | found_count = 0; | |
| 951 | 401 | out_end = 0; | |
| 952 | 401 | out_key = 0; | |
| 953 | 401 | incomplete = false; | |
| 954 | 401 | std::uintptr_t first_site = 0; | |
| 955 | 401 | std::uintptr_t first_end = 0; | |
| 956 | 401 | std::uintptr_t first_key = 0; | |
| 957 | |||
| 958 | ZydisDecoder decoder; | ||
| 959 | 401 | if (!ZYAN_SUCCESS(ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64))) | |
| 960 | { | ||
| 961 | // A decoder that will not initialize cannot verify any reference; fail closed as "no reference" | ||
| 962 | // rather than guess a site. | ||
| 963 | ✗ | return 0; | |
| 964 | } | ||
| 965 | |||
| 966 | 401 | std::size_t faulted_windows = 0; | |
| 967 | // Cross-window back-carry, mirroring the narrow scan (and phase 1). A variable-length reference can | ||
| 968 | // straddle the split between two abutting execute-readable windows, decodable by neither window's own | ||
| 969 | // sweep (the previous window's decoder truncates at its end, and this window decodes from its base, | ||
| 970 | // mid-instruction). When this window abuts the previous, decode from ZYDIS_MAX_INSTRUCTION_LENGTH - 1 | ||
| 971 | // bytes earlier so the straddler is decoded whole. A count floor at this window's real base then | ||
| 972 | // de-duplicates: an instruction ending at or before the base was already counted by the previous | ||
| 973 | // window, so scan_window_broad_body skips it (see there). The carry is bounded by the previous window's | ||
| 974 | // span so it never reads before it; page-granular regions make that bound a formality. | ||
| 975 | 401 | constexpr std::size_t broad_carry = ZYDIS_MAX_INSTRUCTION_LENGTH - 1; | |
| 976 | 401 | std::uintptr_t prev_end = 0; | |
| 977 | 401 | std::size_t prev_span = 0; | |
| 978 | 401 | bool have_prev = false; | |
| 979 | 1289 | for (const detail::ExecutableWindow &window : windows) | |
| 980 | { | ||
| 981 | 490 | detail::ExecutableWindow effective = window; | |
| 982 | 490 | if (have_prev && window.base == prev_end) | |
| 983 | { | ||
| 984 | 2 | const std::size_t carry = (broad_carry < prev_span) ? broad_carry : prev_span; | |
| 985 | 2 | effective.base = window.base - carry; | |
| 986 | 2 | effective.span = window.span + carry; | |
| 987 | } | ||
| 988 | 490 | prev_end = window.base + window.span; | |
| 989 | 490 | prev_span = window.span; | |
| 990 | 490 | have_prev = true; | |
| 991 | |||
| 992 | 490 | if (scan_window_broad_guarded( | |
| 993 | decoder, | ||
| 994 | effective, | ||
| 995 | string_addr, | ||
| 996 | 490 | window.base, | |
| 997 | found_count, | ||
| 998 | first_site, | ||
| 999 | first_end, | ||
| 1000 | first_key | ||
| 1001 | )) | ||
| 1002 | { | ||
| 1003 | 70 | ++faulted_windows; | |
| 1004 | 70 | continue; | |
| 1005 | } | ||
| 1006 | 420 | if (found_count >= 2) | |
| 1007 | { | ||
| 1008 | // resolve_string_xref maps a merged count of 2 or more to AmbiguousReference. | ||
| 1009 | 3 | incomplete = faulted_windows > 0; | |
| 1010 | 3 | log_faulted_windows(faulted_windows); | |
| 1011 | 3 | return 0; | |
| 1012 | } | ||
| 1013 | } | ||
| 1014 | // Surface any skipped faulted window (see scan_string_ref_narrow). resolve_string_xref fails closed on | ||
| 1015 | // a lower-bound count and does not report a lone surviving reference as unique. | ||
| 1016 | 398 | incomplete = faulted_windows > 0; | |
| 1017 | 398 | log_faulted_windows(faulted_windows); | |
| 1018 | 398 | if (found_count != 1) | |
| 1019 | { | ||
| 1020 | 4 | return 0; | |
| 1021 | } | ||
| 1022 | 394 | out_end = first_end; | |
| 1023 | 394 | out_key = first_key; | |
| 1024 | 394 | return first_site; | |
| 1025 | } | ||
| 1026 | |||
| 1027 | // `[B-64]` requires the x64 exception directory before the heuristic scan. RtlLookupFunctionEntry reports | ||
| 1028 | // the image base and innermost RUNTIME_FUNCTION for registered code. A null or invalid result permits the | ||
| 1029 | // fallback. | ||
| 1030 | // | ||
| 1031 | // UNW_FLAG_CHAININFO links a funclet or hot/cold fragment to its primary function. The chained record | ||
| 1032 | // follows the padded unwind-code array at UnwindData + 4 + 2 * ((CountOfCodes + 1) & ~1). Guarded reads and | ||
| 1033 | // a bounded hop count convert invalid metadata into fallback. The | ||
| 1034 | // StringXrefTest.EnclosingFunctionFollowsChainInfoToPrimaryFunction and | ||
| 1035 | // EnclosingFunctionFallsBackWhenChainInfoIsCyclic cases pin both outcomes. | ||
| 1036 | 16 | std::uintptr_t function_entry_via_pdata(std::uintptr_t instr_addr) noexcept | |
| 1037 | { | ||
| 1038 | 16 | DWORD64 image_base = 0; | |
| 1039 | 16 | PRUNTIME_FUNCTION entry = RtlLookupFunctionEntry(instr_addr, &image_base, nullptr); | |
| 1040 | 16 | if (entry == nullptr || image_base == 0) | |
| 1041 | { | ||
| 1042 | 7 | return 0; | |
| 1043 | } | ||
| 1044 | 9 | const std::uintptr_t base = static_cast<std::uintptr_t>(image_base); | |
| 1045 | |||
| 1046 | // The live image backing image_base bounds every RVA this walk dereferences. A dynamically registered | ||
| 1047 | // table (RtlAddFunctionTable over a non-image allocation) has no PE header, so the span stays invalid | ||
| 1048 | // and only the checked arithmetic and the fault guard apply; a normally loaded module always resolves, | ||
| 1049 | // and its unwind metadata is additionally required to stay inside [base, base + SizeOfImage) so a | ||
| 1050 | // malformed or wrapped RVA that merely happens to land on other mapped memory cannot escape the image. | ||
| 1051 | 9 | const detail::ModuleSpan image = detail::module_span(detail::module_image_region(Address{base})); | |
| 1052 | const auto resolve_rva_span = | ||
| 1053 | 67 | [base, image](std::uint64_t rva, std::size_t need) -> std::optional<std::uintptr_t> | |
| 1054 | { | ||
| 1055 | 67 | if (rva > static_cast<std::uint64_t>(UINTPTR_MAX - base)) | |
| 1056 | { | ||
| 1057 | ✗ | return std::nullopt; | |
| 1058 | } | ||
| 1059 | 67 | const std::uintptr_t address = base + static_cast<std::uintptr_t>(rva); | |
| 1060 | 67 | if (need > UINTPTR_MAX - address) | |
| 1061 | { | ||
| 1062 | ✗ | return std::nullopt; | |
| 1063 | } | ||
| 1064 | 67 | if (image.valid() && (address < image.base || address >= image.end || image.end - address < need)) | |
| 1065 | { | ||
| 1066 | 4 | return std::nullopt; | |
| 1067 | } | ||
| 1068 | 63 | return address; | |
| 1069 | 9 | }; | |
| 1070 | |||
| 1071 | // Copy the resolved record under the guard before walking .xdata by hand. RtlLookupFunctionEntry may | ||
| 1072 | // return a module .pdata record or a dynamically registered table record; both are still process memory | ||
| 1073 | // that must fail closed on an unexpected fault. | ||
| 1074 | 9 | RUNTIME_FUNCTION current{}; | |
| 1075 | 9 | const std::uintptr_t entry_address = reinterpret_cast<std::uintptr_t>(entry); | |
| 1076 | 9 | if ((image.valid() && | |
| 1077 | 17 | (!image.contains(entry_address) || image.end - entry_address < sizeof(current))) || | |
| 1078 | 8 | !detail::guarded_read_bytes(entry_address, ¤t, sizeof(current))) | |
| 1079 | { | ||
| 1080 | 1 | return 0; | |
| 1081 | } | ||
| 1082 | |||
| 1083 | 8 | constexpr int MAX_CHAIN_HOPS = 16; | |
| 1084 | 26 | for (int hop = 0; hop < MAX_CHAIN_HOPS; ++hop) | |
| 1085 | { | ||
| 1086 | 25 | if (current.EndAddress <= current.BeginAddress) | |
| 1087 | { | ||
| 1088 | 7 | return 0; | |
| 1089 | } | ||
| 1090 | const std::optional<std::uintptr_t> function_start = | ||
| 1091 | 25 | resolve_rva_span(current.BeginAddress, current.EndAddress - current.BeginAddress); | |
| 1092 | 25 | if (!function_start) | |
| 1093 | { | ||
| 1094 | 2 | return 0; | |
| 1095 | } | ||
| 1096 | |||
| 1097 | // UNWIND_INFO fixed header: byte 0 packs Version:3 | Flags:5 (so Flags = byte0 >> 3) and byte 2 is | ||
| 1098 | // CountOfCodes. Only these two fields drive chain traversal, so read the 4-byte header instead of | ||
| 1099 | // modelling the whole variable-length structure. | ||
| 1100 | 23 | std::uint8_t unwind_header[4] = {}; | |
| 1101 | const std::optional<std::uintptr_t> unwind_addr = | ||
| 1102 | 23 | resolve_rva_span(current.UnwindData, sizeof(unwind_header)); | |
| 1103 | 23 | if (!unwind_addr || !detail::guarded_read_bytes(*unwind_addr, unwind_header, sizeof(unwind_header))) | |
| 1104 | { | ||
| 1105 | 1 | return 0; | |
| 1106 | } | ||
| 1107 | 22 | if (((unwind_header[0] >> 3) & UNW_FLAG_CHAININFO) == 0) | |
| 1108 | { | ||
| 1109 | 3 | return *function_start; | |
| 1110 | } | ||
| 1111 | // The chained RUNTIME_FUNCTION follows the unwind-code array, padded up to an even entry count; | ||
| 1112 | // UnwindData is that structure's own RVA. Compute the offset in 64-bit so a large CountOfCodes or | ||
| 1113 | // UnwindData cannot wrap the 32-bit RVA before it is bounded against the image. | ||
| 1114 | 19 | const std::uint32_t count_of_codes = unwind_header[2]; | |
| 1115 | 19 | const std::uint64_t chained_rva = | |
| 1116 | 19 | static_cast<std::uint64_t>(current.UnwindData) + 4u + | |
| 1117 | 19 | 2u * ((static_cast<std::uint64_t>(count_of_codes) + 1u) & ~std::uint64_t{1}); | |
| 1118 | const std::optional<std::uintptr_t> chained_addr = | ||
| 1119 | 19 | resolve_rva_span(chained_rva, sizeof(RUNTIME_FUNCTION)); | |
| 1120 | 19 | RUNTIME_FUNCTION chained{}; | |
| 1121 | 19 | if (!chained_addr || !detail::guarded_read_bytes(*chained_addr, &chained, sizeof(chained))) | |
| 1122 | { | ||
| 1123 | 1 | return 0; | |
| 1124 | } | ||
| 1125 | 18 | current = chained; | |
| 1126 | } | ||
| 1127 | |||
| 1128 | 1 | return 0; | |
| 1129 | } | ||
| 1130 | |||
| 1131 | // `[B-64]` requires the authoritative lookup before this bounded heuristic. Absent or malformed metadata | ||
| 1132 | // permits the fallback. The scan finds the nearest 0xC3 RET or 0xCC INT3 boundary and then requires a | ||
| 1133 | // plausible prologue. It returns zero when the bounded window contains no boundary. The Windows x64 ABI | ||
| 1134 | // cleans the stack in the caller, so no epilogue uses RET imm16 and 0xC2 stays out of the boundary set. | ||
| 1135 | 16 | std::uintptr_t enclosing_function_start(std::uintptr_t instr_addr, std::uintptr_t window_lo) noexcept | |
| 1136 | { | ||
| 1137 | 16 | if (const std::uintptr_t via_pdata = function_entry_via_pdata(instr_addr); via_pdata != 0) | |
| 1138 | { | ||
| 1139 | 3 | return via_pdata; | |
| 1140 | } | ||
| 1141 | |||
| 1142 | 13 | constexpr std::uintptr_t back_scan_window = 0x2000; // 8 KiB. | |
| 1143 | 13 | const std::uintptr_t floor = | |
| 1144 | 13 | (instr_addr - window_lo > back_scan_window) ? instr_addr - back_scan_window : window_lo; | |
| 1145 | 13 | if (instr_addr <= floor) | |
| 1146 | { | ||
| 1147 | ✗ | return 0; | |
| 1148 | } | ||
| 1149 | |||
| 1150 | // Buffer the back-scan window once instead of issuing a guarded read per byte. The window is read in | ||
| 1151 | // page-sized chunks from high address to low: a chunk that faults stops the read exactly where the | ||
| 1152 | // byte-at-a-time walk would have faulted going downward, so any boundary in the already-buffered higher | ||
| 1153 | // bytes is still found, and a fault below them returns 0 just as the per-byte version did. window[k] | ||
| 1154 | // holds the byte at floor + k; valid_lo is the lowest address actually buffered. | ||
| 1155 | // | ||
| 1156 | // Left deliberately uninitialized: the chunk-read loop fills exactly [valid_lo, instr_addr) via | ||
| 1157 | // guarded_read_bytes, and both probe loops below read only indices within that filled span (they are | ||
| 1158 | // bounded by valid_lo / instr_addr), so no unwritten byte is ever observed. Value-initializing the full | ||
| 1159 | // 8 KiB every call would be dead work on this control-plane path. | ||
| 1160 | // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init) filled before any read, see above | ||
| 1161 | std::array<std::uint8_t, static_cast<std::size_t>(back_scan_window)> window; | ||
| 1162 | 13 | std::uintptr_t valid_lo = instr_addr; | |
| 1163 | // Windows x86/x64 base page size; guarded_read_bytes faults at this granularity. Chunks are | ||
| 1164 | // page-aligned, not instr-aligned, so a single guarded read covers at most one page and never straddles | ||
| 1165 | // a readable/unreadable page boundary. | ||
| 1166 | 13 | constexpr std::uintptr_t page_size = 0x1000; | |
| 1167 | 34 | for (std::uintptr_t hi = instr_addr; hi > floor;) | |
| 1168 | { | ||
| 1169 | 22 | const std::uintptr_t page_lo = (hi - 1) & ~(page_size - 1); | |
| 1170 | 22 | const std::uintptr_t lo = (page_lo > floor) ? page_lo : floor; | |
| 1171 | 22 | const std::size_t len = static_cast<std::size_t>(hi - lo); | |
| 1172 | 22 | if (!detail::guarded_read_bytes(lo, window.data() + (lo - floor), len)) | |
| 1173 | { | ||
| 1174 | // This page is unreadable; stop. The byte-at-a-time walk would fault on the first byte here | ||
| 1175 | // too, after exhausting the readable bytes buffered above it. | ||
| 1176 | 1 | break; | |
| 1177 | } | ||
| 1178 | 21 | valid_lo = lo; | |
| 1179 | 21 | hi = lo; | |
| 1180 | } | ||
| 1181 | 13 | if (valid_lo >= instr_addr) | |
| 1182 | { | ||
| 1183 | // Not even the highest chunk was readable: the first probed byte would have faulted. | ||
| 1184 | ✗ | return 0; | |
| 1185 | } | ||
| 1186 | |||
| 1187 | 33752 | for (std::uintptr_t probe = instr_addr; probe > valid_lo; --probe) | |
| 1188 | { | ||
| 1189 | 33746 | const std::uint8_t boundary_byte = window[static_cast<std::size_t>(probe - 1 - floor)]; | |
| 1190 | 33746 | if (boundary_byte != 0xCC && boundary_byte != 0xC3) | |
| 1191 | { | ||
| 1192 | 33739 | continue; | |
| 1193 | } | ||
| 1194 | // The boundary byte ends the previous function (or its padding); the enclosing function begins at | ||
| 1195 | // the first non-INT3 byte after it. | ||
| 1196 | 7 | std::uintptr_t start = probe; | |
| 1197 | 7 | while (start < instr_addr && window[static_cast<std::size_t>(start - floor)] == 0xCC) | |
| 1198 | { | ||
| 1199 | ✗ | ++start; | |
| 1200 | } | ||
| 1201 | 7 | return is_likely_function_prologue(Address{start}) ? start : 0; | |
| 1202 | } | ||
| 1203 | 6 | return 0; | |
| 1204 | } | ||
| 1205 | } // namespace | ||
| 1206 | |||
| 1207 | namespace | ||
| 1208 | { | ||
| 1209 | /** | ||
| 1210 | * @brief Returns the address of the only occurrence of @p pattern in the readable pages, or the phase-1 | ||
| 1211 | * failure. | ||
| 1212 | * @details The linker pools identical literals, so a second occurrence makes the anchor ambiguous and must | ||
| 1213 | * fail closed. | ||
| 1214 | */ | ||
| 1215 | 732 | [[nodiscard]] Result<std::uintptr_t> locate_unique_string( | |
| 1216 | const detail::EnginePattern &pattern, | ||
| 1217 | const detail::ModuleSpan &range, | ||
| 1218 | const detail::ScanExclusions *provided_exclusions | ||
| 1219 | ) noexcept | ||
| 1220 | { | ||
| 1221 | // One traversal counts zero, one, or two-or-more occurrences, so the located address and the uniqueness | ||
| 1222 | // verdict describe the same view of memory; two independent passes could straddle a concurrent write | ||
| 1223 | // and certify a pairing that never existed. compile_string_pattern emits literal bytes only, so this | ||
| 1224 | // pattern carries no bounded jumps and a skipped faulted region is its only truncation channel. | ||
| 1225 | 732 | const detail::MatchResult located = detail::scan_module_readable( | |
| 1226 | pattern, | ||
| 1227 | range, | ||
| 1228 | 732 | detail::ScanQuery{ | |
| 1229 | .occurrence = 1, | ||
| 1230 | .count_beyond = true, | ||
| 1231 | .exclusions = provided_exclusions, | ||
| 1232 | } | ||
| 1233 | ); | ||
| 1234 | 732 | if (located.match == nullptr) | |
| 1235 | { | ||
| 1236 | 7 | if (located.truncated()) | |
| 1237 | { | ||
| 1238 | // A truncated sweep never read part of the image, so it cannot report the literal absent. | ||
| 1239 | ✗ | return std::unexpected(Error{ErrorCode::IncompleteScan, "scan::find_string_xref"}); | |
| 1240 | } | ||
| 1241 | 7 | return std::unexpected(Error{ErrorCode::StringNotFound, "scan::find_string_xref"}); | |
| 1242 | } | ||
| 1243 | 725 | if (located.count > 1) | |
| 1244 | { | ||
| 1245 | // A second pooled copy was actually observed, so the anchor is genuinely non-unique. That verdict | ||
| 1246 | // is authoritative and stays ambiguous even when the sweep was also truncated. | ||
| 1247 | 2 | return std::unexpected(Error{ErrorCode::StringAmbiguous, "scan::find_string_xref"}); | |
| 1248 | } | ||
| 1249 | 723 | if (located.truncated()) | |
| 1250 | { | ||
| 1251 | // One copy was seen, but a truncated sweep makes the count a lower bound: a second pooled copy | ||
| 1252 | // could hide in bytes that were never read, so uniqueness is unproven. Fail closed on the | ||
| 1253 | // truncation itself rather than on an ambiguity verdict, so the reason survives to the caller and | ||
| 1254 | // the ladder resolver's typed-failure latch cannot degrade it to NoMatch. | ||
| 1255 | 267 | return std::unexpected(Error{ErrorCode::IncompleteScan, "scan::find_string_xref"}); | |
| 1256 | } | ||
| 1257 | 456 | return reinterpret_cast<std::uintptr_t>(located.match); | |
| 1258 | } | ||
| 1259 | |||
| 1260 | /** | ||
| 1261 | * @brief Returns the narrow reference result over @p windows, merged with the broad result when the broad | ||
| 1262 | * sweep runs. | ||
| 1263 | * @details The narrow sweep writes @p lea_info. | ||
| 1264 | */ | ||
| 1265 | 453 | [[nodiscard]] ReferenceScanResult sweep_string_references( | |
| 1266 | const StringRefQuery &query, | ||
| 1267 | std::uintptr_t string_addr, | ||
| 1268 | std::span<const detail::ExecutableWindow> windows, | ||
| 1269 | LeaReferenceInfo &lea_info | ||
| 1270 | ) | ||
| 1271 | { | ||
| 1272 | 453 | ReferenceScanResult references{}; | |
| 1273 | 453 | std::size_t narrow_count = 0; | |
| 1274 | 453 | bool narrow_incomplete = false; | |
| 1275 | const std::uintptr_t narrow_site = | ||
| 1276 | 453 | scan_string_ref_narrow(string_addr, windows, narrow_count, lea_info, narrow_incomplete); | |
| 1277 | // The narrow shape is REX + opcode + ModRM + disp32, so its disp32 field sits exactly three bytes into | ||
| 1278 | // the instruction and the reference key is derivable from the site alone. | ||
| 1279 | 883 | merge_reference_scan( | |
| 1280 | references, | ||
| 1281 | narrow_site, | ||
| 1282 | 430 | (narrow_site != 0) ? narrow_site + lea_info.instr_len : 0, | |
| 1283 | (narrow_site != 0) ? narrow_site + 3 : 0, | ||
| 1284 | narrow_count, | ||
| 1285 | narrow_incomplete | ||
| 1286 | ); | ||
| 1287 | |||
| 1288 | // `[B-63]` requires a broad second check before a derived return can certify one narrow hit. The | ||
| 1289 | // narrow shape accepts REX.W LEA and MOV, so the broad sweep re-counts whichever reference the | ||
| 1290 | // narrow phase returned. A rarer-shape twin raises the count to 2 and fails closed. lea_info stays | ||
| 1291 | // unchanged, so EnclosingFunction keeps that site and StringPointerSlot still requires the narrow | ||
| 1292 | // lea. ReferencingInstruction stays on the narrow path. | ||
| 1293 | // | ||
| 1294 | // A derived anchor that remains unique pays one complete executable-range sweep. | ||
| 1295 | // ReferencingInstruction avoids that confirmation unless broad_match requests the broad phase. | ||
| 1296 | 453 | const bool derived_return = query.return_mode != XrefReturn::ReferencingInstruction; | |
| 1297 | 453 | const bool confirm_derived_uniqueness = derived_return && references.count == 1; | |
| 1298 | 453 | if (references.count < 2 && (query.broad_match || confirm_derived_uniqueness)) | |
| 1299 | { | ||
| 1300 | 401 | std::size_t broad_count = 0; | |
| 1301 | 401 | std::uintptr_t broad_end = 0; | |
| 1302 | 401 | std::uintptr_t broad_key = 0; | |
| 1303 | 401 | bool broad_incomplete = false; | |
| 1304 | 401 | const std::uintptr_t broad_site = scan_string_ref_broad( | |
| 1305 | string_addr, | ||
| 1306 | windows, | ||
| 1307 | broad_count, | ||
| 1308 | broad_end, | ||
| 1309 | broad_key, | ||
| 1310 | broad_incomplete | ||
| 1311 | ); | ||
| 1312 | 401 | merge_reference_scan(references, broad_site, broad_end, broad_key, broad_count, broad_incomplete); | |
| 1313 | } | ||
| 1314 | 906 | return references; | |
| 1315 | } | ||
| 1316 | |||
| 1317 | /** | ||
| 1318 | * @brief Maps the unique reference to the address that query.return_mode selects. | ||
| 1319 | * @details A non-null @p physical_source receives the reference extent, which StringPointerSlot extends | ||
| 1320 | * through the store. | ||
| 1321 | */ | ||
| 1322 | 365 | [[nodiscard]] Result<Address> derive_xref_return( | |
| 1323 | const StringRefQuery &query, | ||
| 1324 | const ReferenceScanResult &references, | ||
| 1325 | const LeaReferenceInfo &lea_info, | ||
| 1326 | const detail::ModuleSpan &range, | ||
| 1327 | Region *physical_source | ||
| 1328 | ) noexcept | ||
| 1329 | { | ||
| 1330 | 365 | if (physical_source != nullptr) | |
| 1331 | { | ||
| 1332 | // The referencing instruction is the byte evidence every return mode rides on. Publish its whole | ||
| 1333 | // extent, not its first byte: a co-voting selector that matches the reference from any later byte | ||
| 1334 | // would otherwise abut this span instead of overlapping it and would double-vote one instruction. | ||
| 1335 | // Both sweeps carry that extent (the narrow shape's decoded length, the broad sweep's framed end), | ||
| 1336 | // so the single-byte floor below is only a defensive fallback for a site with no recorded end. | ||
| 1337 | ✗ | const std::size_t reference_length = | |
| 1338 | 22 | (references.site_end > references.site) ? references.site_end - references.site : 1; | |
| 1339 | 22 | *physical_source = Region{Address{references.site}, reference_length}; | |
| 1340 | } | ||
| 1341 | |||
| 1342 | 365 | if (query.return_mode == XrefReturn::StringPointerSlot) | |
| 1343 | { | ||
| 1344 | // Store-xref needs the unique reference to be the narrow `lea reg, [rip+string]` whose loaded | ||
| 1345 | // pointer a following `mov [rip+slot], reg` caches. A broad-only surviving reference never | ||
| 1346 | // populates lea_info, and a `mov reg, [rip+string]` load has no store to attribute, so is_lea is | ||
| 1347 | // the whole test: it is set only by the narrow sweep, and only for the site it returned. | ||
| 1348 | 19 | if (!lea_info.is_lea) | |
| 1349 | { | ||
| 1350 | 2 | return std::unexpected(Error{ErrorCode::StoreNotFound, "scan::find_string_xref"}); | |
| 1351 | } | ||
| 1352 | 17 | std::uintptr_t store_end = 0; | |
| 1353 | 17 | const std::uintptr_t slot = scan_store_slot_after_lea( | |
| 1354 | 17 | references.site, | |
| 1355 | 17 | lea_info.instr_len, | |
| 1356 | 17 | lea_info.window_end, | |
| 1357 | 17 | lea_info.reg, | |
| 1358 | range, | ||
| 1359 | store_end | ||
| 1360 | ); | ||
| 1361 | 17 | if (slot == 0) | |
| 1362 | { | ||
| 1363 | 13 | return std::unexpected(Error{ErrorCode::StoreNotFound, "scan::find_string_xref"}); | |
| 1364 | } | ||
| 1365 | 4 | if (physical_source != nullptr && store_end > references.site) | |
| 1366 | { | ||
| 1367 | // The slot address is the store's own disp32 resolved against its next-IP, so the store is the | ||
| 1368 | // evidence this mode returns; the reference only selects which store. The forward walk decoded | ||
| 1369 | // every instruction from the reference to it, so the run between them is evidence too and the | ||
| 1370 | // union is one contiguous extent rather than two disjoint spans. | ||
| 1371 | 1 | *physical_source = Region{Address{references.site}, store_end - references.site}; | |
| 1372 | } | ||
| 1373 | 4 | return Address{slot}; | |
| 1374 | } | ||
| 1375 | |||
| 1376 | 346 | if (query.return_mode == XrefReturn::EnclosingFunction) | |
| 1377 | { | ||
| 1378 | 16 | const std::uintptr_t function_start = enclosing_function_start(references.site, range.base); | |
| 1379 | 16 | if (function_start == 0) | |
| 1380 | { | ||
| 1381 | 6 | return std::unexpected(Error{ErrorCode::FunctionNotFound, "scan::find_string_xref"}); | |
| 1382 | } | ||
| 1383 | 10 | return Address{function_start}; | |
| 1384 | } | ||
| 1385 | 330 | return Address{references.site}; | |
| 1386 | } | ||
| 1387 | |||
| 1388 | /** | ||
| 1389 | * @brief Runs the two-phase resolve behind the three entry points below. | ||
| 1390 | * @details Only the ladder resolver passes an exclusion set, through find_string_xref_with_exclusions. | ||
| 1391 | */ | ||
| 1392 | 792 | Result<Address> resolve_string_xref( | |
| 1393 | const StringRefQuery &query, | ||
| 1394 | Region scope, | ||
| 1395 | const detail::ScanExclusions *provided_exclusions, | ||
| 1396 | std::span<const Region> declared_exclusions, | ||
| 1397 | Region *physical_source = nullptr | ||
| 1398 | ) | ||
| 1399 | { | ||
| 1400 | 792 | if (physical_source != nullptr) | |
| 1401 | { | ||
| 1402 | 56 | *physical_source = Region{}; | |
| 1403 | } | ||
| 1404 | 792 | if (!detail::valid_string_encoding(query.encoding) || !detail::valid_xref_return(query.return_mode)) | |
| 1405 | { | ||
| 1406 | 4 | return std::unexpected(Error{ErrorCode::InvalidArg, "scan::find_string_xref"}); | |
| 1407 | } | ||
| 1408 | 788 | if (query.text.empty()) | |
| 1409 | { | ||
| 1410 | 1 | return std::unexpected(Error{ErrorCode::EmptyQuery, "scan::find_string_xref"}); | |
| 1411 | } | ||
| 1412 | 787 | const detail::ModuleSpan range = detail::module_span(scope); | |
| 1413 | 787 | if (!range.valid()) | |
| 1414 | { | ||
| 1415 | 1 | return std::unexpected(Error{ErrorCode::InvalidRange, "scan::find_string_xref"}); | |
| 1416 | } | ||
| 1417 | // Phase 1 is a readable sweep and follows the Readable authority rule of scan::Pages. | ||
| 1418 | 786 | if (!detail::readable_scan_is_authoritative(range, Pages::Readable, declared_exclusions)) | |
| 1419 | { | ||
| 1420 | ✗ | return std::unexpected(Error{ErrorCode::NotAuthoritative, "scan::find_string_xref"}); | |
| 1421 | } | ||
| 1422 | |||
| 1423 | 786 | detail::ScanExclusions direct_exclusions; | |
| 1424 | 786 | if (provided_exclusions == nullptr) | |
| 1425 | { | ||
| 1426 | 740 | direct_exclusions.restrict_to(range.base, range.end); | |
| 1427 | 740 | direct_exclusions.add_text(query.text); | |
| 1428 | 740 | provided_exclusions = &direct_exclusions; | |
| 1429 | } | ||
| 1430 | 786 | if (provided_exclusions->overflowed()) | |
| 1431 | { | ||
| 1432 | ✗ | return std::unexpected(Error{ErrorCode::NotAuthoritative, "scan::find_string_xref"}); | |
| 1433 | } | ||
| 1434 | |||
| 1435 | 786 | const Result<detail::EnginePattern> pattern = compile_string_pattern(query); | |
| 1436 | 747 | if (!pattern) | |
| 1437 | { | ||
| 1438 | // MalformedQueryText: the query defines no literal, so the caller fixes the query, not the image. | ||
| 1439 | 15 | return std::unexpected(pattern.error()); | |
| 1440 | } | ||
| 1441 | const Result<std::uintptr_t> located_string = | ||
| 1442 | 732 | locate_unique_string(*pattern, range, provided_exclusions); | |
| 1443 | 732 | if (!located_string) | |
| 1444 | { | ||
| 1445 | 276 | return std::unexpected(located_string.error()); | |
| 1446 | } | ||
| 1447 | 456 | const std::uintptr_t string_addr = *located_string; | |
| 1448 | |||
| 1449 | // Phase 2: find the single RIP-relative reference whose target is the string. The narrow sweep is the | ||
| 1450 | // fast, desync-immune default. A broad_match query keeps that coverage and adds a Zydis sweep for rarer | ||
| 1451 | // reference shapes. | ||
| 1452 | // | ||
| 1453 | // Both sweeps run over ONE enumeration of the execute-readable windows. With two enumerations, a | ||
| 1454 | // concurrent reprotect can hide a window from the second sweep only. An absent window is | ||
| 1455 | // indistinguishable from a window that agreed, so the confirmation in sweep_string_references can then | ||
| 1456 | // certify a site that it never examined. The shared list routes any mid-sweep loss through the | ||
| 1457 | // faulted-window channel, which fails closed. | ||
| 1458 | 456 | const std::vector<detail::ExecutableWindow> windows = detail::collect_executable_windows(range); | |
| 1459 | |||
| 1460 | 453 | LeaReferenceInfo lea_info{}; | |
| 1461 | 453 | const ReferenceScanResult references = sweep_string_references(query, string_addr, windows, lea_info); | |
| 1462 | |||
| 1463 | 453 | if (references.count >= 2) | |
| 1464 | { | ||
| 1465 | 7 | return std::unexpected(Error{ErrorCode::AmbiguousReference, "scan::find_string_xref"}); | |
| 1466 | } | ||
| 1467 | 446 | if (references.incomplete) | |
| 1468 | { | ||
| 1469 | // An execute-readable window faulted mid-sweep and was skipped, so the reference count is a lower | ||
| 1470 | // bound. A second reference to the string can hide in the skipped window. A lone surviving | ||
| 1471 | // reference is therefore not provably unique, and a zero count is not a proven absence. Fail closed | ||
| 1472 | // on the truncation itself, as locate_unique_string does in phase 1. The count-driven | ||
| 1473 | // AmbiguousReference check above stays the authoritative multiplicity verdict. Only the | ||
| 1474 | // faulted-window channel reaches here, because merge_reference_scan carries no work budget. | ||
| 1475 | 72 | return std::unexpected(Error{ErrorCode::IncompleteScan, "scan::find_string_xref"}); | |
| 1476 | } | ||
| 1477 | 374 | if (references.count == 0) | |
| 1478 | { | ||
| 1479 | 9 | return std::unexpected(Error{ErrorCode::NoReference, "scan::find_string_xref"}); | |
| 1480 | } | ||
| 1481 | |||
| 1482 | 365 | return derive_xref_return(query, references, lea_info, range, physical_source); | |
| 1483 | 747 | } | |
| 1484 | |||
| 1485 | } // namespace | ||
| 1486 | |||
| 1487 | 736 | Result<Address> find_string_xref(const StringRefQuery &query, Region scope) | |
| 1488 | { | ||
| 1489 | 736 | return resolve_string_xref(query, scope, nullptr, {}); | |
| 1490 | } | ||
| 1491 | } // namespace scan | ||
| 1492 | |||
| 1493 | 46 | Result<Address> detail::find_string_xref_with_exclusions( | |
| 1494 | const scan::StringRefQuery &query, | ||
| 1495 | Region scope, | ||
| 1496 | const ScanExclusions *exclusions, | ||
| 1497 | std::span<const Region> declared_exclusions, | ||
| 1498 | Region *physical_source | ||
| 1499 | ) | ||
| 1500 | { | ||
| 1501 | 46 | return scan::resolve_string_xref(query, scope, exclusions, declared_exclusions, physical_source); | |
| 1502 | } | ||
| 1503 | |||
| 1504 | Result<Address> | ||
| 1505 | 10 | detail::find_string_xref_with_provenance(const scan::StringRefQuery &query, Region scope, Region &physical_source) | |
| 1506 | { | ||
| 1507 | 10 | return scan::resolve_string_xref(query, scope, nullptr, {}, &physical_source); | |
| 1508 | } | ||
| 1509 | } // namespace DetourModKit | ||
| 1510 |