src/scan_rip_relative.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /** | ||
| 2 | * @file scan_rip_relative.cpp | ||
| 3 | * @brief Standalone x86-64 RIP-relative resolvers and candidate semantic verification. | ||
| 4 | * @details Resolves an absolute address from a RIP-relative instruction whose displacement is read under a fault guard, | ||
| 5 | * then screened against the plausible-userspace floor. find_and_resolve_rip_relative scans a Region for an | ||
| 6 | * opcode prefix and returns the first occurrence whose disp32 resolves plausibly to a readable target, | ||
| 7 | * skipping decoy prefixes whose displacement cannot be trusted. A failure on either path surfaces as a typed | ||
| 8 | * ErrorCode in the Scan block rather than undefined behaviour. | ||
| 9 | */ | ||
| 10 | |||
| 11 | #include "DetourModKit/scan.hpp" | ||
| 12 | |||
| 13 | #include "DetourModKit/memory.hpp" | ||
| 14 | |||
| 15 | #include "internal/memory_guarded.hpp" | ||
| 16 | #include "internal/scan_shared.hpp" | ||
| 17 | |||
| 18 | #include <Zydis/Zydis.h> | ||
| 19 | |||
| 20 | #include <cstddef> | ||
| 21 | #include <cstdint> | ||
| 22 | #include <cstring> | ||
| 23 | #include <optional> | ||
| 24 | #include <span> | ||
| 25 | |||
| 26 | namespace DetourModKit | ||
| 27 | { | ||
| 28 | namespace scan | ||
| 29 | { | ||
| 30 | 34 | Result<Address> resolve_rip_relative( | |
| 31 | Address instruction, | ||
| 32 | std::size_t displacement_offset, | ||
| 33 | std::size_t instruction_length | ||
| 34 | ) noexcept | ||
| 35 | { | ||
| 36 |
2/2✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 7 taken 31 times.
|
34 | if (!instruction) |
| 37 | { | ||
| 38 | 3 | return std::unexpected(Error{ErrorCode::NullInput, "scan::resolve_rip_relative"}); | |
| 39 | } | ||
| 40 |
2/2✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 12 taken 30 times.
|
31 | if (!is_valid_rip_relative_layout(displacement_offset, instruction_length)) |
| 41 | { | ||
| 42 | 1 | return std::unexpected(Error{ErrorCode::InvalidArg, "scan::resolve_rip_relative"}); | |
| 43 | } | ||
| 44 | |||
| 45 | 30 | const std::uintptr_t base = instruction.raw(); | |
| 46 | 30 | const std::uintptr_t disp_addr = base + static_cast<std::uintptr_t>(displacement_offset); | |
| 47 | // Read the displacement under one fault guard instead of is_readable + raw memcpy. is_readable is | ||
| 48 | // a time-of-check/time-of-use illusion (the page can change protection or unmap between the check and the | ||
| 49 | // copy), so an unguarded memcpy can fault the host. | ||
| 50 | 30 | const auto displacement = detail::guarded_read<std::int32_t>(disp_addr); | |
| 51 |
2/2✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 19 taken 29 times.
|
30 | if (!displacement) |
| 52 | { | ||
| 53 | 1 | return std::unexpected(Error{ErrorCode::UnreadableDisplacement, "scan::resolve_rip_relative"}); | |
| 54 | } | ||
| 55 | |||
| 56 | // Compute the target in unsigned modular arithmetic so the math stays well-defined on every input, | ||
| 57 | // including kernel-range instruction addresses (where intptr_t would be negative and signed overflow is | ||
| 58 | // UB). The displacement is sign-extended first so negative disp32 values wrap to the correct 64-bit offset. | ||
| 59 | 29 | const std::uintptr_t target = detail::add_rip_displacement(base, instruction_length, *displacement); | |
| 60 | |||
| 61 | // Fail closed on a target that cannot be a real in-process address. A corrupt or hostile displacement can | ||
| 62 | // resolve to 0, a low guard-page address, or a kernel-range value; returning that as "success" would hand | ||
| 63 | // the caller a pointer that faults on first use. is_plausible_ptr is pure arithmetic, so this guard | ||
| 64 | // adds no syscall and no memory access. | ||
| 65 |
2/2✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 26 taken 27 times.
|
29 | if (!detail::is_plausible_ptr(target)) |
| 66 | { | ||
| 67 | 2 | return std::unexpected(Error{ErrorCode::ImplausibleTarget, "scan::resolve_rip_relative"}); | |
| 68 | } | ||
| 69 | 27 | return Address{target}; | |
| 70 | } | ||
| 71 | |||
| 72 | 24 | Result<Address> find_and_resolve_rip_relative( | |
| 73 | Region search, | ||
| 74 | std::span<const std::byte> opcode_prefix, | ||
| 75 | std::size_t instruction_length | ||
| 76 | ) noexcept | ||
| 77 | { | ||
| 78 | 24 | const std::byte *search_start = search.base.ptr<const std::byte>(); | |
| 79 |
6/6✓ Branch 3 → 4 taken 22 times.
✓ Branch 3 → 6 taken 2 times.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 21 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 12 taken 21 times.
|
24 | if (!search_start || opcode_prefix.empty()) |
| 80 | { | ||
| 81 | 3 | return std::unexpected(Error{ErrorCode::NullInput, "scan::find_and_resolve_rip_relative"}); | |
| 82 | } | ||
| 83 | |||
| 84 | 21 | const std::size_t prefix_len = opcode_prefix.size(); | |
| 85 |
2/2✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 18 taken 20 times.
|
21 | if (!is_valid_rip_relative_layout(prefix_len, instruction_length)) |
| 86 | { | ||
| 87 | 1 | return std::unexpected(Error{ErrorCode::InvalidArg, "scan::find_and_resolve_rip_relative"}); | |
| 88 | } | ||
| 89 | 20 | const std::size_t min_bytes = prefix_len + sizeof(std::int32_t); | |
| 90 |
2/2✓ Branch 18 → 19 taken 2 times.
✓ Branch 18 → 22 taken 18 times.
|
20 | if (search.size < min_bytes) |
| 91 | { | ||
| 92 | 2 | return std::unexpected(Error{ErrorCode::RegionTooSmall, "scan::find_and_resolve_rip_relative"}); | |
| 93 | } | ||
| 94 | |||
| 95 | 18 | const std::size_t scan_limit = search.size - min_bytes; | |
| 96 | 18 | const std::byte first = opcode_prefix[0]; | |
| 97 | |||
| 98 | // A byte sequence that matches the opcode prefix but whose disp32 resolves to an | ||
| 99 | // implausible or unreadable target is a decoy, not a hard stop (B-60): the same prefix | ||
| 100 | // can legitimately recur, so the genuine instruction may be a later occurrence. Keep | ||
| 101 | // scanning past each decoy and return the first occurrence whose displacement resolves | ||
| 102 | // plausibly to a readable target. The last resolve failure is retained so an all-decoy | ||
| 103 | // region reports WHY resolution never succeeded (e.g. ImplausibleTarget or | ||
| 104 | // UnreadableTarget) rather than a bare PrefixNotFound. | ||
| 105 | 18 | std::optional<Error> last_resolve_error; | |
| 106 | |||
| 107 |
2/2✓ Branch 49 → 24 taken 203 times.
✓ Branch 49 → 50 taken 3 times.
|
206 | for (std::size_t i = 0; i <= scan_limit; ++i) |
| 108 | { | ||
| 109 |
2/2✓ Branch 24 → 25 taken 184 times.
✓ Branch 24 → 26 taken 19 times.
|
203 | if (search_start[i] != first) |
| 110 | { | ||
| 111 | 188 | continue; | |
| 112 | } | ||
| 113 | |||
| 114 |
6/6✓ Branch 26 → 27 taken 9 times.
✓ Branch 26 → 30 taken 10 times.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 30 taken 8 times.
✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 33 taken 18 times.
|
19 | if (prefix_len > 1 && std::memcmp(&search_start[i + 1], opcode_prefix.data() + 1, prefix_len - 1) != 0) |
| 115 | { | ||
| 116 | 1 | continue; | |
| 117 | } | ||
| 118 | |||
| 119 | 18 | auto resolved = resolve_rip_relative(Address{&search_start[i]}, prefix_len, instruction_length); | |
| 120 |
2/2✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 40 taken 17 times.
|
18 | if (!resolved) |
| 121 | { | ||
| 122 | 1 | last_resolve_error = resolved.error(); | |
| 123 | 1 | continue; | |
| 124 | } | ||
| 125 | // Plausibility is pure arithmetic, so a coincidental disp32 can still name a committed-looking | ||
| 126 | // address on a PAGE_NOACCESS page. One target byte is checked here rather than in | ||
| 127 | // resolve_rip_relative, which stays the guarded arithmetic primitive. | ||
| 128 |
2/2✓ Branch 42 → 43 taken 2 times.
✓ Branch 42 → 47 taken 15 times.
|
17 | if (!memory::is_readable(Region{*resolved, 1})) |
| 129 | { | ||
| 130 | last_resolve_error = | ||
| 131 | 2 | Error{ErrorCode::UnreadableTarget, "scan::find_and_resolve_rip_relative", resolved->raw()}; | |
| 132 | 2 | continue; | |
| 133 | } | ||
| 134 | 15 | return resolved; | |
| 135 | } | ||
| 136 | |||
| 137 | // A prefix was found but no occurrence resolved: surface the concrete decode failure. | ||
| 138 |
2/2✓ Branch 51 → 52 taken 1 time.
✓ Branch 51 → 56 taken 2 times.
|
3 | if (last_resolve_error) |
| 139 | { | ||
| 140 | 1 | return std::unexpected(*last_resolve_error); | |
| 141 | } | ||
| 142 | 2 | return std::unexpected(Error{ErrorCode::PrefixNotFound, "scan::find_and_resolve_rip_relative"}); | |
| 143 | } | ||
| 144 | } // namespace scan | ||
| 145 | |||
| 146 | namespace detail | ||
| 147 | { | ||
| 148 | 24 | std::optional<std::int32_t> decode_rip_displacement( | |
| 149 | std::span<const std::byte> instruction_bytes, | ||
| 150 | std::size_t displacement_offset, | ||
| 151 | std::size_t instruction_length | ||
| 152 | ) noexcept | ||
| 153 | { | ||
| 154 |
5/6✓ Branch 3 → 4 taken 24 times.
✗ Branch 3 → 6 not taken.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 23 times.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 12 taken 23 times.
|
48 | if (!scan::is_valid_rip_relative_layout(displacement_offset, instruction_length) || |
| 155 | 24 | instruction_bytes.size() != instruction_length) | |
| 156 | { | ||
| 157 | 1 | return std::nullopt; | |
| 158 | } | ||
| 159 | |||
| 160 | ZydisDecoder decoder; | ||
| 161 |
1/2✗ Branch 13 → 14 not taken.
✓ Branch 13 → 17 taken 23 times.
|
23 | if (!ZYAN_SUCCESS(ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64))) |
| 162 | { | ||
| 163 | ✗ | return std::nullopt; | |
| 164 | } | ||
| 165 | ZydisDecodedInstruction instruction; | ||
| 166 | ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT]; | ||
| 167 |
1/2✗ Branch 20 → 21 not taken.
✓ Branch 20 → 24 taken 23 times.
|
23 | if (!ZYAN_SUCCESS(ZydisDecoderDecodeFull( |
| 168 | &decoder, | ||
| 169 | instruction_bytes.data(), | ||
| 170 | instruction_bytes.size(), | ||
| 171 | &instruction, | ||
| 172 | operands | ||
| 173 | ))) | ||
| 174 | { | ||
| 175 | ✗ | return std::nullopt; | |
| 176 | } | ||
| 177 | |||
| 178 | // The resolution computes (match + instruction_length + disp), so the declared length must be the true | ||
| 179 | // encoded length or the next-instruction anchor drifts, and the declared field must be a disp32 at exactly | ||
| 180 | // the declared offset. | ||
| 181 |
2/2✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 28 taken 22 times.
|
23 | if (instruction.length != instruction_length) |
| 182 | { | ||
| 183 | 1 | return std::nullopt; | |
| 184 | } | ||
| 185 |
2/4✓ Branch 28 → 29 taken 22 times.
✗ Branch 28 → 30 not taken.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 33 taken 22 times.
|
22 | if (instruction.raw.disp.size != 32 || instruction.raw.disp.offset != displacement_offset) |
| 186 | { | ||
| 187 | ✗ | return std::nullopt; | |
| 188 | } | ||
| 189 | // The disp32 must belong to a RIP-relative memory operand: a same-offset disp32 on an absolute or | ||
| 190 | // SIB-indexed operand is not the reference the metadata resolves. | ||
| 191 |
1/2✓ Branch 42 → 34 taken 41 times.
✗ Branch 42 → 43 not taken.
|
41 | for (std::size_t i = 0; i < instruction.operand_count_visible; ++i) |
| 192 | { | ||
| 193 | 41 | const ZydisDecodedOperand &operand = operands[i]; | |
| 194 |
3/4✓ Branch 34 → 35 taken 22 times.
✓ Branch 34 → 41 taken 19 times.
✓ Branch 35 → 36 taken 22 times.
✗ Branch 35 → 41 not taken.
|
41 | if (operand.type == ZYDIS_OPERAND_TYPE_MEMORY && operand.mem.base == ZYDIS_REGISTER_RIP && |
| 195 |
1/2✓ Branch 36 → 37 taken 22 times.
✗ Branch 36 → 41 not taken.
|
22 | operand.mem.disp.has_displacement) |
| 196 | { | ||
| 197 | 22 | std::int32_t displacement = 0; | |
| 198 | 22 | std::memcpy(&displacement, instruction_bytes.data() + displacement_offset, sizeof(displacement)); | |
| 199 | 22 | return displacement; | |
| 200 | } | ||
| 201 | } | ||
| 202 | ✗ | return std::nullopt; | |
| 203 | } | ||
| 204 | } // namespace detail | ||
| 205 | } // namespace DetourModKit | ||
| 206 |