Молчи и скрывайся. Прячем IAT от антивируса

Содержание статьи

  • Простейшее скрытие
  • Собственные LoadLibrary() и GetProcAddress()
  • IAT Camouflage
  • Псевдохендлы
  • Выводы

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

 

Собственные LoadLibrary() и 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_HEADERIMAGE_NT_HEADERSIMAGE_OPTIONAL_HEADERIMAGE_DATA_DIRECTORYIMAGE_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.

Пос­ледова­тель­ность вызовов при заг­рузке DLL

Источник: xakep.ru

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *