src/string_xref.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /** | ||
| 2 | * @file string_xref.cpp | ||
| 3 | * @brief String-reference (xref) anchor backend: locate an immutable string literal in a module image, then resolve the | ||
| 4 | * unique instruction that references it. | ||
| 5 | * | ||
| 6 | * Two fail-closed phases. Phase 1 locates the single occurrence of the query string in the image's readable pages | ||
| 7 | * (reusing the scanner's module-scoped readable scan). Phase 2 finds the single RIP-relative reference to that string: | ||
| 8 | * a fast, desync-immune shape scan for the dominant lea/mov forms by default, or that same shape scan plus a | ||
| 9 | * Zydis-verified linear sweep (broad_match) that also recognizes the rarer shapes (cmp/push [rip+d], no-REX lea/mov, | ||
| 10 | * ...). | ||
| 11 | * | ||
| 12 | * Zydis is confined to this translation unit, exactly as code_constant.cpp confines | ||
| 13 | * it: no public DetourModKit header exposes a Zydis type, so an installed-package | ||
| 14 | * consumer links DetourModKit (which already links Zydis statically) without ever needing Zydis headers on its own | ||
| 15 | * include path. Keeping the broad sweep here also keeps scanner.cpp -- the core scan engine -- free of the decoder. | ||
| 16 | */ | ||
| 17 | |||
| 18 | #include "DetourModKit/scanner.hpp" | ||
| 19 | #include "DetourModKit/memory.hpp" | ||
| 20 | #include "DetourModKit/logger.hpp" | ||
| 21 | #include "scanner_internal.hpp" | ||
| 22 | #include "memory_internal.hpp" | ||
| 23 | |||
| 24 | #include <windows.h> | ||
| 25 | #include <Zydis/Zydis.h> | ||
| 26 | |||
| 27 | #include <array> | ||
| 28 | #include <cstddef> | ||
| 29 | #include <cstdint> | ||
| 30 | #include <cstring> | ||
| 31 | #include <optional> | ||
| 32 | #include <string> | ||
| 33 | |||
| 34 | namespace DetourModKit | ||
| 35 | { | ||
| 36 | namespace | ||
| 37 | { | ||
| 38 | struct ReferenceScanResult | ||
| 39 | { | ||
| 40 | std::uintptr_t site = 0; | ||
| 41 | std::size_t count = 0; | ||
| 42 | }; | ||
| 43 | |||
| 44 | // The matched narrow reference's lea destination register and instruction length, recovered alongside the | ||
| 45 | // unique-site search so the store-xref forward scan needs no second decode of the reference. Valid only when | ||
| 46 | // the unique reference is a REX.W `lea reg, [rip+string]` (is_lea == true); a `mov reg, [rip+string]` load | ||
| 47 | // already delivered the value to a register, so there is no store to model from the lea. reg is the 4-bit | ||
| 48 | // x86-64 register number (REX.R << 3 | ModRM.reg). | ||
| 49 | struct LeaReferenceInfo | ||
| 50 | { | ||
| 51 | std::uint8_t reg = 0; | ||
| 52 | std::size_t instr_len = 0; | ||
| 53 | std::uintptr_t window_end = 0; | ||
| 54 | bool is_lea = false; | ||
| 55 | }; | ||
| 56 | |||
| 57 | 1256 | void merge_reference_scan(ReferenceScanResult &result, std::uintptr_t site, std::size_t count) noexcept | |
| 58 | { | ||
| 59 |
2/2✓ Branch 2 → 3 taken 16 times.
✓ Branch 2 → 4 taken 1240 times.
|
1256 | if (count == 0) |
| 60 | { | ||
| 61 | 16 | return; | |
| 62 | } | ||
| 63 |
2/2✓ Branch 4 → 5 taken 5 times.
✓ Branch 4 → 6 taken 1235 times.
|
1240 | if (count >= 2) |
| 64 | { | ||
| 65 | 5 | result.count = 2; | |
| 66 | 5 | result.site = 0; | |
| 67 | 5 | return; | |
| 68 | } | ||
| 69 |
2/2✓ Branch 6 → 7 taken 632 times.
✓ Branch 6 → 8 taken 603 times.
|
1235 | if (result.count == 0) |
| 70 | { | ||
| 71 | 632 | result.site = site; | |
| 72 | 632 | result.count = 1; | |
| 73 | 632 | return; | |
| 74 | } | ||
| 75 |
1/2✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 603 times.
|
603 | if (result.site != site) |
| 76 | { | ||
| 77 | ✗ | result.site = 0; | |
| 78 | ✗ | result.count = 2; | |
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | // Builds a literal-byte CompiledPattern from a string query: the raw bytes of the text (UTF-8, or each code | ||
| 83 | // unit widened to UTF-16LE) plus an optional trailing NUL. The query content is emitted as a hex AOB and run | ||
| 84 | // through parse_aob so the string scan reuses the exact same compiled-pattern path as every other AOB. Returns | ||
| 85 | // nullopt on an empty query. Non-ASCII UTF-16 is out of scope: each byte is widened verbatim (Latin-1), which | ||
| 86 | // covers the ASCII identifiers that anchor strings almost always are. | ||
| 87 | 652 | std::optional<Scanner::CompiledPattern> compile_string_pattern(const Scanner::StringRefQuery &query) | |
| 88 | { | ||
| 89 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 652 times.
|
652 | if (query.text.empty()) |
| 90 | { | ||
| 91 | ✗ | return std::nullopt; | |
| 92 | } | ||
| 93 | 652 | const bool wide = (query.encoding == Scanner::StringEncoding::Utf16le); | |
| 94 | 652 | std::string aob; | |
| 95 |
3/4✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 651 times.
✓ Branch 10 → 11 taken 652 times.
✗ Branch 10 → 28 not taken.
|
652 | aob.reserve(query.text.size() * (wide ? 6 : 3) + 6); |
| 96 | 13079 | const auto emit = [&aob](std::uint8_t byte) | |
| 97 | { | ||
| 98 | static constexpr char hex_digits[] = "0123456789ABCDEF"; | ||
| 99 | 13079 | aob.push_back(hex_digits[byte >> 4]); | |
| 100 | 13079 | aob.push_back(hex_digits[byte & 0x0F]); | |
| 101 | 13079 | aob.push_back(' '); | |
| 102 | 13731 | }; | |
| 103 |
2/2✓ Branch 17 → 13 taken 12419 times.
✓ Branch 17 → 18 taken 652 times.
|
13071 | for (const char ch : query.text) |
| 104 | { | ||
| 105 |
1/2✓ Branch 13 → 14 taken 12419 times.
✗ Branch 13 → 28 not taken.
|
12419 | emit(static_cast<std::uint8_t>(ch)); |
| 106 |
2/2✓ Branch 14 → 15 taken 8 times.
✓ Branch 14 → 16 taken 12411 times.
|
12419 | if (wide) |
| 107 | { | ||
| 108 | // High byte of a Latin-1 code unit in UTF-16LE. | ||
| 109 |
1/2✓ Branch 15 → 16 taken 8 times.
✗ Branch 15 → 28 not taken.
|
8 | emit(0x00); |
| 110 | } | ||
| 111 | } | ||
| 112 |
2/2✓ Branch 18 → 19 taken 651 times.
✓ Branch 18 → 22 taken 1 time.
|
652 | if (query.require_terminator) |
| 113 | { | ||
| 114 |
1/2✓ Branch 19 → 20 taken 651 times.
✗ Branch 19 → 28 not taken.
|
651 | emit(0x00); |
| 115 |
2/2✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 22 taken 650 times.
|
651 | if (wide) |
| 116 | { | ||
| 117 |
1/2✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 28 not taken.
|
1 | emit(0x00); |
| 118 | } | ||
| 119 | } | ||
| 120 |
1/2✓ Branch 23 → 24 taken 652 times.
✗ Branch 23 → 28 not taken.
|
652 | return Scanner::parse_aob(aob); |
| 121 | 652 | } | |
| 122 | |||
| 123 | // Best-effort diagnosis for executable windows skipped because they faulted mid-scan. A module image is rarely | ||
| 124 | // decommitted under a live resolve, but collect_executable_windows only proves readability at gate time, so the | ||
| 125 | // window scans below guard their reads and a faulted window is skipped, not fatal. try_log is level-gated and | ||
| 126 | // no-throw. | ||
| 127 | 1256 | void log_faulted_windows(std::size_t faulted_windows) noexcept | |
| 128 | { | ||
| 129 |
2/2✓ Branch 2 → 3 taken 794 times.
✓ Branch 2 → 4 taken 462 times.
|
1256 | if (faulted_windows == 0) |
| 130 | { | ||
| 131 | 794 | return; | |
| 132 | } | ||
| 133 | 462 | (void)Logger::get_instance().try_log( | |
| 134 | LogLevel::Debug, | ||
| 135 | "Scanner::find_string_xref: skipped {} executable window(s) that faulted mid-scan (concurrent " | ||
| 136 | "decommit/reprotect).", | ||
| 137 | faulted_windows); | ||
| 138 | } | ||
| 139 | |||
| 140 | // Inner narrow scan of one already-gated executable window (no fault guard). Mutates found_count / first_site, | ||
| 141 | // returning once a second referencing site is seen (found_count == 2) so the caller fails closed on ambiguity. | ||
| 142 | // The recognized instruction shape is documented on scan_string_ref_narrow. | ||
| 143 | 916 | void scan_window_narrow_body(const Scanner::detail::ExecutableWindow &window, std::uintptr_t string_addr, | |
| 144 | std::size_t instr_len, std::size_t &found_count, std::uintptr_t &first_site, | ||
| 145 | LeaReferenceInfo *info) noexcept | ||
| 146 | { | ||
| 147 | 916 | const auto *bytes = reinterpret_cast<const std::uint8_t *>(window.base); | |
| 148 |
2/2✓ Branch 18 → 3 taken 2706287 times.
✓ Branch 18 → 19 taken 912 times.
|
2707199 | for (std::size_t i = 0; i + instr_len <= window.span; ++i) |
| 149 | { | ||
| 150 | 2706287 | const std::uint8_t rex = bytes[i]; | |
| 151 | 2706287 | const std::uint8_t opcode = bytes[i + 1]; | |
| 152 | 2706287 | const std::uint8_t modrm = bytes[i + 2]; | |
| 153 |
10/10✓ Branch 3 → 4 taken 2463762 times.
✓ Branch 3 → 8 taken 242525 times.
✓ Branch 4 → 5 taken 656 times.
✓ Branch 4 → 8 taken 2463106 times.
✓ Branch 5 → 6 taken 23 times.
✓ Branch 5 → 7 taken 633 times.
✓ Branch 6 → 7 taken 5 times.
✓ Branch 6 → 8 taken 18 times.
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 9 taken 635 times.
|
2706287 | if (rex < 0x48 || rex > 0x4F || (opcode != 0x8D && opcode != 0x8B) || (modrm & 0xC7) != 0x05) |
| 154 | { | ||
| 155 | 2705652 | continue; | |
| 156 | } | ||
| 157 | 635 | std::int32_t disp = 0; | |
| 158 | 635 | std::memcpy(&disp, &bytes[i + 3], sizeof(disp)); | |
| 159 | 635 | const std::uintptr_t instr_addr = window.base + i; | |
| 160 | 635 | const std::uintptr_t target = | |
| 161 | 635 | instr_addr + instr_len + static_cast<std::uintptr_t>(static_cast<std::int64_t>(disp)); | |
| 162 |
1/2✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 635 times.
|
635 | if (target != string_addr) |
| 163 | { | ||
| 164 | ✗ | continue; | |
| 165 | } | ||
| 166 | 635 | ++found_count; | |
| 167 |
2/2✓ Branch 11 → 12 taken 631 times.
✓ Branch 11 → 14 taken 4 times.
|
635 | if (found_count == 1) |
| 168 | { | ||
| 169 | 631 | first_site = instr_addr; | |
| 170 |
1/2✓ Branch 12 → 13 taken 631 times.
✗ Branch 12 → 15 not taken.
|
631 | if (info != nullptr) |
| 171 | { | ||
| 172 | // REX.R (bit 2 of the REX byte) is the high bit of the ModRM.reg field; the narrow shape | ||
| 173 | // accepts REX in 0x48..0x4F, so REX.R may be set for an r8..r15 destination. opcode 0x8D is lea | ||
| 174 | // (a load whose pointer a following store can cache); 0x8B is a mov load with no such store to | ||
| 175 | // model. | ||
| 176 | 631 | const std::uint8_t reg = static_cast<std::uint8_t>(((rex & 0x04) << 1) | ((modrm >> 3) & 0x07)); | |
| 177 | 631 | *info = LeaReferenceInfo{reg, instr_len, window.base + window.span, opcode == 0x8D}; | |
| 178 | } | ||
| 179 | } | ||
| 180 | else | ||
| 181 | { | ||
| 182 | // Ambiguous; caller maps found_count >= 2 to AmbiguousReference. | ||
| 183 | 4 | return; | |
| 184 | } | ||
| 185 | } | ||
| 186 | } | ||
| 187 | |||
| 188 | // Window-granular TOCTOU fault guard around scan_window_narrow_body. collect_executable_windows gated each | ||
| 189 | // window with one VirtualQuery; a concurrent decommit / reprotect before these unguarded byte reads complete | ||
| 190 | // would otherwise fault the host. On MSVC the body runs inside a __try / __except that swallows exactly the | ||
| 191 | // foreign-read faults (Memory::detail::is_guarded_read_fault) and reports the window faulted so the sweep skips | ||
| 192 | // it. On MinGW x64 the body runs through the same process-wide vectored read guard the seh_read paths use | ||
| 193 | // (Memory::detail::run_guarded_region), so the fault is swallowed and the window skipped + counted there too; | ||
| 194 | // only on 32-bit MinGW, where that x64-only vectored guard is unavailable, does the body run directly behind | ||
| 195 | // just the VirtualQuery gate. Mirrors scan_region_guarded in scanner.cpp. Returns true when a fault was | ||
| 196 | // swallowed. | ||
| 197 | 916 | bool scan_window_narrow_guarded(const Scanner::detail::ExecutableWindow &window, std::uintptr_t string_addr, | |
| 198 | std::size_t instr_len, std::size_t &found_count, std::uintptr_t &first_site, | ||
| 199 | LeaReferenceInfo *info) noexcept | ||
| 200 | { | ||
| 201 | #ifdef _MSC_VER | ||
| 202 | const std::size_t original_found_count = found_count; | ||
| 203 | const std::uintptr_t original_first_site = first_site; | ||
| 204 | const LeaReferenceInfo original_info = (info != nullptr) ? *info : LeaReferenceInfo{}; | ||
| 205 | __try | ||
| 206 | { | ||
| 207 | scan_window_narrow_body(window, string_addr, instr_len, found_count, first_site, info); | ||
| 208 | return false; | ||
| 209 | } | ||
| 210 | __except (Memory::detail::is_guarded_read_fault(GetExceptionCode()) ? EXCEPTION_EXECUTE_HANDLER | ||
| 211 | : EXCEPTION_CONTINUE_SEARCH) | ||
| 212 | { | ||
| 213 | // The caller skips faulted windows, so discard any reference count (and recovered lea info) collected | ||
| 214 | // before the fault, or a partially-scanned window could leak a stale site/register. | ||
| 215 | found_count = original_found_count; | ||
| 216 | first_site = original_first_site; | ||
| 217 | if (info != nullptr) | ||
| 218 | { | ||
| 219 | *info = original_info; | ||
| 220 | } | ||
| 221 | return true; | ||
| 222 | } | ||
| 223 | #elif defined(_WIN64) | ||
| 224 | // MinGW x64: arm the process-wide vectored read guard over exactly the bytes the window gate proved | ||
| 225 | // readable, so a concurrent decommit / reprotect that faults the scan is swallowed and the window reported | ||
| 226 | // faulted -- the same skip-the-window contract the MSVC __except arm and scanner.cpp's scan_region_guarded | ||
| 227 | // follow. | ||
| 228 | 916 | const std::size_t original_found_count = found_count; | |
| 229 | 916 | const std::uintptr_t original_first_site = first_site; | |
| 230 |
1/2✓ Branch 2 → 3 taken 916 times.
✗ Branch 2 → 4 not taken.
|
916 | const LeaReferenceInfo original_info = (info != nullptr) ? *info : LeaReferenceInfo{}; |
| 231 | struct NarrowScanContext | ||
| 232 | { | ||
| 233 | const Scanner::detail::ExecutableWindow *window; | ||
| 234 | std::uintptr_t string_addr; | ||
| 235 | std::size_t instr_len; | ||
| 236 | std::size_t *found_count; | ||
| 237 | std::uintptr_t *first_site; | ||
| 238 | LeaReferenceInfo *info; | ||
| 239 | 916 | } scan_ctx{&window, string_addr, instr_len, &found_count, &first_site, info}; | |
| 240 | |||
| 241 | 916 | const auto run_scan = [](void *opaque) noexcept -> void | |
| 242 | { | ||
| 243 | 916 | auto *context = static_cast<NarrowScanContext *>(opaque); | |
| 244 | 916 | scan_window_narrow_body(*context->window, context->string_addr, context->instr_len, | |
| 245 | 916 | *context->found_count, *context->first_site, context->info); | |
| 246 | 654 | }; | |
| 247 | |||
| 248 |
2/2✓ Branch 7 → 8 taken 654 times.
✓ Branch 7 → 9 taken 262 times.
|
916 | if (Memory::detail::run_guarded_region(window.base, window.base + window.span, run_scan, &scan_ctx)) |
| 249 | { | ||
| 250 | 654 | return false; | |
| 251 | } | ||
| 252 | // Faulted: discard any partial count / site / lea info so a partially-scanned window cannot leak a stale | ||
| 253 | // site or register, exactly as the MSVC arm does. | ||
| 254 | 262 | found_count = original_found_count; | |
| 255 | 262 | first_site = original_first_site; | |
| 256 |
1/2✓ Branch 9 → 10 taken 262 times.
✗ Branch 9 → 11 not taken.
|
262 | if (info != nullptr) |
| 257 | { | ||
| 258 | 262 | *info = original_info; | |
| 259 | } | ||
| 260 | 262 | return true; | |
| 261 | #else | ||
| 262 | scan_window_narrow_body(window, string_addr, instr_len, found_count, first_site, info); | ||
| 263 | return false; | ||
| 264 | #endif | ||
| 265 | } | ||
| 266 | |||
| 267 | // Phase 2 (default, "narrow") of find_string_xref: scan the image's execute-readable windows for the dominant | ||
| 268 | // 64-bit string-load forms whose resolved absolute target is string_addr. | ||
| 269 | // | ||
| 270 | // Recognizes a mandatory REX.W prefix (0x48..0x4F), opcode 8D (lea) or 8B (mov), and a ModRM byte in the | ||
| 271 | // RIP-relative form -- mod == 00b and rm == 101b, i.e. (modrm & 0xC7) == 0x05 -- followed by a 4-byte | ||
| 272 | // displacement. That required REX.W byte makes the instruction exactly 7 bytes and the disp32 sits at offset 3, | ||
| 273 | // so the candidate is self-delimiting from its shape: this needs no instruction-aligned linear sweep and | ||
| 274 | // therefore cannot desync on data or jump tables embedded in .text. | ||
| 275 | // | ||
| 276 | // The resolved target is next-instruction-address + sign-extended disp32, done in unsigned modular arithmetic | ||
| 277 | // so it is well-defined for every input. Only a target that exactly equals string_addr is accepted, which is an | ||
| 278 | // extremely strong filter: string_addr is the already-located, plausible | ||
| 279 | // .rdata address, so this equality subsumes the plausible_userspace_ptr floor (an implausible target cannot | ||
| 280 | // equal it) and a coincidental byte sequence resolving to precisely string_addr is not a realistic false | ||
| 281 | // positive. Counting stops at the second hit so the caller can fail closed on ambiguity. | ||
| 282 | 644 | std::uintptr_t scan_string_ref_narrow(std::uintptr_t string_addr, Memory::ModuleRange range, | |
| 283 | std::size_t &found_count, LeaReferenceInfo &info) | ||
| 284 | { | ||
| 285 | 644 | found_count = 0; | |
| 286 | 644 | std::uintptr_t first_site = 0; | |
| 287 | 644 | info = LeaReferenceInfo{}; | |
| 288 | // REX.W + opcode + ModRM + disp32. | ||
| 289 | 644 | constexpr std::size_t instr_len = 7; | |
| 290 | // scan_window_narrow_body reads bytes[i], bytes[i+1], bytes[i+2] and a disp32 at bytes[i+3..i+6], so the | ||
| 291 | // highest index it touches is i+6. The per-window loop only bounds i + instr_len <= span, so instr_len | ||
| 292 | // must cover that widest read or the disp32 fetch could run up to four bytes past the window. Pin the | ||
| 293 | // coupling here, beside the shape's byte count, so a future instr_len change cannot silently reopen it. | ||
| 294 | 644 | constexpr std::size_t narrow_max_read_index = 6; | |
| 295 | static_assert(narrow_max_read_index < instr_len, | ||
| 296 | "instr_len must span scan_window_narrow_body's disp32 tail read at bytes[i+3..i+6]"); | ||
| 297 | 644 | std::size_t faulted_windows = 0; | |
| 298 | |||
| 299 |
3/4✓ Branch 2 → 3 taken 644 times.
✗ Branch 2 → 37 not taken.
✓ Branch 24 → 5 taken 916 times.
✓ Branch 24 → 25 taken 640 times.
|
2200 | for (const auto &window : Scanner::detail::collect_executable_windows(range)) |
| 300 | { | ||
| 301 |
1/2✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 916 times.
|
916 | if (window.span < instr_len) |
| 302 | { | ||
| 303 | ✗ | continue; | |
| 304 | } | ||
| 305 |
2/2✓ Branch 10 → 11 taken 262 times.
✓ Branch 10 → 12 taken 654 times.
|
916 | if (scan_window_narrow_guarded(window, string_addr, instr_len, found_count, first_site, &info)) |
| 306 | { | ||
| 307 | 262 | ++faulted_windows; | |
| 308 | 262 | continue; | |
| 309 | } | ||
| 310 |
2/2✓ Branch 12 → 13 taken 4 times.
✓ Branch 12 → 15 taken 650 times.
|
654 | if (found_count >= 2) |
| 311 | { | ||
| 312 | // Ambiguous; caller maps found_count >= 2 to AmbiguousReference. | ||
| 313 | 4 | log_faulted_windows(faulted_windows); | |
| 314 | 4 | return 0; | |
| 315 | } | ||
| 316 |
2/2✓ Branch 27 → 28 taken 640 times.
✓ Branch 27 → 31 taken 4 times.
|
644 | } |
| 317 | 640 | log_faulted_windows(faulted_windows); | |
| 318 |
2/2✓ Branch 30 → 32 taken 627 times.
✓ Branch 30 → 33 taken 13 times.
|
640 | return (found_count == 1) ? first_site : 0; |
| 319 | } | ||
| 320 | |||
| 321 | // Store-xref forward scan: starting just past a `lea reg, [rip+string]`, decode forward instruction by | ||
| 322 | // instruction (Zydis, the same decoder scan_string_ref_broad uses) and return the slot a `mov [rip+slot], reg` | ||
| 323 | // store caches the loaded pointer into, where reg is the lea destination. Decoding rather than a raw byte sweep | ||
| 324 | // keeps the match instruction-aligned -- a `48 89 05 ...` byte run buried inside another instruction's | ||
| 325 | // immediate or displacement is never mistaken for a store -- and lets the scan stop the moment the loaded | ||
| 326 | // register is overwritten, since a store after that would cache a different value, not this lea's pointer. The | ||
| 327 | // store shape is REX.W MOV [rip+disp32], reg64: a RIP-relative memory destination plus the matching 64-bit | ||
| 328 | // source register (the mirror of the REX.W load). The match is first-within-window, not uniqueness-checked, so | ||
| 329 | // the window is kept tight. Returns 0 (the caller maps it to StoreNotFound) on no store, a write to the loaded | ||
| 330 | // register, a CALL (which clobbers the caller-saved set), a decode failure (alignment lost / non-code), an | ||
| 331 | // unreadable byte, or the bound being hit. Reads go through Memory::seh_read_bytes so a truncated or unmapped | ||
| 332 | // tail cannot fault the host. The scan is bounded to the lea's own executable window (window_end), the module | ||
| 333 | // range, and a small forward window. | ||
| 334 | 8 | std::uintptr_t scan_store_slot_after_lea(std::uintptr_t lea_site, std::size_t lea_len, | |
| 335 | std::uintptr_t window_end, std::uint8_t lea_reg, | ||
| 336 | Memory::ModuleRange range) noexcept | ||
| 337 | { | ||
| 338 | // A cached pointer is stored very close to its load; bound the forward scan so a pathological region cannot | ||
| 339 | // scan unboundedly and the store cannot be attributed to a distant, unrelated reuse of the same register. | ||
| 340 | 8 | constexpr std::size_t forward_window = 0x80; // 128 bytes. | |
| 341 | 8 | const std::uintptr_t scan_lo = lea_site + lea_len; | |
| 342 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 8 times.
|
8 | if (scan_lo < lea_site) |
| 343 | { | ||
| 344 | ✗ | return 0; | |
| 345 | } | ||
| 346 |
2/2✓ Branch 4 → 5 taken 7 times.
✓ Branch 4 → 6 taken 1 time.
|
8 | const std::uintptr_t scan_end = (window_end < range.end) ? window_end : range.end; |
| 347 |
1/2✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 8 times.
|
8 | if (scan_lo >= scan_end) |
| 348 | { | ||
| 349 | ✗ | return 0; | |
| 350 | } | ||
| 351 |
2/2✓ Branch 9 → 10 taken 7 times.
✓ Branch 9 → 11 taken 1 time.
|
8 | const std::uintptr_t scan_hi = (scan_end - scan_lo < forward_window) ? scan_end : scan_lo + forward_window; |
| 352 | |||
| 353 | ZydisDecoder decoder; | ||
| 354 |
1/2✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 8 times.
|
8 | if (!ZYAN_SUCCESS(ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64))) |
| 355 | { | ||
| 356 | ✗ | return 0; | |
| 357 | } | ||
| 358 | // The lea wrote a 64-bit pointer, so the store source and any clobber are tested against the full 64-bit | ||
| 359 | // register. ZydisRegisterEncode maps the x86 register number (REX.R << 3 | ModRM.reg) to the GPR64 | ||
| 360 | // register. | ||
| 361 | 8 | const ZydisRegister target_reg = ZydisRegisterEncode(ZYDIS_REGCLASS_GPR64, lea_reg); | |
| 362 |
1/2✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 8 times.
|
8 | if (target_reg == ZYDIS_REGISTER_NONE) |
| 363 | { | ||
| 364 | ✗ | return 0; | |
| 365 | } | ||
| 366 | |||
| 367 | 8 | std::uintptr_t p = scan_lo; | |
| 368 |
2/2✓ Branch 57 → 19 taken 266 times.
✓ Branch 57 → 58 taken 3 times.
|
269 | while (p < scan_hi) |
| 369 | { | ||
| 370 | 266 | std::array<std::uint8_t, ZYDIS_MAX_INSTRUCTION_LENGTH> code{}; | |
| 371 | 266 | const std::size_t avail = | |
| 372 | 266 | (scan_hi - p < code.size()) ? static_cast<std::size_t>(scan_hi - p) : code.size(); | |
| 373 |
1/2✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 266 times.
|
266 | if (!Memory::seh_read_bytes(p, code.data(), avail)) |
| 374 | { | ||
| 375 | // Unreadable byte: the store sits in the same execute-readable window as the lea, so a fault here | ||
| 376 | // means it is not present in mapped code. | ||
| 377 | 5 | return 0; | |
| 378 | } | ||
| 379 | |||
| 380 | ZydisDecodedInstruction insn; | ||
| 381 | ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT]; | ||
| 382 |
2/2✓ Branch 29 → 30 taken 2 times.
✓ Branch 29 → 31 taken 264 times.
|
266 | if (!ZYAN_SUCCESS(ZydisDecoderDecodeFull(&decoder, code.data(), avail, &insn, operands))) |
| 383 | { | ||
| 384 | // The store must be instruction-aligned with the lea; a decode failure means alignment was lost or | ||
| 385 | // the bytes are not code. Fail closed rather than accept a misaligned match. | ||
| 386 | 2 | return 0; | |
| 387 | } | ||
| 388 | |||
| 389 | // The store: MOV with a RIP-relative memory destination (operand 0) and the matching 64-bit source | ||
| 390 | // register (operand 1). ZydisCalcAbsoluteAddress turns the destination into the slot's effective | ||
| 391 | // address (next-instruction address + sign-extended disp32). | ||
| 392 |
3/4✓ Branch 31 → 32 taken 5 times.
✓ Branch 31 → 42 taken 259 times.
✓ Branch 32 → 33 taken 5 times.
✗ Branch 32 → 42 not taken.
|
264 | if (insn.mnemonic == ZYDIS_MNEMONIC_MOV && insn.operand_count_visible >= 2 && |
| 393 |
3/4✓ Branch 33 → 34 taken 3 times.
✓ Branch 33 → 42 taken 2 times.
✓ Branch 34 → 35 taken 3 times.
✗ Branch 34 → 42 not taken.
|
5 | operands[0].type == ZYDIS_OPERAND_TYPE_MEMORY && operands[0].mem.base == ZYDIS_REGISTER_RIP && |
| 394 |
3/4✓ Branch 35 → 36 taken 3 times.
✗ Branch 35 → 42 not taken.
✓ Branch 36 → 37 taken 2 times.
✓ Branch 36 → 42 taken 1 time.
|
3 | operands[1].type == ZYDIS_OPERAND_TYPE_REGISTER && operands[1].reg.value == target_reg) |
| 395 | { | ||
| 396 | 2 | ZyanU64 absolute = 0; | |
| 397 |
1/2✓ Branch 38 → 39 taken 2 times.
✗ Branch 38 → 40 not taken.
|
2 | if (ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&insn, &operands[0], static_cast<ZyanU64>(p), &absolute))) |
| 398 | { | ||
| 399 | 2 | return static_cast<std::uintptr_t>(absolute); | |
| 400 | } | ||
| 401 | ✗ | return 0; | |
| 402 | } | ||
| 403 | |||
| 404 | // A CALL clobbers the caller-saved registers; a store after it cannot be trusted to cache this lea's | ||
| 405 | // pointer, so stop conservatively rather than attribute a post-call store to the wrong value. | ||
| 406 |
1/2✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 262 times.
|
262 | if (insn.meta.category == ZYDIS_CATEGORY_CALL) |
| 407 | { | ||
| 408 | ✗ | return 0; | |
| 409 | } | ||
| 410 | // Any write to the loaded register (at any width -- a 32-bit write zeroes the upper half) means a later | ||
| 411 | // store would cache a different value. Check every operand, including implicit ones. | ||
| 412 |
2/2✓ Branch 54 → 45 taken 782 times.
✓ Branch 54 → 55 taken 261 times.
|
1043 | for (std::size_t op = 0; op < insn.operand_count; ++op) |
| 413 | { | ||
| 414 | 782 | const ZydisDecodedOperand &operand = operands[op]; | |
| 415 | 2085 | if (operand.type == ZYDIS_OPERAND_TYPE_REGISTER && | |
| 416 |
6/6✓ Branch 45 → 46 taken 521 times.
✓ Branch 45 → 50 taken 261 times.
✓ Branch 46 → 47 taken 261 times.
✓ Branch 46 → 50 taken 260 times.
✓ Branch 51 → 52 taken 1 time.
✓ Branch 51 → 53 taken 781 times.
|
1043 | (operand.actions & ZYDIS_OPERAND_ACTION_MASK_WRITE) != 0 && |
| 417 |
2/2✓ Branch 48 → 49 taken 1 time.
✓ Branch 48 → 50 taken 260 times.
|
261 | ZydisRegisterGetLargestEnclosing(ZYDIS_MACHINE_MODE_LONG_64, operand.reg.value) == target_reg) |
| 418 | { | ||
| 419 | 1 | return 0; | |
| 420 | } | ||
| 421 | } | ||
| 422 | |||
| 423 | 261 | p += insn.length; | |
| 424 | } | ||
| 425 | 3 | return 0; | |
| 426 | } | ||
| 427 | |||
| 428 | // Inner broad scan of one already-gated executable window (no fault guard). Decodes each position with Zydis, | ||
| 429 | // counting RIP-relative operands whose absolute target equals string_addr; mutates found_count / first_site and | ||
| 430 | // returns once a second referencing site is seen. The decode/recovery contract is documented on | ||
| 431 | // scan_string_ref_broad. | ||
| 432 | 814 | void scan_window_broad_body(const ZydisDecoder &decoder, const Scanner::detail::ExecutableWindow &window, | |
| 433 | std::uintptr_t string_addr, std::size_t &found_count, | ||
| 434 | std::uintptr_t &first_site) noexcept | ||
| 435 | { | ||
| 436 | 814 | const auto *bytes = reinterpret_cast<const std::uint8_t *>(window.base); | |
| 437 | 814 | std::size_t offset = 0; | |
| 438 |
2/2✓ Branch 26 → 3 taken 2477584 times.
✓ Branch 26 → 27 taken 613 times.
|
2478197 | while (offset < window.span) |
| 439 | { | ||
| 440 | ZydisDecodedInstruction insn; | ||
| 441 | ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT]; | ||
| 442 | 2477584 | const std::uintptr_t instr_addr = window.base + offset; | |
| 443 |
2/2✓ Branch 4 → 5 taken 5 times.
✓ Branch 4 → 6 taken 2477379 times.
|
2477584 | if (!ZYAN_SUCCESS( |
| 444 | ZydisDecoderDecodeFull(&decoder, bytes + offset, window.span - offset, &insn, operands))) | ||
| 445 | { | ||
| 446 | // Byte-restart recovery: realign past data / jump tables. | ||
| 447 | 5 | ++offset; | |
| 448 | 5 | continue; | |
| 449 | } | ||
| 450 | |||
| 451 | // A referencing instruction has a visible memory operand based on | ||
| 452 | // RIP whose absolute target is the string. Visible operands are ordered first in the array, so | ||
| 453 | // iterating the visible count covers every explicit operand a disassembler would show. | ||
| 454 | 2477379 | bool references_string = false; | |
| 455 |
2/2✓ Branch 19 → 7 taken 52016 times.
✓ Branch 19 → 20 taken 2476769 times.
|
2528785 | for (std::size_t op = 0; op < insn.operand_count_visible; ++op) |
| 456 | { | ||
| 457 | 52016 | const ZydisDecodedOperand &operand = operands[op]; | |
| 458 |
4/4✓ Branch 7 → 8 taken 24207 times.
✓ Branch 7 → 9 taken 27809 times.
✓ Branch 8 → 9 taken 23597 times.
✓ Branch 8 → 10 taken 610 times.
|
52016 | if (operand.type != ZYDIS_OPERAND_TYPE_MEMORY || operand.mem.base != ZYDIS_REGISTER_RIP) |
| 459 | { | ||
| 460 | 51406 | continue; | |
| 461 | } | ||
| 462 | 610 | ZyanU64 absolute = 0; | |
| 463 | 610 | if (ZYAN_SUCCESS( | |
| 464 |
2/4✓ Branch 11 → 12 taken 610 times.
✗ Branch 11 → 14 not taken.
✓ Branch 15 → 16 taken 610 times.
✗ Branch 15 → 17 not taken.
|
1220 | ZydisCalcAbsoluteAddress(&insn, &operand, static_cast<ZyanU64>(instr_addr), &absolute)) && |
| 465 |
1/2✓ Branch 12 → 13 taken 610 times.
✗ Branch 12 → 14 not taken.
|
610 | static_cast<std::uintptr_t>(absolute) == string_addr) |
| 466 | { | ||
| 467 | 610 | references_string = true; | |
| 468 | 610 | break; | |
| 469 | } | ||
| 470 | } | ||
| 471 | |||
| 472 |
2/2✓ Branch 20 → 21 taken 610 times.
✓ Branch 20 → 24 taken 2476769 times.
|
2477379 | if (references_string) |
| 473 | { | ||
| 474 | 610 | ++found_count; | |
| 475 |
2/2✓ Branch 21 → 22 taken 609 times.
✓ Branch 21 → 23 taken 1 time.
|
610 | if (found_count == 1) |
| 476 | { | ||
| 477 | 609 | first_site = instr_addr; | |
| 478 | } | ||
| 479 | else | ||
| 480 | { | ||
| 481 | // Ambiguous; caller maps found_count >= 2 to AmbiguousReference. | ||
| 482 | 1 | return; | |
| 483 | } | ||
| 484 | } | ||
| 485 | |||
| 486 | 2477378 | offset += insn.length; | |
| 487 | } | ||
| 488 | } | ||
| 489 | |||
| 490 | // Window-granular TOCTOU fault guard around scan_window_broad_body; the narrow sibling | ||
| 491 | // scan_window_narrow_guarded documents the rationale. Returns true when a fault was swallowed. | ||
| 492 | 814 | bool scan_window_broad_guarded(const ZydisDecoder &decoder, const Scanner::detail::ExecutableWindow &window, | |
| 493 | std::uintptr_t string_addr, std::size_t &found_count, | ||
| 494 | std::uintptr_t &first_site) noexcept | ||
| 495 | { | ||
| 496 | #ifdef _MSC_VER | ||
| 497 | const std::size_t original_found_count = found_count; | ||
| 498 | const std::uintptr_t original_first_site = first_site; | ||
| 499 | __try | ||
| 500 | { | ||
| 501 | scan_window_broad_body(decoder, window, string_addr, found_count, first_site); | ||
| 502 | return false; | ||
| 503 | } | ||
| 504 | __except (Memory::detail::is_guarded_read_fault(GetExceptionCode()) ? EXCEPTION_EXECUTE_HANDLER | ||
| 505 | : EXCEPTION_CONTINUE_SEARCH) | ||
| 506 | { | ||
| 507 | // The caller skips faulted windows, so discard any reference count collected before the fault. | ||
| 508 | found_count = original_found_count; | ||
| 509 | first_site = original_first_site; | ||
| 510 | return true; | ||
| 511 | } | ||
| 512 | #elif defined(_WIN64) | ||
| 513 | // MinGW x64: same vectored read guard as the narrow sibling, armed over the gated window bytes; a fault is | ||
| 514 | // swallowed and the window reported faulted. Only 32-bit MinGW falls back to the bare VirtualQuery gate. | ||
| 515 | 814 | const std::size_t original_found_count = found_count; | |
| 516 | 814 | const std::uintptr_t original_first_site = first_site; | |
| 517 | struct BroadScanContext | ||
| 518 | { | ||
| 519 | const ZydisDecoder *decoder; | ||
| 520 | const Scanner::detail::ExecutableWindow *window; | ||
| 521 | std::uintptr_t string_addr; | ||
| 522 | std::size_t *found_count; | ||
| 523 | std::uintptr_t *first_site; | ||
| 524 | 814 | } scan_ctx{&decoder, &window, string_addr, &found_count, &first_site}; | |
| 525 | |||
| 526 | 814 | const auto run_scan = [](void *opaque) noexcept -> void | |
| 527 | { | ||
| 528 | 814 | auto *context = static_cast<BroadScanContext *>(opaque); | |
| 529 | 814 | scan_window_broad_body(*context->decoder, *context->window, context->string_addr, *context->found_count, | |
| 530 | 814 | *context->first_site); | |
| 531 | 614 | }; | |
| 532 | |||
| 533 |
2/2✓ Branch 4 → 5 taken 614 times.
✓ Branch 4 → 6 taken 200 times.
|
814 | if (Memory::detail::run_guarded_region(window.base, window.base + window.span, run_scan, &scan_ctx)) |
| 534 | { | ||
| 535 | 614 | return false; | |
| 536 | } | ||
| 537 | // Faulted: discard any partial count / site so a partially-scanned window cannot leak a stale site. | ||
| 538 | 200 | found_count = original_found_count; | |
| 539 | 200 | first_site = original_first_site; | |
| 540 | 200 | return true; | |
| 541 | #else | ||
| 542 | scan_window_broad_body(decoder, window, string_addr, found_count, first_site); | ||
| 543 | return false; | ||
| 544 | #endif | ||
| 545 | } | ||
| 546 | |||
| 547 | // Phase 2 ("broad") add-on for find_string_xref: a Zydis-verified linear sweep that recognizes the rarer | ||
| 548 | // RIP-relative reference shapes the narrow | ||
| 549 | // scan does not model -- cmp [rip+d], imm; push [rip+d]; a no-REX lea/mov; | ||
| 550 | // any instruction whose memory operand is [rip+disp] and resolves to string_addr. The caller merges this with | ||
| 551 | // the narrow scan so broad_match cannot lose coverage for the default lea/mov anchors. | ||
| 552 | // | ||
| 553 | // The sweep decodes each position with the same decoder code_constant.cpp uses (ZydisDecoderDecodeFull + | ||
| 554 | // ZydisCalcAbsoluteAddress). x86-64 is not self-synchronizing, so on a decode failure the cursor advances one | ||
| 555 | // byte to realign past embedded data or jump tables (recovery); on success it advances by the decoded | ||
| 556 | // instruction length, which is always >= 1 so the sweep cannot stall. As in the narrow scan, only a | ||
| 557 | // RIP-relative operand whose absolute target exactly equals string_addr counts. That exact-target filter | ||
| 558 | // subsumes the plausibility floor and neutralizes a mid-.text desync: | ||
| 559 | // a bogus instruction decoded out of embedded data almost never carries a RIP operand resolving to precisely | ||
| 560 | // string_addr. The one residual miss is a reference truncated at the very end of a window (too few bytes to | ||
| 561 | // decode, so the sweep byte-restarts past it); that cannot happen mid-image, since a | ||
| 562 | // PE's executable section is one contiguous window and a real reference never sits in its final few bytes. | ||
| 563 | // Counting stops at the second referencing instruction so the caller fails closed on ambiguity. | ||
| 564 | 612 | std::uintptr_t scan_string_ref_broad(std::uintptr_t string_addr, Memory::ModuleRange range, | |
| 565 | std::size_t &found_count) | ||
| 566 | { | ||
| 567 | 612 | found_count = 0; | |
| 568 | 612 | std::uintptr_t first_site = 0; | |
| 569 | |||
| 570 | ZydisDecoder decoder; | ||
| 571 |
2/4✓ Branch 2 → 3 taken 612 times.
✗ Branch 2 → 38 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 612 times.
|
612 | if (!ZYAN_SUCCESS(ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64))) |
| 572 | { | ||
| 573 | // A decoder that will not initialize cannot verify any reference; | ||
| 574 | // fail closed as "no reference" rather than guess a site. | ||
| 575 | ✗ | return 0; | |
| 576 | } | ||
| 577 | |||
| 578 | 612 | std::size_t faulted_windows = 0; | |
| 579 |
3/4✓ Branch 5 → 6 taken 612 times.
✗ Branch 5 → 37 not taken.
✓ Branch 25 → 8 taken 814 times.
✓ Branch 25 → 26 taken 611 times.
|
2037 | for (const auto &window : Scanner::detail::collect_executable_windows(range)) |
| 580 | { | ||
| 581 |
2/2✓ Branch 11 → 12 taken 200 times.
✓ Branch 11 → 13 taken 614 times.
|
814 | if (scan_window_broad_guarded(decoder, window, string_addr, found_count, first_site)) |
| 582 | { | ||
| 583 | 200 | ++faulted_windows; | |
| 584 | 200 | continue; | |
| 585 | } | ||
| 586 |
2/2✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 16 taken 613 times.
|
614 | if (found_count >= 2) |
| 587 | { | ||
| 588 | // Ambiguous; caller maps found_count >= 2 to AmbiguousReference. | ||
| 589 | 1 | log_faulted_windows(faulted_windows); | |
| 590 | 1 | return 0; | |
| 591 | } | ||
| 592 | } | ||
| 593 | 611 | log_faulted_windows(faulted_windows); | |
| 594 |
2/2✓ Branch 30 → 32 taken 608 times.
✓ Branch 30 → 33 taken 3 times.
|
611 | return (found_count == 1) ? first_site : 0; |
| 595 | } | ||
| 596 | |||
| 597 | // Best-effort enclosing-function entry for a referencing instruction. Walks backward for the nearest function | ||
| 598 | // boundary -- a terminal RET (0xC3) or a run of INT3 (0xCC) alignment padding -- and returns the first byte | ||
| 599 | // after it that passes is_likely_function_prologue, skipping any further INT3 padding. The back-scan is bounded | ||
| 600 | // so a pathological region cannot scan unboundedly, and it fails closed (returns 0) when no boundary is found | ||
| 601 | // in the window. This is a heuristic, not control-flow analysis: a 0xC3 / 0xCC byte that is actually operand | ||
| 602 | // data inside an instruction can mark a false boundary, which is why the default return mode is the exact | ||
| 603 | // referencing instruction. | ||
| 604 | 4 | std::uintptr_t enclosing_function_start(std::uintptr_t instr_addr, std::uintptr_t window_lo) noexcept | |
| 605 | { | ||
| 606 | 4 | constexpr std::uintptr_t back_scan_window = 0x2000; // 8 KiB. | |
| 607 | 4 | const std::uintptr_t floor = | |
| 608 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 3 times.
|
4 | (instr_addr - window_lo > back_scan_window) ? instr_addr - back_scan_window : window_lo; |
| 609 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 4 times.
|
4 | if (instr_addr <= floor) |
| 610 | { | ||
| 611 | ✗ | return 0; | |
| 612 | } | ||
| 613 | |||
| 614 | // Buffer the back-scan window once instead of issuing a guarded read per byte. The window is read in | ||
| 615 | // page-sized chunks from high address to low: a chunk that faults stops the read exactly where the | ||
| 616 | // byte-at-a-time walk would have faulted going downward, so any boundary in the already-buffered higher | ||
| 617 | // bytes is still found, and a fault below them returns 0 just as the per-byte version did. This replaces up | ||
| 618 | // to ~8 KiB of guarded reads (a VirtualQuery / SEH-frame each on MinGW) with at most a few. window[k] holds | ||
| 619 | // the byte at floor + k; valid_lo is the lowest address actually buffered, so the scan and the forward | ||
| 620 | // INT3-padding skip operate only over the buffered range [valid_lo, instr_addr) (the skip only moves toward | ||
| 621 | // instr_addr, i.e. into already-buffered higher addresses). | ||
| 622 | 4 | std::array<std::uint8_t, static_cast<std::size_t>(back_scan_window)> window{}; | |
| 623 | 4 | std::uintptr_t valid_lo = instr_addr; | |
| 624 | // Windows x86/x64 base page size; seh_read_bytes faults at this granularity. Chunks are page-aligned, not | ||
| 625 | // instr-aligned, so a single guarded read covers at most one page and never straddles a readable/unreadable | ||
| 626 | // page boundary: a fault in a lower page then cannot discard bytes already buffered from a readable higher | ||
| 627 | // page (which is exactly where the byte-at-a-time walk would have found a boundary before reaching the | ||
| 628 | // fault going downward). | ||
| 629 | 4 | constexpr std::uintptr_t page_size = 0x1000; | |
| 630 |
2/2✓ Branch 17 → 8 taken 7 times.
✓ Branch 17 → 18 taken 3 times.
|
10 | for (std::uintptr_t hi = instr_addr; hi > floor;) |
| 631 | { | ||
| 632 | 7 | const std::uintptr_t page_lo = (hi - 1) & ~(page_size - 1); | |
| 633 |
2/2✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 10 taken 4 times.
|
7 | const std::uintptr_t lo = (page_lo > floor) ? page_lo : floor; |
| 634 | 7 | const std::size_t len = static_cast<std::size_t>(hi - lo); | |
| 635 |
2/2✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 16 taken 6 times.
|
7 | if (!Memory::seh_read_bytes(lo, window.data() + (lo - floor), len)) |
| 636 | { | ||
| 637 | // This page is unreadable; stop. The byte-at-a-time walk would fault on the first byte here too, | ||
| 638 | // after exhausting the readable bytes buffered above it. | ||
| 639 | 1 | break; | |
| 640 | } | ||
| 641 | 6 | valid_lo = lo; | |
| 642 | 6 | hi = lo; | |
| 643 | } | ||
| 644 |
1/2✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 4 times.
|
4 | if (valid_lo >= instr_addr) |
| 645 | { | ||
| 646 | // Not even the highest chunk was readable: the first probed byte would have faulted. | ||
| 647 | ✗ | return 0; | |
| 648 | } | ||
| 649 | |||
| 650 |
2/2✓ Branch 39 → 21 taken 8615 times.
✓ Branch 39 → 40 taken 1 time.
|
8616 | for (std::uintptr_t probe = instr_addr; probe > valid_lo; --probe) |
| 651 | { | ||
| 652 | 8615 | const std::uint8_t boundary_byte = window[static_cast<std::size_t>(probe - 1 - floor)]; | |
| 653 |
3/4✓ Branch 22 → 23 taken 8612 times.
✓ Branch 22 → 25 taken 3 times.
✓ Branch 23 → 24 taken 8612 times.
✗ Branch 23 → 25 not taken.
|
8615 | if (boundary_byte != 0xCC && boundary_byte != 0xC3) |
| 654 | { | ||
| 655 | 8612 | continue; | |
| 656 | } | ||
| 657 | // The boundary byte ends the previous function (or its padding); the enclosing function begins at the | ||
| 658 | // first non-INT3 byte after it. | ||
| 659 | 3 | std::uintptr_t start = probe; | |
| 660 |
3/6✓ Branch 27 → 28 taken 3 times.
✗ Branch 27 → 31 not taken.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 3 times.
✗ Branch 32 → 26 not taken.
✓ Branch 32 → 33 taken 3 times.
|
3 | while (start < instr_addr && window[static_cast<std::size_t>(start - floor)] == 0xCC) |
| 661 | { | ||
| 662 | ✗ | ++start; | |
| 663 | } | ||
| 664 |
1/2✓ Branch 34 → 35 taken 3 times.
✗ Branch 34 → 36 not taken.
|
3 | return Scanner::is_likely_function_prologue(start) ? start : 0; |
| 665 | } | ||
| 666 | 1 | return 0; | |
| 667 | } | ||
| 668 | } // anonymous namespace | ||
| 669 | |||
| 670 | 654 | std::expected<std::uintptr_t, Scanner::StringXrefError> Scanner::find_string_xref(const StringRefQuery &query, | |
| 671 | Memory::ModuleRange range) | ||
| 672 | { | ||
| 673 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 7 taken 653 times.
|
654 | if (query.text.empty()) |
| 674 | { | ||
| 675 | 1 | return std::unexpected(StringXrefError::EmptyQuery); | |
| 676 | } | ||
| 677 |
2/2✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 12 taken 652 times.
|
653 | if (!range.valid()) |
| 678 | { | ||
| 679 | 1 | return std::unexpected(StringXrefError::InvalidRange); | |
| 680 | } | ||
| 681 | |||
| 682 | // Phase 1: locate the single occurrence of the string in the image's readable pages. The linker pools identical | ||
| 683 | // literals, so a second occurrence makes the anchor ambiguous and must fail closed. | ||
| 684 |
1/2✓ Branch 12 → 13 taken 652 times.
✗ Branch 12 → 73 not taken.
|
652 | const auto pattern = compile_string_pattern(query); |
| 685 |
1/2✗ Branch 14 → 15 not taken.
✓ Branch 14 → 18 taken 652 times.
|
652 | if (!pattern) |
| 686 | { | ||
| 687 | // Non-empty text that does not compile to a pattern cannot be located; | ||
| 688 | // report not-found rather than guess. | ||
| 689 | ✗ | return std::unexpected(StringXrefError::StringNotFound); | |
| 690 | } | ||
| 691 | 652 | const std::byte *first = detail::scan_module_readable(*pattern, range, 1); | |
| 692 |
2/2✓ Branch 20 → 21 taken 6 times.
✓ Branch 20 → 24 taken 646 times.
|
652 | if (first == nullptr) |
| 693 | { | ||
| 694 | 6 | return std::unexpected(StringXrefError::StringNotFound); | |
| 695 | } | ||
| 696 |
2/2✓ Branch 26 → 27 taken 2 times.
✓ Branch 26 → 30 taken 644 times.
|
646 | if (detail::scan_module_readable(*pattern, range, 2) != nullptr) |
| 697 | { | ||
| 698 | 2 | return std::unexpected(StringXrefError::StringAmbiguous); | |
| 699 | } | ||
| 700 | 644 | const auto string_addr = reinterpret_cast<std::uintptr_t>(first); | |
| 701 | |||
| 702 | // Phase 2: find the single RIP-relative reference whose target is the string. The narrow scan is the fast, | ||
| 703 | // desync-immune default; broad_match keeps that coverage and adds a Zydis sweep for rarer reference shapes. | ||
| 704 | 644 | ReferenceScanResult references{}; | |
| 705 | 644 | std::size_t narrow_count = 0; | |
| 706 | 644 | LeaReferenceInfo lea_info{}; | |
| 707 |
1/2✓ Branch 30 → 31 taken 644 times.
✗ Branch 30 → 71 not taken.
|
644 | const std::uintptr_t narrow_site = scan_string_ref_narrow(string_addr, range, narrow_count, lea_info); |
| 708 | 644 | merge_reference_scan(references, narrow_site, narrow_count); | |
| 709 |
4/4✓ Branch 32 → 33 taken 613 times.
✓ Branch 32 → 37 taken 31 times.
✓ Branch 33 → 34 taken 612 times.
✓ Branch 33 → 37 taken 1 time.
|
644 | if (query.broad_match && references.count < 2) |
| 710 | { | ||
| 711 | 612 | std::size_t broad_count = 0; | |
| 712 |
1/2✓ Branch 34 → 35 taken 612 times.
✗ Branch 34 → 70 not taken.
|
612 | const std::uintptr_t broad_site = scan_string_ref_broad(string_addr, range, broad_count); |
| 713 | 612 | merge_reference_scan(references, broad_site, broad_count); | |
| 714 | } | ||
| 715 | |||
| 716 |
2/2✓ Branch 37 → 38 taken 7 times.
✓ Branch 37 → 41 taken 637 times.
|
644 | if (references.count == 0) |
| 717 | { | ||
| 718 | 7 | return std::unexpected(StringXrefError::NoReference); | |
| 719 | } | ||
| 720 |
2/2✓ Branch 41 → 42 taken 5 times.
✓ Branch 41 → 45 taken 632 times.
|
637 | if (references.count >= 2) |
| 721 | { | ||
| 722 | 5 | return std::unexpected(StringXrefError::AmbiguousReference); | |
| 723 | } | ||
| 724 | |||
| 725 |
2/2✓ Branch 45 → 46 taken 10 times.
✓ Branch 45 → 58 taken 622 times.
|
632 | if (query.return_mode == XrefReturn::StringPointerSlot) |
| 726 | { | ||
| 727 | // Store-xref needs the unique reference to be the narrow `lea reg, [rip+string]` whose loaded pointer a | ||
| 728 | // following `mov [rip+slot], reg` caches. A broad-only surviving reference (references.site differs from | ||
| 729 | // the narrow site, so lea_info was never populated for it) or a `mov reg, [rip+string]` load has no such | ||
| 730 | // store to attribute. With broad_match false, references.site == narrow_site whenever count == 1, so the | ||
| 731 | // second guard is a no-op there. | ||
| 732 |
3/4✓ Branch 46 → 47 taken 8 times.
✓ Branch 46 → 48 taken 2 times.
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 51 taken 8 times.
|
10 | if (!lea_info.is_lea || references.site != narrow_site) |
| 733 | { | ||
| 734 | 2 | return std::unexpected(StringXrefError::StoreNotFound); | |
| 735 | } | ||
| 736 | 8 | const std::uintptr_t slot = scan_store_slot_after_lea(references.site, lea_info.instr_len, | |
| 737 | 8 | lea_info.window_end, lea_info.reg, range); | |
| 738 |
2/2✓ Branch 52 → 53 taken 6 times.
✓ Branch 52 → 56 taken 2 times.
|
8 | if (slot == 0) |
| 739 | { | ||
| 740 | 6 | return std::unexpected(StringXrefError::StoreNotFound); | |
| 741 | } | ||
| 742 | 2 | return slot; | |
| 743 | } | ||
| 744 | |||
| 745 |
2/2✓ Branch 58 → 59 taken 4 times.
✓ Branch 58 → 66 taken 618 times.
|
622 | if (query.return_mode == XrefReturn::EnclosingFunction) |
| 746 | { | ||
| 747 | 4 | const std::uintptr_t function_start = enclosing_function_start(references.site, range.base); | |
| 748 |
2/2✓ Branch 60 → 61 taken 1 time.
✓ Branch 60 → 64 taken 3 times.
|
4 | if (function_start == 0) |
| 749 | { | ||
| 750 | 1 | return std::unexpected(StringXrefError::FunctionNotFound); | |
| 751 | } | ||
| 752 | 3 | return function_start; | |
| 753 | } | ||
| 754 | 618 | return references.site; | |
| 755 | 652 | } | |
| 756 | } // namespace DetourModKit | ||
| 757 |