src/scan_code_constant.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /** | ||
| 2 | * @file scan_code_constant.cpp | ||
| 3 | * @brief Zydis-backed extraction of a constant encoded in engine machine code: read_code_constant(). | ||
| 4 | * @details The code-side twin of the RTTI self-heal: the CodeConstant's candidate ladder resolves to an instruction | ||
| 5 | * site (via scan::resolve), the live instruction is decoded, and the requested operand's immediate or memory | ||
| 6 | * displacement is returned as the CURRENT value. The caller's nominal is never a short-circuit, so a | ||
| 7 | * same-shape / different-value drift is reported as the new value. The CodeConstant's Candidate ladder | ||
| 8 | * resolves the site; Zydis is confined to this TU. | ||
| 9 | */ | ||
| 10 | |||
| 11 | #include "DetourModKit/scan.hpp" | ||
| 12 | |||
| 13 | #include "internal/scan_engine.hpp" | ||
| 14 | #include "internal/scan_pages.hpp" | ||
| 15 | #include "internal/scan_shared.hpp" | ||
| 16 | |||
| 17 | #include <Zydis/Zydis.h> | ||
| 18 | |||
| 19 | #include <algorithm> | ||
| 20 | #include <cstddef> | ||
| 21 | #include <cstdint> | ||
| 22 | #include <cstring> | ||
| 23 | #include <span> | ||
| 24 | |||
| 25 | namespace DetourModKit | ||
| 26 | { | ||
| 27 | namespace scan | ||
| 28 | { | ||
| 29 | namespace | ||
| 30 | { | ||
| 31 | // Narrows an already-64-bit-sign-extended value to @p byte_width bytes and re-sign-extends from that width, | ||
| 32 | // so a deliberately narrowed negative value (for example a disp8 of -1) stays negative instead of becoming | ||
| 33 | // a large positive number. @p byte_width 0 returns the value verbatim, since Zydis has already | ||
| 34 | // sign-extended immediates and displacements to 64 bits. | ||
| 35 | 121 | std::int64_t narrow_signed(std::int64_t value, std::uint8_t byte_width) noexcept | |
| 36 | { | ||
| 37 | 121 | if (byte_width == 0 || byte_width >= sizeof(std::int64_t)) | |
| 38 | { | ||
| 39 | 99 | return value; | |
| 40 | } | ||
| 41 | 22 | const unsigned bits = static_cast<unsigned>(byte_width) * 8u; | |
| 42 | 22 | const std::uint64_t mask = (std::uint64_t{1} << bits) - 1u; | |
| 43 | 22 | const std::uint64_t masked = static_cast<std::uint64_t>(value) & mask; | |
| 44 | 22 | const std::uint64_t sign_bit = std::uint64_t{1} << (bits - 1u); | |
| 45 | // Two's-complement sign-extension from the top bit of the chosen width. | ||
| 46 | 22 | const std::uint64_t extended = (masked ^ sign_bit) - sign_bit; | |
| 47 | 22 | return static_cast<std::int64_t>(extended); | |
| 48 | } | ||
| 49 | } // namespace | ||
| 50 | |||
| 51 | namespace | ||
| 52 | { | ||
| 53 | 141 | void overlay_snapshot( | |
| 54 | std::span<std::byte> destination, | ||
| 55 | std::uintptr_t destination_base, | ||
| 56 | std::span<const std::byte> source, | ||
| 57 | std::uintptr_t source_base | ||
| 58 | ) noexcept | ||
| 59 | { | ||
| 60 | 141 | std::size_t destination_offset = 0; | |
| 61 | 141 | std::size_t source_offset = 0; | |
| 62 | 141 | if (destination_base < source_base) | |
| 63 | { | ||
| 64 | 5 | const std::uintptr_t delta = source_base - destination_base; | |
| 65 | 5 | if (delta >= destination.size()) | |
| 66 | { | ||
| 67 | 4 | return; | |
| 68 | } | ||
| 69 | 1 | destination_offset = static_cast<std::size_t>(delta); | |
| 70 | } | ||
| 71 | else | ||
| 72 | { | ||
| 73 | 136 | const std::uintptr_t delta = destination_base - source_base; | |
| 74 | 136 | if (delta >= source.size()) | |
| 75 | { | ||
| 76 | ✗ | return; | |
| 77 | } | ||
| 78 | 136 | source_offset = static_cast<std::size_t>(delta); | |
| 79 | } | ||
| 80 | |||
| 81 | const std::size_t overlap = | ||
| 82 | 137 | std::min(destination.size() - destination_offset, source.size() - source_offset); | |
| 83 | 137 | std::memcpy(destination.data() + destination_offset, source.data() + source_offset, overlap); | |
| 84 | } | ||
| 85 | |||
| 86 | 141 | [[nodiscard]] bool selector_still_resolves_site( | |
| 87 | const Candidate &candidate, | ||
| 88 | Region match_span, | ||
| 89 | Region physical_source, | ||
| 90 | std::uintptr_t decoded_site, | ||
| 91 | std::uintptr_t window_base, | ||
| 92 | std::span<const std::byte> window | ||
| 93 | ) | ||
| 94 | { | ||
| 95 | 141 | constexpr std::size_t MAX_EVIDENCE_SPAN = | |
| 96 | detail::MAX_PATTERN_BYTES + detail::MAX_PATTERN_JUMPS * detail::MAX_JUMP_SPAN; | ||
| 97 | 141 | constexpr std::size_t MAX_SOURCE_SPAN = MAX_EVIDENCE_SPAN + MAX_X86_INSTRUCTION_LENGTH; | |
| 98 | 141 | const Pattern *pattern = detail::byte_pattern_of(candidate); | |
| 99 | 141 | const std::uintptr_t span_base = match_span.base.raw(); | |
| 100 | 141 | const std::uintptr_t source_base = physical_source.base.raw(); | |
| 101 | 141 | if (pattern == nullptr || span_base == 0 || match_span.size == 0 || match_span.size > MAX_EVIDENCE_SPAN) | |
| 102 | { | ||
| 103 | ✗ | return false; | |
| 104 | } | ||
| 105 | 141 | if (source_base != span_base || physical_source.size < match_span.size || | |
| 106 | 141 | physical_source.size > MAX_SOURCE_SPAN) | |
| 107 | { | ||
| 108 | ✗ | return false; | |
| 109 | } | ||
| 110 | std::byte fresh[MAX_SOURCE_SPAN]; | ||
| 111 | 141 | if (!detail::guarded_read_bytes(source_base, fresh, physical_source.size)) | |
| 112 | { | ||
| 113 | ✗ | return false; | |
| 114 | } | ||
| 115 | 141 | overlay_snapshot(std::span<std::byte>{fresh, physical_source.size}, source_base, window, window_base); | |
| 116 | const detail::EnginePattern engine = detail::engine_pattern_from( | ||
| 117 | *pattern, | ||
| 118 | 282 | pattern->has_anchor() ? pattern->anchor_index() : pattern->size() | |
| 119 | 282 | ); | |
| 120 | 141 | const detail::RawMatch fresh_match = detail::find_pattern_raw(fresh, match_span.size, engine); | |
| 121 | 141 | if (fresh_match.budget_exhausted || fresh_match.start != fresh || | |
| 122 | 137 | fresh_match.end != fresh + match_span.size || fresh_match.point == nullptr || | |
| 123 | 137 | fresh_match.point < fresh || fresh_match.point > fresh + match_span.size) | |
| 124 | { | ||
| 125 | 4 | return false; | |
| 126 | } | ||
| 127 | |||
| 128 | 137 | const std::size_t point_offset = static_cast<std::size_t>(fresh_match.point - fresh); | |
| 129 | 137 | if (point_offset > static_cast<std::size_t>(UINTPTR_MAX - source_base)) | |
| 130 | { | ||
| 131 | ✗ | return false; | |
| 132 | } | ||
| 133 | 137 | const std::uintptr_t live_point = source_base + point_offset; | |
| 134 | 137 | std::optional<std::uintptr_t> fresh_site; | |
| 135 | 137 | if (const DirectPattern *direct = candidate.as_direct()) | |
| 136 | { | ||
| 137 | 136 | fresh_site = detail::resolve_direct(live_point, *direct); | |
| 138 | } | ||
| 139 | 1 | else if (const RipRelativePattern *rip = candidate.as_rip_relative()) | |
| 140 | { | ||
| 141 | 1 | if (point_offset > physical_source.size || | |
| 142 | 1 | rip->instruction_length > physical_source.size - point_offset) | |
| 143 | { | ||
| 144 | ✗ | return false; | |
| 145 | } | ||
| 146 | const std::span<const std::byte> instruction_snapshot{ | ||
| 147 | fresh + point_offset, | ||
| 148 | 1 | rip->instruction_length | |
| 149 | 1 | }; | |
| 150 | 1 | fresh_site = detail::resolve_rip_relative_candidate(live_point, *rip, instruction_snapshot); | |
| 151 | } | ||
| 152 | 137 | return fresh_site && *fresh_site == decoded_site; | |
| 153 | 141 | } | |
| 154 | |||
| 155 | /// Extracts the requested visible operand constant from the instruction decoded at @p site. | ||
| 156 | 135 | [[nodiscard]] Result<std::int64_t> extract_operand_constant( | |
| 157 | const CodeConstant &code_constant, | ||
| 158 | const ZydisDecodedInstruction &insn, | ||
| 159 | const ZydisDecodedOperand (&operands)[ZYDIS_MAX_OPERAND_COUNT], | ||
| 160 | std::uintptr_t site | ||
| 161 | ) noexcept | ||
| 162 | { | ||
| 163 | // Index the VISIBLE operands, the ones that a human counts in a disassembler. operand_count also counts | ||
| 164 | // hidden operands (flags, implicit registers, stack writes), so a bound on operand_count admits a | ||
| 165 | // hidden operand whose position differs between mnemonics. | ||
| 166 | 135 | if (code_constant.operand_index >= insn.operand_count_visible) | |
| 167 | { | ||
| 168 | 1 | return std::unexpected(Error{ErrorCode::OperandOutOfRange, "scan::read_code_constant"}); | |
| 169 | } | ||
| 170 | 134 | const ZydisDecodedOperand &operand = operands[code_constant.operand_index]; | |
| 171 | |||
| 172 | 134 | if (code_constant.kind == OperandKind::Immediate) | |
| 173 | { | ||
| 174 | 96 | if (operand.type != ZYDIS_OPERAND_TYPE_IMMEDIATE) | |
| 175 | { | ||
| 176 | 1 | return std::unexpected(Error{ErrorCode::UnexpectedShape, "scan::read_code_constant"}); | |
| 177 | } | ||
| 178 | 95 | return narrow_signed(static_cast<std::int64_t>(operand.imm.value.s), code_constant.byte_width); | |
| 179 | } | ||
| 180 | |||
| 181 | // For MemoryDisplacement, a register-indirect operand without a displacement (for example `[rcx]`) | ||
| 182 | // carries no constant to read. | ||
| 183 | 38 | if (operand.type != ZYDIS_OPERAND_TYPE_MEMORY || !operand.mem.disp.has_displacement) | |
| 184 | { | ||
| 185 | 1 | return std::unexpected(Error{ErrorCode::UnexpectedShape, "scan::read_code_constant"}); | |
| 186 | } | ||
| 187 | |||
| 188 | 37 | if (operand.mem.base == ZYDIS_REGISTER_RIP) | |
| 189 | { | ||
| 190 | // A RIP-relative displacement is relative to the next instruction. Return the absolute target that | ||
| 191 | // the caller wants, not the raw relative offset. | ||
| 192 | 11 | ZyanU64 absolute = 0; | |
| 193 | 11 | if (!ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&insn, &operand, static_cast<ZyanU64>(site), &absolute))) | |
| 194 | { | ||
| 195 | ✗ | return std::unexpected(Error{ErrorCode::DecodeFailed, "scan::read_code_constant"}); | |
| 196 | } | ||
| 197 | 11 | return static_cast<std::int64_t>(absolute); | |
| 198 | } | ||
| 199 | |||
| 200 | 26 | return narrow_signed(static_cast<std::int64_t>(operand.mem.disp.value), code_constant.byte_width); | |
| 201 | } | ||
| 202 | |||
| 203 | 154 | Result<std::int64_t> read_code_constant_impl( | |
| 204 | const CodeConstant &code_constant, | ||
| 205 | Region scope, | ||
| 206 | Region *instruction_span, | ||
| 207 | Region *physical_source | ||
| 208 | ) | ||
| 209 | { | ||
| 210 | 154 | if (instruction_span != nullptr) | |
| 211 | { | ||
| 212 | 108 | *instruction_span = Region{}; | |
| 213 | } | ||
| 214 | 154 | if (physical_source != nullptr) | |
| 215 | { | ||
| 216 | 108 | *physical_source = Region{}; | |
| 217 | } | ||
| 218 | 351 | if ((code_constant.kind != OperandKind::Immediate && | |
| 219 | 306 | code_constant.kind != OperandKind::MemoryDisplacement) || | |
| 220 | 152 | !detail::valid_code_constant_byte_width(code_constant.byte_width)) | |
| 221 | { | ||
| 222 | 5 | return std::unexpected(Error{ErrorCode::InvalidArg, "scan::read_code_constant"}); | |
| 223 | } | ||
| 224 | |||
| 225 | // Resolve the instruction site through the candidate ladder and propagate its typed failure verbatim | ||
| 226 | // (EmptyCandidates, NoMatch, InvalidRange, ...). The resolved address must name an executable | ||
| 227 | // instruction site; require_executable_result rejects an unsuitable rung and lets resolve() try a later | ||
| 228 | // ladder fallback. | ||
| 229 | // | ||
| 230 | // A code constant is encoded in machine code. Restrict byte tiers to execute-readable pages so an | ||
| 231 | // identical run in .rdata / .data cannot win or make the code match ambiguous. A RipRelative or | ||
| 232 | // walked-back candidate can still transform an executable match into a data address, so enforce the | ||
| 233 | // final-page policy in resolve() and recheck below before the fault-safe read and decode. The recheck | ||
| 234 | // narrows, but cannot eliminate, a concurrent protection change; guarded_read_bytes preserves the | ||
| 235 | // fail-closed host-safety guarantee. | ||
| 236 | 149 | const ScanRequest request{ | |
| 237 | .ladder = code_constant.site, | ||
| 238 | .label = "read_code_constant", | ||
| 239 | .scope = scope, | ||
| 240 | .pages = Pages::Executable, | ||
| 241 | .require_executable_result = true, | ||
| 242 | 149 | }; | |
| 243 | 149 | const Result<detail::ResolvedScanHit> resolved = detail::resolve_scan_with_provenance(request); | |
| 244 | 149 | if (!resolved) | |
| 245 | { | ||
| 246 | 6 | return std::unexpected(resolved.error()); | |
| 247 | } | ||
| 248 | 143 | const std::uintptr_t site = resolved->hit.address.raw(); | |
| 249 | 143 | if (physical_source != nullptr) | |
| 250 | { | ||
| 251 | 107 | *physical_source = resolved->physical_source; | |
| 252 | } | ||
| 253 | 143 | if (!detail::is_executable_address(site)) | |
| 254 | { | ||
| 255 | ✗ | return std::unexpected(Error{ErrorCode::DecodeFailed, "scan::read_code_constant"}); | |
| 256 | } | ||
| 257 | |||
| 258 | 143 | const detail::ModuleSpan range = detail::module_span(scope); | |
| 259 | |||
| 260 | // Read a full maximum-length instruction window, clamped to the module so the read never runs past the | ||
| 261 | // end of the image, behind a fault guard. A truncated window that fails to decode is reported as | ||
| 262 | // DecodeFailed below. | ||
| 263 | std::byte buf[ZYDIS_MAX_INSTRUCTION_LENGTH]; | ||
| 264 | 143 | std::size_t avail = sizeof(buf); | |
| 265 | 143 | if (range.valid() && site < range.end) | |
| 266 | { | ||
| 267 | 143 | const std::uintptr_t to_end = range.end - site; | |
| 268 | 143 | if (to_end < avail) | |
| 269 | { | ||
| 270 | 1 | avail = static_cast<std::size_t>(to_end); | |
| 271 | } | ||
| 272 | } | ||
| 273 | 143 | if (avail == 0 || !detail::guarded_read_bytes(site, buf, avail)) | |
| 274 | { | ||
| 275 | ✗ | return std::unexpected(Error{ErrorCode::DecodeFailed, "scan::read_code_constant"}); | |
| 276 | } | ||
| 277 | |||
| 278 | ZydisDecoder decoder; | ||
| 279 | 143 | if (!ZYAN_SUCCESS(ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64))) | |
| 280 | { | ||
| 281 | ✗ | return std::unexpected(Error{ErrorCode::DecodeFailed, "scan::read_code_constant"}); | |
| 282 | } | ||
| 283 | |||
| 284 | ZydisDecodedInstruction insn; | ||
| 285 | ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT]; | ||
| 286 | 143 | if (!ZYAN_SUCCESS(ZydisDecoderDecodeFull(&decoder, buf, avail, &insn, operands))) | |
| 287 | { | ||
| 288 | 1 | return std::unexpected(Error{ErrorCode::DecodeFailed, "scan::read_code_constant"}); | |
| 289 | } | ||
| 290 | 142 | if (!detail::is_executable_range(site, insn.length)) | |
| 291 | { | ||
| 292 | 1 | return std::unexpected(Error{ErrorCode::DecodeFailed, "scan::read_code_constant"}); | |
| 293 | } | ||
| 294 | |||
| 295 | 141 | if (resolved->winning_index < code_constant.site.size() && resolved->match_span.size != 0) | |
| 296 | { | ||
| 297 | 141 | const Candidate &winning_candidate = code_constant.site[resolved->winning_index]; | |
| 298 | 282 | if (detail::byte_pattern_of(winning_candidate) != nullptr && | |
| 299 | 282 | !selector_still_resolves_site( | |
| 300 | winning_candidate, | ||
| 301 | 141 | resolved->match_span, | |
| 302 | 141 | resolved->physical_source, | |
| 303 | site, | ||
| 304 | site, | ||
| 305 | 141 | std::span<const std::byte>{buf, avail} | |
| 306 | )) | ||
| 307 | { | ||
| 308 | 6 | return std::unexpected(Error{ErrorCode::EvidenceMismatch, "scan::read_code_constant"}); | |
| 309 | } | ||
| 310 | } | ||
| 311 | |||
| 312 | // Published only once the decode has proven the length, so the provenance names the instruction's real | ||
| 313 | // extent rather than a one-byte point a co-voting selector could straddle without overlapping. | ||
| 314 | 135 | if (instruction_span != nullptr) | |
| 315 | { | ||
| 316 | 106 | *instruction_span = Region{Address{site}, static_cast<std::size_t>(insn.length)}; | |
| 317 | } | ||
| 318 | |||
| 319 | 135 | return extract_operand_constant(code_constant, insn, operands, site); | |
| 320 | 149 | } | |
| 321 | } // namespace | ||
| 322 | |||
| 323 | 46 | Result<std::int64_t> read_code_constant(const CodeConstant &code_constant, Region scope) | |
| 324 | { | ||
| 325 | 46 | return read_code_constant_impl(code_constant, scope, nullptr, nullptr); | |
| 326 | } | ||
| 327 | } // namespace scan | ||
| 328 | |||
| 329 | Result<detail::ResolvedCodeConstant> | ||
| 330 | 108 | detail::read_code_constant_with_provenance(const scan::CodeConstant &code_constant, Region scope) | |
| 331 | { | ||
| 332 | 108 | Region instruction_span; | |
| 333 | 108 | Region physical_source; | |
| 334 | Result<std::int64_t> value = | ||
| 335 | 108 | scan::read_code_constant_impl(code_constant, scope, &instruction_span, &physical_source); | |
| 336 | 108 | if (!value) | |
| 337 | { | ||
| 338 | 2 | return std::unexpected(value.error()); | |
| 339 | } | ||
| 340 | 106 | return ResolvedCodeConstant{*value, instruction_span, physical_source}; | |
| 341 | } | ||
| 342 | } // namespace DetourModKit | ||
| 343 |