从内存资源中加载DLL 模拟PE加载器.docx
- 文档编号:10131682
- 上传时间:2023-05-23
- 格式:DOCX
- 页数:53
- 大小:215.62KB
从内存资源中加载DLL 模拟PE加载器.docx
《从内存资源中加载DLL 模拟PE加载器.docx》由会员分享,可在线阅读,更多相关《从内存资源中加载DLL 模拟PE加载器.docx(53页珍藏版)》请在冰点文库上搜索。
从内存资源中加载DLL模拟PE加载器
一种保护应用程序的方法 模拟Windows PE加载器,从内存资源中加载DLL
1、前言
目前很多敏感和重要的DLL(Dynamic-link library) 都没有提供静态版本供编译器进行静态连接(.lib文件),即使提供了静态版本也因为兼容性问题导致无法使用,而只提供DLL版本,并且很多专业软件的授权部分的API,都是单独提供一个DLL来完成,而主模块通过调用DLL中的接口来完成授权功能。
虽然这些软件一般都采用了加壳和反调试等保护,但是一旦这些功能失去作用,比如脱壳,反反调试,HOOK API或者干脆写一个仿真的授权DLL(模拟授权DLL的所有导出函数接口),然后仿真的DLL再调用授权DLL,这样所有的输入首先被仿真DLL截获再传递给授权DLL,而授权DLL的输出也首先传递给仿真DLL再传递给主程序,这样就可以轻易的监视二者之间的输入输出之间的关系,从而轻易的截获DLL中的授权信息进行修改再返回给主程序。
2、目前隐式调用敏感DLL中可能存在的安全隐患
以下通过两个软件的授权DLL来说明这种问题的严重性。
如下是两个软件中授权DLL的部分信息,如下图所示:
(图1)
通过工具OllyICE可以轻易的看出IoMonitor.exe调用授权DLL(XKeyAPI.DLL),这样就很容易在调用这些API的地方设置断点,然后判断输入输出的关系,从而达到破解的目的。
(图2)
通过工具OllyICE可以轻易的看出sfeng.DLL中导出了很多函数,其中含义也很明显。
GetHDID获取硬盘的ID,GetCpuId获取cpu的ID,WinAntiDebug反调试接口。
而这些都是主程序需要调用的,比如:
主程序通过GetHDID来获取硬盘编码,以这个硬盘ID的伪码来生成授权码,破解者很容易修改这些接口的输出值或者干脆写一个sfeng.DLL来导出跟目标sfeng.DLL一模一样的导出函数,而主程序却完全不知晓。
只要用户有一套授权码就可以让GetHDID不管什么机器都返回一样的值,从而达到任何机器都可以使用同一套授权码。
(图3)
如上图所示,直接修改DLL中函数GetHDID(RVA地址:
0093FF3C开始)的实现,让它直接返回固定的硬盘ID就可以达到一个授权到处使用的目的。
其中:
”WD-Z=AM9N086529ksaiy”为需要返回的已经授权的硬盘ID,我们直接返回这个值即可。
把原来0093FF3C 部分的代码用nop替换掉,添加Call 008FFF60,后面添加字符串”WD-Z=AM9N086529ksaiy”,Call 008FFF60之后,ESP=Call后的返回地址(Call指令的下一行),也就是字符串”WD-Z=AM9N086529ksaiy”的首地址,然后pop EAX 后,返回值就是字符串的首地址,通过这种简单的修改就可以达到破解的目的,说明这种隐式的调用是非常危险的。
3、模拟Windows PE加载器,从资源中加载DLL
本文主要介绍将DLL文件进行加密压缩后存放在程序的资源段,然后在程序中读取资源段数据进行解压和解密工作后,从内存中加载这个DLL,然后模拟PE加载器完成DLL的加载过程。
本文主要以Visual C++ 6.0为工具进行介绍,其它开发工具实现过程与此类似。
这样作的好处也很明显,DLL文件存放在主程序的资源段,而且经过了加密压缩处理,破解者很难找到下断点的地方,也不能轻易修改资源DLL,因为只有主程序完成解压和解密工作,完成PE加载工作后此DLL才开始工作。
我们知道,要显式加载一个DLL,并取得其中导出的函数地址一般是通过如下步骤:
(1) 用LoadLibrary加载DLL文件,获得该DLL的模块句柄;
(2) 定义一个函数指针类型,并声明一个变量;
(3) 用GetProcAddress取得该DLL中目标函数的地址,赋值给函数指针变量;
(4) 调用函数指针变量。
这个方法要求DLL文件位于硬盘上面,而我们的DLL现在在内存中。
现在假设我们的DLL已经位于内存中,比如通过脱壳、解密或者解压缩得到,能不能不把它写入硬盘文件,而直接从内存加载呢?
答案是肯定的,方法就是完成跟Windows PE加载器同样的工作即可。
加载过程大致包括以下几个部分:
1、调用API读取DLL资源数据拷贝到内存中
2、调用解压和解密函数对内存中的DLL进行处理
3、检查DOS头和PE头判断是否为合法的PE格式
4、计算加载该DLL所需的虚拟地址空间大小
5、向操作系统申请指定大小的虚拟地址空间并提交
6、将DLL数据复制到所分配的虚拟内存块中,注意文件段对齐方式和内存段对齐方式
7、对每个 DLL文件来说都存在一个重定位节(.reloc),用于记录DLL文件的重定位信息,需要处理重定位信息
8、读取DLL的引入表部分,加载引入表部分需要的DLL,并填充需要的函数入口的真实地址
9、根据DLL每个节的属性设置其对应内存页的读写属性
10、调用入口函数DLLMain,完成初始化工作
11、保存DLL的基地址(即分配的内存块起始地址),用于查找DLL的导出函数
12、不需要DLL的时候,释放所分配的虚拟内存,释放所有动态申请的内存
以下部分分别介绍这几个步骤,以改造过的网上下载的CMemLoadDLL类为例程(原类存在几个错误的地方)
A. 调用API读取DLL资源数据拷贝到内存中
//加载资源DLL
#define strKey (char)0x15
char DLLtype[4]={'D' ^ strKey ,'l'^ strKey,'l'^ strKey,0x00};
HINSTANCE hinst=AfxGetInstanceHandle();
HRSRC hr=NULL;
HGLOBAL hg=NULL;
//对资源名称字符串进行简单的异或操作,达到不能通过外部字符串参考下断点
for(int i=0;i { DLLtype[i]^=strKey; } hr=FindResource(hinst,MAKEINTRESOURCE(IDR_DLL),TEXT(DLLtype)); if (NULL == hr) return FALSE; //获取资源的大小 DWORD dwSize = SizeofResource(hinst, hr); if (0 == dwSize) return FALSE; hg=LoadResource(hinst,hr); if (NULL == hg) return FALSE; //锁定资源 LPVOID pBuffer =(LPSTR)LockResource(hg); if (NULL == pBuffer) return FALSE; FreeResource(hg); //在资源使用完毕后我们不需要使用UnlockResource和FreeResource来手动地释放资源,因为它们都是16位Windows遗留下来的,在Win32中,在使用完毕后系统会自动回收 B. 调用解压和解密函数对内存总的DLL进行处理 对于上面获取的pBuffer可以进行解压和解密操作,算法应该跟你加入的资源采取的算法进行逆变换即可,具体算法可以自己选择,此处省略。 C. 检查DOS头和PE头判断是否为合法的PE格式 //CheckDataValide函数用于检查缓冲区中的数据是否有效的DLL文件 //返回值: 是一个可执行的DLL则返回TRUE,否则返回FALSE。 //lpFileData: 存放DLL数据的内存缓冲区 //DataLength: DLL文件的长度 BOOL CMemLoadDLL: : CheckDataValide(void* lpFileData, int DataLength) { //检查长度 if(DataLength < sizeof(IMAGE_DOS_HEADER)) return FALSE; pDosHeader = (PIMAGE_DOS_HEADER)lpFileData; // DOS头 //检查dos头的标记 if(pDosHeader->e_magic ! = IMAGE_DOS_SIGNATURE) return FALSE; //0*5A4D : MZ //检查长度 if((DWORD)DataLength < (pDosHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS)) ) return FALSE; //取得pe头 pNTHeader = (PIMAGE_NT_HEADERS)( (unsigned long)lpFileData + pDosHeader->e_lfanew); // PE头 //检查pe头的合法性 if(pNTHeader->Signature ! = IMAGE_NT_SIGNATURE) return FALSE; //0*00004550 : PE00 if((pNTHeader->FileHeader.Characteristics & IMAGE_FILE_DLL) == 0) //0*2000 : File is a DLL return FALSE; if((pNTHeader->FileHeader.Characteristics & IMAGE_FILE_EXECUTABLE_IMAGE) == 0) //0*0002 : 指出文件可以运行 return FALSE; if(pNTHeader->FileHeader.SizeOfOptionalHeader ! = sizeof(IMAGE_OPTIONAL_HEADER)) return FALSE; //取得节表(段表) pSectionHeader = (PIMAGE_SECTION_HEADER)((int)pNTHeader + sizeof(IMAGE_NT_HEADERS)); //验证每个节表的空间 for(int i=0; i< pNTHeader->FileHeader.NumberOfSections; i++) { if((pSectionHeader[i].PointerToRawData + pSectionHeader[i].SizeOfRawData) > (DWORD)DataLength)return FALSE; } return TRUE; } D. 计算加载该DLL所需的虚拟地址空间大小 计算整个DLL映像文件的尺寸,最大映像尺寸应该为VOffset最大的一个段的VOffset+VSize,然后补齐段对齐即可。 如下图中,最大映像尺寸应该为0x0000D000+0x00000DA6,然后按段对齐(如为: 0x1000对齐)则结果为0x0000E000。 其中DOS Header和PE Header就占用0x1000字节,代码段.text从0x1000开始占用了0x7000字节。 段名称 虚拟地址 虚拟大小 物理地址 物理大小 标志 int CMemLoadDLL: : CalcTotalImageSize() { int Size; if(pNTHeader == NULL)return 0; int nAlign = pNTHeader->OptionalHeader.SectionAlignment; //段对齐字节数 // 计算所有头的尺寸。 包括dos, coff, pe头 和 段表的大小 Size = GetAlignedSize(pNTHeader->OptionalHeader.SizeOfHeaders, nAlign); // 计算所有节的大小 for(int i=0; i < pNTHeader->FileHeader.NumberOfSections; ++i) { //得到该节的大小 int CodeSize = pSectionHeader[i].Misc.VirtualSize ; int LoadSize = pSectionHeader[i].SizeOfRawData; int MaxSize = (LoadSize > CodeSize)? (LoadSize): (CodeSize); int SectionSize = GetAlignedSize(pSectionHeader[i].VirtualAddress + MaxSize, nAlign); if(Size < SectionSize) Size = SectionSize; //Use the Max; } return Size; } //计算对齐边界 int CMemLoadDLL: : GetAlignedSize(int origin, int Alignment) { return (Origin + Alignment - 1) / Alignment * Alignment; } E. 向操作系统申请指定大小的虚拟地址空间并提交 调用操作系统API VirtualAlloc保留指定大小的虚拟内存并提交内存,VirtualAlloc的第一个参数不能指定地址,如果指定地址已经被占用或者指定地址后面没有足够的连续的地址空间来满足提交的大小则会调用失败,而我们也没有必要获取指定地址空间,这样第一个参数必须保留为NULL(0)。 void *pMemoryAddress=VirtualAlloc((LPVOID)NULL, ImageSize,MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE); if(pMemoryAddress == NULL) { return FALSE; } F. 将DLL数据复制到所分配的虚拟内存块中,注意文件段对齐方式和内存段对齐方式 拷贝内存DLL到提交的虚拟地址空间,拷贝的部分包括PE文件的所有部分,DOS Header、 PE Header 、Section Table、Section 1~Section N,如下图所示: DOS MZ header DOS stub PE header Section table Section 1 Section 2 Section ... Section n //CopyDLLDatas函数将DLL数据复制到指定内存区域,并对齐所有节 //pSrc: 存放DLL数据的原始缓冲区 //pDest: 目标内存地址 void CMemLoadDLL: : CopyDLLDatas(void* pDest, void* pSrc) { // 计算需要复制的PE头+段表字节数 int HeaderSize = pNTHeader->OptionalHeader.SizeOfHeaders; int SectionSize = pNTHeader->FileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER); int MoveSize = HeaderSize + SectionSize; //复制头和段信息 memmove(pDest, pSrc, MoveSize); //复制每个节 for(int i=0; i < pNTHeader->FileHeader.NumberOfSections; ++i) { if(pSectionHeader[i].VirtualAddress == 0 || pSectionHeader[i].SizeOfRawData == 0) continue; // 定位该节在内存中的位置 void *pSectionAddress = (void *)((unsigned long)pDest + pSectionHeader[i].VirtualAddress); // 复制段数据到虚拟内存 memmove((void *)pSectionAddress, (void *)((DWORD)pSrc + pSectionHeader[i].PointerToRawData), pSectionHeader[i].SizeOfRawData); } //修正指针,指向新分配的内存 //新的dos头 pDosHeader = (PIMAGE_DOS_HEADER)pDest; //新的pe头地址 pNTHeader = (PIMAGE_NT_HEADERS)((int)pDest + (pDosHeader->e_lfanew)); //新的节表地址 pSectionHeader = (PIMAGE_SECTION_HEADER)((int)pNTHeader + sizeof(IMAGE_NT_HEADERS)); return ; } G. 每个 DLL文件来说都存在一个重定位节(.reloc),用于记录DLL文件的重定位信息,需要处理重定位信息 Windows加载DLL时就可以按照该节的信息对需要重定位的地址进行修正,在32位代码中,凡涉及到直接寻址的指令都是需要重定位的,而PE文件的的(.reloc)段则是可选的,因为PE文件一般都可以加载到默认地址(如: 0x00400000)。 当然系统的DLL其默认加载地址都能满足要求,因为这些DLL都在系统加载其它程序前首先被加载(如: Kernel32.DLL,User32.DLL)等。 对于操作系统来说,其任务就是在对可执行程序透明的情况下完成重定位操作,在现实中,重定位信息是在编译的时候由编译器生成并被保留在可执行文件中的,在程序被执行前由操作系统根据重定位信息修正代码,这样在开发程序的时候就不用考虑重定位问题了。 重定位信息在DLL文件中被存放在重定位表中,重定位的算法可以描述为: 将直接寻址指令中的双字地址加上模块实际装入地址与模块建议装入地址之差。 为了进行这个运算,需要有3个数据,首先是需要修正的机器码地址;其次是模块的建议装入地址;最后是模块的实际装入地址。 在这3个数据中,模块的建议装入地址已经在PE文件头中定义了(编译后就已经确定),而模块的实际装入地址是Windows装载器确定的,到装载文件的时候自然会知道,所以被保存在重定位表中的仅仅是需要修正的代码的地址。 事实上正是如此,DLL文件的重定位表中保存的就是一大堆需要修正的代码的地址。 重定位表一般会被单独存放在一个可丢弃的以“.reloc”命名的节中,但是这并不是必然的,因为重定位表放在其他节中也是合法的,惟一可以肯定的是,假如重定位表存在的话,它的地址肯定可以在DLL文件头中的数据目录中找到。 重定位表的位置和大小可以从数据目录中的第6个 IMAGE_DATA_DIRECTORY结构中获取,虽然重定位表中的有用数据是那些需要重定位机器码的地址指针,但为了节省空间,DLL文件对存放的方式做了一些优化。 在正常的情况下,每个32位的指针占用4个字节,假如有n个重定位项,那么重定位表的总大小是4×n字节大小。 直接寻址指令在程序中还是比较多的,在比较靠近的重定位表项中,32位指针的高位地址总是相同的,假如把这些相近表项的高位地址统一表示,那么就可以省略一部分的空间,当按照一个内存页来分割时,在一个页面中寻址需要的指针位数是12位(一页等于4096字节,等于2的12次方),假如将这12位凑齐16 位放入一个字类型的数据中,并用一个附加的双字来表示页的起始指针,另一个双字来表示本页中重定位项数的话,那么占用的总空间会是4+4+2×n字节大 小,计算一下就可以发现,当某个内存页中的重定位项多于4项的时候,后一种方法的占用空间就会比前面的方法要小。 // 重定向PE用到的地址 void CMemLoadDLL: : DoRelocation( void *NewBase) { /* 重定位表的结构: // DWORD sectionAddress, DWORD size (包括本节需要重定位的数据) // 例如 1000节需要修正5个重定位数据的话,重定位表的数据是 // 00 10 00 00 14 00 00 00 xxxx xxxx xxxx xxxx xxxx 0000 // ———– ———– —- // 给出节的偏移 总尺寸=8+6*2 需要修正的地址 用于对齐4字节 // 重定位表是若干个相连,如果address 和 size都是0 表示结束 // 需要修正的地址是12位的,高4位是形态字,intel cpu下是3 */ //假设NewBase是0×600000,而文件中设置的缺省ImageBase是0×400000,则修正偏移量就是0×200000 DWORD Delta = (DWORD)NewBase - pNTHeader->OptionalHeader.ImageBase; //注意重定位表的位置可能和硬盘文件中的偏移地址不同,应该使用加载后的地址 PIMAGE_BASE_RELOCATION pLoc = (PIMAGE_BASE_RELOCATION)((unsigned long)NewBase + pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress); while((pLoc->VirtualAddress + pLoc->SizeOfBlock) ! = 0) //开始扫描重定位表 { WORD *pLocData = (WORD *)((int)pLoc + sizeof(IMAGE_BASE_RELOCATION)); //计算本节需要修正的重定位项(地址)的数目 int NumberOfReloc = (pLoc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION))/sizeof(WORD); for( int i=0 ; i < NumberOfReloc; i++) { if( (DWORD)(pLocData[i] &
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 从内存资源中加载DLL 模拟PE加载器 内存 资源 加载 DLL 模拟 PE