Windows内核Dll注入
近期在云桌面中遇到许多情况都需要对应用层程序的函数进行HOOK,并需要对函数参数进行各种修改,以便能够在虚拟桌面里面支持一些特性(尤其是一些3D的场景)。对API的HOOK是比较简单的,微软提供了开源的代码就能够实现;因此这里最主要的就是需要对HOOK的进程进行注入,来实现HOOK函数的功能。
在早期开发中,就实现过DLL注入的功能,不过都是基于用户层的注入来实现的,例如有远线程注入,消息钩子注入,用户层OPE注入。本篇中分享的是基于内核的注入技术,其中内核注入主要有:
- HOOK函数注入。
- 远程线程注入(使用
ZwCreateThreadEx
)。 - OEP注入。
- APC注入。
下面的文章主要是针对HOOK函数的注入,这个函数是NtTestAlert
,接下来分析整个实现过程。
1. NtTestAlert
HOOK注入的第一步是需要找到一个函数,这个函数可以确保所有的进程启动的时候都会被调用,这个函数就是NtTestAlert
,如下:
NTSYSAPI
NTSTATUS
NTAPI
NtTestAlert(
);
该函数的作用是什么呢?我们知道,Windows在从内核切换到用户层的时候,就会判断线程的APC状态标记Thread->ApcState.UserApcPending
,如下:
cmp [ebx+_KTHREAD.___u12.ApcState.UserApcPending], 0
如果Thread->ApcState.UserApcPending
为TRUE,那么就会对用户层的APC进行处理,那么什么情况下Thread->ApcState.UserApcPending
才会被设置为TRUE呢?两个情况都满足:
- 调用
Alertable
的函数。 - 当前线程有用户态APC(一般放入线程APC链表中)。
Alertable
的函数有如下:
DWORD SleepEx(
DWORD dwMilliseconds,
BOOL bAlertable
);
DWORD WaitForSingleObjectEx(
HANDLE hHandle,
DWORD dwMilliseconds,
BOOL bAlertable
);
这也是很多地方说的,只有在当前线程处于Alertable
状态的时候,用户层APC才能被分发。
那么有没有一种办法,不用让线程调用Alertable
的函数也能实现APC的分发呢?答案是有的;这个函数就是NtTestAlert
,该函数的作用大致可以理解如下:
if ((AlertMode == UserMode) &&
(IsListEmpty(&Thread->ApcState.ApcListHead[UserMode]) != TRUE))
{
Thread->ApcState.UserApcPending = TRUE;
}
主要就是设置Thread->ApcState.UserApcPending = TRUE
,让APC可以被调度分发。
在每个线程启动的时候,都会调用NtTestAlert
来分发用户层的APC例程,如下:
0:000> k
# Child-SP RetAddr Call Site
00 0000005b`aedef448 00007fff`81703f88 ntdll!NtTestAlert
01 0000005b`aedef450 00007fff`81703ea3 ntdll!_LdrpInitialize+0xac
02 0000005b`aedef4d0 00007fff`81703dce ntdll!LdrpInitializeInternal+0x6b
03 0000005b`aedef750 00000000`00000000 ntdll!LdrInitializeThunk+0xe
因此我们找到了一个进程(线程)启动一定会调用的函数了。注入的原理就是HOOK该函数,当NtTestAlert
被执行的时候,就会跳转到我们HOOK的函数,然后该函数中通过LoadLibrary
加载需要注入的Dll。
2. 技术分析
首先我们需要对进程的事件进行监控,记录进程的一些信息,例如:
- 判断该进程是否需要被注入。
- 在一些环境中,进程被创建的时候,就可以进行注入了。
在内核中,通过注册回调来接收进程启动和退出的事件,该函数为PsSetCreateProcessNotifyRoutine
,声明如下:
NTSTATUS PsSetCreateProcessNotifyRoutine(
PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine,
BOOLEAN Remove
);
只需要提供相关的回调函数,就可以得到进程创建和退出的通知了。
某一些情况下,在进程启动的时候并不能对进程进行HOOK,例如WOW64的进程需要在C:\Windows\SysWOW64\ntdll.dll
加载之后才能注入。因此我们需要对进程模块加载需要监控,需要PsSetCreateProcessNotifyRoutine
注册模块加载的监控,该函数声明如下:
NTSTATUS PsSetCreateProcessNotifyRoutine(
PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine,
BOOLEAN Remove
);
通过上面介绍,我们知道需要对NtTestAlert
进行修改,修改内存,一般需要用到如下函数:
ZwReadVirtualMemory
。ZwWriteVirtualMemory
。ZwQueryVirtualMemory
。ZwProtectVirtualMemory
。
一个困难的地方是这些函数并没有导出来,因此获取的时候会稍微比较麻烦;一般来说,有两种办法可以获取到:
- 通过SSDT表获取到函数的地址。
- 通过导出函数
ZwAllocateVirtualMemory
获取函数地址,然后再获取其他函数地址。
3. 注入实现
注入的时候又两种场景:
- 进程启动的时候注册。
- WOW64进程,在
C:\Windows\SysWOW64\ntdll.dll
被加载的时候调用。
进程启动的时候,注入调用如下:
if (BooleanFlagOn(ProcessContext->Flags, LXI_PROCESS_FLAGS_INJECT) &&
!BooleanFlagOn(ProcessContext->Flags, LXI_PROCESS_FLAGS_WOW64))
{
Status = LxiInjectProcess(ProcessContext);
if (!NT_SUCCESS(Status))
{
//...
}
}
在C:\Windows\SysWOW64\ntdll.dll
被加载的时候,注入过程如下:
if (RtlEqualUnicodeString(FullImageName, &LxiData->Wow64NtdllPath, TRUE) ||
RtlEqualUnicodeString(FullImageName, &LxiWow64NtdllPath, TRUE))
{
Status = LxExQueueWorkItemSync(LxiInjectOnLoadImageWroker, ProcessContext, DelayedWorkQueue);
}
VOID
LxiInjectOnLoadImageWroker(
_In_opt_ PVOID Context
)
{
PAGED_CODE();
if (NULL == Context)
{
return;
}
(VOID)LxiInjectProcess((PLXI_PROCESS_CONTEXT)Context);
}
下面是对LxiInjectProcess
的实现,该函数有两个点:
- HOOK函数
NtTestAlert
(修改指令为JMP指令)。 - JMP后的地址的ShellCode实现。
这里我们看一下X86环境的注入,代码大致如下:
NTSTATUS
LxiInjectLibraryX86(
_In_ HANDLE ProcessHandle,
_In_ PCUNICODE_STRING LibraryFileName
)
{
NTSTATUS Status = STATUS_UNSUCCESSFUL;
LXI_INJECT_INFO InjectInfo = { 0 };
PLXI_INJECT_INFO InjectCode = NULL;
LXI_RELJUMP_INS RelJmp = { 0 };
SIZE_T BytesWritten = 0;
SIZE_T BytesRead = 0;
PAGED_CODE();
if (LibraryFileName->Length > LXI_MAX_INJ_PATH_LEN)
{
return STATUS_UNSUCCESSFUL;
}
InjectCode = (PLXI_INJECT_INFO)LxiAllocVirtualMemory(ProcessHandle, sizeof(LXI_INJECT_INFO) + sizeof(LxiInjectLib32Func), NULL);
if (NULL == InjectCode)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
InjectInfo.MovEax = 0xb8;
InjectInfo.Param = InjectCode;
InjectInfo.MovEcx = 0xb9;
InjectInfo.Proc = (PVOID32)&InjectCode->InjectFunc;
InjectInfo.CallEcx = 0xd1ff;
InjectInfo.OldApi = (PLXI_RELJUMP_INS)(LxiData->Ntdll32Info.NtTestAlert);
InjectInfo.Dll.Length = LibraryFileName->Length;
InjectInfo.Dll.MaximumLength = sizeof (InjectInfo.DllBuffer);
InjectInfo.Dll.Buffer = (ULONG)(&InjectCode->DllBuffer[0]);
RtlCopyMemory(InjectInfo.DllBuffer, LibraryFileName->Buffer, LibraryFileName->Length);
InjectInfo.NtProtectVirtualMemory = (PVOID32)(LxiData->Ntdll32Info.NtProtectVirtualMemory);
InjectInfo.LdrLoadDll = (PVOID32)(LxiData->Ntdll32Info.LdrLoadDll);
if (NULL == InjectInfo.OldApi)
{
return STATUS_UNSUCCESSFUL;
}
Status = LxiZwWriteVirtualMemory(ProcessHandle, InjectCode, &InjectInfo, sizeof (LXI_INJECT_INFO), &BytesWritten);
if (!NT_SUCCESS(Status))
{
return Status;
}
Status = LxiZwWriteVirtualMemory(ProcessHandle, &(InjectCode->InjectFunc), (PVOID)LxiInjectLib32Func, sizeof(LxiInjectLib32Func), &BytesWritten);
if (!NT_SUCCESS(Status))
{
return Status;
}
Status = LxiZwReadVirtualMemory(ProcessHandle, InjectInfo.OldApi, &RelJmp, sizeof(RelJmp), &BytesRead);
if (!NT_SUCCESS(Status))
{
return Status;
}
Status = LxiZwWriteVirtualMemory(ProcessHandle, &(InjectCode->OldApiSaved), &RelJmp, sizeof(RelJmp), &BytesWritten);
if (!NT_SUCCESS(Status))
{
return Status;
}
if (RelJmp.Jmp != 0xe9)
{
ULONG OldAccess = 0;
SIZE_T MemorySize = sizeof(RelJmp);
PVOID BaseAddress = InjectInfo.OldApi;
Status = LxiZwProtectVirtualMemory(ProcessHandle, &BaseAddress, &MemorySize, PAGE_EXECUTE_READWRITE, &OldAccess);
if (!NT_SUCCESS(Status))
{
return Status;
}
RelJmp.Jmp = 0xe9;
RelJmp.Target = (ULONG)InjectCode - (ULONG)(InjectInfo.OldApi) - sizeof(RelJmp);
Status = LxiZwWriteVirtualMemory(ProcessHandle, InjectInfo.OldApi, &RelJmp, sizeof(RelJmp), &BytesWritten);
BaseAddress = InjectInfo.OldApi;
(VOID)LxiZwProtectVirtualMemory(ProcessHandle, &BaseAddress, &MemorySize, OldAccess, &OldAccess);
}
return Status;
}
至于ShellCode,在后续文章中专门分析怎么生成ShellCode,在此不再详述。
4. 应用与参考
Dll动态库的注入,在很多场景下面都非常有用;在Windows平台下面很多产品都还是依赖DLL注入和API的HOOK来实现的,比如:
- 沙盒技术(最近几年突然火起来的零信任,一机两用等)。
- 云桌面技术(比如3D,音视频重定向等)。
- 安全产品(比如安全监控,权限管控等)。
其他参考,可以参见如下链接: