Register hook to SetWindowsHookExW in Rust
Published: 2023-12-27 16:25:54


Attaching [hooks](https://learn.microsoft.com/en-us/windows/win32/winmsg/about-hooks) to process is a common way to monitor and debug applications, especially in production environments. Hooks are meant to listen to a process's events, such as keyboard and mouse interactions,UI updates, and so forth.

I was trying to attach a WH_CALLWNDPROC hook to my Windows 32-bit app via the `SetWindowsHookEx` system call in Rust. However, at the final step, I could not figure out how to pass the `lpfn: HOOKPROC` parameter. It seems a normal callback but some Windows [rules](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms644975(v=vs.85)) must be followed.

The most important rule is that the hook procedure itself must live in a separate library (or DLL); exe files are not accepted. Although this is not mandatory for all different hook types, a special event receiving module should be implemented to handle event in this case. The actual hook is run under the umbrella of the target process. So, using a separate lib as the hook procedure is much easier.

Here, I do not talk about how to implement the separate hook procedure dll. I write some Rust code to attach that lib to a target process. A hook procedure library may, at least, have the following function:
```cpp
// C++ lib named my_proc_hook.dll
extern "C" __declspec(dllexport) LRESULT _stdcall the_hook_fn(int code, WPARAM wParam, LPARAM lParam) {
  // logic goes here
  // firing the next middleware (hook) 
  return CallNextHookEx(NULL, code, wParam, lParam);
}
```
## Implementation
Let's start making the hook attacher app by creating a fresh bin Rust project:
```shell
cargo init --bin hook_attacher_cli
```
### Add Windows interface crate
Making [system call in Windows](https://learn.microsoft.com/en-us/windows/dev-environment/rust/rust-for-windows) has been never as easy as these days are. Crates of `windows-sys` and `windows` are here to ease the work. The difference is that `windows-sys` contains low-level syscalls but `windows` is high-level, safe, and follows the Rust idiomatic pattern.

It is the Cargo conversion; crates wrapped around FFI should have two standalone versions, a simple link and an `extern` crate that does nothing but provide a gateway to the target lib, and a second version that contains Rust idiomatic and safe codes. The former has a `-sys` suffix.

After adding dependencies, the `cargo.toml` file will look like as this:
```toml
[package]
name = "hook_attacher_cli"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

# Windows sys-call interface
[dependencies.windows]
version = "0.51"
features = [
    "Win32_Foundation",
    "Win32_UI_WindowsAndMessaging",
    "Win32_System_LibraryLoader",
]

# other utilities crates
[dependencies]
log = "0.4"
env_logger = "0.10"
thiserror = "1.0"
```

**Roadmap** We find the target process, load the hook lib, and finally do the attachment.
```rust
use log::{info, warn, debug};
use windows::core::{w, s};
use windows::Win32::Foundation::{
    HWND,
    LPARAM,
    BOOL,
    HMODULE,
    FARPROC,
    HANDLE,
    WPARAM,
    LRESULT,
};
use windows::Win32::System::LibraryLoader::{
    LoadLibraryExW,
    GetProcAddress,
    LOAD_LIBRARY_FLAGS,
};
use windows::Win32::UI::WindowsAndMessaging::{
    HHOOK,
    EnumChildWindows,
    GetDesktopWindow,
    GetWindowTextA,
    GetWindowThreadProcessId,
    RealGetWindowClassA,
    SetWindowsHookExA,
    UnhookWindowsHookEx,
    WINDOWS_HOOK_ID,
};

fn start_the_process() {
    unsafe {
        let app_hwnd = GetDesktopWindow();
        let handler: Option<unsafe extern "system" fn(HWND, LPARAM) -> BOOL> = Some(find_process_and_attach);
        EnumChildWindows(app_hwnd, handler, LPARAM::default());
    }
}

unsafe extern "system" fn find_process_and_attach(hwnd: HWND, _lparam: LPARAM) -> BOOL {
    let mut buffer = [0_u8; 128];

    let read_len = RealGetWindowClassA(hwnd, &mut buffer);
    let proc_name = String::from_utf8_lossy(&buffer[..read_len as usize]);
    if !proc_name.starts_with("The::Process::NAME::VERSION") {
        return BOOL(1);
    }

    let mut buffer = [0_u8; 128];
    let read_len = GetWindowTextA(hwnd, &mut buffer);
    let window_title = String::from_utf8_lossy(&buffer[..read_len as usize]);

    let hooker = Hooker::new(hwnd, proc_name.into(), window_title.into());
    hook_registry().lock().unwrap().push(hooker);
    BOOL(1)
}

unsafe fn get_proc_handler() -> (HMODULE, FARPROC) {
    let filename = w!("path\\to\\my_proc_hook.dll");
    let h_module = LoadLibraryExW(filename, HANDLE::default(), LOAD_LIBRARY_FLAGS(0))
        .expect("load the requested hook lib");

    let func_name = s!("the_hook_fn");
    let hook_proc = GetProcAddress(h_module, func_name);

    (h_module, hook_proc)
}

```