本帖最后由 Gslab 于 2016-5-27 10:56 编辑
【游戏安全实验室致力于游戏安全技术分享及交流,以后将在52pojie发布更多游戏安全相关技术分享,欢迎大家一起来讨论交流】
文章最后带傀儡进程源码下载
一、傀儡进程简介
傀儡进程是指将目标进程的映射文件替换为指定的映射文件,替换后的进程称之为傀儡进程。
二、傀儡进程技术要点
实现傀儡进程必须要选择合适的时机,若目标进程已经开始运行则难以进行替换,因此要在目标进程刚加载进内存后还未开始运行之前替换。技术要点如下:
1. 创建挂起进程
系统函数CreateProcess中参数dwCreationFlgs传递CREATE_SUSPEND便可创建一个挂起状态的进程。进程被创建后系统会为它分配足够的资源和初始化必要的操作,如为进程分配空间,加载映像文件,创建主线程,将EIP指向代码入口点,并将主线程挂起等。 [C++] 纯文本查看 复制代码 BOOL WINAPI CreateProcess([/align] __in LPCTSTR lpApplicationName,
__in_out LPTSTR lpCommandLine,
__in LPSECURITY_ATTRIBUTES lpProcessAttributes,
__in LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in BOOL bInheritHandles,
__in DWORD dwCreationFlags,
__in LPVOID lpEnvironment,
__in LPCTSTR lpCurrentDirectory,
__in LPSTARTUPINFO lpStartupInfo,
__out LPPROCESS_INFORMATION lpProcessInformation
);
创建挂起进程实例代码如下:
[C++] 纯文本查看 复制代码 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;
}
2. 保存现场,收集信息 傀儡进程在替换目标进程之前,必须要保存当前线程的上下文环境,在替换完成后要及时恢复。这样系统才能将傀儡进程视为“正常”进程,而不会被发现。另外为了后边清空内存空间的操作,也必须要通过上下文获得进程的加载基地址。利用系统函数GetThreadContext便可得到当前的线程上下文。相关的API和结构信息如下: [C++] 纯文本查看 复制代码 BOOL WINAPI GetThreadContext(
__in HANDLE hThread,
__in_out LPCONTEXT lpContext
);
[C++] 纯文本查看 复制代码 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;
获取线程相关信息示例代码如下: [C++] 纯文本查看 复制代码 CONTEXT stThreadContext;
stThreadContext.ContextFlags = CONTEXT_FULL;
if (GetThreadContext(stPi.hThread, &stThreadContext) == 0)
{
return FALSE;
}
3. 清空目标进程
目标进程被初始化后,进程的映像文件也随之被加载进对应的内存空间。傀儡进程在替换之前必须将目标进程的内容清除掉。此时要用到另外一个系统未文档化的函数NtUnmapViewOfSection,需要自行从ntdll.dll中获取。该函数需要指定的进程加载的基地址,基地址即是从第2步中的上下文取得。相关的函数说明及基地址计算方法如下: [C++] 纯文本查看 复制代码 ULONG WINAPI NtUnmapViewOfSection(
__in HANDLE ProcessHandle,
__in PVOID BaseAddress
);
context.Ebx+ 8 = 基地址的地址,因此从context.Ebx + 8的地址读取4字节的内容并转化为DWORD类型,既是进程加载的基地址。示例代码如下:
[C++] 纯文本查看 复制代码 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。 [C++] 纯文本查看 复制代码 LPVOID WINAPI VirtualAllocEx(
__in HANDLE hProcess,
__in LPVOID lpAddress,
__in SIZE_T dwSize,
__in DWORD flAllocationType,
__in DWORD flProtect
);
一般情况下,在写入傀儡进程之前,需要将傀儡进程对应的文件按照申请空间的首地址作为基地址进行“重定位”,这样才能保证傀儡进程的正常运行。为了避免这一步操作,可以以傀儡进程PE文件头部的建议加载基地址作为VirtualAllocEx 的lpAddress参数,申请与之对应的内存空间,然后以此地址作为基地址将傀儡进程写入目标进程,就不会存在重定位问题。关于“重定位”的原理可以自行网络查找相关资料。示例代码如下
[C++] 纯文本查看 复制代码 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节,如果存在附加数据还需要写入附加数据。示例代码如下: [C++] 纯文本查看 复制代码 // 替换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。 [C++] 纯文本查看 复制代码 BOOL WINAPI SetThreadContext(
__in HANDLE hThread,
__in const CONTEXT* lpContext
);
[C++] 纯文本查看 复制代码 DWORD WINAPI ResumeThread(
__in HANDLE hThread
);
示例代码如下: [C++] 纯文本查看 复制代码 // 替换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);
|