Когда вы запускаете программу на 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 |
4 байта |
RVA на таблицу имён (INT — Import Name Table). Указывает, какие функции нужны. |
| TimeDateStamp |
4 байта |
Временная метка, обычно 0 до привязки (binding). |
| ForwarderChain |
4 байта |
Цепочка форвардеров, используется редко. |
| Name |
4 байта |
RVA на ASCII-строку с именем DLL (например, «KERNEL32.DLL»). |
| FirstThunk |
4 байта |
RVA на таблицу адресов (IAT). Сюда загрузчик записывает реальные адреса функций. |
Ключевая идея: OriginalFirstThunk говорит, что нужно импортировать, а FirstThunk — это место, куда загрузчик запишет, где это находится в памяти.
Как загрузчик разрешает импорт по шагам
Процесс загрузки выглядит так:
- Загручик находит в DataDirectory RVA таблицы импорта и начинает перебирать структуры
IMAGE_IMPORT_DESCRIPTOR.
- По полю
Name он находит имя DLL и пытается загрузить эту библиотеку в память. Если DLL не найдена — программа не запустится с ошибкой вроде «The program can’t start because X.dll is missing».
- Для каждой загруженной DLL загрузчик идёт по цепочке
OriginalFirstThunk (INT). Каждая запись в INT — это либо ординальное число (если старший бит установлен), либо RVA на структуру IMAGE_IMPORT_BY_NAME, содержащую имя функции.
- Загручик ищет нужную функцию в экспорте DLL и записывает её адрес в соответствующую ячейку по цепочке
FirstThunk (IAT).
- После этого все вызовы импортируемых функций в коде программы обращаются через 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-файлами станет заметно проще.