Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Зоркий глаз антивируса так и норовит обнаружить наши пейлоады! Давай покажу, как скрыть все импорты, чтобы спрятаться от назойливого внимания антивирусных программ. Мы попробуем мимикрировать под легитимное приложение с помощью IAT Camouflage.
Все написанные под Windows программы имеют IAT (Import Address Table) — это таблица, которая содержит функции, импортируемые программой из DLL-библиотек. Зачастую нам, как атакующим, следует скрывать импорты, чтобы антивирусным приложениям было сложнее идентифицировать используемые в программе функции. В этой статье разобраны некоторые способы скрытия таблицы импортов.
Итак, пусть у нас есть простенькая программа, которая, например, выводит MessageBox.
#include <Windows.h>int main() { MessageBox(NULL, L"HI", L"HI", MB_OK); return 0;}
Программа с MessageBox
Если мы посмотрим на то, что лежит у нее в IAT, то будем неприятно удивлены. Здесь и CRT, и какие‑то левые функции, которые мы даже не вызываем.
dumpbin /imports .Article.exe
Огромный IAT
Предлагаю сразу же избавиться от CRT. Common Language Runtime — набор функций и макросов для программ на языке С. Функции обычно связаны с управлением памятью (memcpy()
), открытием и закрытием файлов (fopen()
) и работой со строками (strcpy()
).
Библиотеки DLL, которые реализуют CRT, называются vcruntimeXXX.dll
, где XXX
— номер версии используемой библиотеки CRT. Это правило применяется не ко всем библиотекам CRT, встречаются также DLL c именами api-ms-win-crt-stdio-l1-1-0.dll
, api-ms-win-crt-runtime-l1-1-0.dll
и api-ms-win-crt-locale-l1-1-0.dll
. Каждая DLL служит для определенной цели и экспортирует несколько функций. Эти библиотеки DLL компонуются компилятором во время компиляции и поэтому находятся в IAT сгенерированных программ.
Чтобы заставить компилятор сделать статическую линковку (в этом случае библиотека не импортируется, а уже как бы зашита в программу), следует изменить свойства проекта. Сначала открываем раздел в «Проект → Свойства».
Оттуда переходим в «C/C++ → Создание кода → Библиотека времени выполнения → Многопоточная (/MT) → Применить → ОК».
Настройки проекта
Затем перекомпилируем проект и проверяем IAT.
IAT без CRT
Отлично, от CRT в IAT мы избавились. Теперь пришло время поговорить о скрытии импорта функции MessageBoxW()
. Сама функция представлена в библиотеке user32.dll
, поэтому мы можем использовать 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 и видим, что функция пропала.
Пропавшая MessageBox
Казалось бы, неплохо? Всем спасибо, конец статьи.
Но не тут‑то было. Давай настроим компилятор так, чтобы он импортировал не все функции из kernel32.dll
, а лишь нужные, то есть те, которые в явном виде присутствуют в коде. Для этого сначала отключаем SDL.
Затем отключаем оптимизацию программы.
Отключаем исключения C++, делаем статическую линковку, отключаем проверку безопасности.
Включаем игнорирование стандартных библиотек.
Отключаем файл манифеста, убираем создание дебаг‑информации.
Затем устанавливаем точку входа.
Перекомпилируем проект, проверяем IAT и видим, что любой желающий сможет определить использование LoadLibrary()
и GetProcAddress()
в нашем коде.
От этих функций никак не избавиться (через GetProcAddress()
тем более не вызвать — рекурсия), поэтому придется придумать какую‑то альтернативу.
Начнем с написания собственного GetProcAddress()
. У каждой DLL-библиотеки есть раздел EAT (Export Address Table), в котором содержатся экспортируемые из этой библиотеки функции. Буквально — методы, которые могут быть вызваны при включении этой DLL в программу.
Просмотреть экспорты позволяет тот же dumpbin
, но с флагом /exports
.
dumpbin /exports C:WindowsSystem32user32.dll
Экспорты 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
Теперь нырнем в пучины интернета и поищем кастомную реализацию 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.
Последовательность вызовов при загрузке DLL
Источник: xakep.ru