Skip to main content

dyncvoke_core/
peb.rs

1//! In-process PEB / LDR walk. Avoids EnumProcessModules / GetModuleBaseNameW.
2
3use core::ffi::c_void;
4
5/// LIST_ENTRY — Windows doubly-linked list head.
6#[repr(C)]
7#[derive(Clone, Copy)]
8pub struct ListEntry {
9    pub flink: *mut ListEntry,
10    pub blink: *mut ListEntry,
11}
12
13/// UNICODE_STRING — counted UTF-16 string. Matches ntdef layout.
14#[repr(C)]
15#[derive(Clone, Copy)]
16pub struct UnicodeString {
17    pub length: u16,
18    pub maximum_length: u16,
19    pub buffer: *mut u16,
20}
21
22/// LDR_DATA_TABLE_ENTRY prefix used by the load-order walk.
23///
24/// x64 offsets (Geoff Chappell, LDR_DATA_TABLE_ENTRY):
25///   0x00 InLoadOrderLinks, 0x10 InMemoryOrderLinks,
26///   0x20 InInitializationOrderLinks, 0x30 DllBase,
27///   0x38 EntryPoint, 0x40 SizeOfImage,
28///   0x48 FullDllName, 0x58 BaseDllName.
29#[repr(C)]
30pub struct LdrDataTableEntry {
31    pub in_load_order_links: ListEntry,
32    pub in_memory_order_links: ListEntry,
33    pub in_initialization_order_links: ListEntry,
34    pub dll_base: *mut c_void,
35    pub entry_point: *mut c_void,
36    pub size_of_image: u32,
37    pub full_dll_name: UnicodeString,
38    pub base_dll_name: UnicodeString,
39}
40
41/// PEB_LDR_DATA prefix.
42///
43/// phnt / NtDoc: ULONG Length, BOOLEAN Initialized, HANDLE SsHandle,
44/// LIST_ENTRY InLoadOrderModuleList. On x64 that list head is at 0x10.
45#[repr(C)]
46pub struct PebLdrData {
47    pub _length_and_init: [u8; 8],
48    pub _ss_handle: *mut c_void,
49    pub in_load_order_module_list: ListEntry,
50}
51
52/// PEB prefix through Ldr.
53///
54/// winternl.h: `BYTE Reserved1[2]`, `BYTE BeingDebugged`, `BYTE Reserved2[1]`,
55/// `PVOID Reserved3[2]`, `PPEB_LDR_DATA Ldr`.
56/// `repr(C)` supplies the 4-byte x64 alignment pad after Reserved2, so Ldr
57/// lands at 0x18 on x64 and 0x0C on x86.
58/// Source: <https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb>
59#[repr(C)]
60pub struct Peb {
61    pub _reserved1: [u8; 2],
62    pub being_debugged: u8,
63    pub _reserved2: [u8; 1],
64    pub _reserved3: [*mut c_void; 2],
65    pub ldr: *mut PebLdrData,
66}
67
68/// Current process PEB. `gs:[0x60]` on x64, `fs:[0x30]` on x86 (`TEB.ProcessEnvironmentBlock`).
69#[inline(always)]
70pub fn current_peb() -> *mut Peb {
71    let peb: *mut Peb;
72    #[cfg(target_arch = "x86_64")]
73    unsafe {
74        core::arch::asm!(
75            "mov {peb}, gs:[0x60]",
76            peb = out(reg) peb,
77            options(nostack, preserves_flags, readonly)
78        );
79    }
80    #[cfg(target_arch = "x86")]
81    unsafe {
82        core::arch::asm!(
83            "mov {peb}, fs:[0x30]",
84            peb = out(reg) peb,
85            options(nostack, preserves_flags, readonly)
86        );
87    }
88    #[cfg(not(any(target_arch = "x86_64", target_arch = "x86")))]
89    compile_error!("dyncvoke PEB walker only supports x86 / x86_64");
90    peb
91}
92
93/// djb2 over ascii-lowercased input. Const so call sites can embed a u32 hash.
94pub const fn hash_name(bytes: &[u8]) -> u32 {
95    let mut hash: u32 = 5381;
96    let mut i = 0;
97    while i < bytes.len() {
98        let mut c = bytes[i];
99        if c >= b'A' && c <= b'Z' {
100            c += 32;
101        }
102        hash = hash.wrapping_mul(33).wrapping_add(c as u32);
103        i += 1;
104    }
105    hash
106}
107
108unsafe fn unicode_eq_ascii_ci(us: &UnicodeString, target: &[u8]) -> bool {
109    if us.buffer.is_null() {
110        return target.is_empty() && us.length == 0;
111    }
112    let len = (us.length / 2) as usize;
113    if len != target.len() {
114        return false;
115    }
116    for i in 0..len {
117        let wide = *us.buffer.add(i);
118        if wide >= 0x80 {
119            return false;
120        }
121        let mut byte = wide as u8;
122        if byte >= b'A' && byte <= b'Z' {
123            byte += 32;
124        }
125        let mut tgt = target[i];
126        if tgt >= b'A' && tgt <= b'Z' {
127            tgt += 32;
128        }
129        if byte != tgt {
130            return false;
131        }
132    }
133    true
134}
135
136unsafe fn hash_unicode_string(us: &UnicodeString) -> u32 {
137    if us.buffer.is_null() {
138        return 0;
139    }
140    let len = (us.length / 2) as usize;
141    let mut hash: u32 = 5381;
142    for i in 0..len {
143        let wide = *us.buffer.add(i);
144        if wide >= 0x80 {
145            return 0;
146        }
147        let mut byte = wide as u8;
148        if byte >= b'A' && byte <= b'Z' {
149            byte += 32;
150        }
151        hash = hash.wrapping_mul(33).wrapping_add(byte as u32);
152    }
153    hash
154}
155
156unsafe fn walk_modules<F>(mut visit: F) -> usize
157where
158    F: FnMut(&LdrDataTableEntry) -> bool,
159{
160    let peb = current_peb();
161    if peb.is_null() {
162        return 0;
163    }
164    let ldr = (*peb).ldr;
165    if ldr.is_null() {
166        return 0;
167    }
168    let head = &(*ldr).in_load_order_module_list as *const ListEntry as *mut ListEntry;
169    let mut cursor = (*head).flink;
170    while !cursor.is_null() && cursor != head {
171        let entry = cursor as *mut LdrDataTableEntry;
172        if visit(&*entry) {
173            return (*entry).dll_base as usize;
174        }
175        cursor = (*cursor).flink;
176    }
177    0
178}
179
180/// Look up a loaded module by base name (e.g. `"ntdll.dll"`). ASCII, case-insensitive.
181pub fn get_module_by_name(name: &str) -> usize {
182    unsafe { walk_modules(|entry| unicode_eq_ascii_ci(&entry.base_dll_name, name.as_bytes())) }
183}
184
185/// Look up a loaded module by djb2 of its lowercased ascii base name.
186pub fn get_module_by_hash(hash: u32) -> usize {
187    unsafe { walk_modules(|entry| hash_unicode_string(&entry.base_dll_name) == hash) }
188}