Как узнать, использует ли программа импорт по порядковому номеру (Ordinal)

Когда вы открываете чужую DLL в дизассемблере или смотрите на импортируемые функции, вы быстро замечаете две разные картины: где-то функции импортируются по именам (CreateFile, MessageBox), а где-то — по числам (Ordinal 1, Ordinal 42). Второй способ выглядит загадочно, но проверить его реально — и это не требует глубоких знаний внутреннего устройства Windows. Разберёмся, как именно это сделать, и зачем вообще это нужно.

Зачем это вообще проверять

Импорт по ординалу — это когда программа подключает функцию из DLL не по имени, а по номеру. Раньше такой подход использовали для ускорения загрузки: не нужно сравнивать строки-имена, достаточно подставить число в таблицу. Сейчас это встречается реже, но именно по ординалу часто импортируются системные функции из kernel32.dll, ntdll.dll и user32.dll. Если вы занимаетесь реверс-инжинирингом, анализом малвари или просто пытаетесь понять, почему программа не запускается — умение быстро определить тип импорта экономит кучу времени.

Что такое Ordinal в контексте PE-файла

PE-файл (исполняемый формат Windows) хранит информацию об импортах в структуре IMAGE_IMPORT_DESCRIPTOR. Для каждой импортируемой функции есть два варианта хранения:

  • По имени — в таблице указан hint (двухбайтовый индекс в таблице имён библиотеки) и само имя функции в виде ASCII-строки.
  • По ординалу — старший бит поля имени установлен в 1, а младшие 15 бит (или 6 бит для PE32+) содержат непосредственно номер ординала.

Ключевой момент: если у функции старший бит равен 1, значит импорт идёт по числу, а не по строке. Это правило одинаково для 32-битных и 64-битных файлов, хотя конкретные смещения в заголовках отличаются.

Способ 1: Посмотреть в PE-парсере или дизассемблере

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

  1. Откройте файл в CFF Explorer, PEview или PE-bear (последний бесплатный и удобный).
  2. Перейдите в секцию Import Directory (или Import Table).
  3. Найдите нужную DLL и посмотрите список импортируемых функций.
  4. Если вместо имени функции вы видите число в скобках — например, [17], [0x80000011] — это импорт по ординалу.

В IDA Pro или Ghidra это видно ещё проще: в окне импорта функции с ординальным номером обычно имеют имя вида Ordinal_17 или вообще не имеют осмысленного имени. Если вы видите много таких записей — программа активно использует ординальный импорт.

Способ 2: Проверить вручную по дампу

Если под рукой нет специализированного инструмента, можно посмотреть сырые данные. Для этого нужно найти в PE-файле таблицу импортов и проверить значения полей OriginalFirstThunk и FirstThunk.

  1. Найдите в Optional Header поле DataDirectory[1] (Import Table RVA и Size).
  2. Перейдите по этому RVA в файле — вы окажетесь в массиве структур IMAGE_IMPORT_DESCRIPTOR.
  3. Для каждой структуры посмотрите на поле OriginalFirstThunk (или FirstThunk, если биндинг не применён).
  4. Пройдите по массиву thunk-значений. Каждое значение — это либо RVA на строку с именем функции, либо ординальный номер.
  5. Проверьте старший бит: если он установлен (значение ≥ 0x80000000 для PE32, ≥ 0x8000000000000000 для PE32+), то младшие биты содержат ординальный номер.

Пример: если вы видите значение 0x8000002A — отбрасываем старший бит, получаем 0x2A = 42. Это ординальный импорт функции номер 42 из соответствующей DLL.

Способ 3: Использовать dumpbin или objdump

Если вы работаете из командной строки, стандартные утилиты тоже справляются:

Для dumpbin (идёт с Visual Studio):

dumpbin /imports myprogram.exe

В выводе ищите строки вида:

Ordinal 17
Ordinal 42

Если вместо имён функций стоят только числа — весь импорт идёт по ординалу для этой DLL.

Для objdump (MinGW, Cygwin):

objdump -p myprogram.exe | grep -A 20 "Import Table"

Аналогично: смотрите, что указано в колонке с именами функций.

Способ 4: Проверить во время выполнения

Иногда нужно понять, использует ли запущенная программа ординальный импорт прямо в рантайме. Для этого подходят отладчики и инструменты перехвата:

  • API Monitor — показывает все вызовы API с указанием, был ли вызов сделан по имени или по ординалу.
  • x64dbg / x32dbg — поставьте брейкпоинт на функцию и посмотрите, как она вызывается. Если в таблице импортов адрес соответствует ординальному значению — вы увидите это в дампе памяти.
  • Detours или аналогичные хуки — при перехвате вызова можно проверить, был ли он оригинально привязан по ординалу.

Как отличить реальный ординальный импорт от просто отсутствующего имени

Бывает, что реверсеры путают два разных явления:

  • Импорт по ординалу — старший бит thunk-значения установлен, и это штатный механизм PE-формата.
  • Безымянный импорт — имя функции отсутствует в дампе из-за повреждения файла или намеренного стирания информации, но старший бит не установлен.

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

Типичные ситуации, когда встречается ординальный импорт

Ситуация Почему используется ordinal Что делать
Системные DLL (kernel32, ntdll, user32) Microsoft гарантирует стабильность ординалов для критических API Смотрите документацию по ординалам или используйте утилиты вроде ordinals.exe
Малварь и обфусцированный код Скрытие реальных вызовов API от статического анализа Динамический анализ в песочнице + отладчик
Старые программы (эпоха Win9x) Тогда ординальный импорт был стандартом для скорости Современные инструменты корректно показывают ординалы
.NET сборки с P/Invoke через ordinals Редко, но бывает при явном указании EntryPoint как числа Проверьте атрибут DllImport в метаданных сборки

Частые ошибки при определении ординального импорта

Ошибка 1: Путать отсутствие имени с ординальным импортом. Если в дампе пустая строка вместо имени — это не значит, что импорт по ординалу. Проверяйте старший бит thunk-значения.

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

Ошибка 3: Игнорировать разницу между PE32 и PE32+. В 64-битных файлах thunk-значения 8-байтовые, и старший бит проверяется иначе (0x8000000000000000, а не 0x80000000).

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

Что делать дальше в зависимости от вашей задачи

Если вы анализируете малварь: ординальный импорт — это попытка скрыть используемые API. Загрузите DLL в отладчик, посмотрите таблицу экспорта по ординалам (например, через dumpbin /exports ntdll.dll), и сопоставьте номера с реальными функциями. Это даст вам список вызовов без необходимости реверсить каждый вызов вручную.

Если вы отлаживаете чужую программу: ординальный импорт может быть причиной проблем, если DLL обновилась и ординалы изменились. Проверьте версию DLL, которую загружает программа, и сравните с ординалами в её экспортной таблице.

Если вы пишете свой код: избегайте ординального импорта, если не уверены в стабильности номеров в будущих версиях DLL. Используйте имена — это надёжнее и читаемее.

Итог

Определить, использует ли программа импорт по ординалу, можно четырьмя способами: через PE-парсер (CFF Explorer, PE-bear), вручную по дампу (проверка старшего бита thunk-значений), через командную строку (dumpbin, objdump) или во время выполнения (API Monitor, отладчик). Главное правило — смотрите на старший бит в thunk-таблице: если он установлен, перед вами ординальный импорт. Не путайте это с отсутствием имени из-за повреждения файла — это разные вещи, и проверка старшего бита сразу даёт ответ.

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