GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 89.0% 129 / 0 / 145
Functions: 100.0% 6 / 0 / 6
Branches: 77.5% 110 / 0 / 142

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