Saturday, March 1, 2014

Dissecting the PE File Format - 8

The Loader


This post is not generally essential and only for those who want to dig deeper.

A brief overview of the stages involved in the loading process:

  1. Read in the first page of the file with the DOS header, PE header, and section headers.
  2. Determine whether the target area of address space is available, if not allocate another area.
  3. Using info in the section headers, map sections of the file to appropriate places in the allocated address space.
  4. If the file is not loaded at its target address (ImageBase), apply relocation fix-ups.
  5. Go through the list of DLLs in the import sections and load any that aren't already loaded (recursive).
  6. Resolve all imported symbols in the imports section
  7. Create the initial stack and heap using values from the PE header.
  8. Create the initial thread and start the process
What the loader does
When the executable is run, the windows loader creates a virtual address space for the process and maps the executable module from the disk into process' address space. It tries to load the image at the preferred base address but relocates it if that address is already occupied. The loader goes through the section table and maps each section at the address calculated by adding the RVA of the section to the base address. The page attributes are set according to the section's characteristic requirements. After mapping the sections in memory, the loader performs base relocations if the load address is not equal to the preferred base address in ImageBase.

The import table is then checked and any required DLLs are mapped into the process address space. After all the DLL modules are located and mapped in, the loader examines each DLLs export section and the IAT is fixed to point to the actual imported function address. If the symbol does not exist (rare), the loader displays an error. Once all the required modules have been loaded execution passes to the apps entry point.

The area of particular interest is that of loading the DLLs and resolving imports. This process is complicated and accomplished by various internal (forwarded) functions and routines residing in ntdll.dll which are not documented by Microsoft. Function forwarding is a way for M$ to expose a common Win32 API set and hide low level functions which may differ in different versions of the OS. Many familiar kernel32 functions such as GetProcAddress are simply thin wrappers around ntdll.dll exports such as LdrGetProcAddress which do the real work.

In order to see this in action you will need to install windbg and the windows symbol package or another kernel-mode debugger like SoftIce. You can only view these functions in Olly if you configure Olly to use the M$ symbolserver, otherwise all you see is pointers and memory addresses without function names. However, Olly is a user-mode debugger and will onyl show you whats hapenning when you app has been loaded and will not allow you to see the loading process itself. Although the functionality of Windbg is poor when compared to Olly, it does integrate well with the OS to show you the loading process.
























The various APIs associated with loading an executable all converge on the kernel32.dll function LoadLibraryExW  which in turn leads to the internal function LdrpLoadDll in ntdll.dll. This function directly calls 6 subroutines LdrpCheckForLoadedDll, LdrpMapDll, LdrpWalkImportDescriptor, LdrpUpdateLoadCount, LdrpInitializeRoutines and LdrpClearLoadInProgress which perform the following tasks:
1. Check to see if the module is loaded
2. Map the module and supporting information into memory
3. Walk the module's import descriptor table (find other modules this one is importing)
4. Update the modules load count as well as any other brought in by this DLL.
5. Initialize the module.
6. Clear some sort of flag, indicating that the load has finished.
























A DLL may import other modules that start a cascade of additional library loads. The loader will need to loop through each module, checking to see if it needs to be loaded and then checking its dependencies. This is where LdrpWalkImportDescriptor comes in. It has two subroutines - LdrpLoadImportModule and LdrpSnapIAT. First it starts with 2 calls to RtlImageDirectoryEntryToData to locate the bounds import descriptor and the regular import descriptor tables. Note that the loader is checking for bounds imports first - an app which runs but doesnt have an import directory may have bound imports instead.

Next LdrpLoadImportModule constructs a Unicode string for each DLL found in the Import Directory and then employs LdrpCheckForLoadedDll to see if they have already been loaded.

Next the LdrpSnapIAT routine examines every DLL referenced in the Import Directory for a value of -1 (i.e again checks for bound imports first). It then changes the memory protection of the IAT to PAGE_READWRITE and proceeds to examine each entry in the IAT before moving on to LdrpSnapThunk Routine.

LdrpSnapThunk uses a function's ordinal to locate its address and determine whether or not its forwarded. Otherwise it calls LdrpNameToOrdinal which uses the binary search on the export table to quickly locate the ordinal. If the function is not found it returns STATUS_ENTRYPOINT_NOT_FOUND, otherwise it replaces the entry in the IAT with the APIs entry point and returns to LdrpSnapIAT which restores the memory protection it changed at the beginning of its work, calls NtFlushInstructionCache to force a cache refresh on the memory block containing the IAT, and returns back to LdrpWalkImportDescriptor.

Win2k insists that ntdll.dll is either loaded as a bound import or in the regular import directory before allowing it to load, whereas win9x allows an app with no imports to load.

This is a very brief overview but shows how the loader must examine evert imported API in order to calculate a real address in memory and to see if an API is being forwarded. Each imported DLL may bring in additional modules and the process will be repeated over and over again until all dependencies have been checked.

In the next post, we see how to navigate imports on the disk.

No comments:

Post a Comment