PE文件操作-简单的加壳实现

    xiaoxiao2025-04-22  16

    处于一些原因,需要对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相加, AddressOfPoint用于到最后跳回到OEP, 后面则是原始节列表。

    PUCHAR config_buf = (PUCHAR)malloc(opt_hdr->FileAlignment); PPACK_CONFIG pack_config = (PPACK_CONFIG)config_buf; DWORD config_size = 0; memset(pack_config, 0, opt_hdr->FileAlignment); pack_config->OrginialImportDirectory = opt_hdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; pack_config->OriginalIATDirectory = opt_hdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].VirtualAddress; pack_config->NumberOfSections = file_hdr->NumberOfSections; pack_config->ImageBase = opt_hdr->ImageBase; pack_config->AddressOfPoint = opt_hdr->AddressOfEntryPoint; config_size = 20; 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); //修正属性,不然只读属性无法解码 sec_hdr->Characteristics |= (IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE); memcpy(&pack_config->Sections[i], sec_hdr, sizeof(IMAGE_SECTION_HEADER)); config_size += 40; } config_size += 40;

    在原始节列表之后则是我们自己用的IAT,而在这个节偏移512字节处则是INT,这里是考虑到IAT往往占用空间不大,而INT需要占用较大空间,而且这里只用到了两个API。 下面就开始追加代码节和配置节,这里的解码代码是一段汇编代码,用nasm编译而成,最后将bin文件拷贝进代码节:

    //打开解码代码文件 HANDLE inject = CreateFile(L"G:\\Develop\\NASM\\pe_inject\\inject", GENERIC_ALL, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); //代码节 PIMAGE_SECTION_HEADER code_sec; DWORD ret; insert_section_at_eof(ctx, "PACKCODE", 512, IMAGE_SCN_CNT_CODE|IMAGE_SCN_MEM_READ|IMAGE_SCN_MEM_WRITE|IMAGE_SCN_MEM_EXECUTE, &code_sec); PUCHAR code_image = ((PUCHAR)ctx->map_ptr + code_sec->PointerToRawData); ReadFile(inject, code_image, opt_hdr->FileAlignment, &ret, NULL); //配置节 PIMAGE_SECTION_HEADER data_sec; insert_section_at_eof(ctx, "PACKDATA", 1024, IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ, &data_sec); PUCHAR data_image = ((PUCHAR)ctx->map_ptr + data_sec->PointerToRawData); memcpy(data_image, config_buf, opt_hdr->FileAlignment);

    添加后如下: 生成了这两个节后,就需要在文件的配置节中构造自己用的导入表了,构造时按照导入表的格式正确的配置RVA就可以了,这样loader就能正确的填充IAT了。

    PPE_IMPORT_NODE_32 import_node; create_import_node(handler, "kernel32.dll", 0, "GetProcAddress\0LoadLibraryA\0"); DWORD int_fo = data_sec->PointerToRawData + 512; DWORD iat_fo = data_sec->PointerToRawData + config_size; //传进的两个地址分别为INT基址和IAT基址 construct_import_directory(handler, data_image + 512, data_image + config_size, ctx);

    最后需要将几个地址硬编码进解压代码,主要是配置信息VA,GetProcAddress和LoadLibrary的IAT项的VA,这儿为了便捷用了这个不太理想的处理方式,但我看有些加壳软件用了对齐的方法,在代码节的开头放配置信息,而代码放在配置信息之后,这样代码通过向下对齐就可以得到配置信息的VA,有兴趣的可以试试。 然后来看解码代码,我不太会汇编,所以解码代码这一块就比较丑了,解码程序使用nasm编译的。 首先加载并保存在数据节中的配置信息:

    %define OrginialImportDirectory [ebp-4h] %define OriginalIATDirectory [ebp-8h] %define ImageBase [ebp-ch] %define ModuleBase [ebp-10h] %define AddressOfPoint [ebp-14h] push ebp mov ebp,esp sub esp,14h mov eax,11111111h ;保存一些局部变量 mov edx,[eax+pack_config.OrginialImportDirectory] mov OrginialImportDirectory,edx mov edx,[eax+pack_config.OriginalIATDirectory] mov OriginalIATDirectory,edx mov edx,[eax+pack_config.ImageBase] mov ImageBase,edx mov edx,[eax+pack_config.AddressOfPoint] mov AddressOfPoint,edx

    然后遍历我们在配置信息中的节表去解码原始PE数据:

    mov ecx,[eax+pack_config.NumberOfSections] mov esi,0 lea eax,[eax+SIZE_OF_CONFIG_HEADER] ;节表开始地址 jmp loop_section load_section_header: mov ebx,[eax+IMAGE_SECTION_HEADER.VirtualAddress] add ebx,ImageBase push esi ;保存旧计数值并加载新计数值 push ecx mov ecx,[eax+IMAGE_SECTION_HEADER.SizeOfRawData] mov esi,0 jmp unpack_section_loop unpack_section: ;这儿便是进行解码 mov dl,[ebx+esi] xor dl,1 mov [ebx+esi],dl inc esi unpack_section_loop: cmp ecx,esi jne unpack_section pop ecx pop esi sub ecx,1 inc esi add eax,SIZE_OF_SECTION_HEADER loop_section: test ecx,ecx jnz load_section_header

    然后用配置信息中的原始导入表来配置IAT表:

    ;sub esp,10h mov ebx,[esp+10h] ;OrginialImportDirectory add ebx,[esp+8h] mov ecx,0 jmp fill_import_loop fill_import: ;得到INT和IAT mov esi,[ebx+ecx] ;OriginalFirstTrunk add esi,[esp+8h] mov edi,[ebx+ecx+16] ;FirstTrunk add edi,[esp+8h] jmp fill_iat_loop fill_iat: ;计算导入名偏移 mov eax,[esi] add eax,[esp+8h] add eax,2 ;获取地址 push ecx push eax mov eax,ModuleBase push eax call [ds:22222222h] ;这是GetProcAddress,在加壳程序中会将其硬编码为IAT的VA pop ecx ;填充IAT mov [edi],eax ;向后偏移4字节 add esi,4 add edi,4 fill_iat_loop: ;不是0继续,是0恢复导入目录表 mov edx,[esi] cmp edx,0 jnz fill_iat add ecx,20 fill_import_loop: cmp dword [ebx+ecx+IMAGE_IMPORT_DESCRIPTOR.OriginalFirstTrunk],0 jz ready_exit ;是0则退出循环 mov esi,[ebx+ecx+IMAGE_IMPORT_DESCRIPTOR.Name] add esi,ImageBase push esi call [ds:11111111h] ;这是LoadLibrary,在加壳程序中会将其预编码为IAT的VA mov ModuleBase,eax jmp fill_import

    最后会把控制权转交OEP。

    mov eax,AddressOfPoint add eax,ImageBase push eax ret

    这样我们就完成了一个简单的加壳程序,这个程序被OD加载后就会有如下提示: 点否载入后 这儿就是我们的汇编代码,此时OEP处 运行至ret时的OEP处 最终转入OEP执行

    这个程序只是完成了最简单的加壳工作,经测试发现,如果PE文件中有LoadConfig这个目录表程序就会崩溃,不知道是不是SHE的原因。如果有EXPORT,IMPORT,RESOURCE,DEBUG,IAT这几项中的一项或多项,则可以正常工作,因为我们暂时没有考虑重定向,所以ASLR肯定没法载入。其他的几个目录项太过罕见所以没有测试。

    转载请注明原文地址: https://ju.6miu.com/read-1298332.html
    最新回复(0)