Windows内核Dll注入(一)

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呢?两个情况都满足:

  1. 调用Alertable的函数。
  2. 当前线程有用户态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,音视频重定向等)。
  • 安全产品(比如安全监控,权限管控等)。

其他参考,可以参见如下链接:

  • 12
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值