因为是先写代码再写文字,代码在虚拟机里我给还原了...用的以前备份,所以后面一些东西可能有点出入...
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));
}
// 遍历thunk
while (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;
}
}
// xxx
if (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)
{
// 读当前node
if (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)
{
// node
ReadResourceDirectory(node, PIMAGE_RESOURCE_DIRECTORY((DWORD)this->ResourceDirectory + tmpEntry->OffsetToDirectory),
resourceDirectoryCallback, directoryEntryCallback, dataEntryCallback);
}
else
{
// data
if (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就行了。回调里可以取到根、父节点、当前节点,勉强能用,这里演示一下用法,还是按照具体情况来写比较好。
本文详细解读了PE文件中导入表与资源目录的结构与使用方法,包括导入表的查找过程、导入模块与函数的读取、资源目录的层级结构以及如何遍历资源目录等内容。
&spm=1001.2101.3001.5002&articleId=40348677&d=1&t=3&u=374d46af2ab1493b83d5c4bfcaeff238)
1228

被折叠的 条评论
为什么被折叠?



