API Hooking - Detours I

Theory

API hooking is a technique used to intercept and alter the behavior of different API calls. This can be done to monitor or modify the function of an application (used a lot in Game Hacking), etc. AV Vendors / EDR's also use it to monitor suspicious API calls and look at the parameters being passed to them, thereby detecting them before they get a chance to run. There are many different ways to do this which I'll explain in the following posts. For now, I'll use the Detours library provided officially by Microsoft (rewritten unofficially in Rust).

Detours intercepts Win32 functions by re-writing the in-memory code for target functions. Detours preserves the un-instrumented target function (callable through a trampoline) as a subroutine for use by the instrumentation.

Detours

I'll be using the Process Injection code given earlier as an example to show the API hooking.

First we have to add detour library, there are a few libraries related to this, but most of them are quite old so I went with the retour package. we can type cargo add retour (note: you might have to install Nightly). I will start initially hooking the OpenProcess function, and then hook all the general functions used in Process Injection. We can take a look at an example to hook a Windows API here. So basically, we first need to define a type for our function. We know what the arguments this function take, we can take a look at the official MSDN page for OpenProcess. Then we need to define the hook in which we will be hooking the function and giving it our own custom variation of it, and then finally defining the custom function.

// FUNCTION TYPE DEFINITION
type fn_OpenProcess = unsafe extern "system" fn(u32, BOOL, u32) -> HANDLE;

// DEFINING OUR HOOK
static hook_OpenProcess: Lazy<GenericDetour<fn_OpenProcess>> = Lazy::new(|| unsafe {
    let addr = get_module_symbol_address("Kernel32.dll", "OpenProcess").unwrap();
    let og: fn_OpenProcess = std::mem::transmute(addr);
    GenericDetour::new(og, custom_OpenProcess).unwrap()
});

// CUSTOM OpenProcess API
unsafe extern "system" fn custom_OpenProcess(dwDesiredAccess: u32, bInheritHandle: BOOL, dwProcessId: u32) -> HANDLE {
    println!("\n####### [ OpenProcess ] ########\n");
    let access = get_process_flags(dwDesiredAccess);
    println!("[*] PID: {}", dwProcessId);
    // println!("[*] Access: 0x{:X}", dwDesiredAccess);
    println!("[*] Access: {}", access);
    println!("\n####### ############### ########\n");

    hook_OpenProcess.disable().unwrap();
    let result = hook_OpenProcess.call(dwDesiredAccess, bInheritHandle, dwProcessId);
    hook_OpenProcess.enable().unwrap();
    result
}

It is important to disable our hook before calling the actual OpenProcess function. Think of it like this

// After the function has been hooked
Kernel32 -> OpenProcess --> custom_OpenProcess

// If we call without disabling the hook
custom_OpenProcess -> OpenProcess --> custom_OpenProcess

This leads to an infinite loop where the functions call each other and keep doing it until eternity. Now that we have defined our hook, we can enable it and try running the process injection code. We can add a small update in our previous code.

fn main() {
    let _ = unsafe { hook_OpenProcess.enable().unwrap() };
    // defining shellcode
    // ...
}

Running the code, we can see that when It calls the OpenProcess function , it actually instead calls our custom function which we defined.

Note: If it gives error stating something like xyz can't be run in a stable release, you would have to build/run it using nightly, its just cargo +nightly run command. You can check out what's nightly here.

Great, Now that we have successfully hooked an API, it shouldn't be too hard to do the same with others, or so did I thought, but when hooking the VirtualAllocEx API , for whatever reason, the process was crashing every single time. And after a lot of debugging I realized, Rust didn't like me passing the lpAddress parameter directly to the function and it crashed every single time with STATUS_ACCESS_VIOLATION.

After a lot of debugging, I came to the conclusion that this could probably be either because of how the Rust language handles NULL values or maybe something related to process space. It works when the parameter is None or Some(null()) else fails when giving the lpAddress as is.

So I eventually had to update the code for it and use Some(null()) instead. I'll dive deeper into this behavior later when doing it in C. For now, this works in Rust

unsafe extern "system" fn custom_VirtualAllocEx(hProcess: HANDLE, lpAddress: Option<*const std::ffi::c_void>, dwsize: usize, flAllocationType: VIRTUAL_ALLOCATION_TYPE, flProtect: PAGE_PROTECTION_FLAGS) -> *mut c_void {
    
    println!("\n####### [  VirtualAllocEx ] ########\n");
    let pid = get_pid_from_handle(hProcess);
    println!(" The size allocated is: {}", dwsize);
    println!(" The remote process is: {}", pid);
    println!(" The protection is: {}", get_page_protection_flags(flProtect));
    unsafe {
        
        hook_VirtualAllocEx.disable();
        // println!("Disabled Hook");
        
        // here using lpAddress won't work and crash
        // but using Some(null()) succeeds
        // I am totally clueless on this
        // I've even tried just printing it but even that doesn't seem to work
        // I hope any C/Rust Wizard reads this and tells my why
        // println!("The lpAddress seems to be {:?}", lpAddress /*.unwrap_or(null()) */ );

        let res = hook_VirtualAllocEx.call( hProcess, Some(null()), dwsize, flAllocationType, flProtect);

        hook_VirtualAllocEx.enable();
        // println!("Enabled Hook again");
        println!("\n VirtualAllocEx RetVal: {:?}", res);
        println!("\n####### ################## ########\n");

        res
    }
}

Apart from this particular thing, every thing else was smooth and here's how the output looks like when I have hooked all those functions, I've removed all the print statements from the actual process injection code, so whatever we see here is the output from our custom functions.

PS> cargo run -- 20152
   Compiling api_hooking v0.1.0 (C:\Users\Admin\Desktop\batshitCRust\api_hooking)
    Finished dev [unoptimized + debuginfo] target(s) in 0.96s
     Running `target\debug\api_hooking.exe 20152`

####### [ GetProcAddress ] ########

[*] Module : [ "C:\\WINDOWS\\System32\\KERNEL32.DLL" ]  |  Function: "GetModuleHandleW"

####### ################## ########

####### [ OpenProcess ] ########

[*] PID: 20152
[*] Access: PROCESS_ALL_ACCESS

####### ############### ########

####### [  VirtualAllocEx ] ########

 The size allocated is: 276
 The remote process is: 20152
 The protection is: PAGE_EXECUTE_READWRITE

 VirtualAllocEx RetVal: 0xc567560000

####### ################## ########

####### [ WriteProcessMemory ] ########

[*] Size: 276  |  PID: 20152
Data written :
FC 48 83 E4 F0 E8 C0 00 00 00 41 51 41 50 52 51 56 48 31 D2 65 48 8B 52 60 48 8B 52 18 48 8B 52 20 48 8B 72 50 48 0F B7
4A 4A 4D 31 C9 48 31 C0 AC 3C 61 7C 02 2C 20 41 C1 C9 0D 41 01 C1 E2 ED 52 41 51 48 8B 52 20 8B 42 3C 48 01 D0 8B 80 88
00 00 00 48 85 C0 74 67 48 01 D0 50 8B 48 18 44 8B 40 20 49 01 D0 E3 56 48 FF C9 41 8B 34 88 48 01 D6 4D 31 C9 48 31 C0
AC 41 C1 C9 0D 41 01 C1 38 E0 75 F1 4C 03 4C 24 08 45 39 D1 75 D8 58 44 8B 40 24 49 01 D0 66 41 8B 0C 48 44 8B 40 1C 49
01 D0 41 8B 04 88 48 01 D0 41 58 41 58 5E 59 5A 41 58 41 59 41 5A 48 83 EC 20 41 52 FF E0 58 41 59 5A 48 8B 12 E9 57 FF
FF FF 5D 48 BA 01 00 00 00 00 00 00 00 48 8D 8D 01 01 00 00 41 BA 31 8B 6F 87 FF D5 BB F0 B5 A2 56 41 BA A6 95 BD 9D FF
D5 48 83 C4 28 3C 06 7C 0A 80 FB E0 75 05 BB 47 13 72 6F 6A 00 59 41 89 DA FF D5 63 61 6C 63 2E 65 78 65 00

####### ####################### ########

####### [ CreateRemoteThread ] ########

[*] PID: 20152  |  Starting addr:  0xc567560000

####### ####################### ########

####### [ GetModuleHandleW ] ########

[*] Module : "NULL (Current Process)"

####### ################## ########

As Usual, I have uploaded the code on github. Thank you. Next, I'll show how we can achieve the same thing with a DLL, then we can inject this DLL into any process to monitor the API calls in it.

References

Last updated

Was this helpful?