> For the complete documentation index, see [llms.txt](https://ymmfty0.gitbook.io/ymmfty0/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ymmfty0.gitbook.io/ymmfty0/pe-file-format/imports-exports/import-table.md).

# Import Table

## **Import**

Импорт в контексте PE-файлов — это данные, которые помогают загрузчику Windows определить, какие внешние библиотеки (DLL) необходимо подключить и какие функции или данные из этих библиотек должны быть доступны для исполняемого файла.

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

PE файл имеет несколько способов импортирования функций.

Самый простой способ и стандартный метод — это импорт через таблицу импорта (IAT). Загрузчик Windows заполняет для каждой DLL адреса функций во время загрузки приложения в память. Это чем-то напоминает механизм `LoadLibrary` для DLL и получения адреса с помощью `GetProcAddress`.\
Не самый оптимальный, но наиболее часто встречающийся.

Второй способ — это bound-import. Адреса функций DLL записываются ещё на этапе компиляции. Работает он быстрее, так как адреса доступны заранее. Однако эти адреса жёстко зашиты, что может привести к сбоям в случае изменения DLL. Это быстрый, но не универсальный метод.

Delay-import — это способ загрузки DLL и её функций только в момент, когда они действительно нужны, а не при загрузке приложения. Работает он следующим образом: вместо прямых адресов функций хранятся адреса специального обработчика, который возвращает адрес нужной функции. При следующем вызове используется уже непосредственно адрес функции, и обработчик больше не вызывается.

Реализация такого метода сложнее и не всегда стабильна. Поддержка отложенного импорта зависит от компоновщика и может вызывать ошибки.

## **Секция .idata**

Практически все исполняемые файлы (EXE) имеют секцию `.idata`, которая служит для управления импортированными функциями и библиотеками. Эта секция содержит информацию о функциях и библиотеках, которые загружаются в память перед выполнением исполняемого файла. В частности, она включает структуры, описывающие импортируемые библиотеки и функции, что позволяет динамически связывать их во время выполнения.

Если секция `.idata` отсутствует, таблица импорта может быть размещена в любой другой секции, в зависимости от настроек компилятора или линкера. Часто она включается в секцию `.rdata` или `.text`.

## **Import Directory Table**

Информация об импортах начинается с Import Directory Table. Она содержит записи для описания импортированных DLL и их функций. Это массив , каждая запись которой указывается на структуру называемую **IMAGE\_IMPORT\_DESCRIPTOR**, которая содержит информацию о конкретных импортируемых модулях и функциях. Последний элемент заполнен нулями, это означает конец таблицы Import Directory.

`IMAGE_IMPORT_DESCRIPTOR` определяется следующим образом:

```cpp
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;
        DWORD   OriginalFirstThunk;
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;
    DWORD   ForwarderChain;
    DWORD   Name;
    DWORD   FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
```

* Characteristics - 0 для PE32 или RVA на Import Lookup Table
* OriginalFirstThunk - RVA на Import Lookup Table
* TimeDateStamp - Если это значение равно 0, то загрузчик обрабатывает таблицу импорта стандартным методом. Если равно -1 , то будет использоваться bound import. А значения OriginalFirstThunk и FirstThunk игнорирует, загрузчик думает, что все адреса корректны. Любое другое значение TimeDateStamp обозначает действительную временную метку, и, если она совпадает с временной меткой импортируемой DLL, загрузчик просто проецирует ее на адресное пространство процесса, не настраивая таблицу адресов. Предполагается, что эффективные адреса заданы еще на времени компиляции
* ForwarderChain - это значение имеет отношение к цепочке форварда . Обычно равно нулю. Системный загрузчик , это значение просто игнорирует .
* Name - RVA на имя DLL
* FirstThunk - RVA на Import Address Table

Чтобы получить Import Directory , там так же нужно будет обратиться к Data Directory

```cpp
IMAGE_DATA_DIRECTORY import_data_directory = ntHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
auto import_table = reinterpret_cast<PIMAGE_IMPORT_DESCRIPTOR>(pBaseAddr + import_data_directory.VirtualAddress);
```

Давайте переберём все импортированные DLL внутри PE-файла и выведем их имена.\
Как говорилось ранее, *последний элемент заполнен нулями, что означает конец таблицы Import Directory.*

Поэтому просто сделаем небольшой перебор, пока значение не станет равно нулю.

```cpp
while (import_table->Name != 0)
{
	std::cout << (pBaseAddr + import_table->Name) << std::endl;
	import_table++;
}
```

Вот вывод

<figure><img src="/files/cQHZmZGApcoYoExMa15I" alt=""><figcaption></figcaption></figure>

Давайте проверим c PE-Bear и удостоверимся, что все действительно так

<figure><img src="/files/5HaQn82yjBLfoaHwhUHS" alt=""><figcaption></figcaption></figure>

## **Bound Imports**

Выше я уже рассказывал, что это такое. Давайте напомним ещё раз: это метод импорта, при котором адреса функций вычисляются во время компиляции. Это добавляет оптимизации, но имеет свои проблемы. Для Bound Imports используется массив структур `IMAGE_BOUND_IMPORT_DESCRIPTOR`. Эта структура состоит из трёх параметров.

Эта структура выглядит вот так:

```cpp
typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
    DWORD   TimeDateStamp;
    WORD    OffsetModuleName;
    WORD    NumberOfModuleForwarderRefs;
// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} IMAGE_BOUND_IMPORT_DESCRIPTOR,  *PIMAGE_BOUND_IMPORT_DESCRIPTOR;
```

* TimeDateStamp - Временная метка даты импортированной DLL.
* OffsetModuleName - смещение на имя DLL. Смещение от первой структуры IMAGE\_BOUND\_IMPORT\_DESCRIPTOR
* NumberOfModuleForwarderRefs - Количество структур IMAGE\_BOUND\_FORWARDER\_REF, которые следуют непосредственно за этой структурой. Эти дополнительные структуры описывают модули, на которые ссылается текущий модуль через механизм форвардинга. Форвардинг позволяет одной DLL перенаправлять вызовы своих экспортируемых функций на функции другой DLL.

При загрузке приложения загрузчик сравнивает временную метку в `IMAGE_BOUND_IMPORT_DESCRIPTOR` с меткой времени в `IMAGE_FILE_HEADER` импортируемой DLL. Если все временные метки совпадают, то DLL считается Bound. Далее загрузчик позволяет программе действовать самостоятельно.

Мы можем получить структуру, так же Data Directory

```cpp
IMAGE_DATA_DIRECTORY bound_import_directory = ntHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT];
auto bound_import_table = reinterpret_cast<PIMAGE_BOUND_IMPORT_DESCRIPTOR>(pBaseAddr + bound_import_directory.VirtualAddress);
```

## **Delay Import**

Механизм я описывал выше, так что просто рассмотрим , какие поля и за что они отвечают.

```cpp

typedef DWORD RVA; 
typedef struct ImgDelayDescr {
    DWORD           grAttrs;        // attributes
    RVA             rvaDLLName;     // RVA to dll name
    RVA             rvaHmod;        // RVA of module handle
    RVA             rvaIAT;         // RVA of the IAT
    RVA             rvaINT;         // RVA of the INT
    RVA             rvaBoundIAT;    // RVA of the optional bound IAT
    RVA             rvaUnloadIAT;   // RVA of optional copy of original IAT
    DWORD           dwTimeStamp;    // 0 if not bound,
                                    // O.W. date/time stamp of DLL bound to (Old BIND)
    } ImgDelayDescr, * PImgDelayDescr;
```

Разберем поля

* grAttrs - указывает на тип , будут это RVA или VA для структуры отложенного импорта (0 – VA, 1 – RVA); Если **`grAttrs == 0`**, то загрузчик интерпретирует все указатели в структуре ( такие как `rvaHmod`, `rvaDLLName`, `rvaIAT` и т.д.) как **VA**, а не **RVA**.
* rvaDLLName - Название говорит само за себя , это просто RVA/ на имя DLL.
* rvaHmod - Сюда загрузчик записывает RVA на дескриптор динамически загруженной DLL.**Поле `rvaHmod`** остаётся пустым до тех пор, пока библиотека не будет загружена. Всем этим занимается Delay Helper.
* rvaIAT - Хранит в себе RVA на IAT отложенного импорта
* rvaINT - содержит RVA на таблицу имен
* rvaBoundIAT - RVA на таблицу диапазонного импорта
* rvaUnloadIAT - RVA на копию оригинальной IAT, которая может быть использована для восстановления таблицы IAT в её исходное состояние при выгрузке DLL.

## **Import Lookup Table**

Каждая импортированная DLL имеет эту таблицу.\
Import Lookup Table, её также можно назвать Import Name Table.

ILT содержит информацию о функциях, которая помогает загрузчику понять, какие функции импортировать.

По сути, это просто таблица имён и ссылок, которые нужно импортировать.\
ILT представляет собой массив 32-битных чисел для PE32 или массив 64-битных чисел для PE32+.

Каждая запись использует формат битового поля.\
Информация хранится следующим образом:

* **Бит 31/63** (наиболее значимый бит): называется флагом "Ordinal/Name". Он указывает, будет ли функция импортироваться по имени или по порядковому номеру (ординалу).
* **Биты 15-0**: если флаг "Ordinal/Name" установлен в значение 1, эти биты содержат 16-битный порядковый номер (ординал), который используется для импорта функции. Биты 30-15/62-15 для PE32/PE32+ должны быть установлены в 0.
* **Биты 30-0**: если флаг "Ordinal/Name" установлен в значение 0, эти биты содержат RVA (относительный виртуальный адрес) таблицы "Hint/Name".

Давайте разберёмся со старшим битом, который указывает на то, как будет импортироваться функция — по ordinal или по имени. Пока не задумывайтесь о том, что такое ordinal, я расскажу об этом более подробно далее. Просто представьте его как порядковый номер функции.

Для тестов я буду использовать вот такое значение, потому что оно подходит под все параметры, указанные в официальной документации Microsoft.

```cpp
ULONGLONG test = 9223372036854804724;
```

Как упоминалось выше, проверка того, как будет импортироваться функция — по ordinal или по имени — осуществляется по старшему биту. Если старший бит (63 для PE32+ или 31 для PE32) равен 1, то функция импортируется по ordinal, иначе — по имени функции.

В данном случае я брал из PE-файла таблицу ILT и извлекал значение оттуда, чтобы сверить тестовое значение с валидным значением из ILT.

```cpp
auto value = *reinterpret_cast<ULONGLONG*>(pBaseAddr + import_table->OriginalFirstThunk);

std::cout << std::hex << value << std::endl;
std::cout << std::bitset<64>(value) << std::endl;

ULONGLONG test = 9223372036854804724;
std::cout << std::bitset<64>(test) << std::endl;

// ordinal or name 
std::cout << "Valid value ordinal flag: " << ((value & 0x8000000000000000) != NULL) << std::endl;
std::cout << "Test value ordinal flag: " << ( ( test & 0x8000000000000000) != NULL ) << std::endl;
```

`bitset` я использую в данном случае для того, чтобы вывести значение в битовом формате и можно было видеть, установлен флаг или нет.

Далее я вывожу результат проверки старшего бита, чтобы удостовериться, установлен флаг ordinal или нет.\
Проверка выполняется с использованием маски, указанной на MSDN.

```cpp
(value & 0x8000000000000000) != NULL
```

<figure><img src="/files/bOlnSslVmPhxYCz5w16I" alt=""><figcaption></figcaption></figure>

Как видно, валидное значение импортируется по имени.

Далее говорится, что если происходит импорт по ordinal, то значения с 62 по 16 должны быть равны 0. А значения с 15 по 0 будут представлять собой 16-битное значение, которое как раз и будет равно значению ordinal.

Сделаем вот такой небольшой перебор.

```cpp
std::bitset<64> bit_array(test);

for (size_t i = 62; i >= 16; --i)
{
	if (bit_array[i])
	{
		std::cout << std::endl << "Error: Reserved bits must be 0" << std::endl;
		break;
	}
	std::cout << bit_array[i];
}
```

Я просто создаю битовый массив и проверяю , равно ли число 0 или нет. Bitset в массиве, просто будет хранить число 0 или 1 , так что дополнительных проверок делать не надо , хватит одно лишь if

<figure><img src="/files/C8J7r1bUuWTF9FpsEU52" alt=""><figcaption></figcaption></figure>

Как видим, вывод равен нулям, так что действительно все окей.

Теперь мы можем спокойно получить ординал, потому что все проверки пройдены, так что нам просто нужно перевести всё это в WORD.

```cpp
WORD ordinal = static_cast<WORD>(test);
std::cout << ordinal << std::endl; 
```

Вывод 70f4 , это и будет наш ordinal

<figure><img src="/files/uN7Eq2xRylP5WtGEHe21" alt=""><figcaption></figcaption></figure>

А что делать , если у нас не ordinal ? Проверяем флаг , ordinal и приводим все это к DWORD .

```cpp
if ((value & 0x8000000000000000) != NULL)
{
	std::cout << "ordinal flag" << std::endl;
	return -1;
}

auto rva_to_thunk_data = static_cast<DWORD>(value);
auto thunk_data_addr = reinterpret_cast<PVOID>(pBaseAddr + rva_to_thunk_data);
```

Благодаря этому RVA , мы можем получить структуру `IMAGE_THUNK_DATA64`

### **IMAGE\_THUNK\_DATA64**

Эта структура нужна лишь для того, чтобы хранить в себе , либо порядковый номер импортируемого API, либо RVA на структуру **IMAGE\_IMPORT\_BY\_NAME**. Так выглядит структура для PE32 и PE32+.

```cpp
typedef struct _IMAGE_THUNK_DATA64 {
    union {
        ULONGLONG ForwarderString;  // PBYTE 
        ULONGLONG Function;         // PDWORD
        ULONGLONG Ordinal;
        ULONGLONG AddressOfData;    // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA64;
typedef IMAGE_THUNK_DATA64 * PIMAGE_THUNK_DATA64;

typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE 
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
```

Разберем поля:

* ForwarderString - RVA на forwrder string
* Function - Адрес памяти импортируемой функции
* Ordinal - Порядковое значение импортируемого API. Есди это импортирование по имени функции, это значение будет равно RVA на IMAGE\_IMPORT\_BY\_NAME, так как это UNION.
* AddressOfData - Это RVA на структуру IMAGE\_IMPORT\_BY\_NAME , в которой хранится имя импортированной функции, про эту структуру я буду рассказывать позже. Если это импортирование по ordinal , это значение будет равно ordinal , так как это UNION.

### **IMAGE\_IMPORT\_BY\_NAME**

Давайте перейдем к IMAGE\_IMPORT\_BY\_NAME, она так же известная как hint/name table.

```cpp
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    CHAR   Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
```

* Hint - числовое значение типа **WORD**, используется для массива Export Name Pointer Table. Export Name Pointer Table - это массив имен , экспортированных функций . Hint это предположительный индекс для это массива . Если мы получили верное имя функции, поиск прекращается и мы можем получить адрес этой функции . Иначе поиск по **Hint** не удается, загрузчик выполняет бинарный поиск по таблице указателей имен экспорта DLL.
* Name - null-terminated строка, которая хранит имя импортированной функции .

Давайте попробуем, как-то вывести все имена функций импортированной DLL. В данном случае, я буду выводить функции из KERNEL32.DLL , потомучто она первая идет в Import Table.

Как раньше и говорилось ,конец ILT , заканчивается нулевым значением. Так что нам просто следует перебрать это при помощи цикла.

```cpp
auto data = reinterpret_cast<IMAGE_THUNK_DATA64*>(pBaseAddr + import_table->OriginalFirstThunk);

while (data->u1.AddressOfData != NULL)
{
	auto name = reinterpret_cast<IMAGE_IMPORT_BY_NAME*>(pBaseAddr + data->u1.AddressOfData);
	std::cout << "Name: " << name->Name << " ; " << "Hint value: " << name->Hint << std::endl;
	data++;
}
```

Давайте перейдем в PE-Bear и проверим имена импортированных функций с нашим кодом

<figure><img src="/files/ue1bNEsVSTtaYQtOMWdp" alt=""><figcaption></figcaption></figure>

И как видим, все действительно правильно работает.

## Import Address Table

До загрузки в память IAT идентичен ILT. IAT, так же как и ILT, представляет собой массив 32-битных чисел для PE32 или массив 64-битных чисел для PE32+. Этот массив содержит либо порядковый номер импортируемого API, либо RVA на структуру `IMAGE_IMPORT_BY_NAME`.

Возникает вопрос: зачем нужны два одинаковых массива для каждого набора API, импортированных из DLL?\
Ключевое отличие заключается в том, что ILT не изменяется загрузчиком при загрузке в память, тогда как IAT, напротив, перезаписывается адресами импортированных функций. Таким образом, ILT можно использовать для получения исходной информации, если это потребуется.

## Итог

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

Для практического понимания работы с заголовком PE-файла, вы можете ознакомиться с кодом в моем репозитории Github: <https://github.com/ymmfty0/PEFormatTutorial/tree/master/ImportTable>
