1. 程式人生 > >PE檔案操作-動態載入

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;
}