内存加载DLL

本文讨论了敏感DLL的安全隐患,通过内存加载而非硬盘加载来提高安全性。介绍了模拟Windows PE加载器的过程,包括从资源中读取加密压缩的DLL,解压解密,检查PE头,计算内存分配,处理重定位信息,填充引入表等步骤,以实现内存中的DLL加载,减少被篡改的风险。
摘要由CSDN通过智能技术生成
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<sizeof(DLLtype)-1;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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值