102 Patches: Detours to the Rescue
C reference for DttR maintainers and modders.
Loading...
Searching...
No Matches
hook_registry.c
Go to the documentation of this file.
1#include "core_internal.h"
2
3#include <dttr_log.h>
4#include <dttr_sigscan.h>
5
6#include <Zydis/Zydis.h>
7#include <khash.h>
8#include <kvec.h>
9#include <psapi.h>
10#include <xxhash.h>
11
12#include <limits.h>
13#include <stdio.h>
14#include <stdlib.h>
15#include <string.h>
16
21
22typedef struct hook_chain hook_chain;
23
37
47
48uintptr_t DTTR_Core_HookSigscan(HMODULE mod, const char *sig, const char *mask) {
49 MODULEINFO module_info;
50
51 if (!mod) {
52 return 0;
53 }
54
55 if (!GetModuleInformation(
56 GetCurrentProcess(),
57 mod,
58 &module_info,
59 sizeof(module_info)
60 )) {
61 return 0;
62 }
63
64 const uint8_t *base = (const uint8_t *)module_info.lpBaseOfDll;
65 const size_t size = module_info.SizeOfImage;
66 return (uintptr_t)DTTR_Sigscan_Bytes(base, size, sig, mask);
67}
68
69#define DTTR_HOOK_PATCH_SIZE 5u
70#define DTTR_HOOK_MIN_PROLOGUE 5u
71#define DTTR_HOOK_MAX_PROLOGUE 64u
72#define DTTR_HOOK_MAX_INSN 32u
73
74typedef struct {
75 uint8_t offset;
76 uint8_t length;
77 uint8_t rel_offset;
78 uint8_t rel_size;
80
81static ZydisDecoder decoder;
82static bool decoder_initialized = false;
84
88
89void dttr_core_hook_set_last_error(DTTR_Status status, const char *message) {
90 hook_last_error = dttr_core_result(status, message);
91}
92
96
97static bool decoder_init() {
99 return true;
100 }
101
102 const ZyanStatus status = ZydisDecoderInit(
103 &decoder,
104 ZYDIS_MACHINE_MODE_LEGACY_32,
105 ZYDIS_STACK_WIDTH_32
106 );
107 if (!ZYAN_SUCCESS(status)) {
109 "hook_attach_function: ZydisDecoderInit failed (status=0x%08X)",
110 (unsigned)status
111 );
112 return false;
113 }
114
115 decoder_initialized = true;
116 return true;
117}
118
119// Accept page protections that can be safely copied before patching.
120static bool is_readable_page_protect(DWORD protect) {
121 switch (protect & 0xFFu) {
122 case PAGE_READONLY:
123 case PAGE_READWRITE:
124 case PAGE_WRITECOPY:
125 case PAGE_EXECUTE:
126 case PAGE_EXECUTE_READ:
127 case PAGE_EXECUTE_READWRITE:
128 case PAGE_EXECUTE_WRITECOPY:
129 return true;
130 default:
131 return false;
132 }
133}
134
135// Copy original code only from committed readable pages to avoid unsafe trampolines.
136static bool copy_memory_checked(uintptr_t addr, uint8_t *out, size_t size) {
137 size_t copied = 0;
138
139 while (copied < size) {
140 const uintptr_t cur = addr + copied;
141 MEMORY_BASIC_INFORMATION mbi;
142 if (!VirtualQuery((const void *)cur, &mbi, sizeof(mbi))) {
144 "hook_attach_function: VirtualQuery failed at 0x%08X (err=%lu)",
145 (unsigned)cur,
146 GetLastError()
147 );
148 return false;
149 }
150
151 if (mbi.State != MEM_COMMIT) {
153 "hook_attach_function: unreadable memory state=0x%lX at 0x%08X",
154 (unsigned long)mbi.State,
155 (unsigned)cur
156 );
157 return false;
158 }
159
160 if ((mbi.Protect & PAGE_GUARD) || (mbi.Protect & PAGE_NOACCESS)
161 || !is_readable_page_protect(mbi.Protect)) {
163 "hook_attach_function: unreadable memory protect=0x%lX at 0x%08X",
164 (unsigned long)mbi.Protect,
165 (unsigned)cur
166 );
167 return false;
168 }
169
170 const uintptr_t region_end = (uintptr_t)mbi.BaseAddress + mbi.RegionSize;
171 size_t chunk = (size_t)(region_end - cur);
172 if (chunk > size - copied) {
173 chunk = size - copied;
174 }
175
176 memcpy(out + copied, (const void *)cur, chunk);
177 copied += chunk;
178 }
179
180 return true;
181}
182
183// Log the candidate prologue bytes when instruction decoding cannot build a trampoline.
184static void log_prologue_bytes(uintptr_t site, const uint8_t *bytes, size_t size) {
185 char hex[(DTTR_HOOK_MAX_PROLOGUE * 3u) + 1u];
186 size_t pos = 0;
187
188 for (size_t i = 0; i < size && i < DTTR_HOOK_MAX_PROLOGUE; i++) {
189 const int wrote = snprintf(
190 hex + pos,
191 sizeof(hex) - pos,
192 "%02X%s",
193 (unsigned)bytes[i],
194 (i + 1 < size) ? " " : ""
195 );
196 if (wrote <= 0) {
197 break;
198 }
199
200 const size_t w = (size_t)wrote;
201 if (w >= sizeof(hex) - pos) {
202 pos = sizeof(hex) - 1;
203 break;
204 }
205
206 pos += w;
207 }
208
209 hex[pos] = '\0';
211 "hook_validate: site=0x%08X prologue_bytes[%u]=%s",
212 (unsigned)site,
213 (unsigned)size,
214 hex
215 );
216}
217
218// Decode enough whole instructions to cover the requested patch window.
219static bool decode_prologue(
220 uintptr_t addr,
221 int requested_size,
222 trampoline_insn *insns,
223 size_t *out_insn_count,
224 size_t *out_prologue_size,
225 uint8_t *out_prologue_bytes,
226 size_t out_prologue_bytes_cap
227) {
228 if (!decoder_init()) {
229 return false;
230 }
231
232 if (!insns || !out_insn_count || !out_prologue_size || !out_prologue_bytes
233 || out_prologue_bytes_cap == 0) {
235 "hook_attach_function: invalid decode parameters for 0x%08X",
236 (unsigned)addr
237 );
238 return false;
239 }
240
241 size_t need = DTTR_HOOK_MIN_PROLOGUE;
242 if (requested_size > 0) {
243 need = (size_t)requested_size;
244 if (need < DTTR_HOOK_MIN_PROLOGUE) {
246 }
247 } else if (requested_size < 0) {
249 "hook_attach_function: negative requested prologue=%d at 0x%08X; using "
250 "auto",
251 requested_size,
252 (unsigned)addr
253 );
254 }
255
256 if (need > DTTR_HOOK_MAX_PROLOGUE) {
258 "hook_attach_function: invalid requested prologue=%u at 0x%08X",
259 (unsigned)need,
260 (unsigned)addr
261 );
262 return false;
263 }
264
265 size_t decode_window = need + (size_t)ZYDIS_MAX_INSTRUCTION_LENGTH - 1u;
266 if (decode_window > DTTR_HOOK_MAX_PROLOGUE) {
267 decode_window = DTTR_HOOK_MAX_PROLOGUE;
268 }
269
270 uint8_t code_window[DTTR_HOOK_MAX_PROLOGUE] = {0};
271 if (!copy_memory_checked(addr, code_window, decode_window)) {
273 "hook_attach_function: failed to read decode window at 0x%08X (size=%u)",
274 (unsigned)addr,
275 (unsigned)decode_window
276 );
277 return false;
278 }
279
280 size_t offset = 0;
281 size_t count = 0;
282
283 while (offset < need) {
284 if (count >= DTTR_HOOK_MAX_INSN) {
286 "hook_attach_function: too many instructions while decoding 0x%08X",
287 (unsigned)addr
288 );
289 return false;
290 }
291
292 ZydisDecodedInstruction inst;
293 const ZyanStatus status = ZydisDecoderDecodeInstruction(
294 &decoder,
295 NULL,
296 code_window + offset,
297 decode_window - offset,
298 &inst
299 );
300 if (!ZYAN_SUCCESS(status) || inst.length == 0) {
302 "hook_attach_function: decode failed at 0x%08X+0x%X (status=0x%08X)",
303 (unsigned)addr,
304 (unsigned)offset,
305 (unsigned)status
306 );
307 return false;
308 }
309
310 const char *mnemonic = ZydisMnemonicGetString(inst.mnemonic);
311 if (!mnemonic) {
312 mnemonic = "unknown";
313 }
314
315 if (offset + inst.length > DTTR_HOOK_MAX_PROLOGUE) {
317 "hook_attach_function: decoded prologue exceeded %u bytes at 0x%08X",
318 (unsigned)DTTR_HOOK_MAX_PROLOGUE,
319 (unsigned)addr
320 );
321 return false;
322 }
323
324 trampoline_insn *out = &insns[count];
325 out->offset = (uint8_t)offset;
326 out->length = inst.length;
327 out->rel_offset = 0;
328 out->rel_size = 0;
329
330 for (size_t imm_idx = 0; imm_idx < 2; imm_idx++) {
331 if (!inst.raw.imm[imm_idx].size || !inst.raw.imm[imm_idx].is_relative) {
332 continue;
333 }
334
335 out->rel_offset = inst.raw.imm[imm_idx].offset;
336 out->rel_size = (uint8_t)(inst.raw.imm[imm_idx].size / 8);
337 break;
338 }
339
340 if (out->rel_size != 0 && out->rel_size != 4) {
342 "hook_attach_function: unsupported relative immediate size=%u at "
343 "0x%08X+0x%X (%s)",
344 (unsigned)out->rel_size,
345 (unsigned)addr,
346 (unsigned)offset,
347 mnemonic
348 );
349 return false;
350 }
351
352 if (out->rel_size == 4 && (size_t)out->rel_offset + out->rel_size > out->length) {
354 "hook_attach_function: invalid relative-immediate layout at "
355 "0x%08X+0x%X",
356 (unsigned)addr,
357 (unsigned)offset
358 );
359 return false;
360 }
361
363 "hook_decode: site=0x%08X off=0x%02X len=%u mnemonic=%s rel_off=%u "
364 "rel_size=%u",
365 (unsigned)addr,
366 (unsigned)offset,
367 (unsigned)inst.length,
368 mnemonic,
369 (unsigned)out->rel_offset,
370 (unsigned)out->rel_size
371 );
372
373 offset += inst.length;
374 count++;
375 }
376
377 if (offset > out_prologue_bytes_cap) {
379 "hook_attach_function: prologue output overflow at 0x%08X (%u > %u)",
380 (unsigned)addr,
381 (unsigned)offset,
382 (unsigned)out_prologue_bytes_cap
383 );
384 return false;
385 }
386
387 memcpy(out_prologue_bytes, code_window, offset);
388 log_prologue_bytes(addr, out_prologue_bytes, offset);
389
390 *out_insn_count = count;
391 *out_prologue_size = offset;
392 return true;
393}
394
395// Copy decoded prologue instructions into a trampoline and fix supported relative
396// operands.
398 uint8_t *trampoline,
399 uintptr_t site,
400 const trampoline_insn *insns,
401 size_t insn_count
402) {
403 for (size_t i = 0; i < insn_count; i++) {
404 const trampoline_insn *insn = &insns[i];
405 if (insn->rel_size == 0) {
406 continue;
407 }
408
409 if (insn->rel_size != 4) {
411 "hook_attach_function: relocate unsupported rel_size=%u at 0x%08X+0x%X",
412 (unsigned)insn->rel_size,
413 (unsigned)site,
414 (unsigned)insn->offset
415 );
416 return false;
417 }
418
419 int32_t old_rel = 0;
420 memcpy(&old_rel, trampoline + insn->offset + insn->rel_offset, sizeof(old_rel));
421
422 const intptr_t old_next = (intptr_t)(site + insn->offset + insn->length);
423 const intptr_t old_target = old_next + (intptr_t)old_rel;
424 const intptr_t new_next = (intptr_t)((uintptr_t)trampoline + insn->offset
425 + insn->length);
426 const int64_t new_rel64 = (int64_t)(old_target - new_next);
427 if (new_rel64 < INT32_MIN || new_rel64 > INT32_MAX) {
429 "hook_attach_function: relocated target out of range at 0x%08X+0x%X",
430 (unsigned)site,
431 (unsigned)insn->offset
432 );
433 return false;
434 }
435
436 const int32_t new_rel = (int32_t)new_rel64;
437 memcpy(trampoline + insn->offset + insn->rel_offset, &new_rel, sizeof(new_rel));
438
440 "hook_reloc: site=0x%08X off=0x%02X target=0x%08X rel=%d",
441 (unsigned)site,
442 (unsigned)insn->offset,
443 (unsigned)old_target,
444 new_rel
445 );
446 }
447
448 return true;
449}
450
451KHASH_MAP_INIT_INT64(sigscan_cache, uintptr_t)
452
453static khash_t(sigscan_cache) *cache = NULL;
454
455// Build a cache key from the module base and signature bytes.
456static uint64_t sigscan_key(HMODULE mod, const char *sig, const char *mask) {
457 const size_t mask_len = strlen(mask);
458 XXH3_state_t state;
459 XXH3_64bits_reset(&state);
460 XXH3_64bits_update(&state, &mod, sizeof(mod));
461 XXH3_64bits_update(&state, sig, mask_len);
462 XXH3_64bits_update(&state, mask, mask_len);
463 return XXH3_64bits_digest(&state);
464}
465
466// Reuse module signature scan results so generated symbol resolution stays cheap.
467uintptr_t DTTR_Core_HookCachedSigscan(HMODULE mod, const char *sig, const char *mask) {
468 if (!mod || !sig || !mask) {
469 return 0;
470 }
471
472 if (!cache) {
473 cache = kh_init(sigscan_cache);
474 if (!cache) {
475 return DTTR_Core_HookSigscan(mod, sig, mask);
476 }
477 }
478
479 const uint64_t key = sigscan_key(mod, sig, mask);
480 khiter_t it = kh_get(sigscan_cache, cache, key);
481
482 if (it != kh_end(cache)) {
483 return kh_val(cache, it);
484 }
485
486 const uintptr_t result = DTTR_Core_HookSigscan(mod, sig, mask);
487
488 int ret;
489 it = kh_put(sigscan_cache, cache, key, &ret);
490 if (ret < 0) {
491 return result;
492 }
493
494 kh_val(cache, it) = result;
495
496 return result;
497}
498
499typedef kvec_t(DTTR_Core_Hook *) hook_vec;
500
501static hook_vec hooks;
502static void *hook_owner = NULL;
503
504static void hook_destroy(DTTR_Core_Hook *hook) {
505 if (!hook) {
506 return;
507 }
508
509 if (hook->kind == DTTR_HOOK_RECORD_FUNCTION) {
510 if (hook->next_thunk) {
511 VirtualFree(hook->next_thunk, 0, MEM_RELEASE);
512 }
513
514 free(hook);
515 return;
516 }
517
518 if (hook->trampoline) {
519 VirtualFree(hook->trampoline, 0, MEM_RELEASE);
520 }
521
522 free(hook->original);
523 free(hook);
524}
525
526static void hook_chain_destroy(hook_chain *chain) {
527 if (!chain) {
528 return;
529 }
530
531 if (chain->trampoline) {
532 VirtualFree(chain->trampoline, 0, MEM_RELEASE);
533 }
534
535 free(chain->original);
536 free(chain);
537}
538
539// Reject overlapping patch ranges so the registry can restore bytes deterministically.
540static bool check_overlap(uintptr_t addr, size_t size) {
541 const uintptr_t end = addr + size;
542
543 for (size_t i = 0; i < kv_size(hooks); i++) {
544 DTTR_Core_Hook *h = kv_A(hooks, i);
545 const uintptr_t h_end = h->addr + h->size;
546
547 if (addr < h_end && h->addr < end) {
549 "hook overlap: [0x%08X, +%zu) conflicts with [0x%08X, +%zu)",
550 (unsigned)addr,
551 size,
552 (unsigned)h->addr,
553 h->size
554 );
555 return false;
556 }
557 }
558
559 return true;
560}
561
562// Allocate and register one hook record after overlap checks pass.
563static DTTR_Core_Hook *hook_create(const char *op, uintptr_t addr, size_t size) {
564 DTTR_Core_Hook *hook = (DTTR_Core_Hook *)calloc(1, sizeof(DTTR_Core_Hook));
565 if (!hook) {
566 DTTR_LOG_ERROR("%s: hook alloc failed for 0x%08X", op, (unsigned)addr);
567 return NULL;
568 }
569
570 hook->addr = addr;
571 hook->size = size;
572 hook->owner = hook_owner;
573 hook->original = (uint8_t *)malloc(size);
574 if (!hook->original) {
575 DTTR_LOG_ERROR("%s: original-bytes alloc failed for 0x%08X", op, (unsigned)addr);
576 free(hook);
577 return NULL;
578 }
579
580 return hook;
581}
582
583// Find a registered hook handle before detach or overlap checks mutate state.
584static size_t hook_find_index(DTTR_Core_Hook *hook) {
585 for (size_t i = 0; i < kv_size(hooks); i++) {
586 if (kv_A(hooks, i) == hook) {
587 return i;
588 }
589 }
590
591 return kv_size(hooks);
592}
593
594static hook_chain *hook_find_function_chain(uintptr_t addr) {
595 for (size_t i = 0; i < kv_size(hooks); i++) {
596 DTTR_Core_Hook *hook = kv_A(hooks, i);
597 if (hook->kind == DTTR_HOOK_RECORD_FUNCTION && hook->chain
598 && hook->chain->addr == addr) {
599 return hook->chain;
600 }
601 }
602
603 return NULL;
604}
605
606// Flush the CPU instruction cache for a freshly patched range.
607static void flush_patched_range(const char *op, uintptr_t addr, size_t size) {
608 if (!FlushInstructionCache(GetCurrentProcess(), (const void *)addr, size)) {
609 DTTR_LOG_WARN("%s: FlushInstructionCache failed for 0x%08X", op, (unsigned)addr);
610 }
611}
612
613static bool write_bytes(const char *op, uintptr_t addr, const uint8_t *bytes, size_t size) {
614 DWORD old_protect;
615 if (!VirtualProtect((void *)addr, size, PAGE_EXECUTE_READWRITE, &old_protect)) {
616 DTTR_LOG_ERROR("%s: VirtualProtect failed for 0x%08X", op, (unsigned)addr);
617 return false;
618 }
619
620 memcpy((void *)addr, bytes, size);
621 VirtualProtect((void *)addr, size, old_protect, &old_protect);
622 flush_patched_range(op, addr, size);
623 return true;
624}
625
627 uintptr_t site,
628 void *target,
629 uint8_t out[DTTR_HOOK_PATCH_SIZE]
630) {
631 out[0] = 0xE9;
632 const int64_t rel64 = (int64_t)(uintptr_t)target
633 - (int64_t)(site + DTTR_HOOK_PATCH_SIZE);
634 if (rel64 < INT32_MIN || rel64 > INT32_MAX) {
635 return false;
636 }
637
638 const int32_t rel = (int32_t)rel64;
639 memcpy(out + 1, &rel, sizeof(rel));
640 return true;
641}
642
643static bool write_function_jump(uintptr_t site, void *target) {
644 uint8_t jmp[DTTR_HOOK_PATCH_SIZE];
645 if (!build_rel32_jump(site, target, jmp)) {
647 "hook_attach_function: handler jump out of range at 0x%08X -> 0x%08X",
648 (unsigned)site,
649 (unsigned)(uintptr_t)target
650 );
651 return false;
652 }
653
654 return write_bytes("hook_attach_function", site, jmp, sizeof(jmp));
655}
656
657static bool hook_thunk_set_target(uint8_t *thunk, void *target) {
658 if (!thunk || !target) {
659 return false;
660 }
661
662 thunk[0] = 0xFF;
663 thunk[1] = 0x25;
664 const uint32_t slot = (uint32_t)(uintptr_t)(thunk + 6);
665 const uint32_t target32 = (uint32_t)(uintptr_t)target;
666 memcpy(thunk + 2, &slot, sizeof(slot));
667 memcpy(thunk + 6, &target32, sizeof(target32));
668 flush_patched_range("hook_chain_thunk", (uintptr_t)thunk, 10u);
669 return true;
670}
671
672static void *function_link_next_target(const DTTR_Core_Hook *hook) {
673 if (hook->next) {
674 return hook->next->detour;
675 }
676
677 return hook->chain ? hook->chain->trampoline : NULL;
678}
679
681 hook_chain *chain,
682 void *detour,
683 void *next_target
684) {
685 DTTR_Core_Hook *hook = (DTTR_Core_Hook *)calloc(1, sizeof(DTTR_Core_Hook));
686 if (!hook) {
688 "hook_attach_function: hook link alloc failed for 0x%08X",
689 (unsigned)(chain ? chain->addr : 0u)
690 );
691 return NULL;
692 }
693
694 hook->next_thunk = (uint8_t *)
695 VirtualAlloc(NULL, 10u, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
696 if (!hook->next_thunk) {
698 "hook_attach_function: hook link thunk alloc failed for 0x%08X",
699 (unsigned)(chain ? chain->addr : 0u)
700 );
701 free(hook);
702 return NULL;
703 }
704
706 hook->addr = chain->addr;
707 hook->size = chain->patch_size;
708 hook->owner = hook_owner;
709 hook->chain = chain;
710 hook->detour = detour;
711 if (!hook_thunk_set_target(hook->next_thunk, next_target)) {
712 hook_destroy(hook);
713 return NULL;
714 }
715
716 return hook;
717}
718
720 hook_chain *chain,
721 DTTR_Core_Hook *hook,
722 void **out_original
723) {
724 if (chain->head) {
725 hook->next = chain->head;
726 chain->head->prev = hook;
727 } else {
728 chain->tail = hook;
729 }
730
731 chain->head = hook;
732
733 if (!write_function_jump(chain->addr, hook->detour)) {
734 chain->head = hook->next;
735 if (chain->head) {
736 chain->head->prev = NULL;
737 } else {
738 chain->tail = NULL;
739 }
740
741 hook->next = NULL;
742 return false;
743 }
744
745 if (out_original) {
746 *out_original = hook->next_thunk;
747 }
748
749 return true;
750}
751
753 hook_chain *chain,
754 void *handler,
755 void **out_original
756) {
758 chain,
759 handler,
760 chain->head ? chain->head->detour : chain->trampoline
761 );
762 if (!hook) {
763 return NULL;
764 }
765
766 if (!function_chain_push_head(chain, hook, out_original)) {
767 hook_destroy(hook);
768 return NULL;
769 }
770
771 kv_push(DTTR_Core_Hook *, hooks, hook);
772 return hook;
773}
774
776 hook_chain *chain = hook->chain;
777 if (!chain) {
778 return false;
779 }
780
781 const bool removing_head = chain->head == hook;
782 const bool removing_last = !hook->prev && !hook->next;
783
784 if (removing_last) {
785 if (!write_bytes("hook_detach", chain->addr, chain->original, chain->patch_size)) {
787 "hook_detach: leaving function hook registered because restore failed "
788 "at "
789 "0x%08X",
790 (unsigned)chain->addr
791 );
792 return false;
793 }
794 } else if (removing_head) {
795 if (!write_function_jump(chain->addr, hook->next->detour)) {
797 "hook_detach: leaving function hook registered because relink failed "
798 "at "
799 "0x%08X",
800 (unsigned)chain->addr
801 );
802 return false;
803 }
804 }
805
806 DTTR_Core_Hook *prev = hook->prev;
807 DTTR_Core_Hook *next = hook->next;
808 if (prev) {
809 prev->next = next;
811 } else {
812 chain->head = next;
813 }
814
815 if (next) {
816 next->prev = prev;
817 } else {
818 chain->tail = prev;
819 }
820
821 hook->prev = NULL;
822 hook->next = NULL;
823 hook->chain = NULL;
824
825 if (!chain->head) {
826 hook_chain_destroy(chain);
827 }
828
829 return true;
830}
831
832// Detach one registered hook by index while keeping the registry dense.
833static bool hook_detach_index(size_t index) {
834 DTTR_Core_Hook *hook = kv_A(hooks, index);
835 if (hook->kind == DTTR_HOOK_RECORD_FUNCTION) {
836 if (!function_link_detach(hook)) {
837 return false;
838 }
839
840 kv_A(hooks, index) = kv_A(hooks, kv_size(hooks) - 1);
841 kv_pop(hooks);
842 hook_destroy(hook);
843 return true;
844 }
845
846 if (!write_bytes("hook_detach", hook->addr, hook->original, hook->size)) {
848 "hook_detach: leaving hook registered because restore failed at 0x%08X",
849 (unsigned)hook->addr
850 );
851 return false;
852 }
853
854 kv_A(hooks, index) = kv_A(hooks, kv_size(hooks) - 1);
855 kv_pop(hooks);
856 hook_destroy(hook);
857 return true;
858}
859
860// Install a JMP detour and build a trampoline for the overwritten prologue.
862 uintptr_t addr,
863 int prologue_size,
864 void *handler,
865 void **out_original
866) {
868 if (!addr || !handler) {
870 "hook_attach_function: invalid parameters site=0x%08X handler=0x%08X",
871 (unsigned)addr,
872 (unsigned)(uintptr_t)handler
873 );
874 return NULL;
875 }
876
877 hook_chain *existing_chain = hook_find_function_chain(addr);
878 if (existing_chain) {
879 if (prologue_size > 0 && (size_t)prologue_size > existing_chain->prologue_size) {
881 "hook chain unsupported: site=0x%08X requested prologue %d exceeds "
882 "active prologue %u",
883 (unsigned)addr,
884 prologue_size,
885 (unsigned)existing_chain->prologue_size
886 );
889 "function hook chain does not support a larger prologue"
890 );
891 return NULL;
892 }
893
894 return function_chain_append(existing_chain, handler, out_original);
895 }
896
900 "function hook chain is unsupported for an overlapping hook range"
901 );
902 return NULL;
903 }
904
906 uint8_t prologue_bytes[DTTR_HOOK_MAX_PROLOGUE] = {0};
907 size_t insn_count = 0;
908 size_t actual_prologue_size = 0;
909 if (!decode_prologue(
910 addr,
911 prologue_size,
912 insns,
913 &insn_count,
914 &actual_prologue_size,
915 prologue_bytes,
916 sizeof(prologue_bytes)
917 )) {
918 return NULL;
919 }
920
922 "hook_attach_function: site=0x%08X requested=%d aligned=%u insn_count=%u",
923 (unsigned)addr,
924 prologue_size,
925 (unsigned)actual_prologue_size,
926 (unsigned)insn_count
927 );
928
929 uint8_t *trampoline = (uint8_t *)VirtualAlloc(
930 NULL,
931 actual_prologue_size + DTTR_HOOK_PATCH_SIZE,
932 MEM_COMMIT | MEM_RESERVE,
933 PAGE_EXECUTE_READWRITE
934 );
935
936 if (!trampoline) {
938 "hook_attach_function: trampoline alloc failed for 0x%08X",
939 (unsigned)addr
940 );
941 return NULL;
942 }
943
944 memcpy(trampoline, prologue_bytes, actual_prologue_size);
945 if (!trampoline_relocate(trampoline, addr, insns, insn_count)) {
946 VirtualFree(trampoline, 0, MEM_RELEASE);
947 return NULL;
948 }
949
950 trampoline[actual_prologue_size] = 0xE9;
951 const int64_t jmp_back64 = (int64_t)(addr + actual_prologue_size)
952 - (int64_t)((uintptr_t)trampoline + actual_prologue_size
954 if (jmp_back64 < INT32_MIN || jmp_back64 > INT32_MAX) {
956 "hook_attach_function: trampoline jmp-back out of range at 0x%08X",
957 (unsigned)addr
958 );
959 VirtualFree(trampoline, 0, MEM_RELEASE);
960 return NULL;
961 }
962
963 const int32_t jmp_back = (int32_t)jmp_back64;
964 memcpy(trampoline + actual_prologue_size + 1, &jmp_back, 4);
965
966 hook_chain *chain = (hook_chain *)calloc(1, sizeof(hook_chain));
967 if (!chain) {
969 "hook_attach_function: chain alloc failed for 0x%08X",
970 (unsigned)addr
971 );
972 VirtualFree(trampoline, 0, MEM_RELEASE);
973 return NULL;
974 }
975
976 chain->original = (uint8_t *)malloc(DTTR_HOOK_PATCH_SIZE);
977 if (!chain->original) {
979 "hook_attach_function: original-bytes alloc failed for 0x%08X",
980 (unsigned)addr
981 );
982 VirtualFree(trampoline, 0, MEM_RELEASE);
983 free(chain);
984 return NULL;
985 }
986
987 memcpy(chain->original, prologue_bytes, DTTR_HOOK_PATCH_SIZE);
988 chain->addr = addr;
990 chain->prologue_size = actual_prologue_size;
991 chain->trampoline = trampoline;
992
993 DTTR_Core_Hook *hook = function_link_create(chain, handler, trampoline);
994 if (!hook) {
995 hook_chain_destroy(chain);
996 return NULL;
997 }
998
999 if (!function_chain_push_head(chain, hook, out_original)) {
1000 hook_destroy(hook);
1001 hook_chain_destroy(chain);
1002 return NULL;
1003 }
1004
1005 kv_push(DTTR_Core_Hook *, hooks, hook);
1006 return hook;
1007}
1008
1009// Patch a pointer slot and return the previous slot value when requested.
1011 uintptr_t addr,
1012 void *new_value,
1013 void **out_original
1014) {
1015 if (!addr) {
1016 DTTR_LOG_ERROR("hook_attach_pointer: target address is NULL");
1017 return NULL;
1018 }
1019
1020 if (out_original) {
1021 *out_original = *(void **)addr;
1022 }
1023
1024 const uintptr_t value = (uintptr_t)new_value;
1025 return DTTR_Core_HookPatchBytes(addr, (const uint8_t *)&value, sizeof(value));
1026}
1027
1028// Patch arbitrary bytes while retaining originals for later detach.
1030 uintptr_t addr,
1031 const uint8_t *bytes,
1032 size_t size
1033) {
1034 if (!addr || !bytes || !size) {
1036 "hook_patch_bytes: invalid parameters addr=0x%08X bytes=0x%08X size=%zu",
1037 (unsigned)addr,
1038 (unsigned)(uintptr_t)bytes,
1039 size
1040 );
1041 return NULL;
1042 }
1043
1044 if (!check_overlap(addr, size)) {
1045 return NULL;
1046 }
1047
1048 DTTR_Core_Hook *hook = hook_create("hook_patch_bytes", addr, size);
1049 if (!hook) {
1050 return NULL;
1051 }
1052
1053 memcpy(hook->original, (const void *)addr, size);
1054 if (!write_bytes("hook_patch_bytes", addr, bytes, size)) {
1055 hook_destroy(hook);
1056 return NULL;
1057 }
1058
1059 kv_push(DTTR_Core_Hook *, hooks, hook);
1060 return hook;
1061}
1062
1064 if (!hook) {
1065 return true;
1066 }
1067
1068 const size_t index = hook_find_index(hook);
1069 if (index == kv_size(hooks)) {
1070 DTTR_LOG_DEBUG("hook_detach: ignoring stale or already detached hook handle");
1071 return true;
1072 }
1073
1074 return hook_detach_index(index);
1075}
1076
1077// Detach one registered hook or patch if the handle is still active.
1081
1083 return hook && hook_find_index(hook) != kv_size(hooks);
1084}
1085
1087 if (!owner) {
1088 return true;
1089 }
1090
1091 bool ok = true;
1092 for (size_t i = kv_size(hooks); i > 0; i--) {
1093 if (kv_A(hooks, i - 1)->owner != owner) {
1094 continue;
1095 }
1096
1097 ok = hook_detach_index(i - 1) && ok;
1098 }
1099
1100 return ok;
1101}
1102
1103// Set the owner tag applied to subsequent hooks so SDK contexts can clean up their work.
1104void *DTTR_Core_HookSetOwner(void *owner) {
1105 void *previous = hook_owner;
1106 hook_owner = owner;
1107 return previous;
1108}
1109
1110// Detach every hook tagged with an owner during context teardown.
1114
1116 if (cache) {
1117 kh_destroy(sigscan_cache, cache);
1118 cache = NULL;
1119 }
1120}
1121
1123 bool ok = true;
1124 while (kv_size(hooks) > 0) {
1125 const size_t previous_count = kv_size(hooks);
1126 if (!hook_detach_index(previous_count - 1)) {
1127 ok = false;
1128 break;
1129 }
1130 }
1131
1132 if (kv_size(hooks) == 0) {
1133 kv_destroy(hooks);
1134 kv_init(hooks);
1135 hook_owner = NULL;
1136 }
1137
1139 return ok;
1140}
1141
1145 "hook_cleanup_all: stopped after restore failure; hooks remain registered"
1146 );
1147 }
1148}
DTTR_Graphics_COM_Direct3DDevice7 void DWORD flags DWORD count
const DTTR_BackendState * state
DTTR_Graphics_COM_DirectDraw7 *self DWORD DWORD h
void DWORD DWORD * free
DTTR_Graphics_COM_DirectDraw7 *self DWORD w
const DWORD size
DTTR_Graphics_COM_DirectDrawSurface7 DWORD flags void NULL
DTTR_Graphics_COM_DirectDrawSurface7 DWORD DWORD void void DWORD flags DTTR_Graphics_COM_DirectDrawSurface7 void void *cb void * target
static DTTR_Result dttr_core_result(DTTR_Status status, const char *message)
#define DTTR_LOG_DEBUG(...)
Definition dttr_log.h:28
#define DTTR_LOG_WARN(...)
Definition dttr_log.h:30
#define DTTR_LOG_ERROR(...)
Definition dttr_log.h:31
DTTR_Status
Definition dttr_result.h:13
@ DTTR_ERR_HOOK_CHAIN_UNSUPPORTED
Definition dttr_result.h:24
@ DTTR_OK
Definition dttr_result.h:14
static const uint8_t * DTTR_Sigscan_Bytes(const uint8_t *base, size_t size, const void *sig, const char *mask)
Definition dttr_sigscan.h:9
static khash_t(dttr_config_lookup)
Definition schema.c:77
static bool write_bytes(const char *op, uintptr_t addr, const uint8_t *bytes, size_t size)
#define DTTR_HOOK_MAX_PROLOGUE
void dttr_core_hook_set_last_error(DTTR_Status status, const char *message)
static DTTR_Result hook_last_error
static void log_prologue_bytes(uintptr_t site, const uint8_t *bytes, size_t size)
static bool decoder_initialized
static void * function_link_next_target(const DTTR_Core_Hook *hook)
DTTR_Core_Hook * DTTR_Core_HookAttachPointer(uintptr_t addr, void *new_value, void **out_original)
hook_record_kind
@ DTTR_HOOK_RECORD_PATCH
@ DTTR_HOOK_RECORD_FUNCTION
static size_t hook_find_index(DTTR_Core_Hook *hook)
static bool trampoline_relocate(uint8_t *trampoline, uintptr_t site, const trampoline_insn *insns, size_t insn_count)
static bool write_function_jump(uintptr_t site, void *target)
static bool function_link_detach(DTTR_Core_Hook *hook)
bool DTTR_Core_HookIsActive(DTTR_Core_Hook *hook)
static bool decode_prologue(uintptr_t addr, int requested_size, trampoline_insn *insns, size_t *out_insn_count, size_t *out_prologue_size, uint8_t *out_prologue_bytes, size_t out_prologue_bytes_cap)
static DTTR_Core_Hook * function_link_create(hook_chain *chain, void *detour, void *next_target)
static bool check_overlap(uintptr_t addr, size_t size)
bool DTTR_Core_HookDetachOwnerChecked(void *owner)
static bool is_readable_page_protect(DWORD protect)
DTTR_Core_Hook * DTTR_Core_HookAttachFunction(uintptr_t addr, int prologue_size, void *handler, void **out_original)
static hook_chain * hook_find_function_chain(uintptr_t addr)
DTTR_Core_Hook * DTTR_Core_HookPatchBytes(uintptr_t addr, const uint8_t *bytes, size_t size)
#define DTTR_HOOK_MAX_INSN
#define DTTR_HOOK_PATCH_SIZE
static bool function_chain_push_head(hook_chain *chain, DTTR_Core_Hook *hook, void **out_original)
bool DTTR_Core_HookCleanupAllChecked()
void DTTR_Core_HookCleanupAll()
Detach all remaining hooks and free the sigscan cache.
static void cleanup_sigscan_cache()
typedef kvec_t(DTTR_Core_Hook *)
static bool hook_detach_index(size_t index)
static void flush_patched_range(const char *op, uintptr_t addr, size_t size)
uintptr_t DTTR_Core_HookCachedSigscan(HMODULE mod, const char *sig, const char *mask)
void DTTR_Core_HookDetach(DTTR_Core_Hook *hook)
uintptr_t DTTR_Core_HookSigscan(HMODULE mod, const char *sig, const char *mask)
static DTTR_Core_Hook * hook_create(const char *op, uintptr_t addr, size_t size)
static bool copy_memory_checked(uintptr_t addr, uint8_t *out, size_t size)
#define DTTR_HOOK_MIN_PROLOGUE
static DTTR_Core_Hook * function_chain_append(hook_chain *chain, void *handler, void **out_original)
static bool hook_thunk_set_target(uint8_t *thunk, void *target)
static void hook_chain_destroy(hook_chain *chain)
static void hook_error_clear()
static bool decoder_init()
static ZydisDecoder decoder
static bool build_rel32_jump(uintptr_t site, void *target, uint8_t out[DTTR_HOOK_PATCH_SIZE])
DTTR_Result dttr_core_hook_last_error()
bool DTTR_Core_HookDetachChecked(DTTR_Core_Hook *hook)
void DTTR_Core_HookDetachOwner(void *owner)
void * DTTR_Core_HookSetOwner(void *owner)
DTTR_Core_Hook * next
DTTR_Core_Hook * prev
uint8_t * next_thunk
hook_chain * chain
uint8_t * original
uint8_t * trampoline
hook_record_kind kind
size_t prologue_size
uintptr_t addr
uint8_t * original
uint8_t * trampoline
DTTR_Core_Hook * head
size_t patch_size
DTTR_Core_Hook * tail