Содержание статьи
Все написанные под Windows программы имеют IAT (Import Address Table) — это таблица, которая содержит функции, импортируемые программой из DLL-библиотек. Зачастую нам, как атакующим, следует скрывать импорты, чтобы антивирусным приложениям было сложнее идентифицировать используемые в программе функции. В этой статье разобраны некоторые способы скрытия таблицы импортов.
Простейшее скрытие
Итак, пусть у нас есть простенькая программа, которая, например, выводит MessageBox.
#include <Windows.h>int main() { MessageBox(NULL, L"HI", L"HI", MB_OK); return 0;}
Если мы посмотрим на то, что лежит у нее в IAT, то будем неприятно удивлены. Здесь и CRT, и какие‑то левые функции, которые мы даже не вызываем.
dumpbin /imports .\Article.exe
Предлагаю сразу же избавиться от CRT. Common Language Runtime — набор функций и макросов для программ на С. Функции обычно связаны с управлением памятью (memcpy(
), открытием и закрытием файлов (fopen(
) и работой со строками (strcpy(
).
Библиотеки DLL, которые реализуют CRT, называются vcruntimeXXX.
, где XXX
— номер версии используемой библиотеки CRT. Это правило применяется не ко всем библиотекам CRT, встречаются также DLL c именами api-ms-win-crt-stdio-l1-1-0.
, api-ms-win-crt-runtime-l1-1-0.
и api-ms-win-crt-locale-l1-1-0.
. Каждая DLL служит для определенной цели и экспортирует несколько функций. Эти библиотеки DLL компонуются компилятором во время компиляции и поэтому находятся в IAT сгенерированных программ.
Чтобы заставить компилятор сделать статическую линковку (в этом случае библиотека не импортируется, а уже как бы зашита в программу), следует изменить свойства проекта. Сначала открываем раздел в «Проект → Свойства».
Оттуда переходим в «C/C++ → Создание кода → Библиотека времени выполнения → Многопоточная (/MT) → Применить → ОК».
Затем перекомпилируем проект и проверяем IAT.
Отлично, от CRT в IAT мы избавились. Теперь пришло время поговорить о скрытии импорта функции MessageBoxW(
. Сама функция представлена в библиотеке user32.
, поэтому мы можем использовать API GetProcAddress(
для получения адреса этой функции и ее вызова.
FARPROC GetProcAddress( [in] HMODULE hModule, [in] LPCSTR lpProcName);
Здесь hModule
— хендл на DLL, в которой реализована функция, а lpProcName
— имя этой функции. Для успешного вызова MessageBoxW(
нужно лишь создать прототип функции в нашем коде.
#include <Windows.h>typedef int (WINAPI* MessageBoxWFunc)( HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType );int main() { HMODULE user32Module = LoadLibrary(L"user32.dll"); MessageBoxWFunc MessageBoxWPtr = (MessageBoxWFunc)(GetProcAddress(user32Module, "MessageBoxW")); MessageBoxWPtr(NULL, L"HI", L"HI", MB_OK); return 0;}
Компилируем, проверяем IAT и видим, что функция пропала.
Казалось бы, неплохо? Всем спасибо, конец статьи.
Но не тут‑то было. Давай настроим компилятор так, чтобы он импортировал не все функции из kernel32.
, а лишь нужные, то есть те, которые в явном виде присутствуют в коде. Для этого сначала отключаем SDL.
Затем отключаем оптимизацию программы.
Отключаем исключения C++, делаем статическую линковку, отключаем проверку безопасности.
Включаем игнорирование стандартных библиотек.
Отключаем файл манифеста, убираем создание дебаг‑информации.
Затем устанавливаем точку входа.
Перекомпилируем проект, проверяем IAT и видим, что любой желающий сможет определить использование LoadLibrary(
и GetProcAddress(
в нашем коде.
От этих функций никак не избавиться (через GetProcAddress(
тем более не вызвать — рекурсия), поэтому придется придумать какую‑то альтернативу.
Собственные LoadLibrary() и GetProcAddress()
Начнем с написания собственной GetProcAddress(
. У каждой DLL-библиотеки есть раздел EAT (Export Address Table), в котором содержатся экспортируемые из этой библиотеки функции. Буквально — методы, которые могут быть вызваны при включении этой DLL в программу.
Просмотреть экспорты позволяет тот же dumpbin
, но с флагом /
.
dumpbin /exports C:\Windows\System32\user32.dll
Нам нужно лишь как‑то получить базовый адрес загрузки библиотеки, а затем от него можно добраться до EAT. Пока для простоты эксперимента базовый адрес загрузки предлагаю получать через LoadLibrary(
. Эта функция позволяет загрузить библиотеку в текущий процесс, а затем получить на нее хендл.
HMODULE LoadLibraryA( [in] LPCSTR lpLibFileName);
Этот хендл является базовым адресом загрузки DLL в память процесса. После получения базового адреса следует начать парсить EAT, до него можно добраться по пути IMAGE_DOS_HEADER
→ IMAGE_NT_HEADERS
→ IMAGE_OPTIONAL_HEADER
→ IMAGE_DATA_DIRECTORY
→ IMAGE_EXPORT_DIRECTORY
. Подробнее о парсинге PE можно прочитать в различных публикациях, вот несколько ссылок.
www
- Parsing PE File Headers with C++ (ired.team)
- PE (Portable Executable): На странных берегах («Хабрахабр»)
- PE-формат. Часть 1 — Базовая информация (kaimi.io)
Теперь нырнем в пучины интернета и поищем кастомную реализацию GetProcAddress(
. Я выбрал самый аккуратный и милый вариант.
FARPROC myGetProcAddress(HMODULE hModule, LPCSTR lpProcName) { PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hModule; PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hModule + dosHeader->e_lfanew); PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hModule + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); DWORD* addressOfFunctions = (DWORD*)((BYTE*)hModule + exportDirectory->AddressOfFunctions); WORD* addressOfNameOrdinals = (WORD*)((BYTE*)hModule + exportDirectory->AddressOfNameOrdinals); DWORD* addressOfNames = (DWORD*)((BYTE*)hModule + exportDirectory->AddressOfNames); for (DWORD i = 0; i < exportDirectory->NumberOfNames; ++i) { if (strcmp(lpProcName, (const char*)hModule + addressOfNames[i]) == 0) { return (FARPROC)((BYTE*)hModule + addressOfFunctions[addressOfNameOrdinals[i]]); } } return NULL;}
Функция достаточно проста: принимает базовый адрес загрузки библиотеки, а также имя функции, адрес которой надо получить. Работает так, как нам и нужно, — путем парсинга EAT.
Помнишь, мы отключили CRT? Поэтому использование функции strcmp(
из этого кода невозможно. К счастью, сравнение двух строк — базовый алгоритм, который пишут еще на паскале в седьмом классе. На C++ я вынес его в отдельную функцию custom_strcmp(
.
int custom_strcmp(const char* str1, const char* str2) { while (*str1 || *str2) { if (*str1 < *str2) { return -1; } else if (*str1 > *str2) { return 1; } str1++; str2++; } return 0;}
Разобравшись, как и что делать, получаем следующий код.
#include <Windows.h>#include <winternl.h>typedef int (WINAPI* MessageBoxWFunc)( HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType );int custom_strcmp(const char* str1, const char* str2) { while (*str1 || *str2) { if (*str1 < *str2) { return -1; } else if (*str1 > *str2) { return 1; } str1++; str2++; } return 0;}FARPROC myGetProcAddress(HMODULE hModule, LPCSTR lpProcName) { PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hModule; PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hModule + dosHeader->e_lfanew); PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hModule + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); DWORD* addressOfFunctions = (DWORD*)((BYTE*)hModule + exportDirectory->AddressOfFunctions); WORD* addressOfNameOrdinals = (WORD*)((BYTE*)hModule + exportDirectory->AddressOfNameOrdinals); DWORD* addressOfNames = (DWORD*)((BYTE*)hModule + exportDirectory->AddressOfNames); for (DWORD i = 0; i < exportDirectory->NumberOfNames; ++i) { if (custom_strcmp(lpProcName, (const char*)hModule + addressOfNames[i]) == 0) { return (FARPROC)((BYTE*)hModule + addressOfFunctions[addressOfNameOrdinals[i]]); } } return NULL;}int main() { HMODULE user32Module = LoadLibrary(L"user32.dll"); MessageBoxWFunc MessageBoxWPtr = (MessageBoxWFunc)(myGetProcAddress(user32Module, "MessageBoxW")); MessageBoxWPtr(NULL, L"HI", L"HI", MB_OK); return 0;}
Компилируем, запускаем. Видим, что остался лишь один импорт.
Чем же заменить LoadLibrary(
? Если мы глянем на последовательность вызовов функций, то увидим, что LoadLibrary(
вызывает LdrLoadDll(
, она — LdrpLoadDll(
, а та — еще одну функцию... Процесс, скажу честно, достаточно сложный. Вот картинка, которая хорошо описывает последовательность вызовов при загрузке DLL.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»