
Thread Stack Spoofing - スキャナーやアナリストから注入されたシェルコードのメモリ割り当てをより良く隠すことを可能にする、高度なインメモリ回避技術のPoC
スレッドコールスタックを偽装する高度なメモリ内回避手法のPoC実装です。この手法により、スレッドベースのメモリ検査ルールをバイパスし、プロセス内メモリ内でシェルコードをより適切に隠すことができます。
これは、マルウェアアナリスト、AV、EDRが調査対象のスレッドのコールスタック内でシェルコードのフレームへの参照を探すのを回避することを目的とした、_スレッドスタックスプーフィング_手法の実装例です。アイデアは、スレッドのコールスタック上でシェルコードへの参照を隠し、マルウェアのコードを含む割り当てを偽装することです。
私のShellcodeFluctuationと併せて、この実装は、商用C2製品が提供する機能に追いつくためのサンプル実装を攻撃的セキュリティコミュニティに提供し、Red Teamツールでも劣らないようにするものです。💪
現在の実装は、元公開されたものとは大きく異なります。これは、制御する最初のフレームの戻りアドレスに単に0を書き込むことで、スレッドのコールスタック処理を終了し、シェルコード関連のフレームを隠す、はるかにシンプルなアプローチがあることに気づいたためです:
void WINAPI MySleep(DWORD _dwMilliseconds)
{
[...]
auto overwrite = (PULONG_PTR)_AddressOfReturnAddress();
const auto origReturnAddress = *overwrite;
*overwrite = 0;
[...]
*overwrite = origReturnAddress;
}
StackWalk64を使用した以前の実装は、このコミット c250724でアクセスできます。
この実装ははるかに安定しており、DebugとReleaseの両方で、x64とx86の2つのアーキテクチャで良好に動作します。
これは、偽装されていない場合のコールスタックの例です:

次に、スレッドスタックスプーフィングが有効な場合:

上記では、コールスタックの最後のフレームがMySleepコールバックであることがわかります。これはすぐに新しいIOCの機会をもたらすのでしょうか?ハンティングルールでは、コールスタックがシステムライブラリ内にある以下の期待されるスレッドエントリポイントにアンワインドしないスレッドを探すことができます:
kernel32!BaseThreadInitThunk+0x14
ntdll!RtlUserThreadStart+0x21
しかし、偽装されたスレッドのコールスタックは一見かなり奇妙に見えるかもしれませんが、私のシステムを簡単に調べたところ、上記のエントリポイントにアンワインドしない他のスレッドも存在することがわかりました:

上記のスクリーンショットは、未修正のTotal Commander x64のスレッドを示しています。見ての通り、そのコールスタックは初期のコールスタックフレームの点で、ほぼ我々のものと似ています。
単に模倣できる特性を示すプロセスが存在するのに、なぜ注意深くコールスタックを偽装することを気にする必要があるのでしょうか?
大まかなアルゴリズムは次のとおりです:
dbghelp.dllから必要な関数ポインタをすべて取得し、SymInitializeを呼び出す。kernel32!Sleepをフックし、自分のコールバックを指すようにする。VirtualAlloc + memcpy + CreateThreadを介してシェルコードを注入して起動する。スレッドは、スレッドの_StartAddress_が予期しない異常な場所(ntdll!RtlUserThreadStart+0x21など)を指さないように、自分のrunShellcode関数から開始する必要があります。MySleepコールバックが呼び出される。0で上書きし、これにより実質的にコールスタックが終了する。::SleepExを呼び出して、Beaconがさらなる通信を待機中にスリープできるようにする。関数の戻りアドレスは、スレッドのスタックメモリ領域全体に散らばっており、RBP/EBPレジスタによって指されています。スタック上でそれらを見つけるには、まずフレームポインタを収集し、その後、上書きのためにそれらを間接参照する必要があります:

(上記の画像は、Eli Bendersky氏の記事Stack frame layout on x86-64から引用しました)
*(PULONG_PTR)(frameAddr + sizeof(void*)) = Fake_Return_Address;
ThreadStackSpooferの初期実装では、walkCallStack関数とspoofCallStack関数でこれを行っていましたが、現在の実装では、これらの努力は_ステルス性のあるコールスタックを維持するために必要ではない_ことが示されています。
使用例:
C:\> ThreadStackSpoofer.exe <shellcode> <spoof>
ここで:
<shellcode> はシェルコードファイルへのパス<spoof> が1またはtrueの場合、スレッドスタックスプーフィングが有効になり、それ以外は無効になります。Beaconのスレッドコールスタックを偽装する実行例:
PS D:\dev2\ThreadStackSpoofer> .\x64\Release\ThreadStackSpoofer.exe .\tests\beacon64.bin 1
[.] Reading shellcode bytes...
[.] Hooking kernel32!Sleep...
[.] Injecting shellcode...
[+] Shellcode is now running.
[>] Original return address: 0x1926747bd51. Finishing call stack...
===> MySleep(5000)
[<] Restoring original return address...
[>] Original return address: 0x1926747bd51. Finishing call stack...
===> MySleep(5000)
[<] Restoring original return address...
[>] Original return address: 0x1926747bd51. Finishing call stack...
コードとその実装を確認し、概念を理解した上で、Red Teamの活動を遂行するために使用する独自のシェルコードローダーに概念を再実装してください。これは、高度なメモリ内回避のためのもう一つのテクニックであり、チームがアンチウイルス、EDR、およびインプラントを調査するマルウェアアナリストに捕まる可能性を減らします。
高度なシェルコードローダーを開発する際には、以下も実装することを検討してください:
BeaconEyeのようなBeacon設定抽出ツールを回避できます。RWに変更し(RX/RWXから)、その内容を暗号化する - Shellcode Fluctuationテクニックを使用して、スリープ直前に実行します(これにより、Monetaやpe-sieveなどのスキャナーを回避できます)。指摘されたように、ここでのテクニックは_まだ_真に_スタックスプーファー_という名前にふさわしくありません。単にスレッドのスタック上の戻りアドレスを上書きしているだけであり、スタック自体の残りの領域を偽装しているわけではありません。さらに、コールスタックを_アンワインド可能_なままにしているため、システムがコールスタックフレームチェーン全体を適切に辿ることができず、異常に見えます。
しかし、これらの欠点は認識しており、現時点ではそのままにしています。なぜなら、主にプロセスを反復処理し、そのスレッドを列挙し、スレッドのスタックを辿り、非イメージメモリ(SEC_PRIVATE - VirtualAllocなどによって動的に割り当てられたもの)を指す戻りアドレスを拾い上げる可能性のある自動スキャナーを回避することに関心があったからです。注意深いマルウェアアナリストはすぐにその異常に気づき、スレッドをかなり異常と見なして、インプラントを追跡するでしょう。それについては確信しています。しかし、今日のAV/EDRのような自動スキャナーが、実際に各スレッドのスタックを辿ってアンワインド可能かどうかを検証するようなヒューリスティックを実装しているとは思いません ¯\_(ツ)_/¯。
確かに、このプロジェクト(およびC2フレームワークに見られる商用実装)は、AVおよびEDRベンダーに、このような新しい回避テクニックをカバーする適切なヒューリスティックの実装を検討する根拠を提供します。
このテクニックを改善するには、逆アンワインドプロセスで確立された注意深く作成された偽のスタックフレームを挿入することにより、真の_スレッドスタックスプーファー_を目指すことができます。このアイデアの詳細は以下をお読みください。
namazsoとの何時間にもわたる会話から、適切なスレッドスタックスプーファーを目指すには、x64のコールスタックアンワインドプロセスをリバースエンジニアリングする必要があることを学びました。
まず、以下にリンクされた(a)で説明されているスタックアンワインドプロセスを注意深く理解する必要があります。x64アーキテクチャでスレッドのコールスタックを辿る際、システムは単にスレッドのスタック上に散らばった戻りアドレスに依存するのではなく、次のように処理します:
RUNTIME_FUNCTION、UNWIND_INFO、UNWIND_CODE構造体を返す。これらの構造体は、関数の開始アドレス、終了アドレス、およびRBPまたはRSPを変更するすべてのコードシーケンスの場所を記述します。UNWIND_CODEを処理し、そのフレームの戻りアドレスとスタックポインタ値の位置を正確に計算します。このプロセスに干渉するには、RtlVirtualUnwindの逆転バージョンを作成して_それを逆転させる_必要があります。モジュール(例:kernel32)で定義された関数を反復処理し、各関数のUNWIND_CODEコードをスキャンし、それを逆方向に綿密にエミュレートして(RtlVirtualUnwind、特にRtlpUnwindPrologueと比較して)、偽の戻りアドレスを配置するスタック上の場所を見つける必要があります。
namazsoは、コールスタックをうまくつなぎ合わせるために、3つの偽のスタックフレームを導入する必要性について言及しています:
MySleepの呼び出し元とは異なるアンワインド(異なるUWOP - アンワインド操作コードを持つ)を行います。モジュール内のすべての関数を調べ、それらのUWOPを確認し、偽のフレームのサイズを計算することでこれを行います。このフレームは、MySleepの呼び出し元とは異なるUWOPを持たなければなりません。RBPにポップすることでアンワインドする関数です - 基本的にはUWOP_PUSH_NONVOLコードを経由します。UWOP_SET_FPREGを介してRBPからRSPを復元する関数です。復元されたRSPは、制御フローがMySleepに入った場所から取得したRSPで設定されなければならず、その結果、3つ目のガジェットがアンワインドされることで、すべてのフレームが隠されます。
このプロセスを開始するには、IMAGE_DIRECTORY_ENTRY_EXCEPTIONデータディレクトリエントリを間接参照して、実行可能ファイルの.pdataを反復処理することができます。
以下の例を考えてみてください:
ULONG_PTR imageBase = (ULONG_PTR)GetModuleHandleA("kernel32");
PIMAGE_NT_HEADERS64 pNthdrs = PIMAGE_NT_HEADERS64(imageBase + PIMAGE_DOS_HEADER(imageBase)->e_lfanew);
auto excdir = pNthdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];
if (excdir.Size == 0 || excdir.VirtualAddress == 0)
return;