Молодогвардейцев 454015 Россия, Челябинская область, город Челябинск 89085842764
MindHalls logo

Реализация загрузчика PE файлов в Windows

Продолжается погружение в запутанный мир системного программирования. Это вторая вторая часть и сегодня мы реализуем загрузчик исполняемых файлов Windows в точности по этому алгоритму.

Так как я сам являюсь гордым пользователем Linux Ubuntu, вся работа выполнена с помощью виртуальной машины. И вообще говоря, если вы занимаетесь системным программированием или разработкой драйверов, никогда не тестируйте свой код на реальной машине. Думаю тут без комментариев все понятно. Итак, виртуальная машина с win, язык низкого уровня и полный запас энтузиазма, поехали.

Система: Windows XP SP2
Язык программирования: C

Интерфейс загрузчика

Пара слов об интерфейсе. Программа будет консольной, аргументом принимать полный путь до исполняемого файла, который необходимо запустить. Поэтому функция main проще простого, весь ужас запрятан подальше. Сейчас мы пробежимся по каждому файлу проекта, весь код снабжен подробными комментариями.

Структура проекта

Проект будет состоять из следующих файлов исходного кода:
— loader_main.c
— pe_loader.c
— pe_loader.h

Используемые библиотеки:
— windows.h
— stdio.h

Исходный код файла loader_main.с с комментариями

#include <windows.h>
#include <stdio.h>
#include "pe_loader.h"

int main(unsigned int argc, char *argv[], char *envp[]) {
    //Вспомогательная структура загружаемого файла
    //Описана в файле pe_loader.h
    PeHeaders pe;

    //Если имя загружаемого файла не пришло аргументом командной строки,
    //выводим подсказку и завершаемся с ошибкой
    if (argc < 1) {
        printf("Usage: %s <filename>\n", argv[0]);
        return -1;
    }

    //Стуктура и функция описаны в pe_loader.h и pe_loader.c
    peb = GetPeb();
    //Отдаем имя файла функции загрузки
    if (!LoadPeImage(&pe, argv[1], argv[1])) {
        printf("Error on loading PE image %s\n", argv[1]);
        return -1;
    }

    return 0;
}

Исходный код файла pe_loader.h с комментариями

#ifndef _PE_LOADER_H_
#define _PE_LOADER_H_

#include <windows.h>
#include "win.h"

//----------------------------------------

// вспомогательная структура загруженного PE-файла
typedef struct _PeHeaders {

    char                *filename;      // имя файла

    HANDLE              fd;             // хендл открытого файла
    HANDLE              mapd;           // хендл файловой проекции
    PBYTE               mem;            // указатель на память спроецированного файла
    DWORD               filesize;       // размер спроецированной части файла

    IMAGE_DOS_HEADER    *doshead;       // указатель на DOS заголовок
    IMAGE_NT_HEADERS    *nthead;        // указатель на NT заголовок

    IMAGE_IMPORT_DESCRIPTOR *impdir;    // указатель на массив дескрипторов таблицы импорта
    DWORD               sizeImpdir;     // размер таблицы импорта
    DWORD               countImpdes;    // количество элементов в таблице импорта

    IMAGE_EXPORT_DIRECTORY  *expdir;    // указатель на таблицу экспорта
    DWORD               sizeExpdir;     // размер таблицы экспорта

    IMAGE_BASE_RELOCATION    *reldir;    // указатель на таблицу релоков
    DWORD                sizeReldir;        // размер таблицы релоков

    IMAGE_SECTION_HEADER    *sections;  // указатель на таблицу секций (на первый элемент)
    DWORD                   countSec;   // количество секций

} PeHeaders;

//----------------------------------------

/*
* PEB - Process Environment Blocks - структура процесса в windows, 
* заполняется загрузчиком на этапе создания процесса, которая содержит 
* информацию об окружении, загруженных модулях (LDR_DATA), базовой информации 
* по текущему модулю и другие критичные данные необходимые для функционирования процесса
*/
PEB *peb;
PEB *GetPeb();

//----------------------------------------

#endif  // _PE_LOADER_H_

Файл pe_loader.c по частям с комментариями

Весь функционал загрузчика сосредоточен в файле pe_loader.c. С первого взгляда в этом коде легко заблудиться и полностью разочароваться в программировании, поэтому рассмотрим его медленно и по порядку.

Список функций исходного кода загрузчика в порядке исполнения:

BOOL LoadPeImage(PeHeaders *pe, char *filename, char *filenameNoPath)
Точка входа для загрузчика, получает на вход структуру PeHeaders и имя файла. При первом вызове из функции main аргументы filename и filenameNoPath будут совпадать. Второй аргумент необходим потому что загрузка PE файла происходит рекурсивно(помним, что должны загрузится все необходимые библиотеки) и в рекурсивных вызовах эти аргументы не будут совпадать.

Исходный код функции

//
//Инициализирует в памяти образ PE-файла.
//
BOOL LoadPeImage(PeHeaders *pe, char *filename, char *filenameNoPath) {
    //Инициализируем поле с именем файла
    pe->filename = filename;

    //Загрузка заголовков и проецирование файла в пямять
    if (!LoadPeHeaders(filename, pe)) {
        return FALSE;
    }

    //Загрузка секций
    if (!LoadPeSections(pe)) {
        CloseHandle(pe->fd);
        return FALSE;
    }

    //Внести информацию о загружаемом модуле в списки поля LoaderData в PEB'е
    AddDllIntoPeb(pe, filename, filenameNoPath);

    //Информация о табилцах
    GetHeaderInfo(pe, filename);

    //Обработка релоков(если ожидаемый адрес загрузки файла не совпадает 
    //с реальным адресом в памяти)
    if (pe->nthead->OptionalHeader.ImageBase != (DWORD)pe->mem) {
        SettingRelocs(pe);
    }

    //Обработка импорта(получение символов из библиотек)
    if (pe->nthead->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress) {
        if (!ProcessImport(pe)) {
            return FALSE;
        }
    }        

    //После обработки образа выставить права доступа для секций
    SetPeSectionsProtect(pe);    

    //Передача управления точке входа
    //Если это dll
    if (pe->nthead->FileHeader.Characteristics & IMAGE_FILE_DLL) {
        return ((BOOL(__stdcall*)(HINSTANCE, DWORD, LPVOID))(pe->mem + pe->nthead->OptionalHeader.AddressOfEntryPoint))(NULL, DLL_PROCESS_ATTACH, NULL);
    }
    //Если это exe
    else if (pe->nthead->FileHeader.Characteristics & IMAGE_FILE_EXECUTABLE_IMAGE) {
        ((void(*)(void))(pe->mem + pe->nthead->OptionalHeader.AddressOfEntryPoint))();
    }

    return TRUE;
}

Видно, что в функции очень четко прослеживается алгоритм загрузки PE файла. Теперь рассмотрим шаг за шагом реализацию всех функций этого алгоритма.

BOOL LoadPeHeaders(char *filename, PeHeaders *pe)
Функция, реализующая 1-4 шаги первого этапа алгоритма и инициализирующая поля структуры PeHeaders.

Исходный код функции

//
//Функция открывает файл(проецирует его в память) и загружает в память заголовки PE-файла.
//
BOOL LoadPeHeaders(char *filename, PeHeaders *pe) {
    BYTE dosHeader[sizeof(IMAGE_DOS_HEADER)];
    BYTE ntHeader[sizeof(IMAGE_NT_HEADERS)];
    DWORD numberOfBytesRead;

    //Получение дискриптора файла
    pe->fd = CreateFile(filename, // имя файла
        GENERIC_READ, // права доступа
        0,
        NULL,
        OPEN_EXISTING, // открываемый файл должен существовать
        FILE_ATTRIBUTE_NORMAL,
        NULL);
    if (pe->fd == INVALID_HANDLE_VALUE) {
        printf("Error open file %s\n", filename);
        return FALSE;
    } else {
        printf("Opened file %s\n", filename);
    }

    //Читаем в файле DOS-заголовок
    if (!ReadFile(pe->fd, dosHeader, sizeof(IMAGE_DOS_HEADER), &numberOfBytesRead, NULL)) {
        CloseHandle(pe->fd);
        printf("Error read dos header %s\n", filename);
        return FALSE;
    }

    //Ставим указатель в начало PE-заголовка(NT-заголовка(одно и тоже)) 
    //(его адрес находится в DOS-заголовке)
    SetFilePointer(pe->fd, ((IMAGE_DOS_HEADER*)dosHeader)->e_lfanew, NULL, FILE_BEGIN);

    //Считываем
    if (!ReadFile(pe->fd, ntHeader, sizeof(IMAGE_NT_HEADERS), &numberOfBytesRead, NULL)) {
        CloseHandle(pe->fd);
        printf("Error read nt headers %s\n", filename);
        return FALSE;
    }

    //Запоминаем в PE
    pe->nthead = (IMAGE_NT_HEADERS*)ntHeader;

    //Выделяем память под образ
    if (!AllocImageMemory(pe)) {
        CloseHandle(pe->fd);
        printf("Error alloc image memory %s\n", filename);
        return FALSE;
    }

    //Возвращаемся в начало файла
    SetFilePointer(pe->fd, 0, NULL, FILE_BEGIN);

    //Считываем все секции
    if (!ReadFile(pe->fd, pe->mem, pe->nthead->OptionalHeader.SizeOfHeaders, &numberOfBytesRead, NULL)) {
        VirtualFree(pe->mem, 0, MEM_RELEASE);
        CloseHandle(pe->fd);
        printf("Error read nt headers %s\n", filename);
        return FALSE;
    }

    //Инициализация указателей

    //На DOS-заголовок(начало файла)
    pe->doshead = (IMAGE_DOS_HEADER*)pe->mem;
    //На NT(PE) заголовок - смещение в дос заголовке
    pe->nthead = (IMAGE_NT_HEADERS*)(pe->mem + ((IMAGE_DOS_HEADER*)dosHeader)->e_lfanew);
    //На секции
    pe->sections = (IMAGE_SECTION_HEADER*)((DWORD)&(pe->nthead->OptionalHeader) + pe->nthead->FileHeader.SizeOfOptionalHeader);
    //Количество секций
    pe->countSec = pe->nthead->FileHeader.NumberOfSections;
    //Размер всего файла
    pe->filesize = GetFileSize(pe->fd, NULL);

    return TRUE;
}

BOOL LoadPeSections(PeHeaders *pe)
Функция загружает секции, то есть реализует 5 и 6 шаги первого этапа.

Исходный код функции

//
// Загружает в память секции PE-файла.
//
BOOL LoadPeSections(PeHeaders *pe) {
    DWORD i;
    DWORD numberOfBytesRead;

    //Заполняем в pe каждую секцию
    for (i = 0; i < pe->nthead->FileHeader.NumberOfSections; i++) {
        //Ставим указатель в начало секции(указатель на данные в самом файле)
        SetFilePointer(pe->fd, pe->sections[i].PointerToRawData, NULL, FILE_BEGIN);
        //считываем по виртуальному адресу(в память)
        if (!ReadFile(pe->fd, pe->mem + pe->sections[i].VirtualAddress,
            pe->sections[i].SizeOfRawData, &numberOfBytesRead, NULL)) {
                printf("Error load %d section\n", i);
                return FALSE;
        }
    }

    return TRUE;
}

void AddDllIntoPeb(PeHeaders *dllPe, char *fullName, char *shortName)
Функция заносит информацию о загружаемом процессе в структуру PEB. Подробнее в комментариях.

Исходный код функции

/* 
* В начале загрузки загрузчик должен внести информацию о загружаемом модуле(dll) в
* три списка(двусвязных) поля LoaderData в PEB'е текущего процесса:
* InLoadOrderModuleListAddDllIntoPeb, InMemoryOrderModuleList, InInitializationOrderModuleList.
*/
void AddDllIntoPeb(PeHeaders *dllPe, char *fullName, char *shortName) {
    PWCHAR fullNameWC, shortNameWC;
    PLDR_MODULE pDllPeb;
    LDR_MODULE dllPeb;
    PEB_LDR_DATA *Ldr = peb->LoaderData;
    unsigned int i;

    fullNameWC = (PWCHAR)malloc(sizeof(WORD)*(strlen(fullName) + 1));
    shortNameWC = (PWCHAR)malloc(sizeof(WORD)*(strlen(shortName) + 1));

    for (i = 0; i < strlen(shortName); i++) {
        shortNameWC[i] = (WORD)shortName[i];
    }
    shortNameWC[i] = 0;
    for (i = 0; i < strlen(fullName); i++) {
        fullNameWC[i] = (WORD)fullName[i];
    }
    fullNameWC[i] = 0;

    pDllPeb = (PLDR_MODULE)malloc(sizeof(LDR_MODULE));

    dllPeb.BaseAddress = dllPe->mem;
    dllPeb.SizeOfImage = dllPe->nthead->OptionalHeader.SizeOfImage;
    dllPeb.EntryPoint = dllPe->mem + dllPe->nthead->OptionalHeader.AddressOfEntryPoint;
    dllPeb.Flags = dllPe->nthead->FileHeader.Characteristics;
    dllPeb.FullDllName.Buffer = fullNameWC;
    dllPeb.FullDllName.Length = strlen(fullName);
    dllPeb.FullDllName.MaximumLength = 512;
    dllPeb.BaseDllName.Buffer = shortNameWC;
    dllPeb.BaseDllName.Length = strlen(shortName);
    dllPeb.BaseDllName.MaximumLength = 255;
    dllPeb.LoadCount = -1;
    dllPeb.TlsIndex = 0;

    *pDllPeb = dllPeb;

    //ForwardLink, BackLink - двусвязный список. Добавление нового элемента
    //в списоки модулей peb`а текущего процесса
    pDllPeb->InLoadOrderModuleList.Flink = &Ldr->InLoadOrderModuleList;
    pDllPeb->InLoadOrderModuleList.Blink = Ldr->InLoadOrderModuleList.Blink;
    Ldr->InLoadOrderModuleList.Blink->Flink = &pDllPeb->InLoadOrderModuleList;
    Ldr->InLoadOrderModuleList.Blink = &pDllPeb->InLoadOrderModuleList;

    pDllPeb->InMemoryOrderModuleList.Flink = &Ldr->InMemoryOrderModuleList;
    pDllPeb->InMemoryOrderModuleList.Blink = Ldr->InMemoryOrderModuleList.Blink;
    Ldr->InMemoryOrderModuleList.Blink->Flink = &pDllPeb->InMemoryOrderModuleList;
    Ldr->InMemoryOrderModuleList.Blink = &pDllPeb->InMemoryOrderModuleList;

    pDllPeb->InInitializationOrderModuleList.Flink = &Ldr->InInitializationOrderModuleList;
    pDllPeb->InInitializationOrderModuleList.Blink = Ldr->InInitializationOrderModuleList.Blink;
    Ldr->InInitializationOrderModuleList.Blink->Flink = &pDllPeb->InInitializationOrderModuleList;
    Ldr->InInitializationOrderModuleList.Blink = &pDllPeb->InInitializationOrderModuleList;

    return;
}

void GetHeaderInfo(PeHeaders *pe, char *filename)
Функция получает информацию о таблицах импорта, экспорта и релоков.

Исходный код функции

//
//Функция получает информацию о таблицах импорта/экспорта/релоков
//
void GetHeaderInfo(PeHeaders *pe, char *filename) {
    //Информация находится в секциях, доступ получаем с помощью
    //констант из WinNT.h
    
    //Получаем инфомацию об экспорте
    if (pe->nthead->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress) {
        pe->expdir = (IMAGE_EXPORT_DIRECTORY*)(pe->mem + pe->nthead->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
        pe->sizeExpdir = pe->nthead->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;
    } else {
        pe->expdir = 0;
        pe->sizeExpdir = 0;
        printf("Export directory not found for %s\n", filename);
    }

    //Получаем информацию об импорте
    if (pe->nthead->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress) {
        pe->impdir = (IMAGE_IMPORT_DESCRIPTOR*)(pe->mem + pe->nthead->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
        pe->sizeImpdir = pe->nthead->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size;
    } else {
        pe->impdir = 0;
        pe->sizeImpdir = 0;
        printf("Import directory not found for %s\n", filename);
    }

    //Получаем информацию о релоках
    if (pe->nthead->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress) {
        pe->reldir = (IMAGE_BASE_RELOCATION*)(pe->mem + pe->nthead->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
        pe->sizeReldir = pe->nthead->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size;
    } else {
        pe->reldir = 0;
        pe->sizeReldir = 0;
        printf("Relocation directory not found for %s\n", filename);
    }

    return;
}

BOOL SettingRelocs(PeHeaders *pe)
Функция настройки релоков. Вызывается только в то случае, если требуемый адрес загрузки не совпадает с реальным. Это первый шаг второго этапа в алгоритме.

Исходный код функции

/*
* Функция настройки релоков
* !Обработка базозависимых команд, например, mov eax, offset xxx. offset будет уже
* другой, т.к. база изменилась.
*
* Источник: http://cs.usu.edu.ru/docs/pe/dirs4.html#1
* Список всех настраиваемых RVA и способ их настройки содержится в таблице настройки,
* которая представляет собой набор блоков.
* Каждый блок содержит настройки для 4 Кб данных и начинается с заголовка IMAGE_BASE_RELOCATION
*
* ->IMAGE_BASE_RELOCATION
* Смещение(hex)    Размер    Тип        Название        Описание
* 00                4        DWORD    VirtualAddress    Начальный RVA.
* 04                4        DWORD    SizeOfBlock        Размер блока в байтах.
* ->sizeof(IMAGE_BASE_RELOCATION)
* Type, offset | type, offset | ...
* ->sizeof(pe->SizeOfBlock)
*
* IMAGE_BASE_RELOCATION    *reldir;    // указатель на таблицу релоков
* DWORD                sizeReldir;        // размер таблицы релоков
*/
BOOL SettingRelocs(PeHeaders *pe) {
    //Величина, на которую нужно изменить все адреса
    DWORD Delta = pe->mem - pe->nthead->OptionalHeader.ImageBase; 
    DWORD RelocDirSize = pe->sizeReldir;
    WORD *TypeAndOffset;
    IMAGE_BASE_RELOCATION *Reloc;
    DWORD* EditableAddress;
    unsigned int OffsetBufSize, i;
    DWORD offset = 0;

    //printf("ImageBase = %x \n", pe->nthead->OptionalHeader.ImageBase);
    //printf("pe->mem  = %x \n", pe->mem);
    //printf("Delta = %x \n", Delta);

    //Начинаем с начального блока в таблице релоков
    Reloc = pe->reldir;    

    while (offset != RelocDirSize) {
        //Количество описателей в массиве настроек(в одном блоке)
        OffsetBufSize = (Reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD); 
        //Перепрыгнуть заголовок
        TypeAndOffset = (WORD*)((DWORD)Reloc + sizeof(IMAGE_BASE_RELOCATION)); 
        for (i = 0; i != OffsetBufSize; i++) {
            if (TypeAndOffset[i] >> 12) {
                //Получение адреса, который нужно подправить
                //Каждый описатель настройки в четырех старших битах содержит тип настройки, 
                //а в 12 младших битах – смещение от начального RVA до настраиваемых данных
                EditableAddress = (DWORD*)(pe->mem + Reloc->VirtualAddress + (TypeAndOffset[i] & 0x0FFF));
                
                //Подправить
                *EditableAddress += Delta;
            }
        }
        //Прибавляем размер текущего блока таблицы(index)
        offset += Reloc->SizeOfBlock; 

        //Увеличиваем Релок на размер текущего блока таблицы - получаем следующий блок
        Reloc = (IMAGE_BASE_RELOCATION*)((DWORD)Reloc + Reloc->SizeOfBlock); 
    }

    return TRUE;
}

BOOL ProcessImport(PeHeaders *pe)
Обработка таблицы импорта, в которой перечислены необходимые библиотеки, это 3 шаг второго этапа и одновременно подготовка к 4 шагу. Они все между собой связаны, поэтому сначала нужно проверить встречалась ли уже такая библиотека. Если нет, вызывается функция LoadDll, которая ищет библиотеку по стандартным путям в системе. Если же библиотека уже описана в Peb процесса, просто инициализируем Peb этой библиотеки. В любом из исходов нужно вызвать процедуру экспорта символов библиотеки — ImportDll.

Исходный код функции

//
//Функция обрабатывает таблицу импорта
//
BOOL ProcessImport(PeHeaders *pe) {
    //Указатель на начало таблицы импорта(первая библиотека)
    IMAGE_IMPORT_DESCRIPTOR *impDir = pe->impdir;
    DWORD pebDllBaseAddress;

    while (!(impDir->FirstThunk == 0 && impDir->Characteristics == 0 && impDir->ForwarderChain == 0
        && impDir->Name == 0 && impDir->OriginalFirstThunk == 0 && impDir->TimeDateStamp == 0)) {

        //Для импортированной dll
        PeHeaders dllPe; 

        //Имя библиотеки
        char *dllName = (char*)(pe->mem + impDir->Name);            

        //Ищем библиотеку в Peb

        //Если нашли - инициализируем Peb
        if (pebDllBaseAddress = (DWORD) SearchDllInPeb(dllName)) {    
            //Запоминаем имя библиотеки
            dllPe.filename = pe->mem + impDir->Name;
            InitPebDll(&dllPe, pebDllBaseAddress);    
        //Если не нашли - загружаем ее, перебирая пути
        } else if (!LoadDll(dllName, &dllPe)) {                        
            printf("Error load lib %s in LoadPeImage\n", dllName);

            return FALSE;
        }

        //"импортируем" символы (функции) из таблицы экспорта библиотеки
        if (!ImportDll(pe, &dllPe, impDir)) {                        
            return FALSE;    
        }

        //Переходим к следующей библиотеке
        impDir++;                                                    
    }

    return;
}

BOOL LoadDll(char *dllName, PeHeaders *impDllPe)
Функция осуществляет поиск библиотеки по стандартным путям и вызывает ее рекурсивную загрузку.

Исходный код функции

/* 
* Если библиотеки нет в списке(в peb), то ищем ее в одном из стандартных
* каталогов и вызываем рекурсивно процедуру загрузки PE-файла.
* При следующем обращении к этой библиотеке, она уже будет содержаться в списке 
* и загружаться заново не будет.
* 
* Функция осуществляет поиск и рекурсивный вызов процедуры
*/
BOOL LoadDll(char *dllName, PeHeaders *impDllPe) {
    unsigned int j;
    char dllPath[4][256] = {
        "\0",
        "C:\\Windows\\\0",
        "C:\\Windows\\System32\\\0",
        "C:\\Windows\\System32\\Wbem\\\0",
    };

    printf("!Recursive loading DLL: %s\n", dllName);
    for (j = 0; j < 4; j++) {
        strcat(dllPath[j], dllName);
        if (LoadPeImage(impDllPe, dllPath[j], dllName)) {
            //Библиотека найдена и загружена
            return TRUE;
        }
    }

    //Если библиотека не найдена по стандартным путям - завершаем процесс
    return FALSE;  
}

BOOL ImportDll(PeHeaders *pe, PeHeaders *libpe, IMAGE_IMPORT_DESCRIPTOR *impDir)
Функция загрузки символов(функций) из библиотеки, 4 шаг второого этапа. Подробнее в комментариях исходного кода.

Исходный код функции

//
//функция импортирует символы из таблицы импорта
//RVA до имени функции заменяются адресами этих функций в нужной библиотеке
//
BOOL ImportDll(PeHeaders *pe, PeHeaders *libpe, IMAGE_IMPORT_DESCRIPTOR *impDir) {
    DWORD pebDllBase;
    DWORD *funcsAddrs;
    DWORD *funcsNames;
    WORD *funcsNamesOrdinals;
    char *symFromFT;
    char *symFromExp;
    char *redirSym;
    char redirSymName[255];
    char redirSymDll[255];
    unsigned int *fromFT, *toFT;
    unsigned int k, j, i, tmp_buf;
    unsigned int dllExpdirEndAddr = (unsigned int) libpe->expdir + libpe->sizeExpdir;
    PeHeaders redirPe;

    //Указатель на массив адресов функций
    funcsAddrs = (DWORD*) (libpe->mem + libpe->expdir->AddressOfFunctions);
    //Указатель на массив адресов имён функций
    funcsNames = (DWORD*) (libpe->mem + libpe->expdir->AddressOfNames);
    //Указатель на массив ординалов именованных функций
    funcsNamesOrdinals = (WORD*) (libpe->mem + libpe->expdir->AddressOfNameOrdinals);

    //lib = LoadLibrary ((char*)pe->mem + impDir->Name, peb->LoaderData);

    /*
    * Адреса функций проставляются в массив FirstThunk.
    * А имена(RVA имени) и ординалы(порядковые номера) брать из массива OriginalFirstThunk,
    * если это поле нулевое, то из поля FirstThunk.
    */
    if(impDir->OriginalFirstThunk == 0) {
        fromFT = (unsigned int *)(pe->mem + impDir->FirstThunk);
    } else {
        fromFT = (unsigned int *)(pe->mem + impDir->OriginalFirstThunk);
    }
    //В последствии перезаписывается адресами функций
    toFT = (unsigned int *)(pe->mem + impDir->FirstThunk);

    //Цикл, пока не встретим нулевой элемент
    for (j = 0; fromFT[j]; ++j) {
        //Если по ординалу
        if (fromFT[j] & 0x80000000) {
            //Запоминаем ординал(старший бит)
            tmp_buf = fromFT[j] & ~0x80000000 - libpe->expdir->Base;
            
            //Получаем адрес экспортируемой функции
            //toFT[j] = (unsigned int)GetProcAddress (libpe->mem, fromFT[j] & ~0x80000000);
            toFT[j] = funcsAddrs[tmp_buf] + libpe->mem;
        
        //Eсли по имени
        } else {
            //Получаем имя
            symFromFT = (char *)pe->mem + fromFT[j] + 2;
            //бежим по списку экспорта, перебираем имена оттуда
            for (i = 0; i < libpe->expdir->NumberOfNames; i++) {
                //запоминаем текущее имя из списка
                symFromExp = (char *)(funcsNames[i] + libpe->mem);

                //если совпало с тем, что ищем
                if (!strcmp(symFromFT, symFromExp)) {
                    //получаем адрес функции
                    //toFT[j] = (unsigned int)GetProcAddress (libpe->mem, funcsNamesOrdinals[i]);
                    toFT[j] = funcsAddrs[funcsNamesOrdinals[i]] + libpe->mem;

                    /*
* При обработке таблицы экспорта библиотеки возможен случай перенаправления экспорта.
* лемент массива AddressOfFunctions содержит не RVA функции, а RVA строчки вида "lib.FunName" с именем функции и библиотеки,
* в которую перенаправляется экспорт. Этот случай можно опознать по адресу функции в массиве AddressOfFunctions,
* который будет указывать внутрь директории экспорта, чего не может быть для настоящего адреса функции.
                    */
                    //Если случай перенаправления экспорта
                    if (toFT[j] >= libpe->expdir && toFT[j] < dllExpdirEndAddr) {            
                        //Запоминаем текущий символ
                        redirSym = toFT[j];                                                    
                        printf("ImportDll: redir of symbol: %s\n", redirSym);
                        
                        //Отделить имя бибилиотеки от имени функции
                        k = 0;
                        //Ищем точку
                        while (redirSym[k] != '.' && k < strlen(redirSym)) {        
                            //Количество символов до точки
                            k++;                                                            
                        }
                        
                        //Берем строку до точки(библиотека)
                        memcpy(redirSymDll, redirSym, k);                                     
                        redirSymDll[k] = 0;
                        printf("ImportDll: DLL of redir symbol: %s \n", redirSymDll);
                        
                        //Дописать расширение
                        strcat(redirSymDll, ".dll");                                        

                        //Берем строку после точки(имя функции)
                        memcpy(redirSymName, &redirSym[k + 1], strlen(redirSym) - k - 1);    
                        redirSymName[strlen(redirSym) - k - 1] = 0;                            
                        printf("ImportDll: name of redir symbol: %s \n", redirSymName);

                        //Обрабатываем новую библиотеку
                        pebDllBase = SearchDllInPeb(redirSymDll, peb->LoaderData);
                        //Если нашли ее в peb - инициализируем ее pe
                        if (pebDllBase) {
                            InitPebDll(&redirPe, pebDllBase);
                        //Если нет - ищем по стандартным путям
                        } else if (!LoadDll(redirSymDll, &redirPe, peb->LoaderData)) {
                            printf("ImportDll: Error load lib $s\n", impDir->Name + pe->mem);
                            return FALSE;
                        }

                        //Ищем этот символ в библиотеке указанной в перенаправлении
                        //и получаем его адрес
                        if (!(toFT[j] = RedirImportDll(&redirPe, redirSymName))) {
                            return FALSE;    
                        }

                    }
                    
                    //завершить перебор символов в таблице экспорта
                    break;
                }
            }
        }
    }
}

На этом самая главная обработка образа закончилась, мы исправили адреса с помощью релоков и загрузили все функции из вспомогательных библиотек. Осталось два последних шага, настройка прав доступа и вызов точки входа.

void SetPeSectionsProtect(PeHeaders *pe)
Функция выполняет 4 шаг второго этапа, выставляет права доступа к секциям загружаемого файла.

Исходный код функции

//
// Устанавливает права доступа на области памяти секций PE-файла.
//
void SetPeSectionsProtect(PeHeaders *pe) {
    unsigned int i;
    //Предыдущие права доступа
    DWORD PrevAccessFlags; 

    //Для каждой секции
    for (i = 0; i < pe->nthead->FileHeader.NumberOfSections; i++) {
        VirtualProtect(
            //Адрес секции для установки флага
            pe->sections[i].VirtualAddress + pe->mem,
            //Размер секции
            pe->sections[i].Misc.VirtualSize,                            
            //Флаг
            GetSectionProtection(pe->sections[i].Characteristics),        
            &PrevAccessFlags                                            
            );
    }

    return;
}

— Последний шаг это вызов точки входа исполняемого файла и выглядит он следующим образом:

//Передача управления точке входа
//Если это dll
if (pe->nthead->FileHeader.Characteristics & IMAGE_FILE_DLL) {
    return ((BOOL(__stdcall*)(HINSTANCE, DWORD, LPVOID))(pe->mem + pe->nthead->OptionalHeader.AddressOfEntryPoint))(NULL, DLL_PROCESS_ATTACH, NULL);
}
//Если это exe
else if (pe->nthead->FileHeader.Characteristics & IMAGE_FILE_EXECUTABLE_IMAGE) {
    ((void(*)(void))(pe->mem + pe->nthead->OptionalHeader.AddressOfEntryPoint))();
}

Заключение

Вот и подошла к концу огромная работа по реализации собственного загрузчика PE файлов, он в точности выполняет действия системного и запускает исполняемые файлы. Естественно, система будет сильно с ругаться, показывать много сообщений об ошибках. Но самое главное, что загрузчик запускает другую программу, даже с WinAPI интерфейсом. На этом все, спасибо за внимание!