前面我们提到一个函数 CopySections ,该函数将文件中的段拷贝到我们申请的内存中,并按照内存页面的大小进行对齐。拷贝过程中设置了每个段的PhysicalAddress的值为该段的虚拟地址,用于后面的操作。
要理解这个段,首先应该理解_IMAGE_SECTION_HEADER 中 的联合体
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
其中VirtualSize(程序中使用的是PhysicalAddress)我觉得VirtualSize更加准确。这个VirtualSize 指的是这个段,在加载进内存之后的实际大小,而_IMAGE_SECTION_HEADER 中的SizeOfRawData 指的是该段磁盘中已经初始化了的数据大小,该大小按照文件粒度对齐之后的大小,如果该节只包含未初始化的数据,大小为0。
static BOOL
CopySections(const unsigned char *data, size_t size, PIMAGE_NT_HEADERS old_headers, PMEMORYMODULE module)
{
int i, section_size;
unsigned char *codeBase = module->codeBase;
unsigned char *dest;
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(module->headers);
for (i=0; i<module->headers->FileHeader.NumberOfSections; i++,section++) {
if (section->SizeOfRawData == 0) {
// 这个段所包含的数据都没有初始化, 为该段申请一个页面的大小,然后直接填充零即可。
section_size = old_headers->OptionalHeader.SectionAlignment;
if (section_size >0) {
dest = (unsigned char *)module->alloc(codeBase+ section->VirtualAddress,
section_size,
MEM_COMMIT,
PAGE_READWRITE,
module->userdata);
if (dest == NULL) {
return FALSE;
}
// 这里申请内存的时候直接使用了codeBase+section->VirtualAddress,之所以这样做,是为了使PE文件在加载入内存之后保持其格式,函数之所以成功,因为前面已经从codeBase开始预留了整个文件的空间
dest = codeBase +section->VirtualAddress;
// NOTE: On 64bit systems we truncate to 32bit here butexpand
// again later when "PhysicalAddress" is used.
section->Misc.PhysicalAddress = (DWORD) ((uintptr_t) dest &0xffffffff);
// 在这之后,PhysicalAddress就是这个段的虚拟地址。
memset(dest, 0, section_size);
}
// section is empty
continue;
}
if (!CheckSize(size, section->PointerToRawData +section->SizeOfRawData)) {
return FALSE;
}
// 提交内存块并拷贝数据
dest = (unsigned char *)module->alloc(codeBase + section->VirtualAddress,
section->SizeOfRawData,
MEM_COMMIT,
PAGE_READWRITE,
module->userdata);
if (dest == NULL) {
return FALSE;
}
dest = codeBase + section->VirtualAddress;
memcpy(dest, data + section->PointerToRawData,section->SizeOfRawData);
// NOTE: On 64bit systems we truncate to32bit here but expand
// again later when"PhysicalAddress" is used.
section->Misc.PhysicalAddress = (DWORD) ((uintptr_t) dest &0xffffffff);
}
return TRUE;
}
题外话:我们都知道,PE 文件的加载是通过内存映射文件来实现的,但是不同于普通的作为数据的映射文件,PE 文件的内存映射文件对应的控制区对象后面的每个子内存区对象都对应于PE 文件中的一个段,而普通的数据型的内存映射文件对应的控制区对象后面的子内存区对象都对应的是一定大小的文件的一部分,详情请参考http://blog.csdn.net/qq_18218335/article/details/65626899
接下来我们看一个函数,FinalizeSections 该函数的注释为:根据段头标记内存页,并释放标记为“可废弃”的段。
static BOOL
FinalizeSections(PMEMORYMODULE module)
{
int i;
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(module->headers);
#ifdef _WIN64
// "PhysicalAddress" might havebeen truncated to 32bit above, expand to
// 64bits again.
uintptr_t imageOffset = ((uintptr_t)module->headers->OptionalHeader.ImageBase & 0xffffffff00000000);
#else
static const uintptr_t imageOffset = 0;
#endif
SECTIONFINALIZEDATA sectionData;
sectionData.address = (LPVOID)((uintptr_t)section->Misc.PhysicalAddress | imageOffset);
sectionData.alignedAddress = AlignAddressDown(sectionData.address, module->pageSize);
sectionData.size = GetRealSectionSize(module, section);
sectionData.characteristics = section->Characteristics;
sectionData.last = FALSE;
section++;
// 循环访问所有的段,然后改变其访问标识
for (i=1; i<module->headers->FileHeader.NumberOfSections; i++,section++) {
LPVOID sectionAddress = (LPVOID)((uintptr_t)section->Misc.PhysicalAddress| imageOffset);
LPVOID alignedAddress = AlignAddressDown(sectionAddress, module->pageSize);
SIZE_T sectionSize = GetRealSectionSize(module, section);
// 如果前后两个段的其实地址相同,或者前一个段的末尾地址大于当前段起始地址,即有重叠的部分,将两个段的操作合并起来。我们知道,前面申请内存的时候其地址都是页面对齐的,而且申请得到的内存块的大小也是页面对齐的(VirtualAlloc函数的提交操作就是按照页面操作的),因此这个判断语句不会进去,不知道作者如何考虑的
if (sectionData.alignedAddress == alignedAddress || (uintptr_t)sectionData.address + sectionData.size > (uintptr_t) alignedAddress) {
// Section shares page with previous
if((section->Characteristics & IMAGE_SCN_MEM_DISCARDABLE) == 0 || (sectionData.characteristics & IMAGE_SCN_MEM_DISCARDABLE) == 0) {
sectionData.characteristics =(sectionData.characteristics | section->Characteristics) & ~IMAGE_SCN_MEM_DISCARDABLE;
} else {
sectionData.characteristics |=section->Characteristics;
}
sectionData.size = (((uintptr_t)sectionAddress) +((uintptr_t) sectionSize)) - (uintptr_t)sectionData.address;
continue;
}
if (!FinalizeSection(module, §ionData)){
return FALSE;
}
sectionData.address = sectionAddress;
sectionData.alignedAddress = alignedAddress;
sectionData.size = sectionSize;
sectionData.characteristics = section->Characteristics;
}
sectionData.last = TRUE;
if (!FinalizeSection(module, §ionData)){
return FALSE;
}
return TRUE;
}
上面的操作对每个段都进行了FinalizeSection操作,该函数如下,操作如注释所示
static BOOL
FinalizeSection(PMEMORYMODULE module, PSECTIONFINALIZEDATA sectionData) {
DWORD protect, oldProtect;
BOOL executable;
BOOL readable;
BOOL writeable;
if (sectionData->size == 0) {
return TRUE;
}
if (sectionData->characteristics & IMAGE_SCN_MEM_DISCARDABLE) {
// 这个段是可以被回收的
if (sectionData->address == sectionData->alignedAddress &&
(sectionData->last ||
module->headers->OptionalHeader.SectionAlignment== module->pageSize ||
(sectionData->size % module->pageSize) == 0)
) {
// 满足可以释放整个页面的要求之后才可以释放页面
module->free(sectionData->address, sectionData->size, MEM_DECOMMIT, module->userdata);
}
return TRUE;
}
// 得到读写执行属性
executable = (sectionData->characteristics & IMAGE_SCN_MEM_EXECUTE) != 0;
readable = (sectionData->characteristics& IMAGE_SCN_MEM_READ) != 0;
writeable = (sectionData->characteristics& IMAGE_SCN_MEM_WRITE) != 0;
protect = ProtectionFlags[executable][readable][writeable];
if (sectionData->characteristics & IMAGE_SCN_MEM_NOT_CACHED) {
protect |= PAGE_NOCACHE;
}
// 改变页面的访问标识
if (VirtualProtect(sectionData->address, sectionData->size, protect,&oldProtect) == 0) {
OutputLastError("Errorprotecting memory page");
return FALSE;
}
return TRUE;
}
这样我们就理解了函数的注释的含义:改变每个段的保护属性,并将段头部中包含有“可以将此资源释放”标识的内存页释放。
我们可以看到
重定向段标记为可回收,这很好理解,我们使用它进行重定向项的修复之后,我们就可以将其释放。