Как обфусцировать вызовы WinAPI в своем приложении
Образцы серьезной малвари и вымогателей часто содержат интересные методики заражения, скрытия активности и нестандартные отладочные приемы. В вирусах типа Potato или вымогателях вроде SynAsk используется простая, но мощная техника скрытия вызовов WinAPI. Об этом мы и поговорим, а заодно напишем рабочий пример скрытия WinAPI в приложении.
Итак, есть несколько способов скрытия вызовов WinAPI.
- Виртуализация. Важный код скрывается внутри самодельной виртуальной машины.
- Прыжок в тело функции WinAPI после ее пролога. Для этого нужен дизассемблер длин инструкций.
- Вызов функций по их хеш-значениям.
Все остальные техники — это разные вариации или развитие трех этих атак. Первые две встречаются нечасто — слишком громоздкие. Как минимум приходится всюду таскать с собой дизассемблер длин и прологи функций, рассчитанные на две разные архитектуры. Вызов функций по хеш-именам прост и часто используется в более-менее видной малвари (даже кибершпионской).
Наша задача — написать легко масштабируемый мотор для реализации скрытия вызовов WinAPI. Они не должны читаться в таблице импорта и не должны бросаться в глаза в дизассемблере. Давай напишем короткую программу для экспериментов и откомпилируем ее для x64.
#include <Windows.h> int main() { HANDLE hFile = CreateFileA("C:\\test\\text.txt", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); Sleep(5000); return 0; }
Как видишь, здесь используются две функции WinAPI — CreateFileA
и Sleep
. Функцию CreateFileA
я решил привести в качестве примера не случайно — по ее аргументу "C:\\test\\text.txt"
мы ее легко и найдем в уже обфусцированном виде.
Давай глянем на дизассемблированный код этого приложения. Чтобы листинг на ASM был выразительнее, программу необходимо откомпилировать, избавившись от всего лишнего в коде. Откажемся от некоторых проверок безопасности и библиотеки CRT. Для оптимизации приложения необходимо выполнить следующие настройки компилятора:
- предпочитать краткость кода (
/Os
), - отключить проверку безопасности (
/Gs-
), - отключить отладочную информацию,
- в настройках компоновщика отключить внесение случайности в базовый адрес (
/DYNAMICBASE:NO
), - включить фиксированный базовый адрес (
/FIXED
), - обозначить самостоятельно точку входа (в нашем случае это main),
- игнорировать все стандартные библиотеки (
/NODEFAULTLIB
), - отказаться от манифеста (
/MANIFEST:NO
).
Эти действия помогут уменьшить размер программы и избавить ее от вставок неявного кода. В моем случае получилось, что программа занимает 3 Кбайт. Ниже — ее полный листинг.
public start start proc near dwCreationDisposition= dword ptr -28h dwFlagsAndAttributes= dword ptr -20h var_18= qword ptr -18h sub rsp, 48h and [rsp+48h+var_18], 0 lea rcx, FileName ; "C:\\test\\text.txt" xor r9d, r9d ; lpSecurityAttributes mov [rsp+48h+dwFlagsAndAttributes], 80h ; dwFlagsAndAttributes mov edx, 80000000h ; dwDesiredAccess mov [rsp+48h+dwCreationDisposition], 3 ; dwCreationDisposition lea r8d, [r9+1] ; dwShareMode call cs:CreateFileA mov ecx, 1388h ; dwMilliseconds call cs:Sleep xor eax, eax add rsp, 48h retn start endp
Как видишь, функции WinAPI явно читаются в коде и видны в таблице импорта приложения.
Теперь давай создадим модуль, который поможет скрывать от любопытных глаз используемые нами функции WinAPI. Напишем таблицу хешей функций.
static DWORD hash_api_table[] = { 0xe976c80c, // CreateFileA 0xb233e4a5, // Sleep }
Как хешировать
В статье нет смысла приводить алгоритм хеширования — их десятки, и они доступны в Сети, даже в Википедии. Могу посоветовать алгоритмы, с возможностью выставления вектора начальной инициализации (seed), чтобы хеши функций были уникальными. Например, подойдет алгоритм MurmurHash.
Давай условимся, что у нас макрос хеширования будет иметь прототип HASH_API(name, name_len, seed)
, где name
— имя функции, name_len
— длина имени, seed
— вектор начальной инициализации. Так что все значения хеш-функций у тебя будут другими, не как в статье!
Поскольку мы договорились писать легко масштабируемый модуль, определимся, что функция получения WinAPI у нас будет вида
LPVOID get_api(DWORD api_hash, LPCSTR module);
Но до этого еще нужно дойти, а сейчас напишем универсальную функцию, которая будет разбирать экспортируемые функции WinAPI передаваемой в нее системной библиотеки.
LPVOID parse_export_table(HMODULE module, DWORD api_hash) { PIMAGE_DOS_HEADER img_dos_header; PIMAGE_NT_HEADERS img_nt_header; PIMAGE_EXPORT_DIRECTORY in_export; img_dos_header = (PIMAGE_DOS_HEADER)module; img_nt_header = (PIMAGE_NT_HEADERS)((DWORD_PTR)img_dos_header + img_dos_header->e_lfanew); in_export = (PIMAGE_EXPORT_DIRECTORY)((DWORD_PTR)img_dos_header + img_nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
По ходу написания этой функции я буду пояснять, что к чему, потому что путешествие по заголовку PE-файла — дело непростое (у динамической библиотеки будет именно такой заголовок). Сначала мы объявили используемые переменные, с этим не должно было возникнуть проблем.
Далее, в первой строчке кода, мы получаем из переданного в нашу функцию модуля DLL ее IMAGE_DOS_HEADER
. Вот его структура:
typedef struct _IMAGE_DOS_HEADER { WORD e_magic; WORD e_cblp; WORD e_cp; WORD e_crlc; WORD e_cparhdr; WORD e_minalloc; WORD e_maxalloc; WORD e_ss; WORD e_sp; WORD e_csum; WORD e_ip; WORD e_cs; WORD e_lfarlc; WORD e_ovno; WORD e_res[4]; WORD e_oemid; WORD e_oeminfo; WORD e_res2[10]; LONG e_lfanew; } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
Здесь нас интересует поле e_lfanew
— это RVA (Relative Virtual Address, смещение) до заголовка IMAGE_NT_HEADERS
, который, в свою очередь, имеет такую структуру:
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Нужное нам поле OptionalHeader
указывает на еще одну структуру — IMAGE_OPTIONAL_HEADER
. Она громоздкая, и я ее сократил до нужных нам полей, точнее до элемента DataDirectory
, который содержит 16 полей. Нужное нам поле называется IMAGE_DIRECTORY_ENTRY_EXPORT
. Оно описывает символы экспорта, а поле VirtualAddress
указывает смещение секции экспорта.
typedef struct _IMAGE_OPTIONAL_HEADER { ... IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
Итак, мы в секции экспорта, в IMAGE_EXPORT_DIRECTORY
. Продолжаем ее читать:
PDWORD rva_name; UINT rva_ordinal; rva_name = (PDWORD)((DWORD_PTR)img_dos_header + in_export->AddressOfNames); rva_ordinal = (PWORD)((DWORD_PTR)img_dos_header + in_export->AddressOfNameOrdinals);
Чтобы было понятнее, структура IMAGE_EXPORT_DIRECTORY
:
typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; DWORD AddressOfNames; DWORD AddressOfNameOrdinals; } IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;
Наконец-то мы пробрались сквозь дебри заголовка PE к нужным нам данным. Остальное — дело техники. Как ты уже понял по коду, здесь нас интересуют два поля: AddressOfNames
и AddressOfNameOrdinals
. Первое содержит имена функций, второе — их индекс (читай: номер). Суть дальнейших действий проста: в цикле будем просматривать и сверять переданный в нашу функцию хеш с хешами функций в таблице экспорта и, как найдем совпадение, выходим из цикла.
UINT ord = -1; for (i = 0; i < in_export->NumberOfNames; i++) { api_name = (PCHAR)((DWORD_PTR)img_dos_header + rva_name[i]); get_hash = HASH_API(api_name, name_len, seed); if (api_hash == get_hash) { ord = (UINT)rva_ordinal[i]; break; } }
Нашли! Теперь получаем ее адрес и возвращаем его:
func_addr = (PDWORD)((DWORD_PTR)img_dos_header + in_export->AddressOfFunctions); func_find = (LPVOID)((DWORD_PTR)img_dos_header + func_addr[ord]); return func_find; }
В коде отсутствуют проверки корректности обрабатываемых и поступающих в функции данных. Это сделано умышленно, чтобы не засорять код и не отвлекать читателя от сути статьи.
Функция получилась весьма короткая и понятная. Теперь перейдем к написанию основной функции. Помнишь, мы ее обозначили как LPVOID get_api
? Она будет, по сути, оберткой над parse_export_table
, но сделает ее универсальной.
Дело в том, что наша функция parse_export_table
слишком «сырая» — она просматривает таблицы импортов передаваемых в нее библиотек, но не читает эти библиотеки в память (если их там нет). Для этого мы используем функцию LoadLibrary
, точнее ее хешированный вариант. Заодно посмотрим на работоспособность parse_export_table
.
Функция экспортируется библиотекой Kernel32.dll
. Чтобы начать с ней работать, мы должны найти эту библиотеку в адресном пространстве нашего процесса через PEB. Я буду писать сразу универсальный код, который подойдет для обеих архитектур.
LPVOID get_api(DWORD api_hash, LPCSTR module) { HMODULE krnl32, hDll; LPVOID api_func; #ifdef _WIN64 int ModuleList = 0x18; int ModuleListFlink = 0x18; int KernelBaseAddr = 0x10; INT_PTR peb = __readgsqword(0x60); #else int ModuleList = 0x0C; int ModuleListFlink = 0x10; int KernelBaseAddr = 0x10; INT_PTR peb = __readfsdword(0x30); #endif // Теперь получим адрес kernel32.dll INT_PTR mod_list = *(INT_PTR*)(peb + ModuleList); INT_PTR list_flink = *(INT_PTR*)(mod_list + ModuleListFlink); LDR_MODULE *ldr_mod = (LDR_MODULE*)list_flink; for (; list_flink != (INT_PTR)ldr_mod ;) { ldr_mod = (LDR_MODULE*)ldr_mod->e[0].Flink; if (!lstrcmpiW(ldr_mod->dllname.Buffer, L"kernel32.dll")) break; } krnl32 = (HMODULE)ldr_mod->base;
Далее нам необходимо объявить прототип нашей функции LoadLibraryA
. Это нужно сделать в начале файла. Вот прототип:
HMODULE (WINAPI *temp_LoadLibraryA)(__in LPCSTR file_name) = NULL; HMODULE hash_LoadLibraryA(__in LPCSTR file_name) { return temp_LoadLibraryA(file_name); }
Кроме того, объявим прототипы наших функций из тестового приложения, которое мы писали в самом начале:
HANDLE (WINAPI *temp_CreateFileA)(__in LPCSTR file_name, __in DWORD access, __in DWORD share, __inopt LPSECURITY_ATTRIBUTES security, __in DWORD creation_disposition, __in DWORD flags, __inopt HANDLE template_file) = NULL; HANDLE hash_CreateFileA(__in LPCSTR file_name, __in DWORD access, __in DWORD share_mode, __inopt LPSECURITY_ATTRIBUTES security, __in DWORD creation_disposition, __in DWORD flags, __inopt HANDLE template_file) { temp_CreateFileA = (HANDLE (WINAPI *)(LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE))get_api(hash_api_table[0], "Kernel32.dll"); return temp_CreateFileA(file_name, access, share_mode, security, creation_disposition, flags, template_file); } VOID (WINAPI *temp_Sleep)(DWORD time) = NULL; VOID hash_Sleep(__in DWORD time) { temp_Sleep = (VOID (WINAPI *)(DWORD))get_api(hash_api_table[1], "Kernel32.dll"); return temp_Sleep(time); }
Прототип для LoadLibraryA
— упрощенный. Мы здесь не используем нашу таблицу хешей hash_api_table[]
, потому что хеш LoadLibraryA
мы захардкодим дальше. Хеш будет у каждого свой, в зависимости от алгоритма хеширования.
temp_LoadLibraryA = (HMODULE (WINAPI *)(LPCSTR))parse_export_table(krnl32, 0x731faae5); hDll = hash_LoadLibraryA(module); api_func = (LPVOID)parse_export_table(hDll, api_hash); return api_func; }
Итак, все готово. Этот мотор для вызова функций по хешу можно вынести в отдельный файл и расширять, добавляя новые прототипы и хеши. Теперь, после всех манипуляций, изменим наш тестовый файл и откомпилируем его.
int main() { HANDLE hFile = hash_CreateFileA("C:\\test\\text.txt", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); hash_Sleep(5000); return 0; }
Первое, что бросается в глаза, — наш файл работает!
Текстовый файл создается, программа засыпает на пять секунд и закрывается. Теперь давай посмотрим на таблицу импорта…
…и увидим там только функцию lstrcmpiW
. Ты ведь помнишь, что мы ее использовали для сравнения строк? Больше никаких функций нет! Теперь заглянем в дизассемблер.
Здесь мы тоже не видим никаких вызовов. Если углубиться в исследование программы, мы, разумеется, обнаружим наши хеши, строки типа kernel32.dll
и прочее. Но это просто учебный пример, демонстрирующий базу, которую можно развивать.
Хеши можно защитить различными математическими операциями, а строки зашифровать. Для закрепления знаний попробуй скрыть функцию lstrcmpiW
по аналогии с другими WinAPI. Даю подсказку: эта функция экспортируется библиотекой Kernel32.dll
.