关于
KMDllInjector 是一个基于内核模式的 DLL 注入器。该驱动程序可以配置为DllInjectorClient.exe使用PsSetLoadImageNotifyRoutine或PsSetCreateProcessNotifyRoutineEx注册内核回调。一旦触发回调(加载映像或创建进程),它就会将 DLL 注入目标用户模式进程。
它是如何工作的?
为了在调用进程入口点之前注入 dll,驱动程序可以使用两种技术:
PsSetCreateProcessNotifyRoutineEx + Shellcode注入
此技术用于PsSetCreateProcessNotifyRoutineEx注册一个回调函数,每当新进程创建时都会触发该回调函数。由于ntdll.dll该函数是从内核模式加载的,因此我们将在回调函数中hookNtdll!LdrLoadDll一段detour shellcode。
问题在于,在进程创建的这个阶段,PEB->Ldr结构尚未初始化。
那么我们如何找到的基地址ntdll.dll?
我想到解决方案是,由于ntdll.dll已映射到进程虚拟内存空间,我可以使用它ntoskernel!ZwQueryVirtualMemory来枚举图像映射类型的内存区域,检查内存区域基址是否包含有效的 PE 头,然后解析 PE 头以确定它是否是 DLL。
while ( TRUE ) { status = ZwQueryVirtualMemory( ZwCurrentProcess( ), baseAddress, MemoryBasicInformation, &memInfo, sizeof( memInfo ), &returnLength );if ( !NT_SUCCESS( status ) )break;// Check if this region is a mapped imageif ( memInfo.Type == MEM_IMAGE ) {if ( IsDllModule( memInfo.BaseAddress ) ) { DBG_PRINT( "Ntdll: %p", memInfo.BaseAddress ); *NtdllBase = memInfo.BaseAddress;return STATUS_SUCCESS; } }// Move to the next region baseAddress = ( PVOID ) ( ( ULONG_PTR ) memInfo.BaseAddress + memInfo.RegionSize );}
ntdll.dll在我们找到目标进程的基地址之后,我们Ntdll!LdrLoadDll使用 detour shellcode 进行钩住,shellcode 将执行以下操作:
-
恢复 LdrLoadDll 的原始序言(删除钩子),
-
使用传递的参数调用 LdrLoadDll。
-
然后将我们自定义的 DLL 加载到进程中。
我没有用汇编语言编写 Shellcode,而是使用了Rhydon1337 的一个技巧:windows-kernel-dll-injector,将一个函数用作 Shellcode。由于该函数是位置无关的代码,我禁用了堆栈 Cookie、优化和控制流保护 (CFG)。我还#pragma code_seg(".text$")确保函数的顺序与 cpp 文件中的顺序一致。
#pragma optimize("", off)#pragma code_seg(".text$A")__declspec( safebuffers ) // disable stack cookies// CFG can be disabled from Properties > C/C++ > Code Generation > Control Flow Guad > NoNTSTATUS HookLdrLoadDll( PWCHAR pwPathToFile, ULONG ulFlags, PUNICODE_STRING puModuleFileName, PHANDLE phModuleHandle ){ PHOOK_CONTEXT pContext = ( PHOOK_CONTEXT ) 0xBAADF00DBAADBEEF;}#pragma code_seg(".text$B")DWORD HookLdrLoadDllEnd( ){return2;}#pragma optimize("", on)
shellcode 需要一个上下文,其中包含 ldrloaddll 序言的保存副本(使用它来恢复Ntdll!LdrLoadDll)和一些 ntdll 导出(NtProtectVirtualMemory,LdrLoadDLl,RtlInitUnicodeString)
初始化上下文后,我们扫描模式0xBAADF00DBAADBEEF并将其替换为上下文的地址。
// Search for '0xBAADF00DBAADBEEF' pattern and replace it with the address to the contextpbFunctionStart = ( PBYTE ) ( ( DWORD_PTR ) pBuffer + sizeof( HOOK_CONTEXT ) );for ( DWORD dwIndex = 0; dwIndex < sTotalSize; dwIndex++ ){if ( *( DWORD64* ) ( pbFunctionStart + dwIndex ) == 0xBAADF00DBAADBEEF ) { *( DWORD64* ) ( pbFunctionStart + dwIndex ) = ( DWORD64 ) pBuffer; bIsFound = TRUE;break; }}
图中显示了目标进程中分配的内存情况:
演示 01
PsImageLoadNotify + APC注入
PsImageLoadNotify用于注册内核回调函数,该回调函数在图像加载时触发。由于我们只想将 DLL 注入到新创建的用户模式进程中,因此我们将使用以下 if 语句进行过滤:
if (// Exclude system images. !ImageInfo->SystemModeImage &&// Exclude images loaded remotely. ProcessId == PsGetCurrentProcessId( ) &&// Exclude image name that not end with kernel32.dll. (the first dll that is get loaded from user-mode on process creation is kernel32.dll) Utils::EndsWithUnicodeString( FullImageName, &uKernel32, TRUE ) &&// Exclude images that not get loaded via `LdrLoadDll`.// (This is checked by verifying if Teb->ArbitraryUserPointer == L"...kernel32.dll".) Utils::IsLoadedByLdrLoadDll( &uKernel32 )){// At this point, we're in a good position to inject the DLL// right after kernel32.dll has been loaded. }
LdrInitializeThunk是进程在用户模式下执行的第一个函数,此时进程仍处于创建阶段。该函数执行的最后一件事是Ntdll!NtTestAlert释放 APC 队列。想要了解更多关于该函数的信息,可以阅读这篇博客@outflank:早期级联注入简介:https://www.outflank.nl/blog/2024/10/15/introducing-early-cascade-injection-from-windows-process-creation-to-stealthy-injection
这为我们注入 DLL 提供了一个绝佳的机会,如果我们在调用之前将 APC 排队Ntdll!NtTestAlert,我们的代码就会作为进程正常流程的一部分执行。我们可以使用 和 从内核模式注入/排队 KeInitializeApcAPC KeInsertQueueApc。
KeInitializeApc( apc, Thread, OriginalApcEnvironment, KernelAPC, NULL, APCCallbackCodeCave, UserMode, Arguments );KeInsertQueueApc( apc, NULL, NULL, 0 );
演示 02
项目地址:
https://github.com/0xPrimo/KMDllInjector
原文始发于微信公众号(Ots安全):KMDllInjector 是一个基于内核模式的 DLL 注入器
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论