Таблица импорта PE: что даёт её сокрытие и какие альтернативные признаки остаются для анализа

Таблица импорта (Import Directory Table, IDT) — один из ключевых элементов PE-файла, который перечисляет все внешние функции, вызываемые исполняемым образом из загруженных DLL. Для аналитика это первый источник информации о намерениях программы: какие API используются, какие библиотеки подключены, есть ли сетевые вызовы, работа с реестром или криптографией. Сокрытие таблицы импорта — распространённая техника усложнения статического анализа, при которой файл лишается явного перечня импортируемых функций.

Структура таблицы импорта в PE-файле​


Согласно спецификации Microsoft PE/COFF, каждый каталог данных в необязательном заголовке (Optional Header) содержит RVA и размер соответствующей таблицы. Запись IMAGE_DIRECTORY_ENTRY_IMPORT указывает на массив структур IMAGE_IMPORT_DESCRIPTOR, каждая из которых описывает одну импортируемую DLL.

C:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD Characteristics;
        DWORD OriginalFirstThunk;   // RVA к INT (Import Name Table)
    };
    DWORD TimeDateStamp;
    DWORD ForwarderChain;
    DWORD Name;                     // RVA к имени DLL (ASCII)
    DWORD FirstThunk;               // RVA к IAT (Import Address Table)
} IMAGE_IMPORT_DESCRIPTOR;

Массив завершается нулевой записью. Для каждой DLL существуют две параллельные таблицы:

  • INT (Import Name Table) — содержит RVA к структурам IMAGE_IMPORT_BY_NAME с именами функций. Используется загрузчиком для разрешения импортов.
  • IAT (Import Address Table) — после загрузки содержит реальные адреса функций в памяти. До загрузки дублирует INT.

Именно строки имён функций и DLL в INT делают таблицу импорта информативной для аналитика.

Что даёт сокрытие таблицы импорта​


Когда таблица импорта отсутствует или обнулена, статический анализатор не может:

  • Перечислить вызываемые API по имени.
  • Определить набор используемых DLL без дополнительных эвристик.
  • Построить граф вызовов на основе импортов.
  • Сопоставить файл с известными семействами по характерному набору функций.

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

Механика сокрытия: обход GetProcAddress и GetModuleHandle​


Классический подход — заменить стандартные вызовы GetProcAddress и GetModuleHandle собственными реализациями, которые не оставляют следов в таблице импорта. Для этого программа самостоятельно разрешает адреса функций через структуры ядра.

Доступ к PEB и списку загруженных модулей​


На x64 адрес PEB доступен через GS-сегмент по смещению 0x60, на x86 — через FS по смещению 0x30:

C:
#ifdef _WIN64
    PPEB pPeb = (PEB*)(__readgsqword(0x60));
#elif _WIN32
    PPEB pPeb = (PEB*)(__readfsdword(0x30));
#endif

Из PEB через поле Ldr получается указатель на PEB_LDR_DATA, который содержит двусвязные списки загруженных модулей (InMemoryOrderModuleList, InLoadOrderLinks, InInitializationOrderLinks). Обход списка позволяет найти базовый адрес любой загруженной DLL без вызова GetModuleHandle.

C:
PPEB_LDR_DATA pLdr = (PPEB_LDR_DATA)(pPeb->Ldr);
PLDR_DATA_TABLE_ENTRY pDte =
    (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);

while (pDte) {
    if (pDte->FullDllName.Length != NULL) {
        // Сравнение имени модуля с целевым
    }
    pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte);
}

Разрешение адреса функции через Export Directory​


После получения базового адреса DLL программа парсит её заголовок экспорта (IMAGE_EXPORT_DIRECTORY), находящийся по DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]. Три ключевых массива:

МассивПолеСодержимое
Имена функцийAddressOfNamesRVA к ASCII-именам экспортируемых функций
Адреса функцийAddressOfFunctionsRVA к коду функций
Порядковые номераAddressOfNameOrdinalsИндекс в массиве адресов для каждого имени

C:
PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)
    (pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
PWORD FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++) {
    CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
    PVOID pFunctionAddress = (PVOID)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
    // Сравнение имени или хеша с целевым
}

Такой подход полностью исключает необходимость в GetProcAddress и, соответственно, в записи этой функции в таблице импорта.

Хеширование имён вместо строкового сравнения​


Чтобы дополнительно затруднить анализ, имена функций и модулей заменяются предвычисленными хешами. Вместо хранения строки "VirtualAllocEx" в бинарнике хранится только 32-битное значение, например 0xF10E27CA. При выполнении программа хеширует каждое имя из таблицы экспорта и сравнивает с целевым хешем.

C:
// Пример использования хешей вместо строк
#define USER32DLL_HASH   0x81E3778E
#define MessageBoxA_HASH 0xF10E27CA

HMODULE hUser32 = GetModuleHandleH(USER32DLL_HASH);
FARPROC pMsgBox = GetProcAddressH(hUser32, MessageBoxA_HASH);

Это делает невозможным поиск по строкам (strings) в бинарном файле: аналитик не увидит ни имён DLL, ни имён функций.

Какие альтернативные признаки остаются для анализа​


Сокрытие таблицы импорта устраняет один канал информации, но не все. Ниже — артефакты, которые сохраняются.

Секции и их характеристики​


Даже без таблицы импорта PE-файл содержит заголовки секций. Необычные имена секций, флаги IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_WRITE (одновременно исполняемая и записываемая память), аномальные размеры или энтропия секций — всё это индикаторы, не зависящие от импортов.

Строки и данные в секциях​


Хеширование имён устраняет строки API, но не устраняет другие строки: URL, пути реестра, имена мьютексов, форматные строки printf, сообщения об ошибках. Поиск по строкам остаётся полезным первым шагом.

Точки входа и код секции .text​


Дизассемблирование точки входа (AddressOfEntryPoint) позволяет обнаружить паттерны, характерные для PEB-обхода:

  • Инструкции mov rax, gs:[0x60] или mov eax, fs:[0x30] — доступ к PEB.
  • Обход связного списка с характерными смещениями.
  • Циклы сравнения хешей.

Эти паттерны хорошо распознаются как сигнатуры в YARA-правилах.

Динамический анализ: перехват системных вызовов​


Независимо от того, как программа получила адрес VirtualAllocEx, сам вызов проходит через syscall/int 0x2e (x86) или syscall (x64). Мониторинг системных вызовов через ETW, гипервизор или API-монитор показывает фактическое поведение: выделение памяти, создание потоков, сетевые соединения.

IAT в рантайме​


После загрузки процесса в память загрузчик Windows заполняет IAT для тех импортов, которые всё же присутствуют. Если таблица импорта полностью удалена, IAT пуста — но это само по себе аномалия, которую детектируют инструменты вроде PE-sieve или Moneta.

Delay-load imports​


Спецификация PE описывает отдельную структуру — Delay-Load Import Table (IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT). Некоторые образцы скрывают основную таблицу импорта, но оставляют delay-load записи, которые раскрывают часть используемых API.

Поведенческие индикаторы​


ИндикаторКак обнаруживается
Выделение исполняемой памятиМониторинг VirtualAlloc/VirtualAllocEx с флагом PAGE_EXECUTE_READWRITE
Создание удалённого потокаCreateRemoteThread, NtCreateThreadEx
Инъекция в процессЗапись в чужое адресное пространство через WriteProcessMemory
Сетевая активностьСоединения на нестандартные порты, DNS-запросы к подозрительным доменам
Работа с реестромЗаписи в Run, RunOnce, Services

Эти индикаторы не зависят от способа разрешения адресов функций.

Ограничения техники сокрытия​


Сокрытие таблицы импорта эффективно против поверхностного статического анализа, но имеет ряд слабых мест:

  • Увеличение размера кода. Собственные реализации GetProcAddress и GetModuleHandle добавляют сотни байт кода, которые сами по себе являются сигнатурой.
  • Хрупкость. Прямой доступ к PEB и структурам LDR_DATA_TABLE_ENTRY зависит от версии Windows и может ломаться при обновлениях.
  • Обнаружение по паттерну. Инструкции доступа к GS:[0x60] / FS:[0x30] и последующий обход списка — один из наиболее известных индикаторов shellcode и упаковщиков.
  • Не защищает от динамического анализа. Поведение процесса наблюдаемо независимо от способа получения адресов.

Практический чек-лист для аналитика​


При получении PE-файла без таблицы импорта:

  1. Проверить заголовки секций на аномальные флаги и энтропию.
  2. Извлечь строки из всех секций (не только .rdata).
  3. Дизассемблировать точку входа, искать паттерны PEB-обхода.
  4. Проверить наличие delay-load import table.
  5. Запустить в изолированной среде с мониторингом системных вызовов.
  6. Сравнить хеш-константы в коде с известными хешами API (существуют открытые базы хешей для распространённых функций).
  7. Проверить файл сканерами, детектирующими аномалии PE-структуры (PE-sieve, Moneta, Detect It Easy).

Источники​


 
Назад
Верх Низ