
Основанная на памяти техника обхода, которая делает шеллкод невидимым от начала до конца процесса.
Техника уклонения на основе памяти, которая делает шелл-код невидимым от начала до завершения процесса.
Я хотел поделиться этой POC самовнедрения шелл-кода, чтобы продемонстрировать некоторые концепции обхода AV/EDR, которые могут быть полезны для Red Teaming. Всего несколько недель назад я придумал пользовательскую технику уклонения в памяти, которую назвал ShellGhost. Эта техника возникла из необходимости иметь код, который выполняет «невидимый» шелл-код от начала до конца процесса.
ShellGhost полагается на Vectored Exception Handling в сочетании с программными точками останова, чтобы циклически останавливать выполнение потока, заменять выполненную точку останова на RC4-зашифрованную инструкцию шелл-кода, расшифровывать инструкцию и возобновлять выполнение после восстановления защиты памяти на RX. Когда возникает последующее EXCEPTION_BREAKPOINT, обработчик исключений заменяет предыдущую инструкцию шелл-кода новой точкой останова, так что выделение никогда не раскроет полный шелл-код в незашифрованном состоянии. Это происходит внутри закрытой страницы памяти, которая изначально помечена как READ/WRITE. Наличие выделения RW PRV не будет считаться «индикатором компрометации» сканерами памяти, такими как PE-Sieve и Moneta. Когда выделение становится RX и страница сканируется, будут найдены только точки останова. Это происходит в то время, когда шелл-код фактически выполняется. Следующее изображение показывает, что обратная оболочка работает, но Moneta не находит IOC (кроме того, что двоичный файл не подписан).

Попытка сканировать процесс с помощью Pe-Sieve дает еще лучший результат:

Отображение шелл-кода — это основная функциональность ShellGhost. Эта тактика позволяет потоку периодически выполнять инструкции, никогда не раскрывая весь шелл-код в памяти. Это возможно, потому что позиция каждой отдельной инструкции шелл-кода, которую выполняет поток, соответствует позиции определенной точки останова внутри выделенной страницы памяти. ShellGhost определяет эту позицию, вычисляя относительный виртуальный адрес (RVA) от RIP потока до базового адреса выделенной страницы памяти и добавляет его к базовому адресу зашифрованного шелл-кода / зашифрованных инструкций. Количество заменяемых точек останова не всегда одинаковое, а варьируется в зависимости от количества опкодов, необходимых для правильной генерации и интерпретации каждой инструкции (QUOTA). Например, инструкция 'POP RBP' равна '5D', что означает, что будет заменена только одна точка останова. Напротив, инструкция 'JMP RAX' требует опкодов 'FF E0', поэтому будут заменены две точки останова. По этой причине я создал следующую структуру данных C.
typedef struct CRYPT_BYTES_QUOTA {
DWORD RVA; // смещение к зашифрованной инструкции
DWORD quota; // количество опкодов, образующих инструкцию
} CRYPT_BYTES_QUOTA, * PCRYPT_BYTES_QUOTA;
Точки останова не заменяются сразу на соответствующие инструкции. Это связано с тем, что инструкции должны пройти процедуру расшифровки перед выполнением. Здесь в дело вступает DWORD quota. ShellGhost полагается на теперь уже популярную 'SystemFunction032' для выполнения расшифровки RC4. В отличие от XOR, RC4 не является одно-байтовой схемой шифрования. Это означает, что шелл-код не может быть зашифрован и расшифрован весь сразу. Это также еще одна причина, по которой каждая инструкция обрабатывается отдельно. После замены точек останова длина буфера, необходимая для SystemFunction032, будет равна 'квоте инструкции', которая опять же представляет собой количество опкодов, из которых состоит конкретная инструкция. Так, например, рассмотрим следующий фрагмент.
CRYPT_BYTES_QUOTA instruction[200];
instruction[5].quota = 2
USTRING buf = { 0 }; // будет содержать буфер для расшифровки и его длину
USTRING key = { 0 }; // будет содержать ключ RC4 и длину
buf.Length = 2 // длина буфера или длина инструкции для расшифровки
Мы знаем, что инструкция шелл-кода номер 5 состоит из 2 опкодов, поэтому длина буфера 2 будет передана в SystemFunction032. Это важно, потому что попытка расшифровать весь шелл-код одним вызовом SystemFunction032 полностью его испортит.
Шелл-код необходимо отобразить с помощью ShellGhost_mapping.py перед компиляцией. Скрипт извлекает каждую отдельную инструкцию и обрабатывает ее как маленький независимый шелл-код. Инструкции шифруются одна за другой и выводятся в формате C все вместе как unsigned char. Результат может быть жестко закодирован внутри кода C. Ниже приведен пример того, как выглядят зашифрованные инструкции шелл-кода MSF для calc.exe.

Этот шелл-код содержит 98 инструкций, поэтому объявляется 98 структур CRYPT_BYTES_QUOTA. При выполнении кода эти структуры должны быть заполнены соответствующими RVA и QUOTA инструкций. Параметр '-1' указывает скрипту отображения вывести фрагмент кода, который это делает.

Шелл-коды Metasploit x64 обычно содержат строковые параметры winapi, хранящиеся между инструкциями. Иными словами, шелл-код MSF x64, вызывающий Winexec, не помещает в стек последовательность байтов с нулевым байтом в конце, чтобы получить первый строковый параметр. Вместо этого регистр RCX (первый параметр) является указателем внутри самого шелл-кода, как показано на следующем изображении.
