This page has been machine-translated from the original page.
Continuing from the previous articles, I wanted to keep trying out various programs that call the Windows API from Rust.
This time, I wrote a program in Rust that creates a thread and triggers an APC function registered to that thread.
Table of contents
About Windows Asynchronous Procedure Calls (APC)
Windows Asynchronous Procedure Calls (APC) are one of the mechanisms that can execute work asynchronously on a specific thread.
APCs are a mechanism that can run user-mode programs and system code in the context of a specific user thread (that is, within the execution address space of a particular process).
Reference: Asynchronous Procedure Calls - Win32 apps | Microsoft Learn
APCs are represented by kernel objects called APC objects, which are registered in one of the two APC queues that exist for each thread and wait there to be executed.
Of the two APC queues that each thread maintains, one is a queue for kernel APCs and the other is a queue for user APCs.
The APC objects registered in these queues wait to be executed via a software interrupt.
Types of APCs
Broadly speaking, APCs come in two modes: user-mode APCs and kernel-mode APCs.
A user-mode APC is an APC generated by an application, while a kernel-mode APC is an APC generated by the system.
In addition, each mode has both Normal and Special APCs.
The differences in behavior between each mode are described in considerable detail on pp. 67-69 of Inside Windows, 7th Edition, Part 2, but that is not the point I want to focus on here, so I will omit it.
When APCs run
APCs are usually executed when a thread enters an Alertable state.
In particular, for user-mode APCs, as described in the documentation below, the APC function runs when a call such as SleepEx moves the thread into an Alertable state.
Reference: Asynchronous Procedure Calls - Win32 apps | Microsoft Learn
This time, I will aim to implement a program in Rust that uses this user-mode APC mechanism.
How to register a user-mode APC
Registering a user-mode APC is very simple: just call the QueueUserAPC API with a PAPCFUNC, which is a pointer to the APC function you want to execute, and a handle to the thread where you want to register the APC.
Reference: QueueUserAPC function (processthreadsapi.h) - Win32 apps | Microsoft Learn
This makes the APC function execute when the target thread changes to an Alertable state.
Code for registering the user-mode APC I created
To verify the behavior of a user-mode APC, I wrote the following code in Rust.
// cargo run --bin UserModeApc
use std::{thread, time};
use windows::Win32::System::Threading::{GetCurrentThread, GetCurrentThreadId, QueueUserAPC, SleepEx};
extern "system" fn apc_func(_: usize) {
println!("User mode APC triggered !!");
return;
}
fn non_alertable_func() {
let sleep_time: time::Duration = time::Duration::from_millis(5000);
let now = time::Instant::now();
println!("Non alertable sleep started.");
thread::sleep(sleep_time);
assert!(now.elapsed() >= sleep_time);
println!("Non alertable sleep started.");
return;
}
fn alertable_func() {
let now = time::Instant::now();
// SleepEx による Alertable な Sleep
println!("Alertable sleep started.");
unsafe {
SleepEx(9999999, true);
}
// APC がコールさ SleepEx が中断されるので実際には長時間の Sleep は発生しない
println!("Alertable sleep fineshed in {} nanos.", now.elapsed().as_nanos());
return;
}
fn main() {
let non_alertable_thread_handle = thread::spawn(|| {
unsafe {
let h_spawned_thread = GetCurrentThread();
let spawned_thread_id = GetCurrentThreadId();
println!("Thread id {} started.", spawned_thread_id);
// Add User APC into queue
if QueueUserAPC(Some(apc_func), h_spawned_thread, 0) != 0 {
println!("Add User mode APC into non alertable thread id {}.", spawned_thread_id);
}
}
// Non alertable なスレッドを作成
non_alertable_func();
});
non_alertable_thread_handle.join().unwrap();
let alertable_thread_handle = thread::spawn(|| {
unsafe {
let h_spawned_thread = GetCurrentThread();
let spawned_thread_id = GetCurrentThreadId();
println!("Thread id {} started.", spawned_thread_id);
// Add User APC into queue
if QueueUserAPC(Some(apc_func), h_spawned_thread, 0) != 0 {
println!("Add User mode APC into alertable thread id {}.", spawned_thread_id);
}
}
// Alertable なスレッドを作成
alertable_func();
});
alertable_thread_handle.join().unwrap();
return;
}Loading modules
This time I wanted to use the four API functions GetCurrentThread, GetCurrentThreadId, QueueUserAPC, and SleepEx, so I added windows::Win32::System::Threading, which includes all of them, as a dependency.
windows = { version = "0.60.0", features = [
"Win32_System_Threading"
] }Reference: windows::Win32::System::Threading - Rust
Also, because I use thread and time from Rust’s standard library, I load the following modules.
use std::{thread, time};
use windows::Win32::System::Threading::{GetCurrentThread, GetCurrentThreadId, QueueUserAPC, SleepEx};Implementing alertable functions
Next, I define two functions: non_alertable_func and alertable_func.
Each of these functions is executed on a separate thread via thread::spawn.
fn non_alertable_func() {
let sleep_time: time::Duration = time::Duration::from_millis(5000);
let now = time::Instant::now();
println!("Non alertable sleep started.");
thread::sleep(sleep_time);
assert!(now.elapsed() >= sleep_time);
println!("Non alertable sleep started.");
return;
}
fn alertable_func() {
let now = time::Instant::now();
// SleepEx による Alertable な Sleep
println!("Alertable sleep started.");
unsafe {
SleepEx(9999999, true);
}
// APC がコールされて SleepEx が中断されるので実際には長時間の Sleep は発生しない
println!("Alertable sleep fineshed in {} nanos.", now.elapsed().as_nanos());
return;
}non_alertable_func uses the standard library’s thread::sleep(sleep_time); to sleep in a non-alertable way.
By contrast, alertable_func performs its sleep with the Windows API SleepEx, which causes the thread to transition into an Alertable state.
Defining the APC function
The following is the definition of the APC function that I want to execute via an APC.
Because it is used as an argument to the Windows API, I use extern "system".
extern "system" fn apc_func(_: usize) {
println!("User mode APC triggered !!");
return;
}I also set the argument to match the definition of the PAPCFUNC type used as an argument to QueueUserAPC.
Reference: PAPCFUNC in windows::Win32::Foundation - Rust
Creating the thread and registering the APC function
Finally, I create the thread with Rust’s thread::spawn and register the APC function with QueueUserAPC.
This time I also wanted more Rust practice, so I tried creating a thread using something other than the Windows API CreateThread.
let alertable_thread_handle = thread::spawn(|| {
unsafe {
let h_spawned_thread = GetCurrentThread();
let spawned_thread_id = GetCurrentThreadId();
println!("Thread id {} started.", spawned_thread_id);
// Add User APC into queue
if QueueUserAPC(Some(apc_func), h_spawned_thread, 0) != 0 {
println!("Add User mode APC into alertable thread id {}.", spawned_thread_id);
}
}
// Alertable なスレッドを作成
alertable_func();
});
alertable_thread_handle.join().unwrap();A thread created by spawn in Rust’s thread module returns an object called JoinHandle, so it seems that you can wait for the thread to finish by calling .join() on it.
Because this JoinHandle is different from a Windows thread handle, I call the GetCurrentThread API inside the thread to obtain the thread handle needed to register the APC function with QueueUserAPC.
Finally, I register the APC by calling QueueUserAPC with the APC function and the thread handle as arguments.
At this point, the first argument to QueueUserAPC needs an Option-wrapped PAPCFUNC, so I wrap the apc_func function defined in the previous section in Some. (I still do not fully understand the concept of the Option enum and Some…)
Reference: QueueUserAPC in windows::Win32::System::Threading - Rust
Analyzing the Rust binary
I had intended to analyze the binary I built at the same time as implementing the Windows API in Rust, but I completely forgot, so I will start doing that from this article onward.
Finding the main function
For now, when I decompiled without a PDB, the main function was output as follows (it clearly is not the actual implementation of main…).
In reality, the function loaded into rcx by lea seems to be the actual main function, and the function called with that as an argument (0x2a90) appears to be the lang_start function.
Reference: Who calls lang_start? - community - The Rust Programming Language Forum
When I actually checked that function, there were places that looked like they used conditional branches and the unwrap function, so it seems reasonable to treat it as the main function.
By the way, Binary Ninja has a pseudo-Rust decompiler mode, so I used it this time, but aside from replacing every variable declaration with let, I did not really feel much difference from the pseudo-C output.
Creating the thread
If you check the function executed at the very beginning of the decompiled result, you notice that the text failed to spawn thread is embedded in it.
This text is likely part of the implementation of the spawn function used to create the thread.
Reference: mod.rs - source
Honestly, it seemed impossible to determine from any other information that this function was thread::spawn. (Even looking at the disassembly results and the decompiled output, I could not make sense of it.)
If I assume that up front, then if this function is thread::spawn, I can conclude that var_90 stores a JoinHandle.
That means this function, which takes a JoinHandle as an argument, is probably non_alertable_thread_handle.join().unwrap() here.
More precisely, because the string embedded when this function’s return value is not 0 is the error text used when panic! is called inside unwrap—called Result::unwrap() on an Err—it seems likely that the function above corresponds to join() up to that point, and that the contents of the following branch are unwrap().
Incidentally, the code that should have been executed inside the thread created at this point was not included in the analysis results up to here.
When I looked into where it was executed, I found that it was embedded among the objects referenced further down from the first call inside the following function that looked like spawn.
However, understanding the details around this area was still difficult even when checking a binary with symbols, so I gave up for the time being.
It probably means I need to read the implementation of Rust’s modules themselves.
Summary
This time, I wrote code in Rust that creates a thread and executes an arbitrary function with QueueUserAPC.
I also tried analyzing the Rust binary with a decompiler, but even with symbols there were many parts that I could not make sense of, so my reaction was basically, “How are you supposed to analyze this?”
I suspect that if I were more familiar with the implementation of Rust’s modules themselves, there would be a bit more room for informed guessing.
Going forward, I would like to build up more know-how for analyzing Rust.