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 | 784 | region_in_span(const ModuleSpan &span, std::uintptr_t start, std::uintptr_t bytes) noexcept | |
| 44 | { | ||
| 45 | 784 | if (bytes == 0) | |
| 46 | { | ||
| 47 | ✗ | return true; | |
| 48 | } | ||
| 49 | 784 | 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 | 784 | const std::uintptr_t last = start + (bytes - 1); | |
| 55 | 784 | 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 | 659 | checked_rva(const ModuleSpan &span, std::uint32_t rva, std::uintptr_t bytes) noexcept | |
| 63 | { | ||
| 64 | 659 | if (static_cast<std::uintptr_t>(rva) > std::numeric_limits<std::uintptr_t>::max() - span.base) | |
| 65 | { | ||
| 66 | ✗ | return std::nullopt; | |
| 67 | } | ||
| 68 | 659 | const std::uintptr_t address = span.base + static_cast<std::uintptr_t>(rva); | |
| 69 | 659 | if (!region_in_span(span, address, bytes)) | |
| 70 | { | ||
| 71 | 6 | return std::nullopt; | |
| 72 | } | ||
| 73 | 653 | 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 | 312 | export_name_matches(const ModuleSpan &span, std::uintptr_t name_addr, std::string_view target) noexcept | |
| 83 | { | ||
| 84 | 2608 | for (std::size_t index = 0; index < target.size(); ++index) | |
| 85 | { | ||
| 86 | 2552 | if (index > std::numeric_limits<std::uintptr_t>::max() - name_addr) | |
| 87 | { | ||
| 88 | ✗ | return std::nullopt; | |
| 89 | } | ||
| 90 | 2552 | const std::uintptr_t byte_addr = name_addr + index; | |
| 91 | 2552 | if (!span.contains(byte_addr)) | |
| 92 | { | ||
| 93 | ✗ | return std::nullopt; | |
| 94 | } | ||
| 95 | 2552 | const std::optional<char> byte = guarded_read<char>(byte_addr); | |
| 96 | 2552 | if (!byte) | |
| 97 | { | ||
| 98 | ✗ | return std::nullopt; | |
| 99 | } | ||
| 100 | 2552 | if (*byte != target[index]) | |
| 101 | { | ||
| 102 | 256 | return false; | |
| 103 | } | ||
| 104 | } | ||
| 105 | 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 | 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 | 56 | if (!terminator) | |
| 116 | { | ||
| 117 | 1 | return std::nullopt; | |
| 118 | } | ||
| 119 | 55 | return *terminator == '\0'; | |
| 120 | } | ||
| 121 | |||
| 122 | /** | ||
| 123 | * @brief Reads and checks the DOS and NT headers inside @p supplied_span. | ||
| 124 | * @details The checks cover both signatures, the PE32+ magic, and the export data-directory entry. An | ||
| 125 | * invalid span or a bad header returns InvalidRange, and a missing export data-directory entry | ||
| 126 | * returns ExportNotFound. On success the helper writes the NT header address to @p nt_address. | ||
| 127 | */ | ||
| 128 | [[nodiscard]] Result<IMAGE_NT_HEADERS64> | ||
| 129 | 66 | read_nt_headers(const ModuleSpan &supplied_span, std::uintptr_t &nt_address) noexcept | |
| 130 | { | ||
| 131 | 66 | if (!supplied_span.valid()) | |
| 132 | { | ||
| 133 | 2 | return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"}); | |
| 134 | } | ||
| 135 | 64 | const std::uintptr_t base = supplied_span.base; | |
| 136 | 64 | if (!region_in_span(supplied_span, base, sizeof(IMAGE_DOS_HEADER))) | |
| 137 | { | ||
| 138 | 1 | return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"}); | |
| 139 | } | ||
| 140 | |||
| 141 | // The library is x64-only, and an #error arch gate enforces it. The explicit PE32+ magic check on | ||
| 142 | // IMAGE_NT_HEADERS64 rejects a wrong-bitness image and is not a portability branch. | ||
| 143 | // | ||
| 144 | // This parse stays separate from the RTTI image walk because the error models differ. Here a bad header | ||
| 145 | // fails closed with an ErrorCode. The RTTI image walk returns a range count, and its caller falls back | ||
| 146 | // to the whole module. A shared helper couples the two subsystems and removes no real duplication. | ||
| 147 | 63 | const std::optional<IMAGE_DOS_HEADER> dos = guarded_read<IMAGE_DOS_HEADER>(base); | |
| 148 | 63 | if (!dos || dos->e_magic != IMAGE_DOS_SIGNATURE) | |
| 149 | { | ||
| 150 | ✗ | return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"}); | |
| 151 | } | ||
| 152 | 63 | if (dos->e_lfanew < 0) | |
| 153 | { | ||
| 154 | 1 | return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"}); | |
| 155 | } | ||
| 156 | const std::optional<std::uintptr_t> nt_addr = | ||
| 157 | 62 | checked_rva(supplied_span, static_cast<std::uint32_t>(dos->e_lfanew), sizeof(IMAGE_NT_HEADERS64)); | |
| 158 | 62 | if (!nt_addr) | |
| 159 | { | ||
| 160 | ✗ | return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"}); | |
| 161 | } | ||
| 162 | 62 | const std::optional<IMAGE_NT_HEADERS64> nt = guarded_read<IMAGE_NT_HEADERS64>(*nt_addr); | |
| 163 | 124 | if (!nt || nt->Signature != IMAGE_NT_SIGNATURE || | |
| 164 | 62 | nt->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) | |
| 165 | { | ||
| 166 | ✗ | return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"}); | |
| 167 | } | ||
| 168 | |||
| 169 | 62 | constexpr std::size_t export_directory_end = | |
| 170 | offsetof(IMAGE_OPTIONAL_HEADER64, DataDirectory) + | ||
| 171 | (IMAGE_DIRECTORY_ENTRY_EXPORT + 1) * sizeof(IMAGE_DATA_DIRECTORY); | ||
| 172 | 123 | if (nt->FileHeader.SizeOfOptionalHeader < export_directory_end || | |
| 173 | 61 | nt->OptionalHeader.NumberOfRvaAndSizes <= IMAGE_DIRECTORY_ENTRY_EXPORT) | |
| 174 | { | ||
| 175 | 1 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 176 | } | ||
| 177 | 61 | nt_address = *nt_addr; | |
| 178 | 61 | return *nt; | |
| 179 | } | ||
| 180 | |||
| 181 | /** | ||
| 182 | * @brief Returns @p supplied_span clipped to the image size that the NT headers declare. | ||
| 183 | * @details The helper returns InvalidRange when SizeOfImage is zero, when the declared image end wraps, or | ||
| 184 | * when the NT headers fall outside the clipped span. @p supplied_span is only an outer safety | ||
| 185 | * boundary. The SizeOfImage that @p nt declares is the authoritative inner boundary for every EAT | ||
| 186 | * read. Stale or oversized mapped backing therefore cannot make a declared out-of-image RVA appear | ||
| 187 | * valid. | ||
| 188 | */ | ||
| 189 | 61 | [[nodiscard]] Result<ModuleSpan> clip_span_to_declared_image( | |
| 190 | const ModuleSpan &supplied_span, | ||
| 191 | const IMAGE_NT_HEADERS64 &nt, | ||
| 192 | std::uintptr_t nt_address | ||
| 193 | ) noexcept | ||
| 194 | { | ||
| 195 | 61 | const std::uintptr_t base = supplied_span.base; | |
| 196 | 61 | const std::uintptr_t image_size = nt.OptionalHeader.SizeOfImage; | |
| 197 | 61 | if (image_size == 0 || image_size > std::numeric_limits<std::uintptr_t>::max() - base) | |
| 198 | { | ||
| 199 | ✗ | return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"}); | |
| 200 | } | ||
| 201 | 61 | const std::uintptr_t declared_end = base + image_size; | |
| 202 | 61 | const ModuleSpan span{base, declared_end < supplied_span.end ? declared_end : supplied_span.end}; | |
| 203 | 61 | if (!region_in_span(span, nt_address, sizeof(IMAGE_NT_HEADERS64))) | |
| 204 | { | ||
| 205 | ✗ | return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"}); | |
| 206 | } | ||
| 207 | 61 | return span; | |
| 208 | } | ||
| 209 | |||
| 210 | /** | ||
| 211 | * @brief Reads the IMAGE_EXPORT_DIRECTORY that @p dir locates inside @p span. | ||
| 212 | * @details The helper returns ExportNotFound when @p dir is empty, too small, outside @p span, or | ||
| 213 | * unreadable. | ||
| 214 | */ | ||
| 215 | [[nodiscard]] Result<IMAGE_EXPORT_DIRECTORY> | ||
| 216 | 61 | read_export_directory(const ModuleSpan &span, const IMAGE_DATA_DIRECTORY &dir) noexcept | |
| 217 | { | ||
| 218 | // A module with no exports leaves dir zeroed. That is not a fault: the module has no name for this | ||
| 219 | // backend to resolve. | ||
| 220 | 61 | if (dir.VirtualAddress == 0 || dir.Size == 0) | |
| 221 | { | ||
| 222 | ✗ | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 223 | } | ||
| 224 | 61 | const std::optional<std::uintptr_t> export_va = checked_rva(span, dir.VirtualAddress, dir.Size); | |
| 225 | 61 | if (!export_va || dir.Size < sizeof(IMAGE_EXPORT_DIRECTORY)) | |
| 226 | { | ||
| 227 | 2 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 228 | } | ||
| 229 | 59 | const std::optional<IMAGE_EXPORT_DIRECTORY> exports = guarded_read<IMAGE_EXPORT_DIRECTORY>(*export_va); | |
| 230 | 59 | if (!exports) | |
| 231 | { | ||
| 232 | ✗ | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 233 | } | ||
| 234 | 59 | return *exports; | |
| 235 | } | ||
| 236 | |||
| 237 | /** | ||
| 238 | * @brief Maps the matched name @p index through the ordinal and function arrays to its physical slot. | ||
| 239 | * @details A forwarder returns ExportForwarded. A bad ordinal, an unreadable or empty slot, or a target | ||
| 240 | * outside @p span returns ExportNotFound. | ||
| 241 | */ | ||
| 242 | 52 | [[nodiscard]] Result<ExportResolution> resolve_export_slot( | |
| 243 | const ModuleSpan &span, | ||
| 244 | const IMAGE_DATA_DIRECTORY &dir, | ||
| 245 | std::uintptr_t ordinals_va, | ||
| 246 | std::uintptr_t funcs_va, | ||
| 247 | std::uint32_t func_count, | ||
| 248 | std::uint32_t index | ||
| 249 | ) noexcept | ||
| 250 | { | ||
| 251 | // The name index maps to the function index AddressOfNameOrdinals[index]. That WORD is a 0-based index | ||
| 252 | // into AddressOfFunctions. The directory's Base biases only the ordinal exposed to callers, not this | ||
| 253 | // array index. The helper uses the WORD as-is after the bounds check. | ||
| 254 | 52 | const std::optional<std::uint16_t> ordinal = guarded_read<std::uint16_t>( | |
| 255 | 52 | ordinals_va + static_cast<std::uintptr_t>(index) * sizeof(std::uint16_t) | |
| 256 | ); | ||
| 257 | 52 | if (!ordinal || *ordinal >= func_count) | |
| 258 | { | ||
| 259 | 1 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 260 | } | ||
| 261 | 51 | const std::optional<std::uint32_t> func_rva = guarded_read<std::uint32_t>( | |
| 262 | 51 | funcs_va + static_cast<std::uintptr_t>(*ordinal) * sizeof(std::uint32_t) | |
| 263 | ); | ||
| 264 | 51 | if (!func_rva || *func_rva == 0) | |
| 265 | { | ||
| 266 | // A zero RVA in the functions array marks an unused or absent slot, not a resolvable address. | ||
| 267 | 1 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 268 | } | ||
| 269 | |||
| 270 | // A function RVA inside the export directory is a forwarder. The DWORD addresses an ASCII | ||
| 271 | // "TargetDll.TargetFunc" string, not code in this image. Only the loader can resolve it. The helper | ||
| 272 | // fails closed so that a declared forwarder never becomes a code anchor to hook or read through. | ||
| 273 | 50 | const std::uint64_t forwarder_begin = dir.VirtualAddress; | |
| 274 | 50 | const std::uint64_t forwarder_end = forwarder_begin + dir.Size; | |
| 275 | 64 | if (static_cast<std::uint64_t>(*func_rva) >= forwarder_begin && | |
| 276 | 14 | static_cast<std::uint64_t>(*func_rva) < forwarder_end) | |
| 277 | { | ||
| 278 | 1 | return std::unexpected(Error{ErrorCode::ExportForwarded, "scan::resolve_export"}); | |
| 279 | } | ||
| 280 | |||
| 281 | 49 | const std::optional<std::uintptr_t> target = checked_rva(span, *func_rva, 1); | |
| 282 | 49 | if (!target) | |
| 283 | { | ||
| 284 | // An RVA outside the mapped image marks a corrupt entry, not a usable code address. | ||
| 285 | 2 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 286 | } | ||
| 287 | 94 | return ExportResolution{ | |
| 288 | 47 | .module_base = span.base, | |
| 289 | 47 | .function_index = *ordinal, | |
| 290 | 47 | .function_rva = *func_rva, | |
| 291 | 47 | .target = Address{*target}, | |
| 292 | 47 | }; | |
| 293 | } | ||
| 294 | } // namespace | ||
| 295 | |||
| 296 | Result<Address> | ||
| 297 | 68 | resolve_export_with_provenance(std::string_view export_name, Region module, ExportResolution &out) noexcept | |
| 298 | { | ||
| 299 | 68 | out = ExportResolution{}; | |
| 300 | |||
| 301 | // PE names are non-empty NUL-terminated byte strings. The check rejects an embedded terminator up front so | ||
| 302 | // that a query cannot match zero padding after the real name's terminator. | ||
| 303 | 68 | if (export_name.empty() || export_name.find('\0') != std::string_view::npos) | |
| 304 | { | ||
| 305 | 2 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 306 | } | ||
| 307 | |||
| 308 | 66 | const ModuleSpan supplied_span = module_span(module); | |
| 309 | 66 | std::uintptr_t nt_address = 0; | |
| 310 | 66 | const Result<IMAGE_NT_HEADERS64> nt = read_nt_headers(supplied_span, nt_address); | |
| 311 | 66 | if (!nt) | |
| 312 | { | ||
| 313 | 5 | return std::unexpected(nt.error()); | |
| 314 | } | ||
| 315 | 61 | const Result<ModuleSpan> declared_span = clip_span_to_declared_image(supplied_span, *nt, nt_address); | |
| 316 | 61 | if (!declared_span) | |
| 317 | { | ||
| 318 | ✗ | return std::unexpected(declared_span.error()); | |
| 319 | } | ||
| 320 | 61 | const ModuleSpan &span = *declared_span; | |
| 321 | |||
| 322 | 61 | const IMAGE_DATA_DIRECTORY dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; | |
| 323 | 61 | const Result<IMAGE_EXPORT_DIRECTORY> exports = read_export_directory(span, dir); | |
| 324 | 61 | if (!exports) | |
| 325 | { | ||
| 326 | 2 | return std::unexpected(exports.error()); | |
| 327 | } | ||
| 328 | |||
| 329 | 59 | const std::uint32_t name_count = exports->NumberOfNames; | |
| 330 | 59 | const std::uint32_t func_count = exports->NumberOfFunctions; | |
| 331 | 59 | if (name_count == 0 || func_count == 0 || name_count > MAX_EXPORT_ENTRIES || | |
| 332 | func_count > MAX_EXPORT_ENTRIES) | ||
| 333 | { | ||
| 334 | // No name table to match against, or an implausibly large count from a corrupt/hostile directory. | ||
| 335 | 1 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 336 | } | ||
| 337 | |||
| 338 | 116 | const std::optional<std::uintptr_t> names_va = checked_rva( | |
| 339 | span, | ||
| 340 | 58 | exports->AddressOfNames, | |
| 341 | 58 | static_cast<std::uintptr_t>(name_count) * sizeof(std::uint32_t) | |
| 342 | ); | ||
| 343 | 116 | const std::optional<std::uintptr_t> ordinals_va = checked_rva( | |
| 344 | span, | ||
| 345 | 58 | exports->AddressOfNameOrdinals, | |
| 346 | 58 | static_cast<std::uintptr_t>(name_count) * sizeof(std::uint16_t) | |
| 347 | ); | ||
| 348 | 116 | const std::optional<std::uintptr_t> funcs_va = checked_rva( | |
| 349 | span, | ||
| 350 | 58 | exports->AddressOfFunctions, | |
| 351 | 58 | static_cast<std::uintptr_t>(func_count) * sizeof(std::uint32_t) | |
| 352 | ); | ||
| 353 | 58 | if (!names_va || !ordinals_va || !funcs_va) | |
| 354 | { | ||
| 355 | 2 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 356 | } | ||
| 357 | |||
| 358 | // Linear scan the parallel name / ordinal arrays. The name table is spec-sorted for GetProcAddress's binary | ||
| 359 | // search, but a linear scan is chosen deliberately: it is O(exports) at setup time (never a hot path) and | ||
| 360 | // stays correct on the unsorted tables some packers emit. A duplicate matching name is rejected as an | ||
| 361 | // ambiguous, malformed table rather than letting array order choose a target. | ||
| 362 | 56 | std::optional<Address> match; | |
| 363 | 56 | ExportResolution resolution; | |
| 364 | 361 | for (std::uint32_t index = 0; index < name_count; ++index) | |
| 365 | { | ||
| 366 | const std::optional<std::uint32_t> name_rva = | ||
| 367 | 313 | guarded_read<std::uint32_t>(*names_va + static_cast<std::uintptr_t>(index) * sizeof(std::uint32_t)); | |
| 368 | 313 | if (!name_rva) | |
| 369 | { | ||
| 370 | ✗ | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 371 | } | ||
| 372 | 313 | const std::optional<std::uintptr_t> name_va = checked_rva(span, *name_rva, 1); | |
| 373 | 313 | if (!name_va) | |
| 374 | { | ||
| 375 | 1 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 376 | } | ||
| 377 | 312 | const std::optional<bool> name_matches = export_name_matches(span, *name_va, export_name); | |
| 378 | 312 | if (!name_matches) | |
| 379 | { | ||
| 380 | 1 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 381 | } | ||
| 382 | 311 | if (!*name_matches) | |
| 383 | { | ||
| 384 | 258 | continue; | |
| 385 | } | ||
| 386 | 53 | if (match) | |
| 387 | { | ||
| 388 | 1 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 389 | } | ||
| 390 | |||
| 391 | const Result<ExportResolution> slot = | ||
| 392 | 52 | resolve_export_slot(span, dir, *ordinals_va, *funcs_va, func_count, index); | |
| 393 | 52 | if (!slot) | |
| 394 | { | ||
| 395 | 5 | return std::unexpected(slot.error()); | |
| 396 | } | ||
| 397 | 47 | match = slot->target; | |
| 398 | // The slot this name mapped to and the value read out of it, so a caller weighing two names can tell | ||
| 399 | // one physical entry point from two. A duplicate name aborts above, so this is written at most once. | ||
| 400 | 47 | resolution = *slot; | |
| 401 | } | ||
| 402 | |||
| 403 | 48 | if (match) | |
| 404 | { | ||
| 405 | 44 | out = resolution; | |
| 406 | 44 | return *match; | |
| 407 | } | ||
| 408 | 4 | return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"}); | |
| 409 | } | ||
| 410 | |||
| 411 | } // namespace detail | ||
| 412 | |||
| 413 | namespace scan | ||
| 414 | { | ||
| 415 | 22 | Result<Address> resolve_export(std::string_view export_name, Region module) noexcept | |
| 416 | { | ||
| 417 | 22 | detail::ExportResolution ignored; | |
| 418 | 22 | return detail::resolve_export_with_provenance(export_name, module, ignored); | |
| 419 | } | ||
| 420 | |||
| 421 | 568 | ImageIdentity image_identity(Region range) noexcept | |
| 422 | { | ||
| 423 | 568 | if (range.base.raw() == 0 || range.size == 0) | |
| 424 | { | ||
| 425 | 1 | return ImageIdentity{}; | |
| 426 | } | ||
| 427 | |||
| 428 | // The supplied range may be a narrow scan scope or a stale cached extent after a same-base remap, so the | ||
| 429 | // authoritative extent is re-read from the PE at the base. The base itself is never folded in, which keeps | ||
| 430 | // a persisted baseline ASLR-insensitive; the RTTI generation token adds the base separately because it | ||
| 431 | // must also separate two modules. | ||
| 432 | 567 | const detail::ImageIdentityFields fields = detail::image_identity_at(range.base.raw()); | |
| 433 | 567 | if (!fields.valid) | |
| 434 | { | ||
| 435 | 11 | return ImageIdentity{}; | |
| 436 | } | ||
| 437 | return ImageIdentity{ | ||
| 438 | 556 | .timestamp = fields.timestamp, | |
| 439 | 556 | .size_of_image = fields.size_of_image, | |
| 440 | 556 | .section_digest = fields.section_digest, | |
| 441 | 556 | }; | |
| 442 | } | ||
| 443 | } // namespace scan | ||
| 444 | } // namespace DetourModKit | ||
| 445 |