Unwinder は SilentMoonWalk 技術の完全な兵器化を提供し、Rust で完全かつ安定したコールスタックスプーフィングを実現します。
この技術には以下の特徴があります:
SilentMoonWalk 技術の作者に感謝します:
そしてもちろん、このプロジェクト全体にインスピレーションを与えた Twitter スレッド を公開した namazso に多大な感謝を捧げます。
このクレートをプロジェクトにインポートするには、cargo.toml に次の行を追加し、release モードでコンパイルしてください:
[dependencies]
unwinder = "=0.1.4"
このクレートの主な機能は2つのマクロにまとめられています:
call_function!() マクロは、クリーンなコールスタックで任意の関数を実行できます。indirect_syscall!() マクロは、指定された(間接)システムコールをクリーンなコールスタックで実行します。これらのマクロを使用するには、std::ffi::c_void データ型をインポートする必要があります。
どちらのマクロも *mut c_void を返し、実行された関数が返す値を取得するために使用できます。詳細は例のセクションを参照してください。
このマクロは、クリーンなコールスタックで任意の関数を呼び出すために使用します。 このマクロは以下のパラメータを期待します:
usize、isize、またはポインタとして渡す必要があります。このマクロは、クリーンなコールスタックで任意の間接システムコールを実行するために使用します。 このマクロは以下のパラメータを期待します:
これら2つのマクロにさまざまな型の引数を渡すには、以下の点に注意する必要があります:
usize に変換できる任意の基本データ型(u8-u64、i8-i64、bool など)は、マクロに直接渡すことができます。&str と String)は、ポインタとして渡す必要があります。ptr::null()、ptr::null_mut() など)は 0 として渡されます(u8、u16、i32 など、どの型でも同じです)。let k32 = dinvoke_rs::dinvoke::get_module_base_address("kernel32.dll");
let sleep = dinvoke_rs::dinvoke::get_function_address(k32, "Sleep"); // Memory address of kernel32.dll!Sleep()
let miliseconds = 1000i32;
unwinder::call_function!(sleep, false, miliseconds);
let k32 = dinvoke_rs::dinvoke::get_module_base_address("kernel32.dll");
let open_process: isize = dinvoke_rs::dinvoke::get_function_address(k32, "Openprocess");
let desired_access: u32 = 0x1000;
let inherit = 0i32;
let pid = 20628i32;
let handle = unwinder::call_function!(open_process, false, desired_access, inherit, pid); // returns *mut c_void
let handle: HANDLE = std::mem::transmute(handle);
println!("Handle id: {:x}", handle.0);
このマクロは *mut c_void を返しますが、これは両方のデータ型のサイズが同じであるため、直接 HANDLE に変換できます。これにより、OpenProcess が返す値(対象プロセスへの新しいハンドル)にアクセスできます。
let large = 0x8000000000000000 as u64; // Sleep indefinitely
let large: *mut i64 = std::mem::transmute(&large);
let alertable = false;
let ntstatus = unwinder::indirect_syscall!("NtDelayExecution", false, alertable, large); // returns *mut c_void
println!("ntstatus: {:x}", ntstatus as i32);
このマクロは *mut c_void を返し、NtDelayExecution が返す NTSTATUS を取得するために使用できます。
スプーフィング処理は、コールスタックサイズが異常に増加することなく、何度でも連結できます。実行フローも維持されます。以下はその例です:
fn main()
{
function_a();
}
fn function_a()
{
unsafe
{
let func_b = function_b as usize;
call_function!(func_b, false);
println!("function_a done.");
}
}
fn function_b()
{
unsafe
{
let func_c = function_c as usize;
call_function!(func_c, false);
println!("function_b done.")
}
}
fn function_c()
{
unsafe
{
let large = 0x0000000000000000 as u64; // Don't sleep so we return to function_b, allowing to check the execution flow preservation.
let large: *mut i64 = std::mem::transmute(&large);
let alertable = false;
let ntstatus = unwinder::indirect_syscall!("NtDelayExecution", false, alertable, large);
println!("ntstatus: {:x}", (ntstatus as usize) as i32); //NTSTATUS is a i32, although that second casting is not really required in this case.
}
}
2つ目のパラメータを true に設定すると(両マクロとも)、スプーフィング処理はコールスタック内にスレッド開始アドレスのフレームを保持して、正当性を高めようとします。

スレッドの開始関数が後続の関数への call を実行しない場合(代わりに jmp 命令が実行される場合など)、スタックにリターンアドレスがプッシュされないことになります。そのようなシナリオでは(2つ目のパラメータを false に設定した場合も)、スプーフィングされたコールスタックは BaseThreadInitThunk のフレームから始まります。

この技術の実装をテストするために、PE-sieve を /threads フラグ付きで使用しました。テスト結果は、このクレートの機能を使用した場合、コールスタックの検査によってペイロードの存在が明らかにならないことを示しています。2枚目の画像でわかるように、unwinder を使用しない場合はペイロードが検出されます。

これは SilentMoonWalk に代わるコールスタックスプーフィング手法で、プログラムの実行中にクリーンなコールスタックを維持できます。この技術の背後にある主なアイデアは、モジュール内で呼び出される各関数が、以前にプッシュされたリターンアドレスを処理し、スプーフィング対象のリターンアドレスと同じフレームサイズを持つ正当な関数を実行時に見つけるというものです。同じフレームサイズを持つ正当な関数が見つかると、その関数内のオフセットが計算され、最終的なアドレスを使用して最後のリターンアドレスを置換し、コールスタック内の異常なエントリを隠して、アンワインド可能な状態を維持します。元のリターンアドレスは unwinder によって保存され、return 命令が実行される前にスタック上の正しい位置に戻されるため、プログラムの通常のフローを継続できます。
これは実験的な機能であり、完全に機能するものの、まだ開発と研究が進行中です。この技術をコードに統合する場合は、必ずテストしてください。
スタック置換機能を使用するには、cargo.toml に次の行を追加し、release モードでコンパイルしてください:
[dependencies]
unwinder = {version = "0.1.4", features = ["Experimental"]}
この機能の主な機能は次のマクロにまとめられています:
start_stack_replacement!()/end_replacement!() のマクロペアは、unwinder にスタック置換プロセスの開始/終了を指示します。これらの2つのマクロは、コードのエントリポイント(例: DLL のエクスポート関数)で呼び出す必要があります。replace_and_continue!()/restore!() のマクロペアは、最後のリターンアドレスの置換/復元を実行します。replace_and_call!()/replace_and_syscall!() のマクロペアは、現在のモジュール外の関数を呼び出すときにスタック置換を実行するために使用されます(例: Windows API を使用する場合、または他の DLL のコードを呼び出す場合)。これらのマクロはどちらも、この方法で呼び出された関数が返す値を含む *mut c_void を返します(つまり、SilentMoonWalk を実行するために使用される call_function および indirect_syscall マクロについて説明したのと同じ方法で動作します)。これらのマクロを使用するには、std::ffi::c_void データ型をインポートする必要があります。
これらのマクロを使用するすべての関数には、最適化プロセス中に Rust コンパイラがインライン化しないように、#[no_mangle] または #[inline(never)] 属性を付ける必要があります。
これらすべての使用方法を示す実践的な例に入る前に、replace_and_call/replace_and_syscall のマクロペアと、期待される引数を渡す方法を簡単に確認しましょう。
このマクロは、スタック置換を使用しながら、現在のモジュール外の任意の関数をクリーンなコールスタックで呼び出すために使用します。 このマクロは以下のパラメータを期待します:
usize、isize、またはポインタとして渡す必要があります。このマクロは、スタック置換を使用しながら、クリーンなコールスタックで任意の間接システムコールを実行するために使用します。 このマクロは以下のパラメータを期待します:
これらのマクロの使用方法を示す最良の方法は、実践的な例だと思います。メモリにリフレクティブ注入される DLL を作成していると仮定しましょう。この DLL は ExportA と ExportB の2つの関数をエクスポートするため、これらの2つの関数をモジュールのエントリポイントと見なします。どちらの関数も、先頭で start_stack_replacement マクロを呼び出し、戻る前に逆の end_replacement マクロを呼び出す必要があります。start_stack_replacement マクロは引数としてモジュールのベースアドレスを期待します。実行時にそのアドレスが分からない場合は 0 を渡してもかまいません。マクロが自分でそれを見つけようとします。
#[no_mangle]
fn ExportedA(base_address: usize) -> bool
{
unwinder::start_replacement!(base_address);
...
unwinder::end_replacement!();
true
}
#[no_mangle]
fn ExportedB() -> bool
{
unwinder::start_replacement!(0);
...
unwinder::end_replacement!();
true
}
スタック置換プロセスの開始には、end_replacement マクロが呼び出されるまで使用される新しいスタックを手作業で構築することが含まれます。次の図は、内部で何が行われているかを示しています:
理論的にはゼロから新しいスタックを開始する必要はありませんが、安定性を確保して何も壊れないようにするために、この方法でプロセスを実装することにしました。
次に、ExportedA 関数が別の2つの内部関数を複数回呼び出すと仮定しましょう。これらの2つの内部関数は、ExportedA 内のどこかを指す元のリターンアドレスの置換/復元を担当します。これに対処しないとコールスタックが壊れます。この置換プロセスでは、内部関数のコードを replace_and_continue マクロと restore マクロで囲みます:
#[no_mangle]
fn ExportedA(base_address: usize) -> bool
{
unwinder::start_replacement!(base_address);
let ret_a = internal_a();
let ret_b = internal_b(ret_a);
unwinder::end_replacement!();
ret_b
}
#[inline(never)] // This attribute is mandatory
fn internal_a() -> bool
{
unwinder::replace_and_continue();
...
unwinder::restore();
some_value
}
#[inline(never)] // This attribute is mandatory
fn internal_b(value: bool) -> bool
{
unwinder::replace_and_continue();
...
unwinder::restore();
some_value
}
最後に、internal_a と internal_b の両方の関数は、いくつかの Windows API 機能を利用します。アンワインド可能なコールスタックを維持するには、これらの呼び出しを replace_and_call(通常の呼び出し)または replace_and_syscall(間接システムコール)マクロを通じて実行する必要があります。
#[no_mangle] // This attribute is mandatory
fn ExportedA(base_address: usize) -> bool
{
unwinder::start_replacement!(base_address);
let ret_a = internal_a();
let ret_b = internal_b(ret_a);
unwinder::end_replacement!();
ret_b
}
#[inline(never)] // This attribute is mandatory
fn internal_a() -> bool
{
unwinder::replace_and_continue();
...
let module_name = "advapi32.dll";
let module_name = CString::new(module_name.to_string()).expect("");
let module_name_ptr: *mut u8 = std::mem::transmute(module_name.as_ptr());
let k32 = dinvoke_rs::dinvoke::get_module_base_address("kernel32.dll");
let load_library = dinvoke_rs::dinvoke::get_function_address(k32, "LoadLibraryA");
let ret = unwinder::replace_and_call!(load_library, module_name_ptr); // Load a dll with an unwindable call stack
println!("advapi.dll base address: 0x{:x}", ret as usize);
...
unwinder::restore();
some_value
}
#[inline(never)] // This attribute is mandatory
fn internal_b(value: bool) -> bool
{
unwinder::replace_and_continue();
...
let large = 0xFFFFFFFFFF676980 as u64; // Sleep one second
let large: *mut i64 = std::mem::transmute(&large);
let alertable = false;
let ntstatus = unwinder::replace_and_syscall!("NtDelayExecution", alertable, large);
println!("ntstatus: {:x}", ntstatus as usize);
...
unwinder::restore();
some_value
}
これは開発中の機能であるため、いくつか注意すべき点があります:
start_stack_replace マクロにモジュールのベースアドレスを渡す必要があります。現時点では、マクロはそれを自分で見つけることができません(次のアップデートで解決予定です)。jmp rbx + concealment フレームの組み合わせを使用します。これは replace_and_call および replace_and_syscall マクロを使用する場合にのみ発生し、次のアップデートで変更される予定です。replace_and_call および replace_and_syscall マクロはどちらも *mut c_void を返し、それを通じて実行された関数が返す値を取得するために使用できます。これは call_function および indirect_syscall マクロについて説明したのと同じ動作です。replace_and_call および replace_and_syscall マクロは最大11個の引数を許可します。この機能の使用中に発生したバグがあれば報告してください。