编写PE文件解析器(二)

来源:互联网 发布:淘宝买家4钻很厉害吗 编辑:程序博客网 时间:2024/05/22 07:52

因为是先写代码再写文字,代码在虚拟机里我给还原了...用的以前备份,所以后面一些东西可能有点出入...


5、IMAGE_DIRECTORY_ENTRY_IMPORT【导入表 - IMAGE_IMPORT_DESCRIPTOR

定位到导入表需要用IMAGE_DIRECTORY_ENTRY_IMPORT作为下标取数据目录的第二项,用之前定义的宏就是:

GET_IMAGE_DIRECTORY(PIMAGE_IMPORT_DESCRIPTOR,IMAGE_DIRECTORY_ENTRY_IMPORT,ImportDirectory);

这样可以定位到导入表数组的开头,导入表结构是一个IMAGE_IMPORT_DESCRIPTOR类型的数组。Name记录导入模块的名称,TimeDateStamp当为0时表示未绑定,-1表示用绑定导入表,其它值为绑定dll的时间戳。数组内的OriginalFirstThunk、FirstThunk 都各自指向(记录的是rva)OriginalThunk和Thunk数组的开头,数组类型是IMAGE_THUNK_DATA,区分32位和64位,不是同一个数组,但是内容一样。数组元素记录了指向IMAGE_IMPORT_BY_NAME结构的偏移(rva),在文件中是指向同一个IMAGE_IMPORT_BY_NAME,这个结构记录了该导入模块下的导入函数名;当被loader载入后,Thunk数组的内容会被修改为导入模块的真实函数地址。

IMAGE_THUNK_DATA中的Ordinal最高位为1表示按序号导入,其低16位为序号,如果最高位为0则AddressOfData(跟Ordinal同属一个联合体)是一个RVA,指向一个IMAGE_IMPORT_BY_NAME结构,用IMAGE_SNAP_BY_ORDINAL判断是否按序号导入,IMAGE_ORDINAL用来获取导入序号。

导入表数组长度可以用导入表大小/sizeof(IMAGE_IMPORT_DESCRIPTOR)得到。

这个表在iat hook的时候会用到。一般怎么用就写怎么读的代码,我用yield return的方法,于是就有了下面的coroutine代码:

bool PE::ReadImportDirectory(IN OUT PIMAGE_IMPORT_DESCRIPTOR& descriptor,OUT PTHUNK_DATA& originalThunk,OUT PTHUNK_DATA& thunk, bool isReadThunk, bool isReadOnceDescriptor, IN OUT PDWORD stat){static DWORD descriptorLength;switch (*stat){default:case 0:*stat = 1;if (ImportDirectory == NULL && descriptor == NULL){return false;}if (descriptor == NULL){descriptor = ImportDirectory;descriptorLength = *ImportDirectorySize / sizeof(IMAGE_IMPORT_DESCRIPTOR) - 1;}else{descriptorLength = 1;isReadThunk = true;isReadOnceDescriptor = true;}while (descriptorLength){case 1:originalThunk = NULL;thunk = NULL;*stat = 3;if (isReadThunk){if (this->isUseRva){originalThunk = PTHUNK_DATA(_filebuf + descriptor->OriginalFirstThunk);thunk = PTHUNK_DATA(_filebuf + descriptor->FirstThunk);}else{originalThunk = PTHUNK_DATA(_filebuf + RvaToRaw(descriptor->OriginalFirstThunk));thunk = PTHUNK_DATA(_filebuf + RvaToRaw(descriptor->FirstThunk));}// 遍历thunkwhile (originalThunk->thunkData32.u1.Ordinal && thunk->thunkData32.u1.Ordinal){*stat = 2;return true;case 2:if (!isReadThunk){break;}if (is32Pe){originalThunk = PTHUNK_DATA(((PIMAGE_THUNK_DATA32)originalThunk) + 1);thunk = PTHUNK_DATA(((PIMAGE_THUNK_DATA32)thunk) + 1);}else{originalThunk = PTHUNK_DATA(((PIMAGE_THUNK_DATA64)originalThunk) + 1);thunk = PTHUNK_DATA(((PIMAGE_THUNK_DATA64)thunk) + 1);}}// goto xxx}else{return true;case 3:if (isReadThunk){continue;}}// xxxif (isReadOnceDescriptor){break;}descriptor++;descriptorLength--;}break;}return false;}

这个函数可以跳过导入函数名不读而只读导入模块名,或者读特定导入模块下的导入函数,或者用来遍历整个表也行,基本上一次遍历可以把想要想改的数据都解决,所以一般使用是没问题了,用这种方式来写纯粹是因为无聊,用前初始化好就行。

为了32位64位通用,里面thunk指针的定义是:

typedef union{IMAGE_THUNK_DATA32 thunkData32;IMAGE_THUNK_DATA64 thunkData64;}*PTHUNK_DATA;
如果想搜索导入了KERNEL32.dll的哪些函数,这里示范一下上面函数的用法(这里为了方便没有对Thunk的Ordinal进行判断,实际上是需要进行区分的,用IMAGE_SNAP_BY_ORDINAL判断的时候需要区分32或64位):

PIMAGE_IMPORT_DESCRIPTOR des = NULL;PE::PTHUNK_DATA ot;PE::PTHUNK_DATA t;DWORD stat;bool isReadThunk = false;bool isReadOnceDescriptor = false;while (x->ReadImportDirectory(des, ot, t, isReadThunk, isReadOnceDescriptor, &stat)){if (isReadThunk){isReadOnceDescriptor = true;auto mm = PIMAGE_IMPORT_BY_NAME(x->_filebuf+x->RvaToRaw( ot->thunkData32.u1.AddressOfData));OutputDebugStringA(mm->Name);OutputDebugStringA("\r\n");}else{if(strcmp( x->_filebuf+x->RvaToRaw( des->Name),"KERNEL32.dll") == 0){isReadThunk = true;}}}

导入表我觉得描述的够清楚了,不用上图了。


6、IMAGE_DIRECTORY_ENTRY_RESOURCE【资源目录 - IMAGE_RESOURCE_DIRECTORY

树结构,节点中记录的偏移量都是相对根的位置。
简单来看就非叶节点是由一堆的IMAGE_RESOURCE_DIRECTORY结构组成的,这个结构的后面紧跟着一个IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组(长度记录在IMAGE_RESOURCE_DIRECTORY结构中,分别以NumberOfNamedEntries、NumberOfIdEntries来区分ENTRY以什么方式命名),这个数组中的元素记录着下一个IMAGE_RESOURCE_DIRECTORY节点或叶子IMAGE_RESOURCE_DATA_ENTRY相对于根的偏移。

IMAGE_RESOURCE_DIRECTORY_ENTRY结构是这样区分指向的,它包含2个字段

第一个为名称或id,32位长度,最高位为1时代表这个字段低31位存的是名字的偏移,否则低16位表示的是id,使用IMAGE_RESOURCE_NAME_IS_STRING作为mask可以方便的在程序里判断,如果是偏移,会指向IMAGE_RESOURCE_DIRECTORY_STRING结构,这个结构不定长,是用WORD存储字符长度的一个字符串,具体看定义就清楚了;

第二个字段为指向节点IMAGE_RESOURCE_DIRECTORY或叶子IMAGE_RESOURCE_DATA_ENTRY的偏移,最高位为1表示低31位为节点相对根的偏移,否则低31位为数据存储位置相对根的偏移,用IMAGE_RESOURCE_DATA_IS_DIRECTORY判断。最后的叶子IMAGE_RESOURCE_DATA_ENTRY结构中存储的OffsetToData不是相对于根的偏移,而是rva,因为这东西是要放到内存用的,所以存rva计算量少会快一些。

一般资源目录要取到数据需要读3层IMAGE_RESOURCE_DIRECTORY,第一层为资源分类,第二层类似数据的序号也就是名字,第三层表示数据的语言,最后的叶子就是数据了。

整个资源目录在文件中是以层的优先顺序来存储的,就是以根->第一层所有的节点->第二层所有的节点->....这样的类似广度优先的方式存储

好像描述清楚了,不清楚翻一下数据结构的书好了,我懒就不画图了。

因为用到递归所以用函数指针吧,这样写就要一次读完所有内容,不过好在这个树结构一般不会太长:

void PE::ResDir_ReadResourceDirectory(PIMAGE_RESOURCE_DIRECTORY item, OUT PIMAGE_RESOURCE_DIRECTORY_ENTRY& entry, DWORD& numberOfEntries){numberOfEntries = item->NumberOfIdEntries + item->NumberOfNamedEntries;if (numberOfEntries>0){entry = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)((DWORD)item + sizeof(IMAGE_RESOURCE_DIRECTORY));}else{entry = NULL;}}void PE::ReadResourceDirectory(PResDir_ReadResourceDirectoryCallback resourceDirectoryCallback,PResDir_ReadDirectoryEntryCallback directoryEntryCallback,PResDir_ReadDataEntryCallback dataEntryCallback){return ReadResourceDirectory(ResourceDirectory, ResourceDirectory, resourceDirectoryCallback, directoryEntryCallback,dataEntryCallback);}void PE::ReadResourceDirectory(PIMAGE_RESOURCE_DIRECTORY father, PIMAGE_RESOURCE_DIRECTORY node,PResDir_ReadResourceDirectoryCallback resourceDirectoryCallback,PResDir_ReadDirectoryEntryCallback directoryEntryCallback,PResDir_ReadDataEntryCallback dataEntryCallback){// 读当前nodeif (resourceDirectoryCallback != NULL){resourceDirectoryCallback(this,father, node);}DWORD numberOfEntries;PIMAGE_RESOURCE_DIRECTORY_ENTRY tmpEntry;ResDir_ReadResourceDirectory(node,tmpEntry,numberOfEntries);if (tmpEntry == NULL){return;}for (int i = 0; i < numberOfEntries; i++){if (directoryEntryCallback != NULL){directoryEntryCallback(this, node, tmpEntry);}if (tmpEntry->DataIsDirectory){// nodeReadResourceDirectory(node, PIMAGE_RESOURCE_DIRECTORY((DWORD)this->ResourceDirectory + tmpEntry->OffsetToDirectory),resourceDirectoryCallback, directoryEntryCallback, dataEntryCallback);}else{// dataif (dataEntryCallback != NULL){dataEntryCallback(this, node, PIMAGE_RESOURCE_DATA_ENTRY((DWORD)this->ResourceDirectory + tmpEntry->OffsetToDirectory));}}tmpEntry++;}}

几个回调的定义是:

typedef void (*PResDir_ReadResourceDirectoryCallback)(PE* pe,PIMAGE_RESOURCE_DIRECTORY father, PIMAGE_RESOURCE_DIRECTORY item);typedef void (*PResDir_ReadDirectoryEntryCallback)(PE* pe,PIMAGE_RESOURCE_DIRECTORY father, PIMAGE_RESOURCE_DIRECTORY_ENTRY item);typedef void (*PResDir_ReadDataEntryCallback)(PE* pe,PIMAGE_RESOURCE_DIRECTORY father, PIMAGE_RESOURCE_DATA_ENTRY item);

然后是这样使用,用lambda比较方便:

auto aa = [](PE* pe,PIMAGE_RESOURCE_DIRECTORY father, PIMAGE_RESOURCE_DIRECTORY item){// 输出};auto bb = [](PE* pe,PIMAGE_RESOURCE_DIRECTORY father, PIMAGE_RESOURCE_DIRECTORY_ENTRY item){// 输出};auto cc = [](PE* pe,PIMAGE_RESOURCE_DIRECTORY father, PIMAGE_RESOURCE_DATA_ENTRY item){// 输出};x->ReadResourceDirectory(aa,bb,cc);

变量名测试方便就乱取了,如果不想输出哪一项就直接置NULL就行了。回调里可以取到根、父节点、当前节点,勉强能用,这里演示一下用法,还是按照具体情况来写比较好。


0 0
原创粉丝点击