Как работает таблица импорта в PE-файле и зачем она нужна

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

Что вообще такое импорт в PE-файле

PE-файл (Portable Executable) — это формат исполняемых файлов в Windows: .exe, .dll, .sys и другие. Любая непрограмма редко живёт в полной изоляции. Ей нужны функции из системных библиотек — kernel32.dll, user32.dll, ntdll.dll и прочих. Чтобы не включать код этих функций внутрь файла, используется импорт: программа говорит «мне понадобится CreateFile из kernel32.dll», а загрузчик подставляет реальный адрес этой функции при запуске.

Таблица импорта — это структура внутри PE-файла, которая как раз и хранит этот список: какие библиотеки нужны, какие функции из них вызываются и куда записать их адреса.

Где в файле находится таблица импорта

PE-файл состоит из заголовков и секций. Таблица импорта — это один из элементов в опциональном заголовке (Optional Header), а именно в массиве записей о каталогах данных (DataDirectory). Запись с индексом 1 указывает на таблицу импорта, индекс 12 — на таблицу импорта по адресам (IAT).

Если вы открываете файл в PE-анализаторе вроде CFF Explorer или PE-bear, вы увидите примерно такую картину:

  • Optional Header → DataDirectory[1] → RVA и размер таблицы импорта (Import Table).
  • Optional Header → DataDirectory[12] → RVA и размер IAT (Import Address Table).

Сама таблица импорта — это массив структур IMAGE_IMPORT_DESCRIPTOR, по одной на каждую подключаемую DLL. Массив завершается нулевой структурой.

Структура одной записи о библиотеке

Каждая структура IMAGE_IMPORT_DESCRIPTOR занимает 20 байт и содержит следующие поля:

Ключевая идея: OriginalFirstThunk говорит, что нужно импортировать, а FirstThunk — это место, куда загрузчик запишет, где это находится в памяти.

Как загрузчик разрешает импорт по шагам

Процесс загрузки выглядит так:

  1. Загручик находит в DataDirectory RVA таблицы импорта и начинает перебирать структуры IMAGE_IMPORT_DESCRIPTOR.
  2. По полю Name он находит имя DLL и пытается загрузить эту библиотеку в память. Если DLL не найдена — программа не запустится с ошибкой вроде «The program can’t start because X.dll is missing».
  3. Для каждой загруженной DLL загрузчик идёт по цепочке OriginalFirstThunk (INT). Каждая запись в INT — это либо ординальное число (если старший бит установлен), либо RVA на структуру IMAGE_IMPORT_BY_NAME, содержащую имя функции.
  4. Загручик ищет нужную функцию в экспорте DLL и записывает её адрес в соответствующую ячейку по цепочке FirstThunk (IAT).
  5. После этого все вызовы импортируемых функций в коде программы обращаются через IAT, и адреса уже подставлены.

Два способа импорта: по имени и по ординалу

Функции можно импортировать двумя путями, и это видно в записях INT:

  • По имени. В записи INT хранится RVA на структуру IMAGE_IMPORT_BY_NAME, которая содержит 2-байтовый хинт и ASCII-строку с именем функции. Например: CreateFileA. Это самый распространённый способ.
  • По ординалу. Если старший бит записи INT равен 1, то младшие биты — это ординальный номер функции. Такой импорт используется реже, в основном для системных функций, которые не имеют строкового имени (например, некоторые функции ntdll.dll).

При анализе вредоносного ПО важно понимать разницу: импорт по ординалу сложнее отследить по имени, но зато он работает даже если имя функции обфусцировано.

Что видно в таблице импорта при анализе

Когда вы открываете PE-файл в дизассемблере или PE-тулзе, таблица импорта даёт вам максимум информации о том, что делает программа. Вот типичные выводы:

  • Импортируются CreateFile, WriteFile, DeleteFile — программа работает с файлами.
  • Есть RegOpenKeyEx, RegSetValueEx — работа с реестром.
  • Присутствуют socket, connect, send, recv — сетевая активность.
  • Видите CreateRemoteThread, WriteProcessMemory — возможен инжект кода в другой процесс (частый признак вредоносного поведения).
  • Импорт из ws2_32.dll или wininet.dll — сетевые операции на разных уровнях.

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

Частые ошибки при работе с таблицей импорта

Многие путают таблицу импорта (Import Table) и таблицу адресов импорта (IAT). Таблица импорта содержит описание нужных библиотек и функций, а IAT — это массив указателей, куда загрузчик записывает реальные адреса. Это разные структуры, хотя и связаны.

Вот ещё несколько типичных ошибок:

  • Попытка читать IAT как INT. После загрузки IAT перезаписывается адресами, и исходные имена функций там уже не хранятся. Если вам нужны имена — смотрите на OriginalFirstThunk, а не на FirstThunk.
  • Игнорирование того, что адреса в таблице — это RVA. Все смещения в таблице импорта заданы как RVA (Relative Virtual Address), а не физические смещения в файле. Чтобы найти данные на диске, нужно конвертировать RVA в file offset через таблицу секций.
  • Предположение, что все DLL загрузятся. Загручик проходит по таблице последовательно. Если первая DLL не найдена, программа упадёт, даже если остальные библиотеки на месте.
  • Путаница между привязанным и непривязанным импортом. Если PE-файл был «привязан» (bound import), в IAT записаны предвычисленные адреса. При загрузке загрузчик всё равно проверяет временные метки и при несовпадении перезаписывает IAT. Но если временная метка в DLL не совпадает, загрузчик может отказаться от привязки и пойти обычным путём.

Практические инструменты для просмотра импорта

Вот что реально работает:

  • Dependency Walker — классическая утилита, показывает дерево зависимостей. Устаревшая, но всё ещё полезная для быстрого взгляда.
  • CFF Explorer — удобный PE-редактор с наглядным отображением всех таблиц, включая импорт.
  • PE-bear — бесплатный инструмент с графическим интерфейсом, хорош для анализа малвари.
  • objdump (из MinGW/MSYS2) — команда objdump -p file.exe покажет таблицу импорта в консоли.
  • pefile (Python-библиотека) — если нужно автоматизировать анализ, скрипт на Python с pefile извлечёт весь импорт за пару строк.

Сценарии: когда вам действительно нужна таблица импорта

Ситуация 1: программа не запускается из-за отсутствующей DLL. Откройте таблицу импорта и найдите, какая библиотека не загружается. Часто проблема не в самой DLL, а в том, что она требует другую DLL — и цепочка отсутствующих зависимостей может быть длинной.

Ситуация 2: вы исследуете подозрительный файл. Посмотрите на импорт. Если в обычном калькуляторе видите CreateRemoteThread и VirtualAllocEx — это повод насторожиться. Импорт — это «меню» программы, и оно сразу показывает намерения.

Ситуация 3: вы пишете свой загрузчик или инжектор. Тогда вам нужно вручную парсить таблицу импорта, резолвить адреса и заполнять IAT. Без понимания структур IMAGE_IMPORT_DESCRIPTOR, INT и IAT тут не обойтись.

Итог

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

Запомните главное: таблица импорта описывает запрос программы, а IAT — это ответ загрузчика. Не путайте их, и большинство задач с PE-файлами станет заметно проще.

Поле Размер Что означает
OriginalFirstThunk 4 байта RVA на таблицу имён (INT — Import Name Table). Указывает, какие функции нужны.
TimeDateStamp 4 байта Временная метка, обычно 0 до привязки (binding).
ForwarderChain 4 байта Цепочка форвардеров, используется редко.
Name 4 байта RVA на ASCII-строку с именем DLL (например, «KERNEL32.DLL»).
FirstThunk 4 байта RVA на таблицу адресов (IAT). Сюда загрузчик записывает реальные адреса функций.
Оцените статью
PEFile — Безопасность и технологии простым языком