
Shelterは、完全に武器化されたスリープ難読化手法であり、ROPを多用してインメモリペイロードを完全に暗号化することができます。
このクレートには以下の特徴があります:
このクレートをプロジェクトにインポートするには、cargo.tomlに以下の行を追加します:
[dependencies]
shelter = "=0.1.2"
その後、プロジェクトを--releaseモードでコンパイルします。
このクレートの主な機能は、次の3つの関数にラップされています:
fluctuate() - 現在のメモリ領域またはPE全体を暗号化します。この関数は、ベースアドレスを動的に取得するためにPEのMZバイトが存在する必要があります。fluctuate_from_address() - PE全体を完全に暗号化します。この関数は、入力パラメータとしてPEのベースアドレスを期待します。fluctuate_from_pattern() - PE全体も完全に暗号化します。この関数は、入力パラメータとして、PEのベースアドレスを特定するためのカスタム2バイトセットを期待します。このカスタムマジックバイトは、従来のMZパターンを置き換えます。PE全体が暗号化されると、元のセクションのメモリ保護がヒープに保存され、後で復元されます。
ShelterはスリープのためにNtWaitForSingleObjectを使用します。スリープ時間(秒)を指定するだけでなく、イベントハンドルを渡し、任意のタイミングでシグナルを送ることでタイムアウト前に復帰させることもできます(例えばSetEventを使用)。ペイロード全体が暗号化されている場合(おそらくそれが目的でしょう)、無制限にスリープする場合にはイベントをシグナルする別の方法が必要になることに注意してください。
この関数は以下のパラメータを期待します:
trueを渡す場合、メモリ内にMZバイトが存在する必要があります。Noneにするとタイムアウトは無限になり、NtWaitForSingleObjectに渡されたイベントがシグナルされるまで実行は戻りません。Noneにできます。このパラメータとタイムアウトの両方をNoneにするとプログラムは停止します。let time_to_sleep = Some(10); // 10秒間スリープ
let _ = shelter::fluctuate(false, time_to_sleep, None); // 現在のメモリ領域のみを暗号化
let time_to_sleep = Some(10); // 10秒間スリープ
let _ = shelter::fluctuate(true, time_to_sleep, None); // PE全体を暗号化
pub type CreateEventW = unsafe extern "system" fn (*const SECURITY_ATTRIBUTES, i32, i32, *const u16) -> HANDLE;
let k32 = dinvoke_rs::dinvoke::get_module_base_address("kernel32.dll");
let create_event: CreateEventW;
let event_handle: Option<HANDLE>;
dinvoke_rs::dinvoke::dynamic_invoke!(k32,"CreateEventW",create_event,event_handle,ptr::null_mut(),0,0,ptr::null());
let time_to_sleep = None; // 無制限にスリープ
let _ = shelter::fluctuate(true, time_to_sleep, event_handle); // イベントがシグナルされるまでPE全体を暗号化
この関数は以下のパラメータを期待します:
Noneにするとタイムアウトは無限になり、NtWaitForSingleObjectに渡されたイベントがシグナルされるまで実行は戻りません。Noneにできます。このパラメータとタイムアウトの両方をNoneにするとプログラムは停止します。この関数を使用する方法の1つは、Dinvoke_rsでペイロードを手動マッピングすることです。これにより、ローダーはペイロードに自身のベースアドレスを送信できるため、ペイロードは必要に応じて自身を難読化できます。この方法により、ローダーは特定のステルス性を達成するためにPEのヘッダーを安全に削除できます。
ローダーの例:
let payload: Vec<u8> = your_download_function();
let mut m = dinvoke_rs::manualmap::manually_map_module(payload.as_ptr(), true).unwrap();
println!("The dll is loaded at base address 0x{:x}", m.1);
let dll_exported_function = dinvoke::get_function_address(m.1, "run");
let run: unsafe extern "Rust" fn (usize) = std::mem::transmute(dll_exported_function);
run(m.1 as usize);
ペイロードの例:
#[no_mangle]
fn run(base_address: usize)
{
...
let time_to_sleep = Some(10); // 10秒間スリープ
let _ = shelter::fluctuate_from_address(time_to_sleep, None, base_address); // この特定のベースアドレスからPE全体を暗号化
...
}
この関数は以下のパラメータを期待します:
Noneにするとタイムアウトは無限になり、NtWaitForSingleObjectに渡されたイベントがシグナルされるまで実行は戻りません。Noneにできます。このパラメータとタイムアウトの両方をNoneにするとプログラムは停止します。[u8;2]配列。この関数を作成した目的は、ローダーがPEのヘッダーや従来のMZバイトを含むその他のシグネチャを削除できるようにすることです。これにより、それらのバイトをShelterがPEのベースアドレスを取得するために検索するカスタムパターンに置き換えることができます。
let time_to_sleep = Some(10); // 10秒間スリープ
let pattern = [0x29,0x07];
let _ = shelter::fluctuate_from_pattern(time_to_sleep, None, pattern); // カスタムパターンをマジックバイトとして使用してPE全体を暗号化
この技術の実装をテストするために、主にPE-sieveが使用されています。デフォルトでは、PE-sieveは実行可能メモリ領域内のインプラントを探します。つまり、現在のメモリ領域(.text)のみを難読化するだけでも検出を回避するのに十分です:

Unwinderを使用しているため、コールスタックが偽装されており、そのため/threadsフラグでもマッピングされたDLLは検出されません。
さて、PE-sieveでは/dataフラグを使用して非実行可能メモリ領域も検査できます。ツールの公式ドキュメントによると、このフラグをalwaysに設定すると「多くのノイズや誤検出が発生する可能性があります」。それにもかかわらず、PE全体の暗号化機能の有効性を確認するためにこのフラグを使用することにしました。これは、インメモリインプラントの存在を示す可能性のあるPEのデータ領域(MZ、DOSヘッダー、セクション名など)を隠すことができるからです。

見ての通り、最初の画像では、PE-sieveが非実行可能メモリページをスキャンする場合、.textセクションのみを難読化するだけでは不十分であることが示されています。これは、一部の領域にDLLの存在を明らかにする文字列(MZ、DOSヘッダー、セクション名など)が含まれている可能性があるためです。一方、2番目の画像では、ShelterのPE全体の難読化メカニズムを使用することでこの問題を解決できることが示されています。ただし、PE-sieveのwikiにも記載されているように、このオプションは大量の誤検出を引き起こします。なぜなら、ヒープ内に".data"や"rdata"などの文字列が存在するだけでも、実際のPEコンテンツがその領域に存在しないにもかかわらず、インプラントされたPEの可能性を警告するからです(その領域から何かをダンプすることはできませんが)。
最後に、PE-sieveには、高エントロピーのメモリ領域を探すことで難読化されたインプラントを検出する比較的新しいオプションがあります。このオプション(/obfusc)と/dataを組み合わせると、ペイロードを含むメモリ領域の高エントロピーによりペイロードの存在を検出できます(ただし、完全に暗号化されているためPEを取得することはできません):
Shelterはすぐに使用でき、OPSECを考慮して開発されていますが、近い将来に追加される予定のいくつかの改良点があります:
BCryptEncrypt/BCryptDecryptを対応するNt関数に置き換える。