Detects thread start on sensitive APIs

This commit is contained in:
flux
2025-08-10 16:07:49 +01:00
parent 6a27dada5f
commit 020d64c6f9
8 changed files with 437 additions and 22 deletions
+1
View File
@@ -4,6 +4,7 @@
"cSpell.words": [
"Addreg",
"addrs",
"alertable",
"AMSI",
"APIC",
+3 -1
View File
@@ -29,6 +29,7 @@ const SSN_COUNT: usize = 0x500;
pub const SSN_NT_OPEN_PROCESS: u32 = 0x26;
pub const SSN_NT_ALLOCATE_VIRTUAL_MEMORY: u32 = 0x18;
pub const SSN_NT_CREATE_THREAD_EX: u32 = 0x00c9;
pub const SSN_NT_WRITE_VM: u32 = 0x003a;
const NT_OPEN_FILE: u32 = 0x0033;
@@ -452,7 +453,8 @@ pub unsafe extern "system" fn syscall_handler(
match ssn {
SSN_NT_OPEN_PROCESS
| SSN_NT_ALLOCATE_VIRTUAL_MEMORY
| SSN_NT_WRITE_VM => KernelSyscallIntercept::from_alt_syscall(ktrap_frame),
| SSN_NT_WRITE_VM
| SSN_NT_CREATE_THREAD_EX => KernelSyscallIntercept::from_alt_syscall(ktrap_frame),
// 0x4e => {
// println!(
// "[create thread] [i] Hook. SSN {:#x}, rcx as usize: {}. Stack ptr: {:p}",
+17 -4
View File
@@ -27,8 +27,7 @@ use wdk_sys::{
};
use crate::{
DRIVER_MESSAGES, REGISTRATION_HANDLE, core::process_monitor::ProcessMonitor,
device_comms::ImageLoadQueueForInjector, utils::unicode_to_string,
core::process_monitor::{LoadedModule, ProcessMonitor}, device_comms::ImageLoadQueueForInjector, utils::unicode_to_string, DRIVER_MESSAGES, REGISTRATION_HANDLE
};
/// Callback function for a new process being created on the system.
@@ -287,9 +286,23 @@ extern "C" fn image_load_callback(
let name_slice = slice_from_raw_parts(image_name.Buffer, (image_name.Length / 2) as usize);
let name = String::from_utf16_lossy(unsafe { &*name_slice }).to_lowercase();
// For now only concern ourselves with image loads where its an exe, except in the event its the sanctum EDR DLL -
// see below comments for why.
// In the event it is a DLL load, we want to grab & track its mappings
if name.contains(".dll") && !name.contains("sanctum.dll") {
// todo hash check on the sanctum DLL to make sure an adversary isn't calling their malicious DLL `sanctum.dll`
// which would interfere with what we are doing in this segment.
// todo is it re-loading NTDLL when NTDLL already exists in the process? Bad, we want to stop this and report
// on it.
let lm = LoadedModule::new(
image_info.ImageBase as _,
image_info.ImageSize as _,
None,
None
);
ProcessMonitor::add_loaded_module(lm, &name, pid as u32);
return;
}
+177 -7
View File
@@ -14,7 +14,7 @@
//! Ghost Hunting telemetry
use core::{
ffi::c_void, mem::replace, ptr::null_mut, time::Duration
arch::asm, ffi::c_void, mem::replace, ptr::null_mut, time::Duration
};
use alloc::{
@@ -34,14 +34,13 @@ use wdk_mutex::{
};
use wdk_sys::{
ntddk::{
IoGetCurrentProcess, KeDelayExecutionThread, KeQuerySystemTimePrecise,
ObOpenObjectByPointer, ObReferenceObjectByHandle, PsCreateSystemThread, PsGetProcessId,
}, PsProcessType, HANDLE, LARGE_INTEGER, LIST_ENTRY, PROCESS_ALL_ACCESS, PROCESS_BASIC_INFORMATION, STATUS_SUCCESS, THREAD_ALL_ACCESS, TRUE, _EPROCESS, _LARGE_INTEGER, _MODE::KernelMode
IoGetCurrentProcess, KeDelayExecutionThread, KeQuerySystemTimePrecise, MmIsAddressValid, ObOpenObjectByPointer, ObReferenceObjectByHandle, PsCreateSystemThread, PsGetCurrentThreadTeb, PsGetProcessId
}, PsProcessType, FALSE, HANDLE, LARGE_INTEGER, LIST_ENTRY, PROCESS_ALL_ACCESS, PROCESS_BASIC_INFORMATION, STATUS_SUCCESS, THREAD_ALL_ACCESS, TRUE, _EPROCESS, _LARGE_INTEGER, _MODE::KernelMode, _PEB
};
use crate::{
ffi::NtQueryInformationProcess,
utils::{DriverError, eprocess_to_process_name},
ffi::{NtQueryInformationProcess, IMAGE_DOS_HEADER, IMAGE_EXPORT_DIRECTORY, IMAGE_NT_HEADERS64},
utils::{eprocess_to_process_name, scan_usermode_module_for_function_address, DriverError},
};
/// A `Process` is a Sanctum driver representation of a Windows process so that actions it preforms, and is performed
@@ -60,11 +59,70 @@ pub struct Process {
pub ghost_hunting_timers: Vec<GhostHuntingTimer>,
targeted_by_apis: Vec<ProcessTargetedApis>,
marked_for_deletion: bool,
// Note: It is possible atm for any processes started before the EDR was switched on that
// we don't readily have this data. If the driver is loaded as ELAM then this wouldn't be such
// a problem.
// todo fix
loaded_modules: Option<LoadedModules>,
}
/// A BTreeMap of loaded modules in the process, with:
///
/// - `key`: Module name
/// `value`: [`LoadedModule`]
#[derive(Debug)]
pub struct LoadedModules {
inner: BTreeMap<String, LoadedModule>,
}
#[derive(Debug)]
pub enum SensitiveAPI {
LdrLoadDLL,
LoadLibraryA,
LoadLibraryW,
}
/// A representation of a module loaded into a process.
#[derive(Debug)]
pub struct LoadedModule {
image_base: *const c_void,
image_sz: usize,
ntdll_addresses: Option<NtdllAddresses>,
kernel32_addresses: Option<Kernel32Addresses>,
}
impl LoadedModule {
pub fn new(
image_base: *const c_void,
image_sz: usize,
ntdll_addresses: Option<NtdllAddresses>,
kernel32_addresses: Option<Kernel32Addresses>,
) -> Self {
Self {
image_base,
image_sz,
ntdll_addresses,
kernel32_addresses,
}
}
}
/// Addresses within NTDLL that we care about detecting access to.
#[derive(Debug)]
pub struct NtdllAddresses {
pub LdrLoadDLL: *const c_void,
}
/// Addresses within NTDLL that we care about detecting access to.
#[derive(Debug)]
pub struct Kernel32Addresses {
pub LoadLibraryW: *const c_void,
pub LoadLibraryA: *const c_void,
}
// todo needs implementing
#[derive(Debug, Default)]
pub struct ProcessTargetedApis {}
pub struct ProcessTargetedApis;
/// A `GhostHuntingTimer` is the timer metadata associated with the Ghost Hunting technique on my blog:
/// https://fluxsec.red/edr-syscall-hooking
@@ -148,11 +206,78 @@ impl ProcessMonitor {
ghost_hunting_timers: Vec::new(),
targeted_by_apis: Vec::new(),
marked_for_deletion: false,
// Setting this to None should be ok now as the module data should come in once the modules
// load into the process.
loaded_modules: None,
});
Ok(())
}
pub fn add_loaded_module(
mut lm: LoadedModule,
image_name: &String,
pid: u32,
) {
// Lookup any relevant addresses in NTDLL and Kernel32.dll that we want to look for at runtime
// with the EDR whilst spending time on a kernel thread. If we make this lookup once per image, it
// should be in the long run, less expensive.
get_monitored_dll_address_fn_addrs(&mut lm, image_name);
let mut process_lock = ProcessMonitor::get_mtx_inner();
let process = match process_lock.get_mut(&pid) {
Some(process) => process,
None => {
println!("[sanctum] [-] PID {pid} not found in active processes when trying to add image load info.");
return;
},
};
if let Some(process_loaded_mods) = process.loaded_modules.as_mut() {
let _ = process_loaded_mods.inner.insert(image_name.clone(), lm);
} else {
let mut b = BTreeMap::new();
b.insert(image_name.clone(), lm);
process.loaded_modules = Some(LoadedModules { inner: b });
}
}
pub fn fn_pointer_to_sensitive_address(pid: u32, requested_addr: *const c_void) -> Option<SensitiveAPI> {
let process_lock = ProcessMonitor::get_mtx_inner();
if let Some(process ) = process_lock.get(&pid) {
match &process.loaded_modules {
Some(lm) => {
for (_, module_info) in &lm.inner {
if let Some(info) = &module_info.kernel32_addresses {
if info.LoadLibraryA == requested_addr {
return Some(SensitiveAPI::LoadLibraryA);
}
if info.LoadLibraryW == requested_addr {
return Some(SensitiveAPI::LoadLibraryW);
}
} else if let Some(info) = &module_info.ntdll_addresses {
if info.LdrLoadDLL == requested_addr {
return Some(SensitiveAPI::LdrLoadDLL);
}
}
}
},
None => {
println!("[sanctum] [-] process.loaded_modules was none.");
println!("{process:#?}");
return None
},
};
}
println!("[sanctum] [-] Could not lock process monitor for fn_pointer_to_sensitive_address");
None
}
// todo need to remove processes from the monitor once they are terminated
pub fn remove_process(pid: u32) {
let mut process_lock = ProcessMonitor::get_mtx_inner();
@@ -567,5 +692,50 @@ fn extract_process_details<'a>(process: *mut _EPROCESS, pid: usize) -> Result<Pr
ghost_hunting_timers: Vec::new(),
targeted_by_apis: Vec::new(),
marked_for_deletion: false,
// todo - do we need to grab these?
loaded_modules: None,
})
}
/// Lookup the addresses of functions we wish to monitor abuse against, populating the [`LoadedModule`]
/// struct mutably.
///
/// `image_name` is the name of the image being loaded.
fn get_monitored_dll_address_fn_addrs(lm: &mut LoadedModule, image_name: &String) {
if image_name.to_lowercase().contains(r"\windows\system32\ntdll.dll") {
let ldr_ld_dll = scan_usermode_module_for_function_address(
lm.image_base,
"LdrLoadDLL"
);
if ldr_ld_dll.is_ok() {
lm.ntdll_addresses = Some(
NtdllAddresses {
LdrLoadDLL: ldr_ld_dll.unwrap(),
}
)
}
}
if image_name.to_lowercase().contains(r"\windows\system32\kernel32.dll") {
let lla = scan_usermode_module_for_function_address(
lm.image_base,
"LoadLibraryA"
);
let llw = scan_usermode_module_for_function_address(
lm.image_base,
"LoadLibraryW"
);
if lla.is_ok() && llw.is_ok() {
lm.kernel32_addresses = Some(
Kernel32Addresses {
LoadLibraryW: llw.unwrap(),
LoadLibraryA: lla.unwrap(),
}
)
}
}
}
+66 -4
View File
@@ -2,14 +2,14 @@
use core::{
ffi::c_void,
mem::{self, MaybeUninit},
mem::{self},
ptr::null_mut,
sync::atomic::{AtomicBool, AtomicPtr, Ordering},
time::Duration,
};
use alloc::{collections::vec_deque::VecDeque, string::ToString, vec::Vec};
use shared_no_std::ghost_hunting::{NtAllocateVirtualMemoryData, NtFunction, NtOpenProcessData, NtWriteVirtualMemoryData, Syscall};
use alloc::{collections::vec_deque::VecDeque, string::ToString};
use shared_no_std::ghost_hunting::{NtAllocateVirtualMemoryData, NtCreateThreadExData, NtFunction, NtOpenProcessData, NtWriteVirtualMemoryData, Syscall};
use wdk::{nt_success, println};
use wdk_mutex::{
fast_mutex::FastMutexGuard,
@@ -21,7 +21,7 @@ use wdk_sys::{
}, CLIENT_ID, FALSE, HANDLE, KTRAP_FRAME, LARGE_INTEGER, STATUS_SUCCESS, THREAD_ALL_ACCESS, _KWAIT_REASON::Executive, _MODE::KernelMode
};
use crate::{alt_syscalls::{SSN_NT_ALLOCATE_VIRTUAL_MEMORY, SSN_NT_OPEN_PROCESS, SSN_NT_WRITE_VM}, core::process_monitor::ProcessMonitor, utils::{handle_to_pid, DriverError}};
use crate::{alt_syscalls::{SSN_NT_ALLOCATE_VIRTUAL_MEMORY, SSN_NT_CREATE_THREAD_EX, SSN_NT_OPEN_PROCESS, SSN_NT_WRITE_VM}, core::process_monitor::ProcessMonitor, utils::{handle_to_pid, DriverError}};
/// Returns a borrowed view over stack argument slots saved in a `_KTRAP_FRAME`.
/// The slice elements are interpreted as raw pointer-width values (`*const c_void`)
@@ -107,6 +107,7 @@ impl KernelSyscallIntercept {
SSN_NT_ALLOCATE_VIRTUAL_MEMORY => Self::nt_allocate_vm(ktrap_frame),
SSN_NT_OPEN_PROCESS => Self::nt_open_process(ktrap_frame),
SSN_NT_WRITE_VM => Self::nt_write_vm(ktrap_frame),
SSN_NT_CREATE_THREAD_EX => Self::nt_create_thread_ex(ktrap_frame),
_ => {
println!("[-] [sanctum] Unknown SSN received, {:?}", ktrap_frame.Rax as u32);
None
@@ -119,6 +120,67 @@ impl KernelSyscallIntercept {
}
}
fn nt_create_thread_ex(
ktrap_frame: KTRAP_FRAME,
) -> Option<Syscall> {
let current_pid = unsafe { PsGetCurrentProcessId() } as u32;
let dest_pid = handle_to_pid(ktrap_frame.R9 as *mut c_void);
// For now, we are not interested in self thread creates
if current_pid == dest_pid {
return None;
}
let stack_args = unsafe { ktrap_get_stack_args(&ktrap_frame, 2) };
// SAFETY: Reading two arguments, within limits from the call to ktrap_get_stack_args
let start_address = stack_args[0];
let argument = stack_args[1];
//
// With the NtCreateThread API, before permitting the syscall to continue, we want to do some sanitisation
// at this point. I accept this is perhaps not a realistic approach, and we would do this on the telemetry server
// as post-processing; BUT this is a POC and this EDR is designed for 'paranoid mode'. Perhaps in the future we can have
// different settings on the EDR: Paranoid, Casual Blocking, Report Only, etc.
//
// First - we need to check there are no outstanding Ghost Hunt's on the process from within the driver; this is to outright
// prevent Hell's Gate type syscall malware behaviour. There is no realistic valid reason for a process to be doing direct
// / indirect syscalls..
//
// Second, we need to determine what the thread is pointing to, is it to:
// - Classic library loading API's?
// - To a shellcode stub?
// - To a valid routine within the process image's .text section?
//
// We can access the loaded modules through the image callback in the driver
//
// Whilst the second listed checks above wont be slow in isolation, at scale, this could impact performance. An interesting debate
// for the future. For now, I want to do these checks at 'syscall-time'.
//
if let Some(resolved) = ProcessMonitor::fn_pointer_to_sensitive_address(
dest_pid,
start_address
) {
println!("**** WARNING: Sensitive API detected in start thread!! {resolved:?}");
}
println!("Start: {start_address:p}, arg: {argument:p}");
let data = Syscall::from_kernel(
current_pid,
NtFunction::NtCreateThreadEx(NtCreateThreadExData {
target_pid: dest_pid,
start_routine: start_address as usize,
argument: argument as usize,
}));
println!("Data from NtCreateThreadEx: {data:?}");
Some(data)
}
fn nt_write_vm(
ktrap_frame: KTRAP_FRAME,
) -> Option<Syscall> {
+98
View File
@@ -86,4 +86,102 @@ unsafe extern "system" {
flags: ULONG,
new_thread_handle: PHANDLE,
) -> NTSTATUS;
}
#[repr(C, packed(2))]
pub struct IMAGE_DOS_HEADER {
pub e_magic: u16,
pub e_cblp: u16,
pub e_cp: u16,
pub e_crlc: u16,
pub e_cparhdr: u16,
pub e_minalloc: u16,
pub e_maxalloc: u16,
pub e_ss: u16,
pub e_sp: u16,
pub e_csum: u16,
pub e_ip: u16,
pub e_cs: u16,
pub e_lfarlc: u16,
pub e_ovno: u16,
pub e_res: [u16; 4],
pub e_oemid: u16,
pub e_oeminfo: u16,
pub e_res2: [u16; 10],
pub e_lfanew: i32,
}
#[repr(C)]
pub struct IMAGE_FILE_HEADER {
pub Machine: u16,
pub NumberOfSections: u16,
pub TimeDateStamp: u32,
pub PointerToSymbolTable: u32,
pub NumberOfSymbols: u32,
pub SizeOfOptionalHeader: u16,
pub Characteristics: u16,
}
#[repr(C)]
pub struct IMAGE_DATA_DIRECTORY {
pub VirtualAddress: u32,
pub Size: u32,
}
#[repr(C, packed(4))]
pub struct IMAGE_OPTIONAL_HEADER64 {
pub Magic: u16,
pub MajorLinkerVersion: u8,
pub MinorLinkerVersion: u8,
pub SizeOfCode: u32,
pub SizeOfInitializedData: u32,
pub SizeOfUninitializedData: u32,
pub AddressOfEntryPoint: u32,
pub BaseOfCode: u32,
pub ImageBase: u64,
pub SectionAlignment: u32,
pub FileAlignment: u32,
pub MajorOperatingSystemVersion: u16,
pub MinorOperatingSystemVersion: u16,
pub MajorImageVersion: u16,
pub MinorImageVersion: u16,
pub MajorSubsystemVersion: u16,
pub MinorSubsystemVersion: u16,
pub Win32VersionValue: u32,
pub SizeOfImage: u32,
pub SizeOfHeaders: u32,
pub CheckSum: u32,
pub Subsystem: u16,
pub DllCharacteristics: u16,
pub SizeOfStackReserve: u64,
pub SizeOfStackCommit: u64,
pub SizeOfHeapReserve: u64,
pub SizeOfHeapCommit: u64,
pub LoaderFlags: u32,
pub NumberOfRvaAndSizes: u32,
pub DataDirectory: [IMAGE_DATA_DIRECTORY; 16],
}
#[repr(C)]
pub struct IMAGE_NT_HEADERS64 {
pub Signature: u32,
pub FileHeader: IMAGE_FILE_HEADER,
pub OptionalHeader: IMAGE_OPTIONAL_HEADER64,
}
#[repr(C)]
pub struct IMAGE_EXPORT_DIRECTORY {
pub Characteristics: u32,
pub TimeDateStamp: u32,
pub MajorVersion: u16,
pub MinorVersion: u16,
pub Name: u32,
pub Base: u32,
pub NumberOfFunctions: u32,
pub NumberOfNames: u32,
pub AddressOfFunctions: u32,
pub AddressOfNames: u32,
pub AddressOfNameOrdinals: u32,
}
+71 -3
View File
@@ -12,13 +12,12 @@ use shared_no_std::constants::SanctumVersion;
use wdk::println;
use wdk_sys::{
ntddk::{
IoThreadToProcess, KeGetCurrentIrql, ObReferenceObjectByHandle, ObfDereferenceObject, PsGetProcessId, RtlInitUnicodeString, RtlUnicodeStringToAnsiString, ZwClose, ZwCreateFile, ZwWriteFile
IoThreadToProcess, KeGetCurrentIrql, MmIsAddressValid, ObReferenceObjectByHandle, ObfDereferenceObject, PsGetProcessId, RtlInitUnicodeString, RtlUnicodeStringToAnsiString, ZwClose, ZwCreateFile, ZwWriteFile
}, PsProcessType, DRIVER_OBJECT, FALSE, FILE_APPEND_DATA, FILE_ATTRIBUTE_NORMAL, FILE_OPEN_IF, FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_SYNCHRONOUS_IO_NONALERT, GENERIC_WRITE, HANDLE, IO_STATUS_BLOCK, LIST_ENTRY, OBJECT_ATTRIBUTES, OBJ_CASE_INSENSITIVE, OBJ_KERNEL_HANDLE, PASSIVE_LEVEL, PETHREAD, PHANDLE, POBJECT_ATTRIBUTES, PROCESS_ALL_ACCESS, PVOID, STATUS_SUCCESS, STRING, ULONG, UNICODE_STRING, _EPROCESS, _KPROCESS, _KTHREAD, _MODE::KernelMode
};
use crate::{
DRIVER_MESSAGES,
ffi::{InitializeObjectAttributes, PsGetProcessImageFileName},
ffi::{InitializeObjectAttributes, PsGetProcessImageFileName, IMAGE_DOS_HEADER, IMAGE_EXPORT_DIRECTORY, IMAGE_NT_HEADERS64}, DRIVER_MESSAGES
};
#[derive(Debug)]
@@ -36,6 +35,7 @@ pub enum DriverError {
ImageSizeNotFound,
ResourceStateInvalid,
MutexError,
UnexpectedSignature(String),
Unknown(String),
}
@@ -495,4 +495,72 @@ pub fn get_process_name() -> String {
}
current_process_thread_name
}
/// Scan a module by its in memory base address for function offsets. The target param should NOT be null
/// terminated.
pub fn scan_usermode_module_for_function_address(
base: *const c_void,
target: &str,
) -> Result<*const c_void, DriverError>{
// The memory should always be valid.. but.. Cannot use ProbeForRead as we don't
// have access to __try :( this is as close as I can get right now I think
if unsafe { MmIsAddressValid(base as _) } == FALSE as u8 {
println!("[sanctum [-] Address of ntdll not valid.");
return Err(DriverError::ResourceStateInvalid);
}
let dos = unsafe { &*(base as *const IMAGE_DOS_HEADER) };
if dos.e_magic != 0x5A4D {
return Err(DriverError::UnexpectedSignature("DOS Header".into()));
}
let nth = unsafe { &*(base.add(dos.e_lfanew as usize) as *const IMAGE_NT_HEADERS64) };
if nth.Signature != 0x00004550 {
return Err(DriverError::UnexpectedSignature("NT Signarue".into()));
}
if nth.OptionalHeader.Magic != 0x20B {
return Err(DriverError::UnexpectedSignature("NT Magic".into()));
}
unsafe {
const IMAGE_DIRECTORY_ENTRY_EXPORT: usize = 0;
let dir = &nth.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if dir.VirtualAddress == 0 || dir.Size < size_of::<IMAGE_EXPORT_DIRECTORY>() as u32 {
return Err(DriverError::Unknown("Invalid length".into()));
}
let exp = &*(rva(base as *const _, dir.VirtualAddress) as *const IMAGE_EXPORT_DIRECTORY);
let names = rva(base as *const _, exp.AddressOfNames) as *const u32;
let ords = rva(base as *const _, exp.AddressOfNameOrdinals) as *const u16;
let funcs = rva(base as *const _, exp.AddressOfFunctions) as *const u32;
for i in 0..exp.NumberOfNames {
let name_ptr = rva(base as *const _, *names.add(i as usize));
// compare ascii of the function name
let mut p = name_ptr;
let mut ok = true;
for b in target.as_bytes() {
if *p != *b { ok = false; break; }
p = p.add(1);
}
if ok && *p == 0 {
let ord = *ords.add(i as usize) as usize;
let rva_fn = *funcs.add(ord) as usize;
let fn_ptr = (base as *const u8).add(rva_fn) as *const u8;
let fn_rva = rva_fn as u32;
if fn_rva >= dir.VirtualAddress && fn_rva < dir.VirtualAddress + dir.Size {
continue;
}
return Ok(fn_ptr as *const c_void);
}
}
return Err(DriverError::FunctionNotFoundInModule);
}
}
#[inline(always)]
unsafe fn rva<'a>(base: *const u8, off: u32) -> *const u8 {
unsafe { base.add(off as usize) }
}
+4 -3
View File
@@ -64,6 +64,7 @@ pub enum NtFunction {
NtOpenProcess(NtOpenProcessData),
NtWriteVirtualMemory(NtWriteVirtualMemoryData),
NtAllocateVirtualMemory(NtAllocateVirtualMemoryData),
NtCreateThreadEx(NtCreateThreadExData),
}
/// todo docs
@@ -81,7 +82,6 @@ pub struct NtWriteVirtualMemoryData {
pub buf_len: usize,
}
unsafe impl Send for Syscall {}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
@@ -94,7 +94,8 @@ pub struct NtAllocateVirtualMemoryData {
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct NtOpenProcess {
pub struct NtCreateThreadExData {
pub target_pid: u32,
pub acces_mask: u32,
pub start_routine: usize,
pub argument: usize,
}