Зор­кий глаз анти­виру­са так и норовит обна­ружить наши пей­лоады! Давай покажу, как скрыть все импорты, что­бы спря­тать­ся от назой­ливого вни­мания анти­вирус­ных прог­рамм. Мы поп­робу­ем мимик­рировать под легитим­ное при­ложе­ние с помощью 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
Прог­рамма с MessageBox

Ес­ли мы пос­мотрим на то, что лежит у нее в IAT, то будем неп­рият­но удив­лены. Здесь и CRT, и какие‑то левые фун­кции, которые мы даже не вызыва­ем.

dumpbin /imports .\Article.exe
Огромный IAT
Ог­ромный 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
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
Про­пав­шая MessageBox

Ка­залось бы, неп­лохо? Всем спа­сибо, конец статьи.

Но не тут‑то было. Давай нас­тро­им ком­пилятор так, что­бы он импорти­ровал не все фун­кции из kernel32.dll, а лишь нуж­ные, то есть те, которые в явном виде при­сутс­тву­ют в коде. Для это­го сна­чала отклю­чаем SDL.

За­тем отклю­чаем опти­миза­цию прог­раммы.

От­клю­чаем исклю­чения C++, дела­ем ста­тичес­кую лин­ковку, отклю­чаем про­вер­ку безопас­ности.

Вклю­чаем игно­риро­вание стан­дар­тных биб­лиотек.

От­клю­чаем файл манифес­та, уби­раем соз­дание дебаг‑информа­ции.

За­тем уста­нав­лива­ем точ­ку вхо­да.

Пе­реком­пилиру­ем про­ект, про­веря­ем IAT и видим, что любой жела­ющий смо­жет опре­делить исполь­зование LoadLibrary() и GetProcAddress() в нашем коде.

От этих фун­кций никак не изба­вить­ся (через GetProcAddress() тем более не выз­вать — рекур­сия), поэто­му при­дет­ся при­думать какую‑то аль­тер­нативу.

 

Собственные LoadLibrary() и GetProcAddress()

Нач­нем с написа­ния собс­твен­ной GetProcAddress(). У каж­дой DLL-биб­лиоте­ки есть раз­дел EAT (Export Address Table), в котором содер­жатся экспор­тиру­емые из этой биб­лиоте­ки фун­кции. Бук­валь­но — методы, которые могут быть выз­ваны при вклю­чении этой DLL в прог­рамму.

Прос­мотреть экспор­ты поз­воля­ет тот же dumpbin, но с фла­гом /exports.

dumpbin /exports C:\Windows\System32\user32.dll
Экспорты user32.dll
Эк­спор­ты user32.dll

Нам нуж­но лишь как‑то получить базовый адрес заг­рузки биб­лиоте­ки, а затем от него мож­но доб­рать­ся до EAT. Пока для прос­тоты экспе­римен­та базовый адрес заг­рузки пред­лагаю получать через LoadLibrary(). Эта фун­кция поз­воля­ет заг­рузить биб­лиоте­ку в текущий про­цесс, а затем получить на нее хендл.

HMODULE LoadLibraryA(
[in] LPCSTR lpLibFileName
);

Этот хендл явля­ется базовым адре­сом заг­рузки DLL в память про­цес­са. Пос­ле получе­ния базово­го адре­са сле­дует начать пар­сить EAT, до него мож­но доб­рать­ся по пути IMAGE_DOS_HEADERIMAGE_NT_HEADERSIMAGE_OPTIONAL_HEADERIMAGE_DATA_DIRECTORYIMAGE_EXPORT_DIRECTORY. Под­робнее о пар­синге PE мож­но про­читать в раз­личных пуб­ликаци­ях, вот нес­коль­ко ссы­лок.

Те­перь ныр­нем в пучины интерне­та и поищем кас­томную реали­зацию 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
Пос­ледова­тель­ность вызовов при заг­рузке DLL

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    1 Комментарий
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии