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 マクロについて説明したのと同じ方法で動作します)。