Skip to content

Creating A Window With Rust

If you've been following along from Creating A DLL With Rust, you should know how to do the following:

  • Create a Windows dynamic-link library (DLL) using Rust
  • Inject DLLs into processes using Process Hacker
  • Inject DLLs into processes using Rust
  • Create application windows using Rust
  • Override a window's behavior using subclassing

In today's exercise, we are going to skip a step and learn about windows: how they work, how to create them, and how to customize them using something called subclassing.

What Are Windows?

We spend all day interacting with windows, but how much do we really know about them?

By the end of this series we'll be elbow deep in them, so it's worth spending a moment to understand them more intimately.

Fortunately, Microsoft’s documentation on this topic is fantastic: About Windows.

Take a few moments to peruse and come back here. I'll wait.


Ok, sketching out what we just read, GUI applications look something like this:

Creating Windows With Rust 2023-03-03 01.03.08.excalidraw.svg

  • Every application has a WinMain entry-point function
  • Every application must create at least one window
  • Every window belongs to one window class
  • Every window has one unique window handle
  • Every window has one GUI thread
  • Every GUI thread has one message queue
  • Every window class contains one window procedure

And, Windows is a message-based system, where:

  • Every message sent or posted to a window is processed by its window procedure
  • Every message posted to a window is placed in its message queue
  • Every window must remove and process messages posted to its message queue

Further Reading


Overall Structure

Your First Windows Program presents a prototypical GUI application:

#include <windows.h>

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow)
{
    // Register the window class.
    const wchar_t CLASS_NAME[]  = L"Sample Window Class";

    WNDCLASS wc = { };

    wc.lpfnWndProc   = WindowProc;
    wc.hInstance     = hInstance;
    wc.lpszClassName = CLASS_NAME;

    RegisterClass(&wc);

    // Create the window.

    HWND hwnd = CreateWindowEx(
        ...
        CLASS_NAME, // Window class
        ...
        );

    if (hwnd == NULL)
    {
        return 0;
    }

    ShowWindow(hwnd, nCmdShow);

    // Run the message loop.

    MSG msg = { };
    while (GetMessage(&msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return 0;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    ...
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

With the code's comments delineating three functional areas:


1. The Window Class

Creates and registers a "window class", which is simply a WNDCLASS struct.

At minimum we have to provide:

  • a pointer to a WNDPROC callback function
  • a handle to the .exe/.dll containing the WNDPROC
  • a unique class name
// Register the window class.

const wchar_t CLASS_NAME[]  = L"Sample Window Class";

WNDCLASS wc = { };

wc.lpfnWndProc   = WindowProc;  // Pointer to "window procedure"
wc.hInstance     = hInstance;
wc.lpszClassName = CLASS_NAME;  // Unique class name

RegisterClass(&wc);             // Tell Windows about the new window class 

2. Window Instantiation

Creates a new window, using the specs we defined in WNDCLASS, and displays the window to the screen:

// Create the window.

HWND hwnd = CreateWindowEx(
    ...
    CLASS_NAME, // Window class
    ...
    );

if (hwnd == NULL)
{
    return 0;
}

// Display window to screen
ShowWindow(hwnd, nCmdShow);
CreateWindowEx returns a "handle" aka “window handle", to the created window.

Window handles are unique to each created window, allowing us to reference them in our code.


3. The Message Loop

Continuously calls GetMessage to grab messages sent to the window and dispatch them to the window's Window Procedure using DispatchMessage:

// Run the message loop.

MSG msg = { };
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

The Window Procedure

Receives and processes the window's messages, handing-off unprocessed messages to the operating system by calling DefWindowProc:

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    ...
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

Further Reading


Actually Creating The Window

The windows-rs crate provides a create_window sample, a Rust version of what we examined above:

main.rs
use windows::{
    core::*, Win32::Foundation::*, Win32::Graphics::Gdi::*,
    Win32::System::LibraryLoader::GetModuleHandleA,
    Win32::UI::WindowsAndMessaging::*,
};

fn main() -> Result<()> {
    unsafe {
        let instance = GetModuleHandleA(None)?;
        debug_assert!(instance.0 != 0);

        let window_class = s!("window");

        let wc = WNDCLASSA {
            hCursor: LoadCursorW(None, IDC_ARROW)?,
            hInstance: instance,
            lpszClassName: window_class,
            style: CS_HREDRAW | CS_VREDRAW,
            lpfnWndProc: Some(wndproc),

            ..Default::default()
        };

        let atom = RegisterClassA(&wc);
        debug_assert!(atom != 0);

        CreateWindowExA(
            WINDOW_EX_STYLE::default(),
            window_class,
            s!("This is a sample window"),
            WS_OVERLAPPEDWINDOW | WS_VISIBLE,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            800,
            600,
            None,
            None,
            instance,
            None,
        );

        let mut message = MSG::default();

        while GetMessageA(&mut message, HWND(0), 0, 0).into() {
            DispatchMessageA(&message);
        }

        Ok(())
    }
}

extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
    unsafe {
        match message {
            WM_PAINT => {
                println!("WM_PAINT");
                ValidateRect(window, None);
                LRESULT(0)
            }
            WM_DESTROY => {
                println!("WM_DESTROY");
                PostQuitMessage(0);
                LRESULT(0)
            }
            _ => DefWindowProcA(window, message, wparam, lparam),
        }
    }
}

Getting the Code

The windows-rs repo is pretty large, so if you want to save time, you can git clone just the create_window folder from my repo here: peddamat/create_window

Cloning it into our workspace folder:

$ git clone https://github.com/peddamat/create_window.git

$ tree
.
├── Cargo.lock
├── Cargo.toml
├── create_window
├── hello
├── hello_runner
└── target
fyi, you can grab all the windows-rs samples: here

Updating Your Workspace

Add the crate to your workspace's Cargo.toml:

Cargo.toml
[workspace]

members = [
    "hello",
    "hello_runner",
    "create_window",
]

Running the Code

Now, do a cargo run:

$ cargo run
error: `cargo run` could not determine which binary to run. Use the `--bin` option to specify a binary, or the `default-run` manifest key.
available binaries: create_window, hello_runner

Ah, so cargo run --bin create_window it is!

$ cargo run --bin create_window
warning: C:\Users\me\source\blog_qa\create_window\Cargo.toml: dependency (windows-sys) specified without providing a local path, Git repository, or version to use. This will be considered an error in future versions
    Updating crates.io index
   Compiling windows-sys v0.45.0
   Compiling create_window v0.0.0 (C:\Users\me\source\blog_qa\create_window)
    Finished dev [unoptimized + debuginfo] target(s) in 4.91s
     Running `target\debug\create_window.exe`
WM_PAINT

And now we're cooking with windows!

Pasted image 20230222232138.png

Further Reading


Window Messages

As we learned earlier, Windows is message-based, meaning, all user input and requests are sent to the window using messages.

”The system” (Windows) internally defines 100’s of messages, but we’re usually only interested in a handful of them.

To get a better handle on which messages matter, let's see them with our own eyes using Window Detective.

Window Detective

It looks like this:

Pasted image 20230306144153.png

Use the 'Pick window' tool to "pick" the create_window window, it should have a red outline:

Pasted image 20230306144434.png

Now right-click on the highlighted row and click "View Properties", which brings up the window's property inspector on the right:

Pasted image 20230306144808.png

Now right-click again and select "Messages...", and move your mouse over the create_window window. You should see messages flooding in:

Pasted image 20230306145005.png

You can filter messages by clicking the filter icon:

Pasted image 20230306145044.png

Reduce the signal-to-noise ratio by selectively excluding messages:

Pasted image 20230306145219.png

Which will help reveal which events are received and emitted when manipulating a window:

Pasted image 20230306145412.png

Spend a few minutes (or hours) playing around. See what messages are sent when dragging and/or maximizing a window.

Further Reading


Window Procedure

Window Procedures states:

Every window has an associated window procedure — a function that processes all messages sent or posted to all windows of the class.

All aspects of a window's appearance and behavior depend on the window procedure's response to these messages.

Seems pretty important, eh?

Yes, the window procedure is the beating heart of the window, where all system-generated messages are eventually delivered to and processed from.

Let’s take things for a spin by handling the WM_PAINT and WM_WINDOWPOSCHANGING messages in our window procedure.

Customizing Our Window

WM_PAINT

The WM_PAINT message indicates that our window needs to be repainted. By responding to it, we can modify what's drawn to the screen.

Let’s do so and add a bit of color to our window.


First, replace the use section:

main.rs
use windows::{
    core::*, Win32::Foundation::*, Win32::Graphics::Gdi::ValidateRect,
    Win32::System::LibraryLoader::GetModuleHandleA,
    Win32::UI::WindowsAndMessaging::*,
};

With:

main.rs
use windows::{
    core::*, 
    Win32::Foundation::*, 
    Win32::Graphics::Gdi::*,
    Win32::System::LibraryLoader::GetModuleHandleA, 
    Win32::UI::WindowsAndMessaging::*,
};

Then replace the WM_PAINT section in wndproc with:

main.rs
WM_PAINT => {
    println!("WM_PAINT");
    let mut msg =  String::from("ZOMG!");
    let mut ps = PAINTSTRUCT::default();
    let psp = &mut ps as *mut PAINTSTRUCT;
    let rp = &mut ps.rcPaint as *mut RECT;
    let hdc = BeginPaint(window, psp);
    let brush = CreateSolidBrush(COLORREF(0x0000F0F0));
    FillRect(hdc, &ps.rcPaint, brush);
    DrawTextA(hdc, msg.as_bytes_mut(), rp, DT_SINGLELINE | DT_CENTER | DT_VCENTER );
    EndPaint(window, &ps);
    LRESULT(0)
}
- source

A cargo run --bin create_window results in:

Pasted image 20230223004313.png

Sweet.

WM_WINDOWPOSCHANGING

Raymond Chen gives this example on WM_WINDOWPOSCHANGING, which we can implement by adding the following to wnd_proc:

hello\src\lib.rs
WM_WINDOWPOSCHANGING => {
    let data = lparam.0 as *mut WINDOWPOS;
    let data = data.as_mut().unwrap();
    data.flags |= SWP_NOSIZE | SWP_NOMOVE;
}

Window Subclassing

We now have a basic understanding of how a window works, how its constructed, and how its window procedure affects its presentation and behavior.

Yet, our knowledge is limited to changing the appearance of windows that we create or have the source code to. To do the same for other people's windows (including our own), we need to use a technique called subclassing:

Subclassing is a technique that allows an application to intercept and process messages sent or posted to a particular window before the window has a chance to process them. By subclassing a window, an application can augment, modify, or monitor the behavior of the window...

Cool, cool.

An application subclasses a window by replacing the address of the window's original window procedure with the address of a new window procedure, called the subclass procedure. Thereafter, the subclass procedure receives any messages sent or posted to the window.

Ah, got it.

...you cannot subclass a window or class that belongs to another application. All subclassing must be performed within the same process.

BOOM! (the crowd goes wild)

See, in Injecting The GD Thing we learned how to inject a DLL into another application's process space.

So, in theory, if we could construct a DLL that automatically subclasses an application’s main window.

We could then inject it into another application’s process space.

Which, in theory, would allow us to subclass any window!

So, then, how do we subclass?!

SetWindowLong

"About Window Procedures" directs us to SetWindowLong.

SetWindowsLong informs us:

Note

This function has been superseded by the SetWindowLongPtr function. To write code that is compatible with both 32-bit and 64-bit versions of Windows, use the SetWindowLongPtr function.

SetWindowLongPtr

SetWindowLongPtr says it: changes an attribute of the specified window. The function also sets a value at the specified offset in the extra window memory.

You use the function like this:

SetWindowLongPtrW
let prev_wndproc = SetWindowLongPtrW( 
    hwnd,
    GWLP_WNDPROC,
    wnd_proc as isize
)

where:

  • hwnd: a handle to the window we want to subclass
  • GWLP_WNDPROC: "Sets a new address for the window procedure"
  • wnd_proc: the replacement window procedure
  • prev_wndproc: a pointer to the previous "window procedure"

Replacement Window Procedure

wnd_proc is simply a "window procedure" with one crucial difference: it calls CallWindowProc instead of DefWindowProcW.

extern "system" fn wnd_proc(
    window: HWND,
    message: u32,
    wparam: WPARAM,
    lparam: LPARAM,
) -> LRESULT {
    unsafe {
        match message {
            // WM_PAINT => { }
            _ => ()
        }

        CallWindowProcW(PREV_WNDPROC, window, message, wparam, lparam)
    }
}

CallWindowProcA

CallWindowProc is used to pass messages to the previous window procedure, i.e. prev_wndproc, and remember, we must call CallWindowProcW on any messages we do not process.

Result

If the function succeeds, it returns a pointer to the previous window procedure.

If the function fails, it returns 0.


Actually Subclassing A Window

Let's refactor our create_window example to use SetWindowLongPtr.

First, let's add our skeleton wnd_proc to main.rs:

create_window\src\main.rs
extern "system" fn wnd_proc(
    window: HWND,
    message: u32,
    wparam: WPARAM,
    lparam: LPARAM,
) -> LRESULT {
    unsafe {
        match message {
            // i.e. WM_PAINT => { }
            _ => ()
        }

        CallWindowProcW(PREV_WNDPROC, window, message, wparam, lparam)
    }
}

We can immediately see a problem, we've gotta get PREV_WNDPROC into wnd_proc. Since we're still learning, let's stick a gross global variable above our main:

static mut PREV_WNDPROC: WNDPROC = None;
...
fn main() -> Result<()> {

Now, let's update our fn main() to grab the window handle returned by CreateWindowExA and pass it to a SetWindowLongPtrA call:

create_window\main.rs
let handle = CreateWindowExA(
                ...
                None,
             );

let result = SetWindowLongPtrW(handle, GWLP_WNDPROC, wnd_proc as isize);
let prev_wnd_proc = transmute::<isize, WNDPROC>(result);

let mut message = MSG::default();
...

Finally, let's add this to the top of main.rs:

create_window\main.rs
use std::mem::transmute;

and a cargo check to confirm things compile:

$ cargo check
    Finished dev [unoptimized + debuginfo] target(s) in 0.26s

Great, now let's tranplant our WM_PAINT into wnd_proc:

extern "system" fn wnd_proc(
    window: HWND,
    message: u32,
    wparam: WPARAM,
    lparam: LPARAM,
) -> LRESULT {
    unsafe {
        match message {
            WM_PAINT => {
                let mut msg =  String::from("ZOMG!");
                let mut ps = PAINTSTRUCT::default();
                let psp = &mut ps as *mut PAINTSTRUCT;
                let rectp = &mut ps.rcPaint as *mut RECT;
                let hdc = BeginPaint(window, psp);
                let brush = CreateSolidBrush(COLORREF(0x0000F0F0));
                FillRect(hdc, &ps.rcPaint, brush);
                DrawTextA(hdc,
                    msg.as_bytes_mut(),
                    rectp,
                    DT_SINGLELINE | DT_CENTER | DT_VCENTER
                );
                EndPaint(window, &ps);
                return LRESULT(0);
            }
            WM_WINDOWPOSCHANGING => {
                let data = lparam.0 as *mut WINDOWPOS;
                let data = data.as_mut().unwrap();
                data.flags |= SWP_NOSIZE | SWP_NOMOVE;
                return LRESULT(0);
            }
            WM_NCDESTROY => {
                let result = transmute::<WNDPROC, isize>(PREV_WNDPROC);
                SetWindowLongPtrW(window, GWLP_WNDPROC, result);
                return DefWindowProcA(window, message, wparam, lparam);
            }
            _ => ()
        }
        CallWindowProcW(PREV_WNDPROC, window, msg, wparam, lparam)
    }
}

Hitting cargo run --bin create_window yields:

Pasted image 20230223004313.png

Doneski!

Further Reading


Subclassing via DLL

Ok, this is where things start to come together. Remember how we mentioned that our ultimate goal is to hijack the window of any application via DLL injection?

Well, let's start by moving stuff from create_window\src\main.rs to hello\src\lib.rs.

Refactoring create_window

First, add a dependency on the hello crate to create_window's Cargo.toml:

Cargo.toml
[dependencies]
hello = { path = "../hello" }

Replacing the SetWindowLongPtr call in create_window\src\main.rs with a LoadLibraryA call which loads hello.dll:

create_window\src\main.rs
...
LoadLibraryA(PCSTR("hello.dll\0".as_ptr()));
//let result = SetWindowLongPtrW(handle, GWLP_WNDPROC, wnd_proc as isize);
//PREV_WNDPROC = transmute::<isize, WNDPROC>(result);
...

We can clean-up the use statements:

create_window\src\main.rs
use windows::{
    core::*,
    Win32::Foundation::*,
    Win32::Graphics::Gdi::*,
    Win32::System::LibraryLoader::{GetModuleHandleA, LoadLibraryA},
    Win32::UI::WindowsAndMessaging::*,
};

Refactoring hello.dll

First, let's put our SetWindowLongPtr call into attach:

hello\src\lib.rs
...
static mut PREV_WNDPROC: WNDPROC = None;
...
fn attach() {
    unsafe {
        let result = SetWindowLongPtrW(handle, GWLP_WNDPROC, wnd_proc as isize);
        PREV_WNDPROC = transmute::<isize, WNDPROC>(result);
    };
}

While we're on the topic, do we need a detach? Of course!

fn detach() {
    unsafe {
        let handle = find_window_by_pid(GetCurrentProcessId()).unwrap();
        SetWindowLongPtrW(handle, GWLP_WNDPROC, transmute::<WNDPROC, isize>(PREV_WNDPROC));
    };
}

And update DllMain:

match call_reason {
    DLL_PROCESS_ATTACH => attach(),
    DLL_PROCESS_DETACH => detach(),
    _ => ()
}

Now let's cut-and-paste wnd_proc and stick it under detach:

...
extern "system" fn wnd_proc(
    window: HWND,
    message: u32,
    wparam: WPARAM,
    lparam: LPARAM,
) -> LRESULT {
    unsafe {
        match message {
            WM_PAINT => {
                let mut msg =  String::from("ZOMG!");
                let mut ps = PAINTSTRUCT::default();
                let psp = &mut ps as *mut PAINTSTRUCT;
                let rectp = &mut ps.rcPaint as *mut RECT;
                let hdc = BeginPaint(window, psp);
                let brush = CreateSolidBrush(COLORREF(0x0000F0F0));
                FillRect(hdc, &ps.rcPaint, brush);
                DrawTextA(hdc,
                    msg.as_bytes_mut(),
                    rectp,
                    DT_SINGLELINE | DT_CENTER | DT_VCENTER
                );
                EndPaint(window, &ps);
                return LRESULT(0);
            }
            WM_WINDOWPOSCHANGING => {
                let data = lparam.0 as *mut WINDOWPOS;
                let data = data.as_mut().unwrap();
                data.flags |= SWP_NOSIZE | SWP_NOMOVE;
                return LRESULT(0);
            }
            WM_NCDESTROY => {
                let result = transmute::<WNDPROC, isize>(PREV_WNDPROC);
                SetWindowLongPtrW(window, GWLP_WNDPROC, result);
                return DefWindowProcA(window, message, wparam, lparam);
            }
            _ => ()
        }
        CallWindowProcW(PREV_WNDPROC, window, message, wparam, lparam)
    }
}

Things are looking good, except we have one major problem, SetWindowLongPtr needs a handle to a window to subclass, specifically, for hello.dll, that would be a handle to the main GUI window of the application it's been injected into.

However, LoadLibrary doesn't provide a means of passing arguments, so we can't pass it from create_window... it seems that hello.dll is going to have to dig up the window handle all by itself.

Now, this is a challenging problem, so we're going to use something ready baked, however, I'll dissect the code in a follow-up article.

Ok, paste the following after wnd_proc:

hello\src\lib.rs
pub fn find_window_by_pid(pid: u32) -> Result<HWND> {
    let mut data = EnumWindowsData {
        wanted_pid: pid,
        handle: HWND::default(),
        found: false,
    };
    unsafe {
        EnumWindows(
            Some(enum_windows_callback),
            LPARAM(&mut data as *mut EnumWindowsData as isize)
        );
    };
    if !data.found {
        return Err(Error::new( ErrorKind::NotFound, "Can't find the window!"));
    }
    Ok(data.handle)
}

#[derive(Default)]
struct EnumWindowsData {
    wanted_pid: u32,
    handle: HWND,
    found: bool,
}

unsafe extern "system" fn enum_windows_callback(handle: HWND, lparam: LPARAM) -> BOOL {
    let data = lparam.0 as *mut EnumWindowsData;
    let mut data = data.as_mut().unwrap();

    let mut pid = MaybeUninit::<u32>::zeroed();
    GetWindowThreadProcessId(handle, Some(pid.as_mut_ptr()));
    let pid = pid.assume_init();

    if pid == data.wanted_pid
        && GetWindow(handle, GW_OWNER).0 == 0
        && IsWindowVisible(handle).as_bool()
    {
        data.handle = handle;
        data.found = true;
        return BOOL(0);
    }

    BOOL(1)
}

Now, finish up by using find_window_by_pid to satisfy SetWindowLongPtr in attach and detach.

After cleaning up our use statements, things should look like:

main.rs
use windows::{ Win32::Foundation::*, Win32::System::SystemServices::*, };
use windows::Win32::System::Threading::GetCurrentProcessId;

use windows::{
    Win32::Graphics::Gdi::*,
    Win32::UI::WindowsAndMessaging::*,
};

use std::mem::{transmute, MaybeUninit};
use std::io::{Result, Error, ErrorKind};

static mut PREV_WNDPROC: WNDPROC = None;

#[no_mangle]
#[allow(non_snake_case, unused_variables)]
extern "system" fn DllMain(
    dll_module: HINSTANCE,
    call_reason: u32,
    _: *mut ())
    -> bool
{
    match call_reason {
        DLL_PROCESS_ATTACH => attach(),
        DLL_PROCESS_DETACH => detach(),
        _ => ()
    }

    true
}

fn attach() {
    unsafe {
        let handle = find_window_by_pid(GetCurrentProcessId()).unwrap();
        let result = SetWindowLongPtrW(handle, GWLP_WNDPROC, wnd_proc as isize);
        PREV_WNDPROC = transmute::<isize, WNDPROC>(result);
    };
}

fn detach() {
    unsafe {
        let handle = find_window_by_pid(GetCurrentProcessId()).unwrap();
        SetWindowLongPtrW(handle, GWLP_WNDPROC, transmute::<WNDPROC, isize>(PREV_WNDPROC));
    };
}

extern "system" fn wnd_proc(
    window: HWND,
    msg: u32,
    wparam: WPARAM,
    lparam: LPARAM,
) -> LRESULT {
    unsafe {
        match msg {
            WM_PAINT => {
                let mut msg =  String::from("ZOMG!");
                let mut ps = PAINTSTRUCT::default();
                let psp = &mut ps as *mut PAINTSTRUCT;
                let rectp = &mut ps.rcPaint as *mut RECT;
                let hdc = BeginPaint(window, psp);
                let brush = CreateSolidBrush(COLORREF(0x0000F0F0));
                FillRect(hdc, &ps.rcPaint, brush);
                DrawTextA(hdc,
                    msg.as_bytes_mut(),
                    rectp,
                    DT_SINGLELINE | DT_CENTER | DT_VCENTER
                );
                EndPaint(window, &ps);
                return LRESULT(1);
            }
            WM_WINDOWPOSCHANGING => {
                let data = lparam.0 as *mut WINDOWPOS;
                let data = data.as_mut().unwrap();
                data.flags |= SWP_NOSIZE | SWP_NOMOVE;
                return LRESULT(0);
            }
            WM_NCDESTROY => {
                let result = transmute::<WNDPROC, isize>(PREV_WNDPROC);
                SetWindowLongPtrW(window, GWLP_WNDPROC, result);
                return DefWindowProcA(window, message, wparam, lparam);
            }
            _ => ()
        }
        CallWindowProcW(PREV_WNDPROC, window, msg, wparam, lparam)
    }
}

pub fn find_window_by_pid(pid: u32) -> Result<HWND> {
    let mut data = EnumWindowsData {
        wanted_pid: pid,
        handle: HWND::default(),
        found: false,
    };
    unsafe {
        EnumWindows(
            Some(enum_windows_callback),
            LPARAM(&mut data as *mut EnumWindowsData as isize)
        );
    };
    if !data.found {
        return Err(Error::new( ErrorKind::NotFound, "Can't find the window!"));
    }
    Ok(data.handle)
}

#[derive(Default)]
struct EnumWindowsData {
    wanted_pid: u32,
    handle: HWND,
    found: bool,
}

unsafe extern "system" fn enum_windows_callback(handle: HWND, lparam: LPARAM) -> BOOL {
    let data = lparam.0 as *mut EnumWindowsData;
    let mut data = data.as_mut().unwrap();

    let mut pid = MaybeUninit::<u32>::zeroed();
    GetWindowThreadProcessId(handle, Some(pid.as_mut_ptr()));
    let pid = pid.assume_init();

    if pid == data.wanted_pid
        && GetWindow(handle, GW_OWNER).0 == 0
        && IsWindowVisible(handle).as_bool()
    {
        data.handle = handle;
        data.found = true;
        return BOOL(0);
    }

    BOOL(1)
}

Hitting cargo run in the Create Window crate results in:

Pasted image 20230223004313.png

Amazing!

Our little hello.dll now automatically overrides the window procedure of any application it's loaded into!

A Taste Of DLL Injection

Injecting into create_window.exe

Let's double-confirm this by:

  • comment out LoadLibrary in create_window
  • do a cargo run --bin create_window
  • inject hello.dll into create_window.exe using Process Hacker

Pretty cool, eh?

Note: For the subclass to be called, you have to trigger a 'WM_PAINT' message, which can be done by resizing the create_window.exe window.

Injecting into notepad.exe

Try injecting hello.dll into notepad.exe.


Part 3 is here: Injecting DLLs With Rust

Having trouble? The code this series can be found here: peddamat/how-to-create-a-dll-using-rust


Last update: 2023-03-16