PE-формат. Часть 3 — Импорт

Импорт - это важнейший элемент любого исполняемого файла. Невозможно найти exe- или dll-файл без каких-либо таблиц, имеющих отношение к импорту, разве что у упакованных файлов и у вирусов. Импорт позволяет указать, какие внешние функции и из каких модулей требуются исполняемому файлу для нормальной работы. Например, если мы пишем простейший "Hello, world!", который выводит приветствие в MessageBox'е и завершает выполнение, то ему потребуются как минимум две функции: MessageBoxA (или MessageBoxW) из user32.dll и ExitProcess из kernel32.dll.

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

Второй механизм - это bound import (привязанный импорт), который сводится к тому, что на адресное пространство исполняемого файла проецируются библиотеки, а в таблице импорта уже зашиты адреса функций. Это быстрый механизм, но любое изменение динамических библиотек повлечет за собой обязательную перекомпиляцию (перепривязку) исполняемого файла, чтобы все зашитые адреса функций пересчитались. Тем не менее, этот механизм используется в Windows для всех стандартных исполняемых файлов (Калькулятор, Сапер, Блокнот и т.д.), так как стандартные библиотеки Windows меняются крайне редко. Привязанный импорт комбинируется с обычным импортом.

Третий, самый неочевидный механизм, это delay import (отложенный импорт), основная идея которого состоит в вызове некоторого обработчика, который должен получить адрес требуемой функции и записать его в таблицу адресов импортируемых функций, по мере необходимости.

Для начала рассмотрим классический импорт, так как без него, как правило, не обходится ни один исполняемый файл. С классическим импортом связано несколько таблиц. На корневую таблицу импортируемых функций указывают элементы IMAGE_DIRECTORY_ENTRY_IMPORT в DATA_DIRECTORY и IMAGE_DIRECTORY_ENTRY_IAT, указывающий на IMPORT_ADDRESS_TABLE (рассматривается далее), начиная с Windows XP.

Элемент DATA_DIRECTORY IMAGE_DIRECTORY_ENTRY_IMPORT указывает на массив расположенных подряд структур IMAGE_IMPORT_DESCRIPTOR, причем последний элемент должен быть нулевым (все поля структуры равны 0). Рассмотрим структуру IMAGE_IMPORT_DESCRIPTOR подробнее:

Эта структура справедлива как для PE, так и для PE64. Поле Name - это RVA строки, содержащей имя DLL, из которой будет производиться импорт. Если содержимое поля TimeDateStamp равно нулю, то импорт обрабатывается как обычно, а если -1, то импорт считается привязанным (bound), и к таблице IMAGE_IMPORT_DESCRIPTOR загрузчик вернется только в том случае, если привязанный импорт завершится неудачей. Если TimeDateStamp содержит отличное от 0 и 1 значение, то при совпадении этого значения с таймстампом библиотеки она просто проецируется на адресное пространство процесса без дальнейших вызовов GetProcAddress. При этом считается, что все адреса должны быть записаны в таблицу адресов еще на этапе компиляции. Если же таймстампы не совпали, то импорт обрабатывается по полной программе, как обычно.
Поле ForwarderChain системным загрузчиком игнорируется и может содержать что угодно.
Самые важные поля - это OriginalFirstThunk и FirstThunk. Первое указывает на lookup-таблицу, содержащую имена импортируемых функций, а второе - на таблицу адресов импортируемых функций (сюда загрузчик будет писать текущие адреса функций).

На данном этапе я выкладываю свой C++-класс, упрощающий работу с PE-файлами, который я достаточно давно начал писать. Сейчас он еще не завершен, можно наращивать функционал, но для наглядного вывода таблицы импорта исполняемого файла он вполне подойдет. Вот исходный код, печатающий таблицу импорта любого 32-разрядного файла (для 64-раздядного можете сделать сами, минимум отличий):

Результат работы программы можно увидеть на скриншоте в начале поста. Исходные коды класса и программы прикреплены в конце статьи. Программы для разбора привязанного и отложенного импорта я приводить не буду, они есть в статье Криса Касперски "Путь воина". Я приведу лишь структуры, связанные с этими типами импорта, и некоторые пояснения касательно их.

С привязанным импортом связана всего одна структура (элемент IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT DATA_DIRECTORY указывает на массив таких структр, завершающийся нулевым элементом):

Поле TimeDateStamp содержит временную метку библиотеки для привязанного импорта, и такой импорт будет осуществлен тогда, когда временные метки структуры и библиотеки совпали, либо когда TimeDateStamp = 0. Поле OffsetModuleName - это указатель на имя библиотеки, отсчитываемый от начала массива структур IMAGE_BOUND_IMPORT_DESCRIPTOR. Поле NumberOfModuleForwarderRefs указывает на количество форвардов, назначение этого поля не ясно.
Я сталкивался с exe-файлами, у которых таблицы отложенного импорта находились вне данных секций (т.е. в оверлеях (промежутках между секциями) исполняемого файла), поэтому приходилось дополнительно подгружать их из файла, чтобы считать.

Остается последний механизм импорта - отложенный импорт. Как всегда, элемент IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT DATA_DIRECTORY указывает на массив нижеописанных структр, завершающийся нулевым элементом):

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

Поле grAttrs задает тип адресации, применяющийся в служебных структурах отложенного импорта (0 - VA, 1 - RVA); поле szName содержит RVA/VA-указатель на ASCIIZ-строку с именем загружаемой DLL (тип адреса определяется особенностями реализации конкретного delay helper'а, внедряемого в программу линкером и варьирующегося от реализации к реализации - короче говоря, будьте готовы ко всяким пакостям). В изначально пустое поле phmod загрузчик (все тот же Delay Helper) помещает дескриптор динамически загружаемой DLL.

Поле pIAT содержит указатель на таблицу адресов отложенного импорта, организованную точно также, как и обычная IAT, с той лишь разницей, что все элементы таблицы отложенного импорта ведут к delay load helper'у - специальному динамическому загрузчику, также называемому переходником (thunk), который вызывает LoadLibrary (если только библиотека уже не была загружена), а затем дает GetProcAddress и замещает текущий элемент таблицы отложенного импорта эффективным адресом импортируемой функции, благодаря чему все последующие вызовы данной функции осуществляются напрямую, в обход delay load helper'а.

При выгрузке DLL из памяти, последняя может восстановить таблицу отложенного импорта в исходное состояние, обратившись к ее оригинальной копии, RVA-указатель на которую хранится в поле pUnloadIAT. Если же копии нет, ее указатель будет обращен в ноль.

Поле pINT содержит RVA-указатель на таблицу имен, во всем повторяющую стандартную таблицу имен (см. name Table). То же самое относится и к полю pBoundIAT, хранящим RVA-указатель на таблицу диапазонного импорта. Если таблица диапазонного импорта не пуста и указанная временная метка совпадает с временной меткой соответствующей DLL, системный загрузчик просто проецирует ее на адресное пространство данного процесса и механизм отложенного импорта дезактивируется.

В статье Криса Касперски можно также найти простой дампер таблиц отложенного импорта.

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

Обещанные исходные коды класса для работы с PE и программа для разбора таблиц импорта (всё без комментариев): Скачать.

PE-формат. Часть 3 — Импорт: 18 комментариев

  1. интересная статейка ))
    есть один вопрос не в тему - какой компонент использовали для ведения логов в программе No Ads ?

      1. Извиняюсь что опять не в тему. Как растянуть текстбокс на пару строк я понял, а как сделать вывод по строкам ? Ну например как в том же No Ads пишется по строчке что программа сделала. У меня только заменяет первую строку. Делал так:
        this->tb_logs->Text = "Command " + this->tb_command->Text+ " | " + this->tb_1param->Text+ " | "+ this->tb_2param->Text + " sent on " + this->tb_host->Text + ".\n";

  2. Строки не понял = порылся в сорцах = заюзал вот это
    this->tb_logs->AppendText(String::Concat(text, "\r\n")); - помогло.
    Спасибо за помощь !

  3. у меня проблемка почему то получаю только системные dll, importsStartRVA = GetImgDirEntryRVA(pNTHeader,IMAGE_DIRECTORY_ENTRY_IMPORT);
    в чем может быть проблема?

    1. Я не понял вопрос. В чем конкретно проблема, и что такое GetImgDirEntryRVA? Судя по гуглу, оно позволяет лишь определить RVA директории импорта.

  4. "Например, если мы пишем простейший "Hello, world!", который выводит приветствие в MessageBox'е и завершает выполнение, то ему потребуются как минимум две функции: MessageBoxA (или MessageBoxW) из user32.dll и ExitProcess из kernel32.dll."
    Поправка: на самом деле и ExitProcess не нужен, достаточно MessageBoxA/MessageBoxW.

    invoke MessageBoxA, 0, chr$("Caption"), chr$("Text"), 0
    ret

  5. dx, отличная работа, безусловно наиполезнейшая либа. Хотелось бы узнать способна ли она собирать ехе с нуля как болванку... т.е. собираем дос хидер, нт хидер, опшионал хидер, добавляем секции там (код, дата, рсрц етц), настраиваем точку входа на секцию кода, имэджбейс, сайзофимэдж етц. и на выходе получаем готовый пе файл-пустышку. Я извеняюсь если это уже реализованно у вас, я пока не смотрел вашей библиотеки, прошолся бегло взглядом единственное по папке с сэмплами....

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

      1. можно взять болванку например пустого файла на масме и уже на базе его строить свой ехе...
        ну хотя мне видится, что в полноценной библиотеке болванка ехе обязательно должна быть:) если появится время, можем списаться, готов посодействовать идеями, и возможно кодом.

        1. Да я думаю, что доработаю этот функционал. Я библиотеку еще не забросил и с момента релиза очень много багов всяких поправил там, функционал нарастил немного, и пока продолжаю :) В хедере pe_base.h даже список TODO заготовил, хотя его пока тоже не начинал делать.

  6. >Я библиотеку еще не забросил и с момента релиза очень много багов всяких поправил там
    супер. очень радует ваше творчество. видимо вами движет очень сильная мотивация.

    p.s.
    мне кажется ваш туду лист еще будет пополняться;) btw релоки с несколькими элементами тоже не видел ни разу.

  7. //Заголовки, определяющие класс pe32 и pe64, а также исключения
    #include "pe3264.h"
    #include "pe_exception.h"

    И где это должен взять читающий ?

Добавить комментарий для Avazart Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *