Skip to content

This article is a work-in-progress

Production Ready DLL Injection

Dear reader, if you haven't, already, please catch up on Part 1, Part 2, and Part and come back here. It'll be worth it.

Everyone else, here's what we've covered:

  • 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 episode, we'll focus on making our DLL "production-ready", by RTFM'ing, adding logging, improving error handling, and, oh yeah, re-writing the whole thing.

RTFM

As a believer in Minimalist Instruction , I've focused on getting your hands dirty as quickly as possible.

However, DLL injection and window subclassing are extraordinarily powerful and invasive tools. Both transcend traditional operating systems process boundaries - boundaries specifically created to protect running applications from poorly written and/or malicious code.

Implementing these sorts of things relying solely on a tutorial, without, studying the underlying system documentation, is a mistake.

To see what I mean, let's read some fscking manuals, starting with, SetWindowLongPtr().

SetWindowLongPtr

I was serious, go ahead and read it: here

It starts with an easy one:

The SetWindowLongPtr function fails if the process that owns the window specified by the hWnd parameter is at a higher process privilege in the UIPI hierarchy than the process the calling thread resides in.

Basically, we can't subclass apps running as "Administrator" if we're a normal user. This isn't surprising, but can we proactively check and/or inform the user of workarounds?

Windows XP/2000: The SetWindowLongPtr function fails if the window specified by the hWnd parameter does not belong to the same process as the calling thread.

This shouldn't be an issue, but good to know if we get a support email.

If you use SetWindowLongPtr with the GWLP_WNDPROC index to replace the window procedure, the window procedure must conform to the guidelines specified in the description of the WindowProc callback function.

Good, we covered this in Creating A Window With Rust#Callback Function.

Calling SetWindowLongPtr with the GWLP_WNDPROC index creates a subclass of the window class used to create the window.

Perfect, that's what we did.

An application can subclass a system class, but should not subclass a window class created by another process.

lol.

The SetWindowLongPtr function creates the window subclass by changing the window procedure associated with a particular window class, causing the system to call the new window procedure instead of the previous one.

Sketching this out, we have a Window with a default window procedure:

Production Ready DLL Injection 2023-03-09 21.46.24.excalidraw.svg

After a SetWindowLongPtr(), the default window procedure is replaced with our new window procedure's address.

The new window procedure calls DefWindowProcW()to hand-off unprocessed messages to the default window procedure:

Production Ready DLL Injection 2023-03-09 22.17.06.excalidraw.svg

Makes sense.

An application must pass any messages not processed by the new window procedure to the previous window procedure by calling CallWindowProc. This allows the application to create a chain of window procedures.

Parsing this out fully:

  • if we don't process the message, then we must hand-off to previous window procedure
  • if we do process a message, we can choose whether to return or to hand it off

Got it.


About Window Procedures mentions:

The application must also have the original window procedure address to remove the subclass from the window. To remove the subclass, the application calls SetWindowLong again, passing the address of the original window procedure with the GWL_WNDPROC flag and the handle to the window.

Removing a subclass using prev_wndproc is straightforward. We just need to make sure we hold onto it.

When an application subclasses a subclassed window, it must remove the subclasses in the reverse order they were performed. If the removal order is not reversed, an unrecoverable system error may occur.

Considering the case where we subclass a window twice, for whatever reason, we need to unsubclass in the reverse order:

# Subclassing
let one = SetWindowLongPtr(a, ...)
let two = SetWindowLongPtr(b, ...)

# Unsubclassing
SetWindowLongPtr(two, ...); // returns b
SetWindowLongPtr(one, ...); // returns a

👍


In Homework assignment about window subclassing and Safer subclassing warns:

One gotcha that isn’t explained clearly in the documentation is that you must remove your window subclass before the window being subclassed is destroyed. This is typically done either by removing the subclass once your temporary need has passed, or if you are installing a permanent subclass, by inserting a call to RemoveWindowSubclass inside the subclass procedure itself:

...
case WM_NCDESTROY:
  RemoveWindowSubclass(hwnd, thisfunctionname, uIdSubclass);
  return DefSubclassProc(...);

So, if the user unexpectedly closes a window that we've subclassed, we must remove our subclass when we receive the WM_NCDESTROY message (which is actually the very last message the system sends us before terminating the window).

Do not assume that subclasses are added and removed in a purely stack-like manner. If you want to unsubclass and find that you are not the window procedure at the top of the chain you cannot safely unsubclass. You will have to leave your subclass attached until it becomes safe to unsubclass. Until that time, you just have to keep passing the messages through to the previous procedure.

So, when removing a subclass (by calling SetWindowLongPtr with prev_wndproc), we have to somehow determine if anyone has subclassed the same window after us, and if so, wait until WM_NCDESTROY to remove our subclass.

This one's a doozy, so let's sketch it out, starting with out default window procedure:

Production Ready DLL Injection 2023-03-09 21.46.24.excalidraw.svg


After a SetWindowLongPtr(), the default window procedure is replaced with our new window procedure's address:

Production Ready DLL Injection 2023-03-09 22.17.06.excalidraw.svg


At some point before we unsubclass ourselves, someone else calls SetWindowLongPtr() and replaces our window procedure, with theirs.

Of course, they think our window procedure is the default window procedure, so they squirrel it away for safe keeping.

Hopefully, they're handing-off unprocessed messages using DefWindowProcW(), in which case, our window procedure receives those messages:

Production Ready DLL Injection 2023-03-09 22.17.15.excalidraw.svg


Then, if we decide to unsubclass before they unsubclass, by calling SetWindowLongPtr() with our prev_wndproc, we end up accidentally unsubclassing them as well:

Production Ready DLL Injection 2023-03-09 22.17.37.excalidraw.svg

Oy vey.


So, it's actually easy to check if someone's subclassed after us by calling GetWindowLongPtr(), and comparing what's returned against prev_wndproc.

But that's rife with race conditions, and probably (definitely) requires locks and mutexes and 🤮

And, even if we get that sorted, how do we get 0xCCC to call 0xAAA, instead of our removed subclass procedure?

Yeah, Raymond's basically saying that we're trapped until WM_NCDESTROY if someone subclasses after us...

Production Ready DLL Injection 2023-03-09 22.17.54.excalidraw.svg

Conclusions

After wading through all that SetWindowLongPtr documentation and errata, it feels like we've hit some really insurmountable issues; unless we're ok with leaving DLLs dangling around, potentially forever, if we get subclassed.

Fortunately, Raymond gives us a glimmer of hope, SetWindowSubclass, which we'll explore next.

Further reading


SetWindowSubclass

Let's start by reading the documentation: here

Everything seems great until we get to this warning:

Warning

You cannot use the subclassing helper functions to subclass a window across threads.

From our earlier SetWindowLongPtr experiments, we saw that DllMain is called from the same process but a different thread than the main GUI thread.

If we want to use SetWindowSubclass, we'll have to work around this.

Let's get our hands dirty by refactoring create_window to use SetWindowSubclass instead of SetWindowLongPtr.

Refactoring create_window.exe

First, replace the existing calls with:

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

SetWindowSubclass(handle, Some(wnd_proc), 0, 0);

Copy over wnd_proc from lib.rs:

lib.rs
extern "system" fn wnd_proc(
    window: HWND,
    message: u32,
    wparam: WPARAM,
    lparam: LPARAM,
    _: usize,
    _: usize,
) -> 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, _>(PREV_WNDPROC);
                // SetWindowLongPtrW(window, GWLP_WNDPROC, result);
                RemoveWindowSubclass(window, Some(wnd_proc), 0);
                return DefWindowProcA(window, message, wparam, lparam);
            }
            _ => ()
        }
        // CallWindowProcW(PREV_WNDPROC, window, message, wparam, lparam)
        DefSubclassProc(window, message, wparam, lparam)
    }
}

Doing a cargo run --bin create_window should have a few unused import warnings, but yield a:

Pasted image 20230309102433.png

Well, that was pretty easy!

Now let's do the same with lib.rs:

Refactoring hello.dll

Do stuff...

cargo run --bin create_window results in:

Pasted image 20230309102433.png

Yay!

hello_inject

cargo run --bin hello_inject doesn't work. Why?

To understand, let's talk about Debugging.

Further Reading

Debugging

Debugging DLLs in any language isn't amazing. Debugging DLLs in Rust is... rudimentary. Let's explore our options, starting with, logging!

Logging

Up until know we've been able to eyeball success vs failure as we're doing stuff with visible side effects.

Now that we've reached the part where... we're going to need to log.

Luckily, the simple-logger crate offers drop-dead simple logging. Add it to the hello.dll crate with a:

cargo add log simple-logger --package hello

And add this to lib.rs:

hello\src\lib.rs
use log::{LevelFilter, info};

fn attach() {
    unsafe {
        simple_logging::log_to_file("C:\\temp\\hello.dll.log", LevelFilter::Info);

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

Tracing

Process Monitor

Pasted image 20230309112332.png


SetWindowSubclass (cont.)

Now that we're certain that SetWindowSubclass is not going to work across threads... we need to figure out how to call SetWindowSubclass from our window's GUI thread.

Enter this nugget from Raymond Chen:

Recall that when an event occurs on a thread, the window hook is called from the same thread that the event occurred on. For example, a WH_CALL­WND­PROC hook procedure is called when a window procedure is about to be called, and the call occurs on the thread that is about to call the window procedure. ... Anyway, if you have a window hook that can be installed per-thread, then it will be installed only for events on that thread. In the above example, it means that only window procedures on that thread will trigger the hook.


SetWindowsHookEx

Let's start by reading SetWindowHookEx.

Great, now let's start refactoring hello.dll, starting with attach():

hello\src\lib.rs
let gui_tid = GetWindowThreadProcessId(handle, None);

let hook = SetWindowsHookExA(
    WH_CALLWNDPROC,
    Some(call_wnd_proc),
    None,
    gui_tid
);

Now let's add call_wnd_proc below attach:

hello\src\lib.rs
#[no_mangle]
unsafe extern "system"
fn call_wnd_proc(n_code: i32, w_param: WPARAM, l_param: LPARAM) -> LRESULT {
    if HC_ACTION as i32 == n_code {
        let origin = w_param.0 as u32;
        let param = unsafe { *(l_param.0 as *const CWPSTRUCT) };

        match param.message {
            WM_SIZING => info!("CallWndProc: Received WM_SIZING"),
            WM_PAINT => {
                info!("CallWndProc: Received WM_PAINT");
                SetWindowSubclass(param.hwnd,
                    Some(wnd_proc),
                    0,
                    GUI_TID as _
                );
            },

            _ => ()
        };
    }

    CallNextHookEx(HHOOK::default(), n_code, w_param, l_param)
}

Further Reading

CreateThread

Read CreateThread.

The ExitProcess, ExitThread, CreateThread, CreateRemoteThread functions, and a process that is starting (as the result of a CreateProcess call) are serialized between each other within a process. Only one of these events can happen in an address space at a time. This means the following restrictions hold:

- During process startup and DLL initialization routines, new threads can be created, but they do not begin execution until DLL initialization is done for the process.
- Only one thread in a process can be in a DLL initialization or detach routine at a time.
- ExitProcess does not return until no threads are in their DLL initialization or detach routines.

Let's refactor attach() so that it creates a new thread:

hello\src\lib.rs
fn attach() {
    unsafe {
        THREAD_HANDLE = CreateThread(
            None,
            0,
            Some(worker_thread),
            None,
            THREAD_CREATION_FLAGS(0),
            None);
    }

    info!("Finished hooking!");
}

And let's create worker_thread():

hello\src\lib.rs
unsafe extern "system" fn worker_thread(_data: *mut std::ffi::c_void) -> u32 {
    unsafe {
        let handle = find_window_by_pid(GetCurrentProcessId()).unwrap();
        GUI_TID = GetWindowThreadProcessId(handle, None);

        let logfile = format!("C:\\Users\\me\\source\\blog_qa\\hello.log");
        simple_logging::log_to_file(logfile, LevelFilter::Info);

        info!("hello.dll attached - tid: {} / pid: {} / target tid: {}",
            GetCurrentThreadId(),
            GetCurrentProcessId(),
            GUI_TID
        );

        let hook = SetWindowsHookExA(
            WH_CALLWNDPROC,
            Some(call_wnd_proc),
            None,
            GUI_TID);

        HOOKS.push(hook);

        while true {
            thread::sleep(time::Duration::from_secs(100));
        };
    };

Production Readiness

Error Handling


Last update: 2023-05-04