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

来源:互联网 发布:淘宝助理5.0旧版本 编辑:程序博客网 时间:2024/05/17 02:22

处于一些原因,需要对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 ebpmov ebp,espsub esp,14hmov eax,11111111h;保存一些局部变量mov edx,[eax+pack_config.OrginialImportDirectory]mov OrginialImportDirectory,edxmov edx,[eax+pack_config.OriginalIATDirectory]mov OriginalIATDirectory,edxmov edx,[eax+pack_config.ImageBase]mov ImageBase,edxmov edx,[eax+pack_config.AddressOfPoint]mov AddressOfPoint,edx

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

mov ecx,[eax+pack_config.NumberOfSections]mov esi,0lea eax,[eax+SIZE_OF_CONFIG_HEADER]         ;节表开始地址jmp loop_sectionload_section_header:mov ebx,[eax+IMAGE_SECTION_HEADER.VirtualAddress]add ebx,ImageBasepush esi                    ;保存旧计数值并加载新计数值push ecxmov ecx,[eax+IMAGE_SECTION_HEADER.SizeOfRawData]mov esi,0jmp 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_HEADERloop_section:test ecx,ecxjnz load_section_header

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

;sub esp,10hmov ebx,[esp+10h]               ;OrginialImportDirectoryadd ebx,[esp+8h]mov ecx,0jmp fill_import_loopfill_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,20fill_import_loop:cmp dword [ebx+ecx+IMAGE_IMPORT_DESCRIPTOR.OriginalFirstTrunk],0jz ready_exit               ;是0则退出循环mov esi,[ebx+ecx+IMAGE_IMPORT_DESCRIPTOR.Name]add esi,ImageBasepush esicall [ds:11111111h]         ;这是LoadLibrary,在加壳程序中会将其预编码为IAT的VAmov ModuleBase,eaxjmp fill_import

最后会把控制权转交OEP。

mov eax,AddressOfPointadd eax,ImageBasepush eaxret

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

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

0 0
原创粉丝点击