Разбираемся с Import Table в PE-файлах: как программа находит свои функции и зачем это знать реверс-инженеру

Когда вы открываете EXE-файл в отладчике или дизассемблере, первой вещью, на которую стоит смотреть, — это не массив инструкций `MOV` или `PUSH`, а таблица импорта (Import Table). Если вы занимаетесь реверс-инжинирингом, анализом вредоносного ПО или даже просто пытаетесь понять, как работает закрытый бинарник, знание того, как устроена эта таблица, сэкономит вам часы работы. Это своего рода «список покупок» программы: здесь написано, какие внешние библиотеки ей нужны и какие конкретно инструменты из этих библиотек она собирается использовать.

Без таблицы импорта современная Windows-программа — это просто набор бесполезного кода. Она не сможет вывести сообщение на экран, не сможет открыть файл или отправить пакет по сети, потому что вся эта логика лежит в системных DLL. Таблица импорта — это мост между вашим кодом и системными возможностями ОС.

Как это работает «под капотом»

Чтобы понять устройство таблицы, нужно перестать думать о ней как о едином списке. На самом деле это иерархическая структура. Когда Windows загружает ваш файл в память, загрузчик (loader) смотрит на структуру PE (Portable Executable), находит там раздел данных, где лежит таблица импорта, и начинает выполнять «магию» под названием import resolution.

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

  1. Загрузчик читает список нужных DLL (например, kernel32.dll, user32.dll).
  2. Он ищет эти библиотеки в системе и загружает их в адресное пространство процесса.
  3. Затем он смотрит на список функций, которые программа хочет вызвать из каждой библиотеки.
  4. Самое важное: загрузчик находит реальные адреса этих функций в памяти и записывает их в специальное место внутри вашего файла — в таблицу адресов (IAT).

Если на этом этапе что-то идет не так (библиотека отсутствует или функция была удалена из новой версии ОС), программа просто не запустится, выдав ошибку о ненайденном модуле.

Анатомия таблицы импорта: из чего она состоит

В PE-файле за импорт отвечают две ключевые структуры, которые часто путают. Чтобы не запутаться, запомните: одна структура говорит, что искать, а вторая — куда записывать результат.

Давайте разберем их подробно.

1. Import Directory Table (Таблица директорий импорта)

Это массив структур IMAGE_IMPORT_DESCRIPTOR. Каждый элемент этого массива соответствует одной конкретной DLL. Если программе нужны ntdll.dll, kernel32.dll и advapi32.dll, то в массиве будет три записи. Каждая запись содержит:

  • Name: Указатель на строку с именем библиотеки.
  • OriginalFirstThunk: Указатель на список имен или порядковых номеров функций (то, что было в файле изначально).
  • FirstThunk: Указатель на IAT (Import Address Table) — то место, куда загрузчик запишет реальные адреса.
  • Size: Размер таблицы для этой конкретной библиотеки.

2. Import Address Table (IAT) — Таблица адресов импорта

Это то, что реально используется во время работы программы. Когда ассемблерная инструкция вызывает функцию, она не обращается к имени CreateFileW напрямую. Она обращается к ячейке в IAT. В момент запуска загрузчик заменяет «заглушку» в этой ячейке на настоящий адрес функции в памяти. Это позволяет коду быть независимым от того, по какому именно адресу функция загрузится в текущем сеансе работы системы.

В зависимости от того, как была скомпилирована программа, в OriginalFirstThunk могут лежать либо имена функций, либо их порядковые номера (ordinals). Использование ординалов работает быстрее, так как системе не нужно сравнивать строки, но это делает бинарник менее гибким и более зависимым от конкретных версий библиотек.

Сравнение способов импорта: Имена vs Ординалы

Часто новички задаются вопросом: почему в одном файле я вижу MessageBoxA, а в другом — просто число 44? Это вопрос использования имен или порядковых номеров.

Характеристика Импорт по имени (Name) Импорт по ординалу (Ordinal)
Читаемость Высокая. Сразу понятно, что делает программа. Низкая. Нужно знать таблицу соответствий для конкретной DLL.
Скорость загрузки Чуть медленнее (нужен поиск по строкам). Максимально быстро (прямой доступ по индексу).
Стабильность Выше. Имя функции редко меняется. Ниже. При обновлении DLL порядок функций может измениться (редко, но бывает).
Скрытность Легко анализируется. Часто используется для обфускации кода.

Что таблица импорта говорит об анализируемом файле

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

Примеры интерпретации:

  • Видите ws2_32.dll и функции connect, send, recv? Программа работает с сетью.
  • Видите advapi32.dll и RegSetValueEx? Программа лезет в реестр Windows (часто для закрепления в системе).
  • Видите user32.dll и SetWindowsHookEx? Это может быть кейлоггер или инструмент для перехвата ввода.
  • Видите kernel32.dll и VirtualAlloc, WriteProcessMemory? Скорее всего, перед вами загрузчик (loader) или вредонос, занимающийся инъекцией кода.

Важный нюанс: Если таблица импорта подозрительно пустая (всего 1-2 функции), это не значит, что программа ничего не делает. Это первый признак того, что автор применил динамический импорт. Программа использует функцию GetProcAddress и LoadLibrary, чтобы подгружать нужные модули прямо во время выполнения, скрывая свои намерения от статического анализатора.

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

Если вы пытаетесь модифицировать PE-файл или писать свой загрузчик, будьте готовы к следующим граблям:

  • Нарушение выравнивания (Alignment): PE-файл очень чувствителен к выравниванию данных. Если вы добавили новую запись в таблицу импорта, но не скорректировали смещения и размеры разделов, файл станет невалидным и не загрузится.
  • Забытая терминальная запись: Массив IMAGE_IMPORT_DESCRIPTOR должен заканчиваться нулевой структурой. Если вы добавили новую библиотеку в список, но не поставили «заглушку» в конце, загрузчик пойдет читать память дальше, пока не упадет (Access Violation).
  • Путаница между Virtual Address и Raw Address: Помните, что в заголовках PE часто указаны виртуальные адреса (как данные будут лежать в памяти), а при работе с файлом на диске вам нужны физические смещения (Raw offsets). Ошибка в этом моменте — классика.
  • Игнорирование архитектуры: Попытка загрузить 32-битную DLL в 64-битный процесс (или наоборот) через импорт приведет к краху. Таблицы импорта жестко привязаны к разрядности.

Практические рекомендации: как действовать в разных ситуациях

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

Ситуация А: Вы проводите статический анализ (подозрительный файл)

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

Ситуация Б: Вы занимаетесь патчингом (изменение поведения программы)

Если вам нужно заблокировать какую-то функцию (например, проверку лицензии), проще всего не удалять её из таблицы импорта (это сложно из-за смещений), а перенаправить адрес в IAT на вашу собственную функцию-пустышку (stub), которая просто возвращает return 1 или return 0.

Ситуация В: Вы разрабатываете свой загрузчик или инъектор

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

Итог: краткий чек-лист

Чтобы эффективно работать с импортами, держите в голове эту последовательность:

  1. Проверьте состав библиотек: Какие модули использует файл? (Сетевые, системные, графические).
  2. Оцените объем импорта: Много функций — стандартный код. Мало функций — возможно, используется обфускация или динамический импорт.
  3. Посмотрите на способ вызова: Имена или ординалы? Это подскажет об уровне подготовки автора.
  4. Проверьте IAT: Если вы в отладчике, посмотрите, какие адреса уже подставил загрузчик. Это даст вам реальную картину того, куда программа обращается на самом деле.

Понимание таблицы импорта — это переход от уровня «я просто запускаю программы» к уровню «я понимаю, как эти программы взаимодействуют с операционной системой».

Оцените статью
PEFile — Безопасность и технологии простым языком