src/x86_decode.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #ifndef DETOURMODKIT_X86_DECODE_HPP | ||
| 2 | #define DETOURMODKIT_X86_DECODE_HPP | ||
| 3 | |||
| 4 | #include "DetourModKit/memory.hpp" | ||
| 5 | |||
| 6 | #include <array> | ||
| 7 | #include <cstdint> | ||
| 8 | #include <cstring> | ||
| 9 | #include <optional> | ||
| 10 | |||
| 11 | namespace DetourModKit::detail | ||
| 12 | { | ||
| 13 | /** | ||
| 14 | * @brief Decodes an E9 rel32 near JMP at @p address and returns its absolute destination. | ||
| 15 | * @details Copies the candidate instruction bytes into a local buffer under a single SEH fault guard | ||
| 16 | * (seh_read_bytes), then inspects the copy. Read-then-test on a local buffer closes the time-of-check to | ||
| 17 | * time-of-use gap that is_readable + a raw dereference leaves open: the target page can change protection | ||
| 18 | * or unmap between the probe and the access, and these decoders run on the nested-hook detection and | ||
| 19 | * prologue-recovery paths where the page state is most likely to be in flux. A faulting read returns | ||
| 20 | * nullopt rather than taking down the host. | ||
| 21 | * @param address Absolute address of the candidate instruction. | ||
| 22 | * @return The absolute jump destination, or std::nullopt when the bytes are unreadable or the opcode is not E9. | ||
| 23 | */ | ||
| 24 | 12 | [[nodiscard]] inline std::optional<std::uintptr_t> decode_e9_rel32(std::uintptr_t address) noexcept | |
| 25 | { | ||
| 26 | 12 | std::array<std::uint8_t, 5> code{}; | |
| 27 |
2/2✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 10 times.
|
24 | if (!Memory::seh_read_bytes(address, code.data(), code.size())) |
| 28 | { | ||
| 29 | 2 | return std::nullopt; | |
| 30 | } | ||
| 31 |
2/2✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 9 times.
|
10 | if (code[0] != 0xE9) |
| 32 | { | ||
| 33 | 1 | return std::nullopt; | |
| 34 | } | ||
| 35 | 9 | std::int32_t disp = 0; | |
| 36 | 9 | std::memcpy(&disp, code.data() + 1, sizeof(disp)); | |
| 37 | 9 | return static_cast<std::uintptr_t>(static_cast<std::int64_t>(address) + 5 + disp); | |
| 38 | } | ||
| 39 | |||
| 40 | /** | ||
| 41 | * @brief Decodes an EB rel8 short JMP at @p address and returns its absolute destination. | ||
| 42 | * @details Copies the candidate instruction bytes into a local buffer under a single SEH fault guard | ||
| 43 | * (seh_read_bytes), then inspects the copy. Read-then-test on a local buffer closes the time-of-check to | ||
| 44 | * time-of-use gap that is_readable + a raw dereference leaves open: the target page can change protection | ||
| 45 | * or unmap between the probe and the access, and this decoder runs on hook-detection pre-flight paths | ||
| 46 | * (the VMT slot pre-flight) where the page state is most likely to be in flux. A faulting read returns | ||
| 47 | * nullopt rather than taking down the host. | ||
| 48 | * @param address Absolute address of the candidate instruction. | ||
| 49 | * @return The absolute jump destination (rel8 sign-extended), or std::nullopt when the bytes are unreadable or the | ||
| 50 | * opcode is not EB. | ||
| 51 | */ | ||
| 52 | 4 | [[nodiscard]] inline std::optional<std::uintptr_t> decode_eb_rel8(std::uintptr_t address) noexcept | |
| 53 | { | ||
| 54 | 4 | std::array<std::uint8_t, 2> code{}; | |
| 55 |
2/2✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 3 times.
|
8 | if (!Memory::seh_read_bytes(address, code.data(), code.size())) |
| 56 | { | ||
| 57 | 1 | return std::nullopt; | |
| 58 | } | ||
| 59 |
2/2✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 2 times.
|
3 | if (code[0] != 0xEB) |
| 60 | { | ||
| 61 | 1 | return std::nullopt; | |
| 62 | } | ||
| 63 | 2 | const auto disp = static_cast<std::int8_t>(code[1]); | |
| 64 | 2 | return static_cast<std::uintptr_t>(static_cast<std::int64_t>(address) + 2 + disp); | |
| 65 | } | ||
| 66 | |||
| 67 | /** | ||
| 68 | * @brief Decodes an FF 25 disp32 indirect JMP at @p address and returns the target stored in its memory slot. | ||
| 69 | * @details FF 25 disp32 on x86-64 is RIP-relative: the 32-bit signed displacement is added to the address of the | ||
| 70 | * next instruction. On x86 (32-bit) the same encoding is absolute, which this decoder does not handle. | ||
| 71 | * Copies the candidate instruction bytes into a local buffer under a single SEH fault guard | ||
| 72 | * (seh_read_bytes), then inspects the copy. Read-then-test on a local buffer closes the time-of-check to | ||
| 73 | * time-of-use gap that is_readable + a raw dereference leaves open: the target page can change protection | ||
| 74 | * or unmap between the probe and the access, and these decoders run on the nested-hook detection and | ||
| 75 | * prologue-recovery paths where the page state is most likely to be in flux. A faulting read returns | ||
| 76 | * nullopt rather than taking down the host. | ||
| 77 | * @param address Absolute address of the candidate instruction. | ||
| 78 | * @return The final indirect target read from the slot, or std::nullopt when the instruction bytes or the slot are | ||
| 79 | * unreadable or the opcode is not FF 25. | ||
| 80 | */ | ||
| 81 | 9 | [[nodiscard]] inline std::optional<std::uintptr_t> decode_ff25_indirect(std::uintptr_t address) noexcept | |
| 82 | { | ||
| 83 | static_assert(sizeof(void *) == 8, "decode_ff25_indirect assumes x86-64 RIP-relative semantics"); | ||
| 84 | 9 | std::array<std::uint8_t, 6> code{}; | |
| 85 |
2/2✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 8 times.
|
18 | if (!Memory::seh_read_bytes(address, code.data(), code.size())) |
| 86 | { | ||
| 87 | 1 | return std::nullopt; | |
| 88 | } | ||
| 89 |
6/6✓ Branch 10 → 11 taken 7 times.
✓ Branch 10 → 13 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 14 taken 6 times.
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 17 taken 6 times.
|
8 | if (code[0] != 0xFF || code[1] != 0x25) |
| 90 | { | ||
| 91 | 2 | return std::nullopt; | |
| 92 | } | ||
| 93 | 6 | std::int32_t disp = 0; | |
| 94 | 6 | std::memcpy(&disp, code.data() + 2, sizeof(disp)); | |
| 95 | 6 | const auto slot_addr = static_cast<std::uintptr_t>(static_cast<std::int64_t>(address) + 6 + disp); | |
| 96 | // The slot stores the final indirect target; read it under the same fault guard rather than is_readable + a raw | ||
| 97 | // dereference. | ||
| 98 | 6 | const auto indirect_destination = Memory::seh_read<std::uintptr_t>(slot_addr); | |
| 99 |
2/2✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 5 times.
|
6 | if (!indirect_destination) |
| 100 | { | ||
| 101 | 1 | return std::nullopt; | |
| 102 | } | ||
| 103 | 5 | return *indirect_destination; | |
| 104 | } | ||
| 105 | |||
| 106 | /** | ||
| 107 | * @brief Decodes a `mov rax, imm64; jmp rax` absolute-jump pair at @p address and returns the imm64 destination. | ||
| 108 | * @details Some inline hooks emit this 12-byte absolute jump -- `48 B8 <imm64>` (REX.W mov rax, imm64) immediately | ||
| 109 | * followed by `FF E0` (jmp rax) -- when the detour trampoline is beyond rel32 reach and the hooking | ||
| 110 | * library does not use the FF 25 RIP-relative form. Unlike FF 25 the absolute target is the imm64 baked | ||
| 111 | * directly into the instruction, so no pointer-slot dereference is needed. Copies the candidate bytes | ||
| 112 | * into a local buffer under a single SEH fault guard (seh_read_bytes), then inspects the copy. | ||
| 113 | * Read-then-test on a local buffer closes the time-of-check to time-of-use gap that is_readable + a raw | ||
| 114 | * dereference leaves open: the target page can change protection or unmap between the probe and the | ||
| 115 | * access, and this decoder | ||
| 116 | * runs on the prologue-recovery path where the page state is most likely to be in flux. A faulting read | ||
| 117 | * returns nullopt rather than taking down the host. | ||
| 118 | * @param address Absolute address of the candidate instruction pair. | ||
| 119 | * @return The absolute jump destination (the imm64), or std::nullopt when the bytes are unreadable or the opcodes | ||
| 120 | * are not `48 B8 ... FF E0`. | ||
| 121 | */ | ||
| 122 | 6 | [[nodiscard]] inline std::optional<std::uintptr_t> decode_mov_rax_imm64_jmp_rax(std::uintptr_t address) noexcept | |
| 123 | { | ||
| 124 | static_assert(sizeof(void *) == 8, "decode_mov_rax_imm64_jmp_rax assumes a 64-bit absolute target"); | ||
| 125 | 6 | std::array<std::uint8_t, 12> code{}; | |
| 126 |
2/2✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 4 times.
|
12 | if (!Memory::seh_read_bytes(address, code.data(), code.size())) |
| 127 | { | ||
| 128 | 2 | return std::nullopt; | |
| 129 | } | ||
| 130 | // 48 B8 = REX.W + mov rax, imm64; bytes [2..9] hold the imm64; FF E0 = jmp rax. | ||
| 131 |
8/10✓ Branch 10 → 11 taken 4 times.
✗ Branch 10 → 17 not taken.
✓ Branch 12 → 13 taken 3 times.
✓ Branch 12 → 17 taken 1 time.
✓ Branch 14 → 15 taken 3 times.
✗ Branch 14 → 17 not taken.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 18 taken 2 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 21 taken 2 times.
|
4 | if (code[0] != 0x48 || code[1] != 0xB8 || code[10] != 0xFF || code[11] != 0xE0) |
| 132 | { | ||
| 133 | 2 | return std::nullopt; | |
| 134 | } | ||
| 135 | 2 | std::uintptr_t destination = 0; | |
| 136 | 2 | std::memcpy(&destination, code.data() + 2, sizeof(destination)); | |
| 137 | 2 | return destination; | |
| 138 | } | ||
| 139 | } // namespace DetourModKit::detail | ||
| 140 | |||
| 141 | #endif // DETOURMODKIT_X86_DECODE_HPP | ||
| 142 |