昨天感冒了,今天鼻子也不是很舒服,不过不想失约,承诺了就一定要做下去,七篇文章一定会在国庆结束前全部献上,不管评论是好与坏,至少我懂得做为男人既已承诺,就一定要现!!
前面几篇文章中我已经对PE文件的结构作了一个比较详细的讲解,如果大家还有不清楚的,请参考相关资料,谢谢,下面我开始讲解PE文件编程方面的知识~~这样理论结合实际,我想大家会有一个更深切的理解!
首先我想对《加密与解密》第三版上的PE分析工具实例进行讲解,因为考虑到大多数人还是对C语言比较熟悉,所以先用C语言进行整体讲解,在后面的三篇中的我将着重讲解PE的Win32汇编的编程,因为必竟汇编我还是比较熟一点,C语言不是很熟,呵呵!!
其实有了上面的理论讲解,基本的算法很简单了,主要就是对PE格式的各个结构进行定位,这里我们定义一个MAP_FILE_STRUCT结构来存放有关信息,结构如下:
typedef struct _MAP_FILE_STRUCT
{
HANDLE hFile; ;文件句柄
HANDLE hMapping; ;映射文件句柄
LPVOID ImageBase; ;映像基址
} MAP_FILE_STRUCT;
PE文件格式的检查
文件格式可能通过PE头开始的标志Signature来检测。检测DOS Header的Magic Mark不是也可以检测此PE文件是否合法吗?但是大家想想如果只检测一个文件的头两个字节是不是MZ,如果一个文件文件的头两个字节是MZ,那不是判断失误!所以要检查PE文件的格式有两个重要的步骤:
判断文件开始的第一个字段是否为IMAGE_DOS_SIGNATURE,即5A4Dh
再通过e_lfanew找到IMAGE_NT_HEADERS,判断Signature字段的值是否为IMAGE_NT_SIGNATURE,即00004550h,如果是IMAGE_NT_SIGNATURE,就可以认为该文件是PE格式。
具体代码实现如下:
BOOL IsPEFile(LPVOID ImageBase)
{
PIMAGE_DOS_HEADER pDH=NULL;
PIMAGE_NT_HEADERS pNtH=NULL;
if(!ImageBase) //判断映像基址
return FALSE;
pDH=(PIMAGE_DOS_HEADER)ImageBase;
if(pDH->e_magic!=IMAGE_DOS_SIGNATURE) //判断是否为MZ
return FALSE;
pNtH=(PIMAGE_NT_HEADERS32)((DWORD)pDH+pDH->e_lfanew); //DOS头+e_lfanew(03Ch)定位PE文件头
if (pNtH->Signature != IMAGE_NT_SIGNATURE ) //判断是否为PE文件头PE
return FALSE;
return TRUE;
}
FileHeader和OptionalHeader内容的读取
IMAGE_NT_HEADERS STRUCT
Signature DWORD ? ;PE文件标识
FileHeader IMAGE_FILE_HEADER <>
OptionalHeader IMAGE_OPTIONAL_HEADER32 <>
IMAGE_NT_HEADERS ENDS
从上面的结构可知,只要得到了IMAGE_NT_HEADERS,根据IMAGE_NT_HEADERS的定义,就可以找到IMAGE_FILE_HEADER和IMAGE_OPTIONAL_HEADER32。
首先我们要得到IMAGE_NT_HEADERS结构指针的函数:
PIMAGE_NT_HEADERS GetNtHeaders(LPVOID ImageBase)
{
if(!IsPEFile(ImageBase)) //通过文件基址来判断文件是否为PE文件
return NULL;
PIMAGE_NT_HEADERS pNtH; //定义PE文件头指针
PIMAGE_DOS_HEADER pDH; //定义DOS头指针
pDH=(PIMAGE_DOS_HEADER)ImageBase; //得到DOS指针
pNtH=(PIMAGE_NT_HEADERS)((DWORD)pDH+pDH->e_lfanew); //得到PE文件头指针
return pNtH;
}
上面得到了IMAGE_NT_HEADERS结构的指针,下面我们来得到两个重要的结构指针:
IMAGE_FILE_HEADER结构的指针,函数如下:
PIMAGE_FILE_HEADER GetFileHeader(LPVOID ImageBase)
{
PIMAGE_NT_HEADERS pNtH = NULL; //定义PE文件头指针
PIMAGE_NT_HEADERS pFH = NULL; //定义映射文件头指针
pNtH = GetNtHeaders(ImageBase); //得到PE文件头指针
if (!pNtH)
return NULL;
pFH=&pNtH->FileHeader; //得到映射文件头指针
return pFH; //返回IMAGE_FILE_HEADER指针
}
IMAGE_OPTIONAL_HEADER32结构的指针,函数如下:
PIMAGE_OPTIONAL_HEADER GetOptionalHeader(LPVOID ImageBase)
{
PIMAGE_NT_HEADERS pNtH = NULL; //定义PE文件头指针
PIMAGE__OPTIONAL_HEADER pOH = NULL; //定义可选映射头指针
pNtH = GetNtHeaders(ImageBase); //得到PE文件头指针
if (!pNtH)
return NULL;
pOH=&pNtH->OptionalHeader; //得到可选映像头指针
return pOH; //返回IMAGE_OPTION_HEADER32指针
}
得到了这两个重要的结构指针之后,其它的事情就变得这样简单,我们只需要将FileHeader和OptionalHeader的信息显示出来,在《加密与解密》第三版中,是把FileHeader和OptionalHeader的信息以十六进制方式显示在编辑控件上,此时先用函数wsprintf将显示的值进行格式化,然后调用API函数中的SetDlgItemText即可,代码如下:
大家先先看看FileHeader的结构如下:
IMAGE_FILE_HEADER STRUCT
Machine WORD ? ;0004h - 运行平台
NumberOfSections WORD ? ;0006h - 文件的节数目
TimeDateStamp DWORD ? ;0008h - 文件创建日期和时间
PointerToSymbolTable DWORD ? ;000ch - 指向符号表(用于调试)
NumberOfSymbols DWORD ? ;0010h - 符号表中的符号数量(用于调试)
SizeOfOptionalHeader WORD ? ;0014h - IMAGE_OPTIONAL_HEADER32结构的长度
Characteristics WORD ? ;0016h - 文件属性
IMAGE_FILE_HEADER ENDS
下面编程将上面的各个信息完全显示出来了!!
void ShowFileHeaderInfo(HWND hWnd)
{
char cBuff[10];
PIMAGE_FILE_HEADER pFH=NULL;
pFH=GetFileHeader(stMapFile.ImageBase); //得到文件头指针
if(!pFH)
{
MessageBox(hWnd,"Can't get File Header ! :(","PEInfo_Example",MB_OK);
return;
}
wsprintf(cBuff, "%04lX", pFH->Machine); //格式化输出内容
SetDlgItemText(hWnd,IDC_EDIT_FH_MACHINE,cBuff);
wsprintf(cBuff, "%04lX", pFH->NumberOfSections);
SetDlgItemText(hWnd,IDC_EDIT_FH_NUMOFSECTIONS,cBuff);
wsprintf(cBuff, "%08lX", pFH->TimeDateStamp);
SetDlgItemText(hWnd,IDC_EDIT_FH_TDS,cBuff);
wsprintf(cBuff, "%08lX", pFH->PointerToSymbolTable);
SetDlgItemText(hWnd,IDC_EDIT_FH_PTSYMBOL,cBuff);
wsprintf(cBuff, "%08lX", pFH->NumberOfSymbols);
SetDlgItemText(hWnd,IDC_EDIT_FH_NUMOFSYM,cBuff);
wsprintf(cBuff, "%04lX", pFH->SizeOfOptionalHeader);
SetDlgItemText(hWnd,IDC_EDIT_FH_SIZEOFOH,cBuff);
wsprintf(cBuff, "%04lX", pFH->Characteristics);
SetDlgItemText(hWnd,IDC_EDIT_FH_CHARACTERISTICS,cBuff);
}
再来看看OptionalHeader结构的信息:
IMAGE_OPTIONAL_HEADER32 STRUCT
Magic WORD ? ;0018h 107h=ROM Image,10Bh=exe Image
MajorLinkerVersion BYTE ? ;001ah 链接器版本号
MinorLinkerVersion BYTE ? ;001bh
SizeOfCode DWORD ? ;001ch 所有含代码的节的总大小
SizeOfInitializedData DWORD? ;0020h所有含已初始化数据的节的总大小
SizeOfUninitializedData DWORD ? ;0024h 所有含未初始化数据的节的大小
AddressOfEntryPoint DWORD ? ;0028h 程序执行入口RVA
BaseOfCode DWORD ? ;002ch 代码的节的起始RVA
BaseOfData DWORD ? ;0030h 数据的节的起始RVA
ImageBase DWORD ? ;0034h 程序的建议装载地址
SectionAlignment DWORD ? ;0038h 内存中的节的对齐粒度
FileAlignment DWORD ? ;003ch 文件中的节的对齐粒度
MajorOperatingSystemVersion WORD ? ;0040h 操作系统主版本号
MinorOperatingSystemVersion WORD ? ;0042h 操作系统副版本号
MajorImageVersion WORD ? ;0044h可运行于操作系统的最小版本号
MinorImageVersion WORD ? ;0046h
MajorSubsystemVersion WORD ?;0048h 可运行于操作系统的最小子版本号
MinorSubsystemVersion WORD ? ;004ah
Win32VersionValue DWORD ? ;004ch 未用
SizeOfImage DWORD ? ;0050h 内存中整个PE映像尺寸
SizeOfHeaders DWORD ? ;0054h 所有头+节表的大小
CheckSum DWORD ? ;0058h
Subsystem WORD ? ;005ch 文件的子系统
DllCharacteristics WORD ? ;005eh
SizeOfStackReserve DWORD ? ;0060h 初始化时的堆栈大小
SizeOfStackCommit DWORD ? ;0064h 初始化时实际提交的堆栈大小
SizeOfHeapReserve DWORD ? ;0068h 初始化时保留的堆大小
SizeOfHeapCommit DWORD ? ;006ch 初始化时实际提交的堆大小
LoaderFlags DWORD ? ;0070h 未用
NumberOfRvaAndSizes DWORD ? ;0074h 下面的数据目录结构的数量
DataDirectory IMAGE_DATA_DIRECTORY 16 dup(<>) ;0078h
IMAGE_OPTIONAL_HEADER32 ENDS
代码和上面是一样的,只是未显示完全,只是显示了几个重要的字段内容!!
得到数据目录表的信息:
数据目录表(DataDirectory)由一组数组构成,每组项目包括执行文件的重要部分的起妈RVA和长度。因为数据目录有16项,书有用了一种简单的方法,就是定义一个编辑控件ID的结构数组,用一个循环就可以了。
我们先来看一个DataDirectory的结构
IMAGE_DATA_DIRECTORY STRUCT
VirtualAddress DWORD ? ;数据的起始RVA
Size DWORD ? ;数据块的长度
IMAGE_DATA_DIRECTORY ENDS
很简单就两个字段,我们就先定义一个结构体用于存放这两个字段,然后在定义一个结构体数组就于存放十六个结构体
typedef struct
{
UINT ID_RVA; //用于存放DataDirectory数据块的起始RVA
UINT ID_SIZE; //用于存放DataDirectory数据块的大小
} DataDir_EditID;
DataDir_EditID EditID_Array[]=
{
{IDC_EDIT_DD_RVA_EXPORT, IDC_EDIT_DD_SIZE_EXPORT},
{IDC_EDIT_DD_RVA_IMPORT, IDC_EDIT_DD_SIZE_IMPORT},
{IDC_EDIT_DD_RVA_RES, IDC_EDIT_DD_SZIE_RES},
{IDC_EDIT_DD_RVA_EXCEPTION, IDC_EDIT_DD_SZIE_EXCEPTION},
{IDC_EDIT_DD_RVA_SECURITY, IDC_EDIT_DD_SIZE_SECURITY},
{IDC_EDIT_DD_RVA_RELOC, IDC_EDIT_DD_SIZE_RELOC},
{IDC_EDIT_DD_RVA_DEBUG, IDC_EDIT_DD_SIZE_DEBUG},
{IDC_EDIT_DD_RVA_COPYRIGHT, IDC_EDIT_DD_SIZE_COPYRIGHT},
{IDC_EDIT_DD_RVA_GP, IDC_EDIT_DD_SIZE_GP},
{IDC_EDIT_DD_RVA_TLS, IDC_EDIT_DD_SIZE_TLS},
{IDC_EDIT_DD_RVA_LOADCONFIG, IDC_EDIT_DD_SIZE_LOADCONFIG},
{IDC_EDIT_DD_RVA_IAT, IDC_EDIT_DD_SIZE_IAT},
{IDC_EDIT_DD_RVA_BOUND, IDC_EDIT_DD_SIZE_BOUND},
{IDC_EDIT_DD_RVA_COM, IDC_EDIT_DD_SIZE_COM},
{IDC_EDIT_DD_RVA_DELAYIMPORT,IDC_EDIT_DD_SIZE_DELAYIMPORT},
{IDC_EDIT_DD_RVA_NOUSE, IDC_EDIT_DD_SIZE_NOUSE}
};
上面正是定义了十六个DataDirectory,这里用一个数组表示,主要是为了方便编程时使用,以避免代码的冗长!!!
显示数据目录表的函数如下:
void ShowDataDirInfo(HWND hDlg)
{
char cBuff[9];
PIMAGE_OPTIONAL_HEADER pOH=NULL;
pOH=GetOptionalHeader(stMapFile.ImageBase); //得到IMAGE_OPTION_HEADER32的结构指针
if(!pOH)
return;
for(int i=0;i<16;i++) //循环显示数据目录表的十六个元素
{
wsprintf(cBuff, "%08lX", pOH->DataDirectory[i].VirtualAddress); //格式化DataDirectory中数据块的RVA
SetDlgItemText(hDlg,EditID_Array[i].ID_RVA,cBuff); //设置DataDirectory中数据块的RVA
wsprintf(cBuff, "%08lX", pOH->DataDirectory[i].Size); //格式化DataDirectory中数据块的Size
SetDlgItemText(hDlg,EditID_Array[i].ID_SIZE,cBuff); //设置DataDirectory中数据块的Size
}
}
得到区块表信息
紧接IMAGE_NT_HEADERS以后就是区块表(Section Table)了,Section Table则是由IMAGE_SECTION_HEADER组成的数组。如何得到Section Table的位置呢?换名话说,也就是如何得到第一个IMAGE_SECTION_HEADER的位置。在Visual C++中,可以利用IMAGE_FIRST_SECTION宏来轻松得到第一个IMAGE_SECTION_HEADER的位置。(这里先讲在VC中得到区块表,到后面会具体讲在汇编中如何得到)
又因为区块的个数已经在文件头中指明了,所以只要得到第一个区块的位置,然后再利用一个循环语句就可以得到所有区块的信息了。
请看下面的函数就是利用IMAGE_FIRST_SECTION宏得到区块表的起始位置。
PIMAGE_SECTION_HEADER GetFirstSectionHeader(PIMAGE_NT_HEADERS pNtH)
{
PIMAGE_SECTION_HEADER pSH; //定义区块表首地址指针
pSH = IMAGE_FIRST_SECTION(pNtH); //得到区块表首地址指针
return pSH; //返回区块首地址指针
}
这里必须强调一下,在一个PE文件中,OptionHeader的大小是可以变化的,虽然它的大小通常为E0h,但是总是有例外,原因是可选文件头的大小是由文件头的SizeOfOptionalHeader字段指定的,并不是个固定值。这也是IMAGE_FIRST_SECTION宏对于可选文件头的大小为什么不直接用固定值的原因。系统的PE加载器在加载PE文件的时候,也是利用了文件头中的SizeOfOptionalHeader字段的值来定位区块表的,而不是用固定值。能否正确定位到区块表,取决于SizeOfOptionalHeader字段的值的正确性。这是个很容易被忽略的问题,因些导致一些程序的BUG。书中使用ListView控件来显示PE文件头的区段信息。具体代码如下:
void ShowSectionHeaderInfo(HWND hDlg)
{
LVITEM lvItem;
char cBuff[9],cName[9];
WORD i;
PIMAGE_FILE_HEADER pFH=NULL; //定义映射头指针
PIMAGE_SECTION_HEADER pSH=NULL; //定义区块表指针
pFH=GetFileHeader(stMapFile.ImageBase); //得到映射头的指针主要是为了得到区块的数目通过NumberOfSections字段
if(!pFH)
return;
pSH=GetFirstSectionHeader(stMapFile.ImageBase); //得到第一个区块表指针
for( i=0;i<pFH->NumberOfSections;i++) //循环得到各区块表的指针
{
memset(&lvItem, 0, sizeof(lvItem));
lvItem.mask = LVIF_TEXT;
lvItem.iItem = i;
memset(cName,0,sizeof(cName)); //设置区块表中的各个字段的值
memcpy(cName, pSH->Name, 8);
lvItem.pszText = cName;
SendDlgItemMessage(hDlg,IDC_SECTIONLIST,LVM_INSERTITEM,0,(LPARAM)&lvItem);
lvItem.pszText = cBuff;
wsprintf(cBuff, "%08lX", pSH->VirtualAddress);
lvItem.iSubItem = 1;
SendDlgItemMessage(hDlg,IDC_SECTIONLIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
wsprintf(cBuff, "%08lX", pSH->Misc.VirtualSize);
lvItem.iSubItem = 2;
SendDlgItemMessage(hDlg,IDC_SECTIONLIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
wsprintf(cBuff, "%08lX", pSH->PointerToRawData);
lvItem.iSubItem = 3;
SendDlgItemMessage(hDlg,IDC_SECTIONLIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
wsprintf(cBuff, "%08lX", pSH->SizeOfRawData);
lvItem.iSubItem = 4;
SendDlgItemMessage(hDlg,IDC_SECTIONLIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
wsprintf(cBuff, "%08lX", pSH->Characteristics);
lvItem.iSubItem = 5;
SendDlgItemMessage(hDlg,IDC_SECTIONLIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
++pSH; //指向下一个区块位置
}
}
得到输出表信息
输出表(Export Table)中的主要成分是一个表格,内含函数名称,输出序数等。输出表是数据目录表的第一个成员,其指向IMAGE_EXPORT_DIRECTORY结构。输出函数的个数是由结构IMAGE_EXPORT_DIRECTORY的字段NumberOfFunctions来说明的。实际上,也有例外,例如在写一个DLL的时候,可以用DEF文件来制定输出函数的名称,序号等。请看下面这个DEF文件内容:
LIBRARY TEST
EXPORTS
Func1 @1
Func2 @12
Func3 @18
Func4 @23
Func5 @31
在这个文件中,共输出了五个函数(Func1到Func5),而输出函数的序号却是1到31,如果没有考虑到这一点的话,很有可能会在这里出错,因为这时IMAGE_EXPORT_DIRECTORY的字段NumberOfFunctions的值为0x1F,即31。如果认为NumberOfFunctions值就为输出函数个数的话,就错了。
首先通过下面的两个函数来得到输出表的指针
LPVOID GetDirectoryEntryToData(LPVOID ImageBase,USHORT DirectoryEntry)
{
DWORD dwDataStartRVA;
LPVOID pDirData=NULL;
PIMAGE_NT_HEADERS pNtH=NULL;
PIMAGE_OPTIONAL_HEADER pOH=NULL;
pNtH=GetNtHeaders(ImageBase);
if(!pNtH)
return NULL;
pOH=GetOptionalHeader(ImageBase);
if(!pOH)
return NULL;
dwDataStartRVA=pOH->DataDirectory[DirectoryEntry].VirtualAddress;
if(!dwDataStartRVA)
return NULL;
pDirData=RvaToPtr(pNtH,ImageBase,dwDataStartRVA);
if(!pDirData)
return NULL;
return pDirData;
}
PIMAGE_EXPORT_DIRECTORY GetExportDirectory(LPVOID ImageBase)
{
PIMAGE_EXPORT_DIRECTORY pExportDir=NULL;
pExportDir=(PIMAGE_EXPORT_DIRECTORY)GetDirectoryEntryToData(ImageBase,IMAGE_DIRECTORY_ENTRY_EXPORT);
if(!pExportDir)
return NULL;
return pExportDir;
}
PIMAGE_IMPORT_DESCRIPTOR GetFirstImportDesc(LPVOID ImageBase)
{
PIMAGE_IMPORT_DESCRIPTOR pImportDesc;
pImportDesc=(PIMAGE_IMPORT_DESCRIPTOR)GetDirectoryEntryToData(ImageBase,IMAGE_DIRECTORY_ENTRY_IMPORT);
if(!pImportDesc)
return NULL;
return pImportDesc;
}
显示输出表信息的函数如下:
void ShowExportFuncsInfo(HWND hDlg)
{
HWND hList;
LVITEM lvItem;
char cBuff[10], *szFuncName;
UINT iNumOfName=0;
PDWORD pdwRvas, pdwNames;
PWORD pwOrds;
UINT i=0,j=0,k=0;
BOOL bIsByName=FALSE;;
PIMAGE_NT_HEADERS pNtH=NULL;
PIMAGE_EXPORT_DIRECTORY pExportDir=NULL;
pNtH=GetNtHeaders(stMapFile.ImageBase);
if(!pNtH)
return ;
pExportDir= (PIMAGE_EXPORT_DIRECTORY)GetExportDirectory(stMapFile.ImageBase); //调用GetExprotDirectory来得到输出表的首地址指针
if (!pExportDir)
return ;
pwOrds=(PWORD)RvaToPtr(pNtH,stMapFile.ImageBase,pExportDir->AddressOfNameOrdinals); //指向输出序列号数组
pdwRvas=(PDWORD)RvaToPtr(pNtH,stMapFile.ImageBase,pExportDir->AddressOfFunctions); //指向函数地址数组
pdwNames=(PDWORD)RvaToPtr(pNtH,stMapFile.ImageBase,pExportDir->AddressOfNames); //函数名字的指针地址
if(!pdwRvas) //如果函数地址数组为NULL,则直接返回
return;
hList=GetDlgItem(hDlg,IDC_EXPORT_LIST);
SendMessage(hList,LVM_SETEXTENDEDLISTVIEWSTYLE,0,(LPARAM)LVS_EX_FULLROWSELECT);
iNumOfName=pExportDir->NumberOfNames; //得到函数名字的指针地址阵列中的元素个数
for( i=0;i<pExportDir->NumberOfFunctions;i++) //得到函数地址数组阵列中的元素个数
{
if(*pdwRvas) //如果函数地址数组中的值不为NULL,则继续显示,否则指向函数地址数组中下一个
{
for( j=0;j<iNumOfName;j++) //以函数名字指针地址阵列中的元素个数为循环
{
if(i==pwOrds[j]) //如果函数地址数组的值等于函数名字的指针地址中元素j的值
{
bIsByName=TRUE;
szFuncName=(char*)RvaToPtr(pNtH,stMapFile.ImageBase,pdwNames[j]);
break;
}
bIsByName=FALSE;
}
//show funcs to listctrl
memset(&lvItem, 0, sizeof(lvItem));
lvItem.mask = LVIF_TEXT;
lvItem.iItem = k;
lvItem.pszText = cBuff;
wsprintf(cBuff, "%04lX", (UINT)(pExportDir->Base+i));
SendDlgItemMessage(hDlg,IDC_EXPORT_LIST,LVM_INSERTITEM,0,(LPARAM)&lvItem);
lvItem.pszText = cBuff;
wsprintf(cBuff, "%08lX", (*pdwRvas));
lvItem.iSubItem = 1;
SendDlgItemMessage(hDlg,IDC_EXPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
if(bIsByName)
lvItem.pszText=szFuncName;
else
lvItem.pszText = "-";
lvItem.iSubItem = 2;
SendDlgItemMessage(hDlg,IDC_EXPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
//
++k;
}
++pdwRvas;
}
}
得到输入表的信息
数据目录表第二个成员指向输入表。输入表以一个IMAGE_IMPORT_DESCRIPTOR结构开始,以一个空的IMAGE_IMPORT_DESCRIPTOR结构结束。在这里可以通过GetFirstImportDesc函数得到ImportTable在文件中的位置。
GetFirstImportDesc函数的定义如下:
PIMAGE_IMPORT_DESCRIPTOR GetFirstImportDesc(LPVOID ImageBase)
{
PIMAGE_IMPORT_DESCRIPTOR pImportDesc;
pImportDesc=(PIMAGE_IMPORT_DESCRIPTOR)GetDirectoryEntryToData(ImageBase,IMAGE_DIRECTORY_ENTRY_IMPORT);
if(!pImportDesc)
return NULL;
return pImportDesc;
}
这个函数同样用到了上面所使用的GetDirectoryEntryToData,这个函数是用于专门得到区块表的各个数据块的位置而准备的,我们找到了输入表的位置,可以通过一个循环来得到整个输入表,循环终止的条件是IMAGE_IMPORT_DESCRIPTOR结构为空。
void ShowImportDescInfo(HWND hDlg)
{
HWND hList;
LVITEM lvItem;
char cBuff[10], * szDllName;
PIMAGE_NT_HEADERS pNtH=NULL;
PIMAGE_IMPORT_DESCRIPTOR pImportDesc=NULL;
memset(&lvItem, 0, sizeof(lvItem));
hList=GetDlgItem(hDlg,IDC_IMPORT_LIST);
SendMessage(hList,LVM_SETEXTENDEDLISTVIEWSTYLE,0,(LPARAM)LVS_EX_FULLROWSELECT);
pNtH=GetNtHeaders(stMapFile.ImageBase);
pImportDesc=GetFirstImportDesc(stMapFile.ImageBase);
if(!pImportDesc)
{
MessageBox(hDlg,"Can't get ImportDesc:(","PEInfo_Example",MB_OK);
return;
}
int i=0;
while(pImportDesc->FirstThunk)
{
memset(&lvItem, 0, sizeof(lvItem));
lvItem.mask = LVIF_TEXT;
lvItem.iItem = i;
szDllName=(char*)RvaToPtr(pNtH,stMapFile.ImageBase,pImportDesc->Name);
lvItem.pszText = szDllName;
SendDlgItemMessage(hDlg,IDC_IMPORT_LIST,LVM_INSERTITEM,0,(LPARAM)&lvItem);
lvItem.pszText = cBuff;
wsprintf(cBuff, "%08lX", pImportDesc->OriginalFirstThunk);
lvItem.iSubItem = 1;
SendDlgItemMessage(hDlg,IDC_IMPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
lvItem.pszText = cBuff;
wsprintf(cBuff, "%08lX", pImportDesc->TimeDateStamp);
lvItem.iSubItem = 2;
SendDlgItemMessage(hDlg,IDC_IMPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
lvItem.pszText = cBuff;
wsprintf(cBuff, "%08lX", pImportDesc->ForwarderChain);
lvItem.iSubItem = 3;
SendDlgItemMessage(hDlg,IDC_IMPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
lvItem.pszText = cBuff;
wsprintf(cBuff, "%08lX", pImportDesc->Name);
lvItem.iSubItem = 4;
SendDlgItemMessage(hDlg,IDC_IMPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
lvItem.pszText = cBuff;
wsprintf(cBuff, "%08lX", pImportDesc->FirstThunk);
lvItem.iSubItem = 5;
SendDlgItemMessage(hDlg,IDC_IMPORT_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
++i;
++pImportDesc;
}
}
在ShowImportDescInfo函数中,首先用GetFirstImportDesc函数得到指向第一个IMAGE_IMPORT_DESCRIPTOR结构和指针pImportDesc,以pImportDesc-->FirstThunk为真来作为循环的条件,循环得到ImportTable的各项信息。
通过上面的ShowImportDescInfo函数,可以得到PE文件所引入的DLL的信息,接下来的任务就是如何分析得到通过DLL所输入的函数的信息,这里必须通过IMAGE_IMPORT_DESCRIPTOR所提供的信息来得到输入的函数的信息。可以通过名字和序号来引入所用的函数,怎么来区分一个函数是如何引入的呢?在于IMAGE_THUNK_DATA值的高位,如果被置位了,低31位被看作是一个序数值。如果高位没有被置位,IMAGE_THUNK_DATA值是一个指向IMAGE_IMPORT_BY_NAME的RVA。如果两者都不是,则可以认为IMAGE_THUNK_DATA值为函数的内存地址。具体参考下面的ShowImportFuncsByDllIndex(HWND hDlg,int index)函数:
void ShowImportFuncsByDllIndex(HWND hDlg,int index)
{
HWND hFuncList;
LVITEM lvItem;
char cBuff[30],cOrd[30],cMemAddr[30], * szFuncName;
DWORD dwThunk, *pdwThunk=NULL, *pdwRVA=NULL;
int i=0;
PIMAGE_NT_HEADERS pNtH=NULL;
PIMAGE_IMPORT_DESCRIPTOR pFistImportDesc=NULL,pCurrentImportDesc=NULL;
PIMAGE_IMPORT_BY_NAME pByName=NULL;
memset(&lvItem, 0, sizeof(lvItem));
hFuncList=GetDlgItem(hDlg,IDC_IMPORTFUNCTIONS_LIST);
SendMessage(hFuncList,LVM_SETEXTENDEDLISTVIEWSTYLE,0,(LPARAM)LVS_EX_FULLROWSELECT);
SendMessage(hFuncList,LVM_DELETEALLITEMS ,0,0);
pNtH=GetNtHeaders(stMapFile.ImageBase);
pFistImportDesc=GetFirstImportDesc(stMapFile.ImageBase);
pCurrentImportDesc=&pFistImportDesc[index];
dwThunk=GETTHUNK(pCurrentImportDesc);
pdwRVA=(DWORD *)dwThunk;
pdwThunk=(DWORD*)RvaToPtr(pNtH,stMapFile.ImageBase,dwThunk);
if(!pdwThunk)
return;
while(*pdwThunk)
{
memset(&lvItem, 0, sizeof(lvItem));
lvItem.mask = LVIF_TEXT;
lvItem.iItem = i;
lvItem.pszText = cBuff;
wsprintf(cBuff, "%08lX",(DWORD)pdwRVA);
SendDlgItemMessage(hDlg,IDC_IMPORTFUNCTIONS_LIST,LVM_INSERTITEM,0,(LPARAM)&lvItem);
lvItem.pszText = cBuff;
wsprintf(cBuff, "%08lX", (DWORD)(*pdwThunk));
lvItem.iSubItem = 1;
SendDlgItemMessage(hDlg,IDC_IMPORTFUNCTIONS_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
if (HIWORD(*pdwThunk)==0x8000) //如果最高位被置位了,那么低31位是一个序数值
{
strcpy(cBuff,"-");
wsprintf(cOrd, "Ord:%08lX",IMAGE_ORDINAL32(*pdwThunk));
szFuncName=cOrd;
}
else //如果最高位没有被置位IMAGE_THUNK_DATA值是指向IMAGE_IMPORT_BY_NAME的RVA
{
pByName =(PIMAGE_IMPORT_BY_NAME)RvaToPtr(pNtH,stMapFile.ImageBase,(DWORD)(*pdwThunk));
if(pByName)
{
wsprintf(cBuff,"%04lX",pByName->Hint);
szFuncName=(char *)pByName->Name;
}
else //如果两者都不是,则可以认为IMAGE_THUNK_DATA值为函数的内存地址
{
strcpy(cBuff,"-");
wsprintf(cMemAddr, "MemAddr:%08lX",(DWORD)(*pdwThunk));
szFuncName=cMemAddr;
}
}
lvItem.pszText = cBuff;
lvItem.iSubItem = 2;
SendDlgItemMessage(hDlg,IDC_IMPORTFUNCTIONS_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
lvItem.pszText = szFuncName;
lvItem.iSubItem = 3;
SendDlgItemMessage(hDlg,IDC_IMPORTFUNCTIONS_LIST, LVM_SETITEM, 0, (LPARAM)&lvItem);
++i;
++pdwRVA;
++pdwThunk;
}
}
到此,一个PE的简单工具的核心代码就基本上分析完毕了,其实当你真正了解了前面三章所讲的内容,再去看这些代码,你会觉得很简单是不是,只要你花时间,你也可以写一人简单的PE分析工具,呵呵,如果还没有弄明白的,我向大家推荐《加密与解密》第三版第十章的PE工具编写的源代码,大家可以再仔细研究研究,由于我的C不是很好,这时我就先说到这里吧,明天我会继续给大家讲解PE编程方面的问题,我相信一定会让大家对PE编程更加清楚明白!!
上篇文章有位朋友说要写上参考文献-----《加密与解密》第三版第十章PE工具的编写