前回までの記事 に続けて、今回も Rust で Windows API を呼び出すプログラムを色々作ってみようと思います。
今回は Rust でスレッドを発行し、そのスレッドに登録した APC 関数を発火させるプログラムを作成してみました。
もくじ
Windows の Asynchronous Procedure Calls (APC) について
Windows の Asynchronous Procedure Calls (APC) は特定のスレッドで非同期に処理を実行することができる仕組みの 1 つです。
APC は、ユーザーモードプログラムとシステムコードを特定のユーザーのスレッドのコンテキスト(特定のプロセスの実行アドレス領域)で実行することができる仕組みです。
参考:非同期プロシージャ呼び出し - Win32 アプリ | Microsoft Learn
APC はカーネルオブジェクトである「APC オブジェクト」によって表されており、スレッドごとに 2 つずつ存在している APC キューのどちらかに登録され、実行を待機します。
各スレッドが保持する 2 つの APC キューのうち 1 つはカーネル APC 用のキューで、もう 1 つはユーザー APC 用のキューです。
これらのキューに登録された APC オブジェクトは、ソフトウェア割り込みによって実行されるのを待つことになります。
APC の種類
APC には大きく分けて「ユーザーモード APC」と「カーネルモード APC」の 2 つのモードがあります。
「ユーザーモード APC」はアプリケーションによって生成された APC で、「カーネルモード APC」はシステムによって生成された APC を指します。
また、それぞれに「Normal」と「Special」な APC が存在しています。
各モードの動作の違いについてはインサイド Windows 第 7 版下巻の P.67 ~ P.69 までの間にかなり詳しく記載がされていますが、今回着目したいポイントではないので割愛します。
APC の実行タイミング
APC は通常、スレッドが Alertable な状態となった際に実行されます。
特にユーザーモード APC の場合は以下の Docs に記載の通り、 SleepEx などの呼び出しによってスレッドが Alertable な状態に移行すると、APC 関数が実行されます。
参考:非同期プロシージャ呼び出し - Win32 アプリ | Microsoft Learn
今回は、このユーザーモード APC を使用するプログラムを Rust で実装することを目指します。
ユーザーモード APC の登録方法
ユーザーモード APC の登録は非常に簡単で、実行したい APC 関数へのポインタである PAPCFUNC と APC を登録したいスレッドのハンドルを引数として QueueUserAPC API を発行すれば OK です。
参考:QueueUserAPC 関数 (processthreadsapi.h) - Win32 apps | Microsoft Learn
これで、対象のスレッドが Alertable な状態に変化した際に APC 関数が実行されるようになります。
作成したユーザーモード APC を登録するコード
今回は ユーザーモード APC の動作を確認するため、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;
}
モジュールのロード
今回は GetCurrentThread/GetCurrentThreadId/QueueUserAPC/SleepEx の 4 つの API 関数を使用したかったので、これらすべてを含む windows::Win32::System::Threading
を依存関係に追加しました。
windows = { version = "0.60.0", features = [
"Win32_System_Threading"
] }
参考:windows::Win32::System::Threading - Rust
また、Rust の標準ライブラリから thread と time を使用するため、以下の各モジュールをロードしています。
use std::{thread, time};
use windows::Win32::System::Threading::{GetCurrentThread, GetCurrentThreadId, QueueUserAPC, SleepEx};
Alertable な関数の実装
続けて、non_alertable_func
と alertable_func
という 2 つの関数を定義しています。
これらはそれぞれ 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
関数の方では、標準ライブラリの thread::sleep(sleep_time);
を使用して、Non Alertable な方法で Sleep を行っています。
一方で、alertable_func
関数では Windows API の SleepEx を使用して Sleep を発行しており、スレッドが Alertable な状態に遷移するようになります。
APC 関数の定義
以下は、APC により実行したい APC 関数の定義です。
Windows API への引数として使用するため extern "system"
を使用しています。
extern "system" fn apc_func(_: usize) {
println!("User mode APC triggered !!");
return;
}
また、QueueUserAPC の引数となる PAPCFUNC 型の定義に合わせて引数を設定しています。
参考:PAPCFUNC in windows::Win32::Foundation - Rust
スレッドの作成と APC 関数の登録
最後に、Rust の thread::spawn
によるスレッドの作成と QueueUserAPC による APC 関数の登録を行っています。
今回は Rust の勉強も兼ねて 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();
Rust の thread モジュールの spawn で生成されたスレッドは JoinHandle というオブジェクトを返すため、これを .join()
で待機することでスレッドの完了を待つことができるようです。
なお、この JoinHandle は Windows のスレッドハンドルとは異なるため、QueueUserAPC の引数とする APC 関数を登録するスレッドハンドルの取得のために、スレッド内で GetCurrentThread API を実行しています。
最後に、APC 関数とスレッドハンドルを引数として QueueUserAPC を実行することで APC の登録を行っています。
この時、QueueUserAPC の第 1 引数には Option 列挙型の PAPCFUNC を渡す必要があるので、前項で定義した apc_func 関数を Some でラップしています。(Option 列挙型の概念と Some についてはいまいちよく理解できていない、、、)
参考:QueueUserAPC in windows::Win32::System::Threading - Rust
Rust バイナリの解析
Rust で Windows API を実装するのと同時にビルドしたバイナリを解析しようと思っていたのですがすっかり忘れてたので今回からやっていきます。
main 関数を探す
とりあえず PDB なしでデコンパイルしたときの main 関数は以下のように出力されました(明らかに main 関数の実装ではない、、、)
実際には、lea で rcx にロードされている関数が本来の main 関数で、これを引数として call で実行されている関数(0x2a90) が lang_start 関数になっているようです。
参考:Who calls lang_start? - community - The Rust Programming Language Forum
実際にこの関数を確認してみると、条件分岐や unwrap 関数を使用してそうに見える箇所もあり、ここが main 関数と考えてよさそうです。
ちなみに Binary Ninja には疑似 Rust デコンパイラモードが存在しているので今回はそれを使用しているのですが、変数宣言が全部 let に置き換わっているくらいであまり疑似 C コードとの差異は実感できませんでした。
スレッドの作成
デコンパイル結果の一番初めに実行されている関数を確認してみると、failed to spawn thread
というテキストが埋まっていることに気づきます。
このテキストは、スレッドの作成に使用した spawn 関数の実装に含まれるものであると考えられます。
正直これ以外の情報からこの関数が thread::spawn 関数であると判断するのは無理そうでした。(逆アセンブル結果やデコンパイル結果を見ても意味わからん)
決め打ちで考えると、この関数が thread::spawn 関数ということは var_90 には JoinHandle が格納されていると判断できます。
そのため、JoinHandle を引数とするこの関数は、ここでは non_alertable_thread_handle.join().unwrap()
であると予想が立ちます。
正確には、この関数の返り値が 0 ではない場合には called Result::unwrap() on an Err
という unwrap 関数内で panic! が呼び出された場合のエラーコードが埋め込まれていることから、上記の関数は join() までであり、以下の分岐の中身が unwrap() であると考えることができそうです。
ちなみに、この時作成したスレッド内で実行するはずのコードについては、ここまでの解析結果には含まれていませんでした。
どこで実行されるのかと思って調べてみたところ、以下の spawn っぽい関数の 1 つ目の call 関数の中でさらに参照されているオブジェクトをたどっていった中に埋まっていることはわかりました。
ただ、この辺の詳細を確認するにはシンボル付きバイナリを確認しても難しかったので、とりあえず断念しました。
たぶん Rust のモジュールそのものの実装を読まないとだめそうです。
まとめ
今回は Rust でスレッドを作成し QueueUserAPC で任意の関数を実行するコードを作成してみました。
また、デコンパイラで Rust バイナリの解析も行ってみましたが、シンボル付きでもわけわからない部分が多かったので、これどうやって解析するの?って感じでした。
恐らくですが、Rust のモジュールそのものの実装に詳しければもう少し guess できる余地がありそうに思います。
今後 Rust の解析のノウハウも蓄積していきたいと思います。