Как выявлять скрытые функции в DLL-файлах через анализ PE-заголовка

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

Почему функции вообще скрывают в DLL

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

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

В любом случае, стандартный способ — посмотреть таблицу экспорта — показывает далеко не всё. Нужно копать глубже в структуру PE.

Что реально даёт PE-заголовок в этом контексте

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

Ключевые структуры, на которые смотрим:

  • IMAGE_EXPORT_DIRECTORY — основная таблица экспорта. Но даже здесь не всё очевидно.
  • IMAGE_IMPORT_DIRECTORY — таблица импорта. Показывает, откуда DLL подтягивает функции, и иногда это подсказка.
  • IMAGE_RESOURCE_DIRECTORY — секция ресурсов. Там могут быть встроенные строки, диалоги, даже дополнительные исполняемые данные.
  • Section headers — описания секций. По ним видно, где код, где данные, а где что-то подозрительное.
  • TLS callbacks — функции, которые выполняются до точки входа. Идеальное место для скрытого кода.

Шаг 1: Смотрим на экспорт, но правильно

Первое, что делает большинство — открывает Dependency Walker или CFF Explorer и смотрит список экспортируемых функций. Это нормально, но недостаточно.

Вот на что обращаю внимание:

  1. Экспорт по ординалам без имён. Если функция экспортируется только по числовому ординалу и не имеет строкового имени — это классический признак намеренного сокрытия. Так делают и легальные разработчики (например, в системных DLL), и малварь.
  2. Имена-плейсхолдеры. Функции с именами вроде sub_10001A30, unknown, nullsub — кандидаты на более пристальный разбор.
  3. Несоответствие количества функций и размера кода. Если в DIRECTORY_ENTRY_EXPORT указано 50 функций, а в кодовой секции места явно больше — часть кода не экспортируется явно.

Шаг 2: Сверяем таблицу экспорта с RVA-адресами

Это ключевой момент, который часто пропускают. В IMAGE_EXPORT_DIRECTORY есть три массива:

  • AddressOfFunctions — RVA самих функций.
  • AddressOfNames — RVA строк с именами функций.
  • AddressOfNameOrdinals — массив ординалов, связывающий имя с функцией.

Что делаю на практике: сверяю количество записей в AddressOfFunctions с AddressOfNames. Если в AddressOfFunctions записей больше — значит, есть функции без имён. Это прямой сигнал.

Далее смотрю на RVA-адреса в AddressOfFunctions. Если несколько адресов указывают в одну и ту же область памяти или в область, которая явно не является кодом (например, в секцию ресурсов) — это повод разбираться детальнее.

Шаг 3: Разбираюсь с секциями — ищу аномалии

Section headers — это то, что сразу показывает структуру файла на уровне памяти. Вот конкретные признаки, которые настораживают:

  • Секция с характериками IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE (RWX). Это не всегда подозрительно, но в сочетании с другими признаками — классика упаковщиков.
  • Секция с нестандартным именем — например, .ndata, .rsrc в необычном месте, или вообще без имени.
  • Размер секции в памяти значительно больше размера на диске. Это признак того, что код распаковывается в рантайме.
  • Несколько секций с исполняемым кодом. В норме одна (.text или CODE). Если их несколько — возможно, часть кода спрятана.

Шаг 4: Проверяю TLS callbacks

TLS (Thread Local Storage) callbacks — это функции, которые вызываются загрузчиком до вызова DllMain. Это значит, что код в них выполняется раньше, чем что-либо ещё, и его легко пропустить при статическом анализе.

Где искать: в IMAGE_DATA_DIRECTORY с индексом IMAGE_DIRECTORY_ENTRY_TLS. Если запись не пустая — смотрю на TLS directory, а именно на поле AddressOfCallbacks.

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

Шаг 5: Анализирую таблицу импорта как подсказку

Импорт — это не только про то, что DLL сама использует. Это ещё и подсказка о том, что она может делать скрыто.

Смотрю на следующее:

  • Импорт функций из нестандартных DLL. Например, если обычная библиотека подключает WinHttpOpen, CryptEncrypt или CreateRemoteThread — это говорит о сетевой активности, шифровании или инъекции.
  • Импорт по ординалам вместо имён. В таблице импорта есть OriginalFirstThunk и FirstThunk. Если значения — это ординалы, а не RVA на строки с именами — возможна намеренная обфускация.
  • Подозрительные комбинации. VirtualAlloc + WriteProcessMemory + CreateRemoteThread — классическая цепочка для инъекций.

Шаг 6: Ресурсная секция — не только иконки

IMAGE_RESOURCE_DIRECTORY — это кладезь информации. Помимо иконок и диалогов, там могут быть:

  • встроенные исполняемые файлы (RT_RCDATA);
  • зашифрованные или закодированные данные;
  • строки, которые используются скрытыми функциями;
  • конфигурационные блоки.

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

Инструменты, которые реально помогают

Не буду перечислять двадцать утилит — только то, чем сам пользуюсь и что даёт конкретный результат в контексте поиска скрытых функций:

Инструмент Что даёт для нашей задачи Когда использовать
CFF Explorer Полная картина PE-заголовка, все data directories, секции, экспорт/импорт Первичный осмотр любого PE-файла
PE-bear Визуальный анализ структуры, удобно сравнивать секции и видеть аномалии Быстрый визуальный осмотр
IDA Pro / Ghidra Декомпиляция, поиск функций по сигнатурам, анализ графа вызовов Глубокий анализ после первичного осмотра
PEview Быстрый просмотр PE-структур без лишнего Когда нужно за 30 секунд понять структуру
pefile (Python) Скриптовый анализ, автоматизация поиска аномалий Массовая проверка файлов или автоматизация

Типичные ошибки при поиске скрытых функций

Вот реальные промахи, которые регулярно вижу (и сам совершал):

  • Смотрю только на экспорт. Это самое очевидное, но скрытый функционал почти никогда не лежит на поверхности в экспортной таблице.
  • Игнорирую ординальный экспорт. Если функция доступна только по ординалу — это не значит, что она не важна. Напротив, часто так прясут самое интересное.
  • Не проверяю TLS. Просто забывают. А между тем это один из самых надёжных способов спрятать код.
  • Не сверяю RVA с физическими смещениями. Если файл был модифицирован после компиляции, смещения могут не совпадать, и анализ будет некорректным.
  • Доверяю имени секции. Секцию можно назвать как угодно. Важны характеристики (Characteristics), а не имя.

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

Если вы исследуете легальную библиотеку и ищете недокументированный функционал: начните с экспорта по ординалам, затем проверьте импорт на предмет необычных вызовов. Далее — ресурсная секция. Скорее всего, недокументированные функции либо экспортируются без имён, либо спрятаны в TLS.

Если вы анализируете подозрительный файл: сначала проверьте секции на RWX и аномалии размера. Потом TLS callbacks. Затем ресурсы — там может быть встроенный шеллкод. Экспорт смотрите в последнюю очередь, потому что малварь часто вообще не экспортирует ничего полезного.

Если вы делаете массовую проверку файлов: автоматизируйте через pefile или аналогичную библиотеку. Скрипт должен проверять: количество экспортируемых функций без имён, наличие TLS, RWX-секции, аномалии в ресурсах. Это отсеет 90% безобидных файлов и оставит те, что стоит посмотреть вручную.

Практический алгоритм действий

Вот пошаговый порядок, который я использую на практике:

  1. Открываю файл в CFF Explorer или PE-bear — получаю общую картину.
  2. Смотрю Section Headers — ищу RWX-секции, аномалии в размерах, нестандартные имена.
  3. Проверяю IMAGE_EXPORT_DIRECTORY — сравниваю количество функций и имён, ищу ординальный экспорт.
  4. Смотрю TLS callbacks — если есть, перехожу в IDA/Ghidra и анализирую каждую.
  5. Проверяю импорт — ищу подозрительные комбинации функций.
  6. Анализирую ресурсы — извлекаю блоки RT_RCDATA и проверяю их содержимое.
  7. Если нашёл что-то подозрительное — перехожу к дизассемблированию и декомпиляции конкретных функций.

На что обращать внимание — короткий чеклист

  • Экспорт без имён (ординальный экспорт)
  • Несоответствие количества функций и имён в экспорте
  • RWX-секции или несколько исполняемых секций
  • Размер секции в памяти больше размера на диске
  • Наличие TLS callbacks
  • Подозрительные импорты (сеть, шифрование, инъекции)
  • Большие блоки в ресурсной секции
  • Нестандартные имена секций или их отсутствие

Заключение

PE-заголовок — это не формальность, а реальная карта того, что происходит внутри DLL. Скрытые функции редко удаётся спрятать бесследно — они всегда оставляют следы в структуре файла: несоответствия в таблицах, лишние секции, TLS callbacks, подозрительные импорты.

Главное — не ограничиваться просмотром экспорта. Системный подход: секции → экспорт → TLS → импорт → ресурсы → дизассемблирование. Такой порядок покрывает практически все известные способы сокрытия функционала в PE-файлах.

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

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