处于一些原因,需要对PE文件进行加壳。所谓加壳就是有某种编码算法对原始文件数据进行编码,并使原始文件内容成为数据部分,而嵌进文件的解密代码成为主体。在loader加载加壳文件后,会将控制权交给解码程序,解码程序在完成解码后,再把控制权交给原始代码。
这个加壳程序是学习之用,所以只实现了最简单的可逆变换-异或运算。对文件操作用了内存映射文件,所以直接操作内存即可。
先说一下思路:对原始PE文件编码,一般是保护数据部分,而文件头则不需要进行编码,所以保留文件头。至于原始文件的节表我们也会保留,而我们会在原始PE文件末尾追加几个节以供解码(见PE文件操作-在末尾添加节)。
- PACKRES节:备份的原始PE的资源数据。这个节是原始文件中唯一需要备份的节,因为图标的显示以及一些程序配置都在这个节中,涉及到程序的加载参数,所以需要保留。
- PACKCODE节:解码代码,负责解码所有节,填充IAT和跳回OEP。
- PACKDATA节:解码参数,其中保存了映像信息,原始节表。由于在填充IAT过程中用到了LoadLibrary和GetProcAddress,所以这个节还包含了我们壳程序的导入表和IAT。
最终文件结构就像下面这样:
原始数据
PACKRES
RES1
RES2
…….
PACKCODE
CODE
PACKDATA
PACK_CONFIG
IAT
INT
首先是备份资源节,这个操作最简单,在PE末尾后添加一个节,把原始资源节的数据拷贝至其中,然后遍历 资目录修正其中数据目录项的DataOffset,这个修正过程和重定位表的修正差不多,大致流程:
PIMAGE_SECTION_HEADER res_sec;
insert_section_at_eof(ctx,
"PACKRES",
opt_hdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].Size,
IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE | IMAGE_SCN_CNT_INITIALIZED_DATA,
&res_sec);
DWORD old_res = opt_hdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress;
//拷贝数据
memcpy((PUCHAR)ctx->map_ptr + res_sec->PointerToRawData,
(PUCHAR)ctx->map_ptr + rva_to_fo(ctx, (opt_hdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress)),
opt_hdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].Size);
//将资源目录定位到新的节
opt_hdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress = res_sec->VirtualAddress;
//修正dataoffset
adjust_resource_rva(ctx, res_sec->VirtualAddress - old_res);
这一步的效果如下图:
这一步备份完后,就可以对原始数据编码了,遍历所有节进行编码,这里要小心一下,因为有些奇怪的程序VirtualSize是比SizeOfRawData大的,不要想当然的觉得VirtualSize肯定小:
for (int i = 0;i < get_section_count(ctx)-1;i++)
{
PIMAGE_SECTION_HEADER sec_hdr;
get_section_entry_by_index(ctx, i, &sec_hdr);
for (int j = 0;j < sec_hdr->SizeOfRawData;j++)
{
*((PUCHAR)ctx->map_ptr + sec_hdr->PointerToRawData + j) ^= 0x1;
}
}
这一步后,PE文件便无法使用了,只能看到其资源数据,而双击是无法打开的。
然后开始构造供解码程序使用的配置信息,这里准备了6个数据,
OriginalImportTable用来让解码程序找到原始导入目录,
OriginalIATDirectory找到原始IAT目录,
NumberOfSections记录了需要解码的节数量,
ImageBase记录了加载机制,用于和RVA相加, <