src/scan_export.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /** | ||
| 2 | * @file scan_export.cpp | ||
| 3 | * @brief The named-export resolver: maps a module + export name to an address by walking the PE Export Address Table. | ||
| 4 | * @details The walk parses the mapped image's own IMAGE_EXPORT_DIRECTORY. It never calls GetProcAddress, enters the | ||
| 5 | * loader, or triggers DllMain. It bound-checks every RVA and reads through the guarded path. scan.hpp's | ||
| 6 | * resolve_export owns the public contract. A forwarder is classified by the loader's own range check: a | ||
| 7 | * function RVA inside the export directory's [VirtualAddress, VirtualAddress + Size) window. The walk lives | ||
| 8 | * in `detail::resolve_export_with_provenance`. The public entry point is a thin wrapper because only the walk | ||
| 9 | * knows the slot and RVA that resolved a name. | ||
| 10 | */ | ||
| 11 | |||
| 12 | #include "DetourModKit/scan.hpp" | ||
| 13 | |||
| 14 | #include "internal/export_resolution.hpp" | ||
| 15 | #include "internal/image_identity.hpp" | ||
| 16 | #include "internal/memory_guarded.hpp" | ||
| 17 | #include "internal/memory_representation_win32.hpp" | ||
| 18 | |||
| 19 | #include <windows.h> | ||
| 20 | |||
| 21 | #include <cstddef> | ||
| 22 | #include <cstdint> | ||
| 23 | #include <limits> | ||
| 24 | #include <optional> | ||
| 25 | #include <string_view> | ||
| 26 | |||
| 27 | namespace DetourModKit | ||
| 28 | { | ||
| 29 | namespace detail | ||
| 30 | { | ||
| 31 | namespace | ||
| 32 | { | ||
| 33 | // Upper bound on the export name / function counts the walk will iterate. No real module approaches this; | ||
| 34 | // the cap exists only so a corrupt or hostile IMAGE_EXPORT_DIRECTORY (whose count fields are entirely | ||
| 35 | // attacker-controlled) cannot steer the scan into an unbounded loop. It is far below any value that can | ||
| 36 | // overflow the byte-size arithmetic on the parallel arrays below. | ||
| 37 | constexpr std::uint32_t MAX_EXPORT_ENTRIES = 1U << 20; | ||
| 38 | |||
| 39 | // Reports whether the half-open byte range [start, start + bytes) lies wholly inside the module image, with | ||
| 40 | // an explicit wrap guard so a hostile RVA/size that overflows the address space is rejected rather than | ||
| 41 | // aliasing a low address. A zero-length range is vacuously contained. | ||
| 42 | [[nodiscard]] bool | ||
| 43 | 732 | region_in_span(const ModuleSpan &span, std::uintptr_t start, std::uintptr_t bytes) noexcept | |
| 44 | { | ||
| 45 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 732 times.
|
732 | if (bytes == 0) |
| 46 | { | ||
| 47 | ✗ | return true; | |
| 48 | } | ||
| 49 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 732 times.
|
732 | if (start > std::numeric_limits<std::uintptr_t>::max() - (bytes - 1)) |
| 50 | { | ||
| 51 | // Address-space wrap: a corrupt RVA or size pushed the range past the top of memory. | ||
| 52 | ✗ | return false; | |
| 53 | } | ||
| 54 | 732 | const std::uintptr_t last = start + (bytes - 1); | |
| 55 |
4/4✓ Branch 8 → 9 taken 729 times.
✓ Branch 8 → 12 taken 3 times.
✓ Branch 10 → 11 taken 725 times.
✓ Branch 10 → 12 taken 4 times.
|
732 | return span.contains(start) && span.contains(last); |
| 56 | } | ||
| 57 | |||
| 58 | // Converts an image-relative address to an absolute range only when the addition cannot wrap and the | ||
| 59 | // entire range lies inside the mapped image. Keeping this arithmetic in one helper prevents a hostile RVA | ||
| 60 | // from wrapping below the module base before a later span check sees it. | ||
| 61 | [[nodiscard]] std::optional<std::uintptr_t> | ||
| 62 | 607 | checked_rva(const ModuleSpan &span, std::uint32_t rva, std::uintptr_t bytes) noexcept | |
| 63 | { | ||
| 64 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 607 times.
|
607 | if (static_cast<std::uintptr_t>(rva) > std::numeric_limits<std::uintptr_t>::max() - span.base) |
| 65 | { | ||
| 66 | ✗ | return std::nullopt; | |
| 67 | } | ||
| 68 | 607 | const std::uintptr_t address = span.base + static_cast<std::uintptr_t>(rva); | |
| 69 |
2/2✓ Branch 6 → 7 taken 6 times.
✓ Branch 6 → 8 taken 601 times.
|
607 | if (!region_in_span(span, address, bytes)) |
| 70 | { | ||
| 71 | 6 | return std::nullopt; | |
| 72 | } | ||
| 73 | 601 | return address; | |
| 74 | } | ||
| 75 | |||
| 76 | // Byte-exact, case-sensitive compare of the NUL-terminated export name at name_addr against target, with | ||
| 77 | // every byte bound-checked and fault-guarded so a truncated name table fails closed. PE export names are | ||
| 78 | // case-sensitive (GetProcAddress matches them exactly), so the anchor must be too. Comparing target.size() | ||
| 79 | // bytes AND the following terminator rejects both a shorter name (its NUL lands early) and a longer one | ||
| 80 | // (its byte at target.size() is not the terminator), so "malloc" never matches "malloc_base". | ||
| 81 | [[nodiscard]] std::optional<bool> | ||
| 82 | 260 | export_name_matches(const ModuleSpan &span, std::uintptr_t name_addr, std::string_view target) noexcept | |
| 83 | { | ||
| 84 |
2/2✓ Branch 28 → 3 taken 2380 times.
✓ Branch 28 → 29 taken 56 times.
|
2436 | for (std::size_t index = 0; index < target.size(); ++index) |
| 85 | { | ||
| 86 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 8 taken 2380 times.
|
2380 | if (index > std::numeric_limits<std::uintptr_t>::max() - name_addr) |
| 87 | { | ||
| 88 | ✗ | return std::nullopt; | |
| 89 | } | ||
| 90 | 2380 | const std::uintptr_t byte_addr = name_addr + index; | |
| 91 |
1/2✗ Branch 9 → 10 not taken.
✓ Branch 9 → 13 taken 2380 times.
|
2380 | if (!span.contains(byte_addr)) |
| 92 | { | ||
| 93 | ✗ | return std::nullopt; | |
| 94 | } | ||
| 95 | 2380 | const std::optional<char> byte = guarded_read<char>(byte_addr); | |
| 96 |
1/2✗ Branch 15 → 16 not taken.
✓ Branch 15 → 19 taken 2380 times.
|
2380 | if (!byte) |
| 97 | { | ||
| 98 | ✗ | return std::nullopt; | |
| 99 | } | ||
| 100 |
2/2✓ Branch 21 → 22 taken 204 times.
✓ Branch 21 → 25 taken 2176 times.
|
2380 | if (*byte != target[index]) |
| 101 | { | ||
| 102 | 204 | return false; | |
| 103 | } | ||
| 104 | } | ||
| 105 |
1/2✗ Branch 31 → 32 not taken.
✓ Branch 31 → 35 taken 56 times.
|
56 | if (target.size() > std::numeric_limits<std::uintptr_t>::max() - name_addr) |
| 106 | { | ||
| 107 | ✗ | return std::nullopt; | |
| 108 | } | ||
| 109 | 56 | const std::uintptr_t terminator_addr = name_addr + target.size(); | |
| 110 |
1/2✗ Branch 37 → 38 not taken.
✓ Branch 37 → 41 taken 56 times.
|
56 | if (!span.contains(terminator_addr)) |
| 111 | { | ||
| 112 | ✗ | return std::nullopt; | |
| 113 | } | ||
| 114 | 56 | const std::optional<char> terminator = guarded_read<char>(terminator_addr); | |
| 115 |
2/2✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 47 taken 55 times.
|
56 | if (!terminator) |
| 116 | { | ||
| 117 | 1 | return std::nullopt; | |
| 118 | } | ||
| 119 | 55 | return *terminator == '\0'; | |
| 120 | } | ||
| 121 | } // namespace | ||
| 122 | |||
| 123 | Result<Address> | ||
| 124 | 68 | resolve_export_with_provenance(std::string_view export_name, Region module, ExportResolution &out) noexcept | |
| 125 | { | ||
| 126 | 68 | out = ExportResolution{}; | |
| 127 | |||
| 128 | // PE names are non-empty NUL-terminated byte strings. Reject an embedded terminator up front so a query | ||
| 129 | // cannot consume the real name's terminator as data and match zero padding after it. | ||
| 130 |
6/6✓ Branch 4 → 5 taken 67 times.
✓ Branch 4 → 7 taken 1 time.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 66 times.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 13 taken 66 times.
|
68 | if (export_name.empty() || export_name.find('\0') != std::string_view::npos) |
| 131 | { | ||
| 132 | 2 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 133 | } | ||
| 134 | |||
| 135 | 66 | const ModuleSpan supplied_span = module_span(module); | |
| 136 |
2/2✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 19 taken 64 times.
|
66 | if (!supplied_span.valid()) |
| 137 | { | ||
| 138 | 2 | return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"}); | |
| 139 | } | ||
| 140 | 64 | const std::uintptr_t base = supplied_span.base; | |
| 141 |
2/2✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 24 taken 63 times.
|
64 | if (!region_in_span(supplied_span, base, sizeof(IMAGE_DOS_HEADER))) |
| 142 | { | ||
| 143 | 1 | return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"}); | |
| 144 | } | ||
| 145 | |||
| 146 | // DOS -> NT header walk, with the same discipline as the RTTI image walk: read the DOS header, bound the NT | ||
| 147 | // offset inside the image, then read the 64-bit NT headers and confirm both signatures and the PE32+ | ||
| 148 | // optional-header magic before trusting any field. The library is x64-only (an #error arch gate enforces | ||
| 149 | // it), so the explicit IMAGE_NT_HEADERS64 + PE32+ magic check is a defensive assertion against a | ||
| 150 | // wrong-bitness image, not a portability branch. The parse is kept local rather than shared with the RTTI | ||
| 151 | // walk: the two differ in error model (fail-closed ErrorCodes here vs a range count with a whole-module | ||
| 152 | // fallback there), so a shared helper would couple the two subsystems without removing real duplication. | ||
| 153 | 63 | const std::optional<IMAGE_DOS_HEADER> dos = guarded_read<IMAGE_DOS_HEADER>(base); | |
| 154 |
3/6✓ Branch 26 → 27 taken 63 times.
✗ Branch 26 → 29 not taken.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 63 times.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 35 taken 63 times.
|
63 | if (!dos || dos->e_magic != IMAGE_DOS_SIGNATURE) |
| 155 | { | ||
| 156 | ✗ | return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"}); | |
| 157 | } | ||
| 158 |
2/2✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 40 taken 62 times.
|
63 | if (dos->e_lfanew < 0) |
| 159 | { | ||
| 160 | 1 | return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"}); | |
| 161 | } | ||
| 162 | const std::optional<std::uintptr_t> nt_addr = | ||
| 163 | 62 | checked_rva(supplied_span, static_cast<std::uint32_t>(dos->e_lfanew), sizeof(IMAGE_NT_HEADERS64)); | |
| 164 |
1/2✗ Branch 43 → 44 not taken.
✓ Branch 43 → 47 taken 62 times.
|
62 | if (!nt_addr) |
| 165 | { | ||
| 166 | ✗ | return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"}); | |
| 167 | } | ||
| 168 | 62 | const std::optional<IMAGE_NT_HEADERS64> nt = guarded_read<IMAGE_NT_HEADERS64>(*nt_addr); | |
| 169 |
4/8✓ Branch 50 → 51 taken 62 times.
✗ Branch 50 → 55 not taken.
✓ Branch 52 → 53 taken 62 times.
✗ Branch 52 → 55 not taken.
✗ Branch 54 → 55 not taken.
✓ Branch 54 → 56 taken 62 times.
✗ Branch 57 → 58 not taken.
✓ Branch 57 → 61 taken 62 times.
|
62 | if (!nt || nt->Signature != IMAGE_NT_SIGNATURE || nt->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) |
| 170 | { | ||
| 171 | ✗ | return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"}); | |
| 172 | } | ||
| 173 | |||
| 174 | 62 | constexpr std::size_t export_directory_end = | |
| 175 | offsetof(IMAGE_OPTIONAL_HEADER64, DataDirectory) + | ||
| 176 | (IMAGE_DIRECTORY_ENTRY_EXPORT + 1) * sizeof(IMAGE_DATA_DIRECTORY); | ||
| 177 |
4/4✓ Branch 62 → 63 taken 61 times.
✓ Branch 62 → 65 taken 1 time.
✓ Branch 67 → 68 taken 1 time.
✓ Branch 67 → 71 taken 61 times.
|
123 | if (nt->FileHeader.SizeOfOptionalHeader < export_directory_end || |
| 178 |
1/2✗ Branch 64 → 65 not taken.
✓ Branch 64 → 66 taken 61 times.
|
61 | nt->OptionalHeader.NumberOfRvaAndSizes <= IMAGE_DIRECTORY_ENTRY_EXPORT) |
| 179 | { | ||
| 180 | 1 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 181 | } | ||
| 182 | |||
| 183 | // The caller's Region is only an outer safety boundary. The PE snapshot's SizeOfImage is the authoritative | ||
| 184 | // inner boundary for every EAT read, so stale or oversized mapped backing cannot make a declared | ||
| 185 | // out-of-image RVA appear valid. | ||
| 186 | 61 | const std::uintptr_t image_size = nt->OptionalHeader.SizeOfImage; | |
| 187 |
3/6✓ Branch 72 → 73 taken 61 times.
✗ Branch 72 → 75 not taken.
✗ Branch 74 → 75 not taken.
✓ Branch 74 → 76 taken 61 times.
✗ Branch 77 → 78 not taken.
✓ Branch 77 → 81 taken 61 times.
|
61 | if (image_size == 0 || image_size > std::numeric_limits<std::uintptr_t>::max() - base) |
| 188 | { | ||
| 189 | ✗ | return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"}); | |
| 190 | } | ||
| 191 | 61 | const std::uintptr_t declared_end = base + image_size; | |
| 192 |
2/2✓ Branch 81 → 82 taken 59 times.
✓ Branch 81 → 83 taken 2 times.
|
61 | const ModuleSpan span{base, declared_end < supplied_span.end ? declared_end : supplied_span.end}; |
| 193 |
1/2✗ Branch 86 → 87 not taken.
✓ Branch 86 → 90 taken 61 times.
|
61 | if (!region_in_span(span, *nt_addr, sizeof(IMAGE_NT_HEADERS64))) |
| 194 | { | ||
| 195 | ✗ | return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"}); | |
| 196 | } | ||
| 197 | |||
| 198 | // The export directory is data-directory entry 0. A module with no exports leaves it zeroed; that is not a | ||
| 199 | // fault, it simply has no name for this backend to resolve. | ||
| 200 | 61 | const IMAGE_DATA_DIRECTORY dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; | |
| 201 |
2/4✓ Branch 91 → 92 taken 61 times.
✗ Branch 91 → 93 not taken.
✗ Branch 92 → 93 not taken.
✓ Branch 92 → 96 taken 61 times.
|
61 | if (dir.VirtualAddress == 0 || dir.Size == 0) |
| 202 | { | ||
| 203 | ✗ | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 204 | } | ||
| 205 | 61 | const std::optional<std::uintptr_t> export_va = checked_rva(span, dir.VirtualAddress, dir.Size); | |
| 206 |
6/6✓ Branch 98 → 99 taken 60 times.
✓ Branch 98 → 100 taken 1 time.
✓ Branch 99 → 100 taken 1 time.
✓ Branch 99 → 101 taken 59 times.
✓ Branch 102 → 103 taken 2 times.
✓ Branch 102 → 106 taken 59 times.
|
61 | if (!export_va || dir.Size < sizeof(IMAGE_EXPORT_DIRECTORY)) |
| 207 | { | ||
| 208 | 2 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 209 | } | ||
| 210 | 59 | const std::optional<IMAGE_EXPORT_DIRECTORY> exports = guarded_read<IMAGE_EXPORT_DIRECTORY>(*export_va); | |
| 211 |
1/2✗ Branch 109 → 110 not taken.
✓ Branch 109 → 113 taken 59 times.
|
59 | if (!exports) |
| 212 | { | ||
| 213 | ✗ | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 214 | } | ||
| 215 | |||
| 216 | 59 | const std::uint32_t name_count = exports->NumberOfNames; | |
| 217 | 59 | const std::uint32_t func_count = exports->NumberOfFunctions; | |
| 218 |
5/8✓ Branch 115 → 116 taken 58 times.
✓ Branch 115 → 119 taken 1 time.
✓ Branch 116 → 117 taken 58 times.
✗ Branch 116 → 119 not taken.
✓ Branch 117 → 118 taken 58 times.
✗ Branch 117 → 119 not taken.
✗ Branch 118 → 119 not taken.
✓ Branch 118 → 122 taken 58 times.
|
59 | if (name_count == 0 || func_count == 0 || name_count > MAX_EXPORT_ENTRIES || |
| 219 | func_count > MAX_EXPORT_ENTRIES) | ||
| 220 | { | ||
| 221 | // No name table to match against, or an implausibly large count from a corrupt/hostile directory. | ||
| 222 | 1 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 223 | } | ||
| 224 | |||
| 225 | 116 | const std::optional<std::uintptr_t> names_va = checked_rva( | |
| 226 | span, | ||
| 227 | 58 | exports->AddressOfNames, | |
| 228 | 58 | static_cast<std::uintptr_t>(name_count) * sizeof(std::uint32_t) | |
| 229 | ); | ||
| 230 | 116 | const std::optional<std::uintptr_t> ordinals_va = checked_rva( | |
| 231 | span, | ||
| 232 | 58 | exports->AddressOfNameOrdinals, | |
| 233 | 58 | static_cast<std::uintptr_t>(name_count) * sizeof(std::uint16_t) | |
| 234 | ); | ||
| 235 | 116 | const std::optional<std::uintptr_t> funcs_va = checked_rva( | |
| 236 | span, | ||
| 237 | 58 | exports->AddressOfFunctions, | |
| 238 | 58 | static_cast<std::uintptr_t>(func_count) * sizeof(std::uint32_t) | |
| 239 | ); | ||
| 240 |
7/8✓ Branch 129 → 130 taken 57 times.
✓ Branch 129 → 134 taken 1 time.
✓ Branch 131 → 132 taken 57 times.
✗ Branch 131 → 134 not taken.
✓ Branch 133 → 134 taken 1 time.
✓ Branch 133 → 135 taken 56 times.
✓ Branch 136 → 137 taken 2 times.
✓ Branch 136 → 140 taken 56 times.
|
58 | if (!names_va || !ordinals_va || !funcs_va) |
| 241 | { | ||
| 242 | 2 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 243 | } | ||
| 244 | |||
| 245 | // Linear scan the parallel name / ordinal arrays. The name table is spec-sorted for GetProcAddress's binary | ||
| 246 | // search, but a linear scan is chosen deliberately: it is O(exports) at setup time (never a hot path) and | ||
| 247 | // stays correct on the unsorted tables some packers emit. A duplicate matching name is rejected as an | ||
| 248 | // ambiguous, malformed table rather than letting array order choose a target. | ||
| 249 | 56 | std::optional<Address> match; | |
| 250 | 56 | ExportResolution resolution; | |
| 251 |
2/2✓ Branch 222 → 141 taken 261 times.
✓ Branch 222 → 223 taken 48 times.
|
309 | for (std::uint32_t index = 0; index < name_count; ++index) |
| 252 | { | ||
| 253 | const std::optional<std::uint32_t> name_rva = | ||
| 254 | 261 | guarded_read<std::uint32_t>(*names_va + static_cast<std::uintptr_t>(index) * sizeof(std::uint32_t)); | |
| 255 |
1/2✗ Branch 144 → 145 not taken.
✓ Branch 144 → 148 taken 261 times.
|
261 | if (!name_rva) |
| 256 | { | ||
| 257 | ✗ | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 258 | } | ||
| 259 | 261 | const std::optional<std::uintptr_t> name_va = checked_rva(span, *name_rva, 1); | |
| 260 |
2/2✓ Branch 151 → 152 taken 1 time.
✓ Branch 151 → 155 taken 260 times.
|
261 | if (!name_va) |
| 261 | { | ||
| 262 | 1 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 263 | } | ||
| 264 | 260 | const std::optional<bool> name_matches = export_name_matches(span, *name_va, export_name); | |
| 265 |
2/2✓ Branch 158 → 159 taken 1 time.
✓ Branch 158 → 162 taken 259 times.
|
260 | if (!name_matches) |
| 266 | { | ||
| 267 | 1 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 268 | } | ||
| 269 |
2/2✓ Branch 163 → 164 taken 206 times.
✓ Branch 163 → 165 taken 53 times.
|
259 | if (!*name_matches) |
| 270 | { | ||
| 271 | 206 | continue; | |
| 272 | } | ||
| 273 |
2/2✓ Branch 166 → 167 taken 1 time.
✓ Branch 166 → 170 taken 52 times.
|
53 | if (match) |
| 274 | { | ||
| 275 | 1 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 276 | } | ||
| 277 | |||
| 278 | // Name index maps to function index AddressOfNameOrdinals[index]. That WORD is a 0-based index into | ||
| 279 | // AddressOfFunctions directly (the directory's Base biases only the ORDINAL exposed to callers, not | ||
| 280 | // this array index), so it is used as-is after the bounds check. | ||
| 281 | 52 | const std::optional<std::uint16_t> ordinal = guarded_read<std::uint16_t>( | |
| 282 | 52 | *ordinals_va + static_cast<std::uintptr_t>(index) * sizeof(std::uint16_t) | |
| 283 | ); | ||
| 284 |
5/6✓ Branch 173 → 174 taken 52 times.
✗ Branch 173 → 176 not taken.
✓ Branch 175 → 176 taken 1 time.
✓ Branch 175 → 177 taken 51 times.
✓ Branch 178 → 179 taken 1 time.
✓ Branch 178 → 182 taken 51 times.
|
52 | if (!ordinal || *ordinal >= func_count) |
| 285 | { | ||
| 286 | 1 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 287 | } | ||
| 288 | 51 | const std::optional<std::uint32_t> func_rva = guarded_read<std::uint32_t>( | |
| 289 | 51 | *funcs_va + static_cast<std::uintptr_t>(*ordinal) * sizeof(std::uint32_t) | |
| 290 | ); | ||
| 291 |
5/6✓ Branch 186 → 187 taken 51 times.
✗ Branch 186 → 189 not taken.
✓ Branch 188 → 189 taken 1 time.
✓ Branch 188 → 190 taken 50 times.
✓ Branch 191 → 192 taken 1 time.
✓ Branch 191 → 195 taken 50 times.
|
51 | if (!func_rva || *func_rva == 0) |
| 292 | { | ||
| 293 | // A zero RVA in the functions array is an unused / absent slot, not a resolvable address. | ||
| 294 | 1 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 295 | } | ||
| 296 | |||
| 297 | // A function RVA that points back inside the export directory region is a FORWARDER: the DWORD | ||
| 298 | // addresses an ASCII "TargetDll.TargetFunc" string, not code in this image. Following it would need the | ||
| 299 | // loader, and this [VirtualAddress, VirtualAddress + Size) window is exactly the loader's own forwarder | ||
| 300 | // test; fail closed so a declared forwarder is never handed back as a code anchor to hook or read | ||
| 301 | // through. | ||
| 302 | 50 | const std::uint64_t forwarder_begin = dir.VirtualAddress; | |
| 303 | 50 | const std::uint64_t forwarder_end = forwarder_begin + dir.Size; | |
| 304 |
4/4✓ Branch 196 → 197 taken 14 times.
✓ Branch 196 → 200 taken 36 times.
✓ Branch 201 → 202 taken 1 time.
✓ Branch 201 → 205 taken 49 times.
|
64 | if (static_cast<std::uint64_t>(*func_rva) >= forwarder_begin && |
| 305 |
2/2✓ Branch 198 → 199 taken 1 time.
✓ Branch 198 → 200 taken 13 times.
|
14 | static_cast<std::uint64_t>(*func_rva) < forwarder_end) |
| 306 | { | ||
| 307 | 1 | return std::unexpected(Error{ErrorCode::ExportForwarded, "scan::resolve_export"}); | |
| 308 | } | ||
| 309 | |||
| 310 | 49 | const std::optional<std::uintptr_t> target = checked_rva(span, *func_rva, 1); | |
| 311 |
2/2✓ Branch 208 → 209 taken 2 times.
✓ Branch 208 → 212 taken 47 times.
|
49 | if (!target) |
| 312 | { | ||
| 313 | // The RVA resolved outside the mapped image: a corrupt entry, not a usable code address. | ||
| 314 | 2 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 315 | } | ||
| 316 | 47 | match = Address{*target}; | |
| 317 | // The slot this name mapped to and the value read out of it, so a caller weighing two names can tell | ||
| 318 | // one physical entry point from two. A duplicate name aborts above, so this is written at most once. | ||
| 319 | 47 | resolution = ExportResolution{ | |
| 320 | .module_base = base, | ||
| 321 | 47 | .function_index = *ordinal, | |
| 322 | 47 | .function_rva = *func_rva, | |
| 323 | 47 | .target = Address{*target}, | |
| 324 | }; | ||
| 325 | } | ||
| 326 | |||
| 327 |
2/2✓ Branch 224 → 225 taken 44 times.
✓ Branch 224 → 227 taken 4 times.
|
48 | if (match) |
| 328 | { | ||
| 329 | 44 | out = resolution; | |
| 330 | 44 | return *match; | |
| 331 | } | ||
| 332 | 4 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 333 | } | ||
| 334 | |||
| 335 | } // namespace detail | ||
| 336 | |||
| 337 | namespace scan | ||
| 338 | { | ||
| 339 | 22 | Result<Address> resolve_export(std::string_view export_name, Region module) noexcept | |
| 340 | { | ||
| 341 | 22 | detail::ExportResolution ignored; | |
| 342 | 22 | return detail::resolve_export_with_provenance(export_name, module, ignored); | |
| 343 | } | ||
| 344 | |||
| 345 | 556 | ImageIdentity image_identity(Region range) noexcept | |
| 346 | { | ||
| 347 |
5/6✓ Branch 3 → 4 taken 555 times.
✓ Branch 3 → 5 taken 1 time.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 555 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 555 times.
|
556 | if (range.base.raw() == 0 || range.size == 0) |
| 348 | { | ||
| 349 | 1 | return ImageIdentity{}; | |
| 350 | } | ||
| 351 | |||
| 352 | // The supplied range may be a narrow scan scope or a stale cached extent after a same-base remap, so the | ||
| 353 | // authoritative extent is re-read from the PE at the base. The base itself is never folded in, which keeps | ||
| 354 | // a persisted baseline ASLR-insensitive; the RTTI generation token adds the base separately because it | ||
| 355 | // must also separate two modules. | ||
| 356 | 555 | const detail::ImageIdentityFields fields = detail::image_identity_at(range.base.raw()); | |
| 357 |
2/2✓ Branch 11 → 12 taken 11 times.
✓ Branch 11 → 13 taken 544 times.
|
555 | if (!fields.valid) |
| 358 | { | ||
| 359 | 11 | return ImageIdentity{}; | |
| 360 | } | ||
| 361 | return ImageIdentity{ | ||
| 362 | 544 | .timestamp = fields.timestamp, | |
| 363 | 544 | .size_of_image = fields.size_of_image, | |
| 364 | 544 | .section_digest = fields.section_digest, | |
| 365 | 544 | }; | |
| 366 | } | ||
| 367 | } // namespace scan | ||
| 368 | } // namespace DetourModKit | ||
| 369 |