PE文件操作-动态加载

有时会有这样的需求,要把一个dll加载到进程空间或者对付地址空间随机化技术,这就需要自己实现一个PELoader。在PE文件中,许多数据在内存中的位置已经以RVA的形式在链接时给出,我们只需要根据节表的描述准确的按位置加载,大部分程序就可以正确运行了,但有几个类型的数据还是需要loader来调整的:

  1. IAT
    在PE文件中,调用静态链接的DLL例程一般都是以call ds:[xxxxxxxxh]的形式,其中这个地址是指向了IAT表的对应项,而在文件中,IAT表的每一项是和INT表一样指向了一个IMAGE_IMPORT_BY_NAME或者是个Id,所以当加载到内存中时需要将表项修正为函数地址。
  2. 重定位表
    一般加载exe文件没有重定位的问题,因为exe文件会加载到默认的基址上,而在进程地址空间初始化后,这个基址几乎肯定是未分配的,但dll或者使用了地址空间随机化技术的exe就会使用到重定位表,因此我们需要根据加载的基址和OPTIONAL_HEADER中的基址来算出偏移

注:在这篇中主要写加载dll,至于exe调试时还有一些不稳定的情况,以后再说。
文章中的ctx是自己写的一个pe解析器,等完善后会放到开源网站上。
首先,为了避开CreateSection检测,我们直接用CreateFile:

    IMAGE_DOS_HEADER dos_header;
    IMAGE_NT_HEADERS32 nt_header;

    SetFilePointer(pe_file, 0, 0, FILE_BEGIN);
    ReadFile(pe_file, &dos_header, sizeof(IMAGE_DOS_HEADER), &ret, NULL);
    if (ret != sizeof(IMAGE_DOS_HEADER))
        return false;

    SetFilePointer(pe_file, dos_header.e_lfanew, NULL, FILE_BEGIN);
    ReadFile(pe_file, &nt_header, sizeof(IMAGE_NT_HEADERS32), &ret, NULL);
    if (ret != sizeof(IMAGE_NT_HEADERS32))
        return false;

    //在OptionalHeader中找到准确的印象大小
    PVOID map_ptr = VirtualAlloc(NULL, 
        nt_header.OptionalHeader.SizeOfImage, 
        MEM_COMMIT, PAGE_READWRITE);
    ZeroMemory(map_ptr, nt_header.OptionalHeader.SizeOfImage);

    SetFilePointer(pe_file, 0, 0, FILE_BEGIN);
    ReadFile(pe_file, map_ptr, nt_header.OptionalHeader.SizeOfHeaders, &ret, NULL);
    if (ret != nt_header.OptionalHeader.SizeOfHeaders)
        return false;

然后我们需要根据内存对齐来将各个节表读取到内存中比加载其相应的属性,这里不急着去加载只读属性,因为后面的IAT和RELOC都需要写操作:

    for (int i = 0;i < get_section_count(ctx);i++)
    {
        PIMAGE_SECTION_HEADER sec_hdr;
        get_section_entry_by_index(ctx, i, &sec_hdr);
        SetFilePointer(pe_file, sec_hdr->PointerToRawData, NULL, FILE_BEGIN);
        if (!ReadFile(pe_file, (PUCHAR)map_ptr + sec_hdr->VirtualAddress, sec_hdr->SizeOfRawData, &ret, NULL))
            return false;

        if ((sec_hdr->Characteristics&IMAGE_SCN_CNT_UNINITIALIZED_DATA) != 0)
        {
            ZeroMemory((PUCHAR)map_ptr + sec_hdr->VirtualAddress, sec_hdr->Misc.VirtualSize);
        }
        if ((sec_hdr->Characteristics&IMAGE_SCN_MEM_EXECUTE) != 0)
        {
            DWORD oldVp;
            VirtualProtect((PUCHAR)map_ptr + sec_hdr->VirtualAddress,
                sec_hdr->Misc.VirtualSize,
                PAGE_EXECUTE_READWRITE, 
                &oldVp);
        }
    }

之后就需要去填充IAT,只是个遍历操作,这里暂时没有考虑序号导入和绑定导入的情况:

    for (int i = 0;i < get_import_count(ctx);i++)
    {

        get_import_descriptor_by_index(ctx, i, &import_desc);
        //获得当前导入项并加载之
        PCHAR import_name;
        get_import_descriptor_name(ctx, import_desc, (PUCHAR*)&import_name);
        HMODULE mod = LoadLibraryA(import_name);
        if (mod == NULL)
            return FALSE;

        get_import_trunk32(ctx, import_desc, &trunk_arr);//获得INT
        for (int j = 0;j < get_import_trunk_count(ctx, import_desc);j++)
        {
            get_import_trunk32_by_index(ctx, import_desc, j, &trunk);
            get_import_item_by_name(ctx, trunk, &import_name);
            //获得地址并填充
            PVOID f_addr = GetProcAddress(mod, import_name->Name);
            trunk_arr[j] = (ULONG)f_addr;
        }
    }

然后就是修正重定位表,需要重定位的地方原本的地址是基于OptionalHeader中ImageBase的线性绝对地址,所以这里需要加一个新基址与默认基址差的偏移。这里有一个问题,就是一般dll都是有重定位表的,但没有ASLR的exe很可能没有重定位表,而我们也没法将PE加载到ImageBase的地址,这样就算PE加载进来也没法运行,这样就要考虑还原重定位表了,这个后面会介绍用递归下降分析还原重定位表。

    PIMAGE_BASE_RELOCATION first_reloc;
    //这里的偏移就是新机制和老基址的偏移
    ULONG offset = ((ULONG_PTR)map_ptr - (((PIMAGE_NT_HEADERS)(ctx->nt_ptr))->OptionalHeader.ImageBase));
    for (int i = 0;i < get_reloc_table_count(ctx, &first_reloc);i++)
    {
        PIMAGE_BASE_RELOCATION base_reloc;
        get_reloc_table_entry(ctx, first_reloc, i, &base_reloc);
        //一个重定位表所描述的RVA基址
        PUCHAR base_va = ((PUCHAR)ctx->map_ptr + base_reloc->VirtualAddress);
        for (int j = 0;j < (base_reloc->SizeOfBlock - 8) / 2;j++)
        {
            //间隔项,不需要考虑了
            if (((*(PUSHORT)((PUCHAR)base_reloc + 8 + j * 2)) & 0xf000) == 0)
                continue;
            //在源地址上加一个偏移项
            *((PULONG_PTR)(base_va + ((*(PUSHORT)((PUCHAR)base_reloc + 8 + j * 2)) & 0xfff))) += offset;
        }

    }

当这一切都完成后,重新修改一下节内存的读写属性后,PE文件就已经可以使用了,只要PE文件没有自己使用硬编码访问内存,基本就没有问题。我们可以创建一个线程然后跳转到入口函数执行PE文件,

typedef BOOL(_stdcall *DllInit)(HINSTANCE hinstDLL,  // handle to DLL module
    DWORD fdwReason,     // reason for calling function
    LPVOID lpReserved);

    DllInit locDllinit = (DllInit)((PUCHAR)ctx->map_ptr + ((PIMAGE_NT_HEADERS)(ctx->nt_ptr))->OptionalHeader.AddressOfEntryPoint);

    locDllinit((HINSTANCE)map_ptr, DLL_PROCESS_ATTACH, 0);
    locDllinit((HINSTANCE)map_ptr, DLL_THREAD_ATTACH, 0);

在加载EXE文件时,还需要处理TLS中的AddressOfIndex,不过通常情况下不会有人用TLS,用了大部分情况下这个索引值也是填0,所以这里不做处理。在pecoff中描述了loader的职责:

The loader assigns the value of the TLS index to the place that was indicated by the Address of Index field.

(??)
而GetProcAddress也就是去遍历导出表:

PVOID get_proc_address(PPE_CONTEXT ctx, LPCSTR func)
{

    PIMAGE_EXPORT_DIRECTORY export_dir;
    get_export_descriptor(ctx, &export_dir);
    if (export_dir == NULL)
        return NULL;

    for (int i = 0;i < export_dir->AddressOfFunctions;i++)
    {
        CHAR* name;
        get_export_name_by_index(ctx, export_dir, i, &name);
        if (!strcmp(name, func))
        {
            PVOID f_addr;
            //这个函数中包含了将RVA转到VA的工作
            get_export_addr_by_index(ctx, export_dir, i, &f_addr);
            return f_addr;
        }
    }

    return NULL;
}

你可能感兴趣的:(Win32)