Таблица импорта (Import Directory Table, IDT) — один из ключевых элементов PE-файла, который перечисляет все внешние функции, вызываемые исполняемым образом из загруженных DLL. Для аналитика это первый источник информации о намерениях программы: какие API используются, какие библиотеки подключены, есть ли сетевые вызовы, работа с реестром или криптографией. Сокрытие таблицы импорта — распространённая техника усложнения статического анализа, при которой файл лишается явного перечня импортируемых функций.
Согласно спецификации Microsoft PE/COFF, каждый каталог данных в необязательном заголовке (Optional Header) содержит RVA и размер соответствующей таблицы. Запись
Массив завершается нулевой записью. Для каждой DLL существуют две параллельные таблицы:
Именно строки имён функций и DLL в INT делают таблицу импорта информативной для аналитика.
Когда таблица импорта отсутствует или обнулена, статический анализатор не может:
Это не делает файл неанализируемым, но существенно повышает порог входа: аналитику приходится переходить к динамическому анализу, реверс-инжинирингу кода или поиску косвенных артефактов.
Классический подход — заменить стандартные вызовы
На x64 адрес PEB доступен через
Из PEB через поле
После получения базового адреса DLL программа парсит её заголовок экспорта (
Такой подход полностью исключает необходимость в
Чтобы дополнительно затруднить анализ, имена функций и модулей заменяются предвычисленными хешами. Вместо хранения строки
Это делает невозможным поиск по строкам (
Сокрытие таблицы импорта устраняет один канал информации, но не все. Ниже — артефакты, которые сохраняются.
Даже без таблицы импорта PE-файл содержит заголовки секций. Необычные имена секций, флаги
Хеширование имён устраняет строки API, но не устраняет другие строки: URL, пути реестра, имена мьютексов, форматные строки
Дизассемблирование точки входа (
Эти паттерны хорошо распознаются как сигнатуры в YARA-правилах.
Независимо от того, как программа получила адрес
После загрузки процесса в память загрузчик Windows заполняет IAT для тех импортов, которые всё же присутствуют. Если таблица импорта полностью удалена, IAT пуста — но это само по себе аномалия, которую детектируют инструменты вроде PE-sieve или Moneta.
Спецификация PE описывает отдельную структуру — Delay-Load Import Table (
Эти индикаторы не зависят от способа разрешения адресов функций.
Сокрытие таблицы импорта эффективно против поверхностного статического анализа, но имеет ряд слабых мест:
При получении PE-файла без таблицы импорта:
Структура таблицы импорта в 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]. Три ключевых массива:| Массив | Поле | Содержимое |
|---|---|---|
| Имена функций | AddressOfNames | RVA к ASCII-именам экспортируемых функций |
| Адреса функций | AddressOfFunctions | RVA к коду функций |
| Порядковые номера | 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-файла без таблицы импорта:
- Проверить заголовки секций на аномальные флаги и энтропию.
- Извлечь строки из всех секций (не только
.rdata).
- Дизассемблировать точку входа, искать паттерны PEB-обхода.
- Проверить наличие delay-load import table.
- Запустить в изолированной среде с мониторингом системных вызовов.
- Сравнить хеш-константы в коде с известными хешами API (существуют открытые базы хешей для распространённых функций).
- Проверить файл сканерами, детектирующими аномалии PE-структуры (PE-sieve, Moneta, Detect It Easy).
