За последние недели инфостилер Vidar стал одним из самых активно обновляемых инструментов для кражи данных. Помимо множественных изменений в обфускации строк и переработанного подхода к защите собственной конфигурации, злоумышленники внедрили новую технику обхода механизма Application-Bound Encryption (ABE) - шифрования, привязанного к конкретному приложению. ABE применяется современными браузерами для защиты сохранённых паролей, данных автозаполнения и других конфиденциальных сведений от чтения сторонними процессами, однако Vidar нашёл способ его нейтрализовать.
Описание
С точки зрения высокоуровневой архитектуры подход Vidar к обходу ABE отчасти напоминает методы, ранее замеченные у стилеров Remus и Lumma. Эти программы также пытаются извлечь v20_master_key - главный ключ шифрования - непосредственно из памяти браузера. Различие заключается в способе получения ключа. Если Remus и Lumma использовали запись кода в память целевого процесса и его последующее выполнение, то Vidar пошёл по пути клонирования адресного пространства браузера и внедрения асинхронных процедур.
Конечная цель любого инфостилера - получить v20_master_key, поскольку этого ключа достаточно для расшифровки любых данных, защищённых ABE в рамках конкретного приложения. Получить его можно двумя способами: либо с диска, где ключ защищён двумя слоями шифрования CryptProtectData (один - в контексте текущего пользователя, второй - от имени системы NT AUTHORITY\SYSTEM), либо из памяти браузера. Видар выбрал второй путь.
Однако в памяти ключ хранится в зашифрованном виде с помощью функции CryptProtectMemory с флагом CRYPTPROTECTMEMORY_SAME_PROCESS. Это означает, что даже если злоумышленник сможет считать зашифрованную версию ключа из памяти, он не сможет просто так её расшифровать - для этого потребуется вызвать функцию CryptUnprotectMemory непосредственно внутри процесса браузера. Таким образом, перед атакующими встают две задачи: найти зашифрованный ключ в памяти браузера и заставить сам браузер его расшифровать.
Для поиска v20_master_key Vidar сканирует всю память работающего браузера в поисках определённого 32-байтового шаблона. Но сначала необходимо получить доступ к памяти браузера. Если браузер уже запущен, Vidar не читает его память напрямую. Вместо этого он создаёт форк процесса - точную копию адресного пространства - с помощью системного вызова NtCreateProcessEx, передавая дескриптор родительского процесса и устанавливая SectionHandle в NULL. Ядро операционной системы клонирует адресное пространство браузера с механизмом копирования при записи. Полученный процесс не имеет потоков и никогда не запускается на исполнение - он существует исключительно как статический снимок памяти.
Для выполнения форка Vidar сначала открывает дескриптор целевого браузера через OpenProcess с правами PROCESS_QUERY_INFORMATION, PROCESS_CREATE_PROCESS и PROCESS_VM_READ. Если первоначальный вызов не удаётся, используется только PROCESS_CREATE_PROCESS. Затем этот дескриптор передаётся как аргумент ParentProcess в NtCreateProcessEx с запросом полного доступа к дочернему процессу. Если форк не удаётся, Vidar возвращается к сканированию живого процесса браузера напрямую.
Если браузер не запущен, злоумышленники создают новый изолированный рабочий стол с помощью функции CreateDesktopA, присваивают ему имя вида v20_%d (где %d заменяется индексом браузера в конфигурации стилера) и запускают браузер на этом рабочем столе. В версии Vidar 2.1 для запуска используется командная строка --no-first-run --disable-gpu about:blank, хотя в разных версиях параметры могут отличаться. Поскольку современные браузеры используют многопроцессную архитектуру, может быть несколько процессов, соответствующих одному браузеру. Vidar перечисляет все дочерние процессы (до 64) и применяет процедуру форка и сканирования к каждому из них независимо.
Имея дескриптор форкнутого процесса, Vidar перечисляет его виртуальные регионы памяти с помощью NtQueryVirtualMemory. Он проходит по всему адресному пространству браузера, собирая регионы, соответствующие определённому профилю: выделенные (MEM_COMMIT), частные (MEM_PRIVATE) и либо только для чтения (PAGE_READONLY), либо для чтения и записи (PAGE_READWRITE). Стилер записывает до 4096 таких регионов вместе с их базовыми адресами и размерами. Само сканирование шаблона выполняется параллельно с использованием до 64 рабочих потоков в зависимости от системы.
Каждый поток сканирует назначенные регионы с помощью NtReadVirtualMemory, ища 32-байтовый шаблон. При нахождении совпадения поток читает 8 байт по смещению +32 от начала совпадения, интерпретирует их как указатель и проверяет, что этот указатель ведёт на выделенную, частную и читаемую память - именно такое состояние ожидается для зашифрованного v20_master_key.
Речь идёт о сигнатуре 76 32 30 00 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 03 00 00 00 00 01 ?? ?? ??, где ?? обозначает подстановочный символ. Первые четыре байта соответствуют ASCII-строке v20\x00 - общему префиксу для данных, связанных с ABE. По оценке исследователей, с высокой долей вероятности этот шаблон нацелен на узел структуры Encryptor::KeyRing в браузере Chromium - структуры, содержащей ключи, используемые классом Encryptor.
Обоснование такое: KeyRing определён как std::map<std::string, std::optional<Key>>. Карта обычно реализована как красно-чёрное дерево, где каждый узел хранит три указателя (левый потомок, родитель, правый потомок), а затем пару ключ-значение. Для v20_master_key тег - это строка "v20", которая умещается в буфер малой строковой оптимизации и хранится непосредственно в узле. Класс Key, следующий сразу за ней, содержит три переменные: необязательный алгоритм, вектор байтов с ключом и булев флаг шифрования. Таким образом, 32-байтовое совпадение покрывает тег и начало объекта Key, а указатель на +32 попадает на начало вектора с зашифрованным ключом.
После сбора совпадений со всех потоков Vidar применяет механизм голосования: кандидаты, состоящие в основном из нулей, отбрасываются, остальные попарно сравниваются, и каждый получает оценку по количеству идентичных копий. Для каждого браузерного процесса выбирается один лучший кандидат.
Для каждого браузерного процесса, давшего совпадение, Vidar теперь имеет адрес потенциального зашифрованного v20_master_key. Поскольку ключ защищён CryptProtectMemory с флагом SAME_PROCESS, расшифровать его может только сам браузер. Поэтому Vidar внедряет асинхронный вызов процедуры (APC) в живой процесс браузера.
Стилер поддерживает два метода APC-инъекции. Если на системе обнаружены продукты ESET или Bitdefender, используется "классический" метод: создаётся приостановленный поток в целевом браузере с помощью CreateRemoteThread (адрес старта - NtTestAlert, флаг CREATE_SUSPENDED), ставится в очередь APC через NtQueueApcThread, поток возобновляется, и вызывается WaitForSingleObject с рандомизированным таймаутом. При возобновлении поток вызывает NtTestAlert, который опустошает очередь APC и выполняет внедрённую процедуру CryptUnprotectMemory с тремя аргументами: адрес зашифрованного ключа, его размер (32 байта) и флаг CRYPTPROTECTMEMORY_SAME_PROCESS.
Если антивирусы не обнаружены, применяется "специальный" метод: находится существующий поток браузера с помощью CreateToolhelp32Snapshot и Thread32First/Thread32Next, открывается через NtOpenThread, и вызывается NtQueueApcThreadEx2 с флагом QUEUE_USER_APC_FLAGS_SPECIAL_USER_APC. Такие специальные пользовательские APC выполняются немедленно, без необходимости перевода потока в состояние ожидания.
После выполнения APC ключ оказывается расшифрован непосредственно в памяти браузера. Чтобы считать его, Vidar создаёт второй форк браузерного процесса и читает данные с того же адреса с помощью NtReadVirtualMemory. Результат сравнивается со снимком зашифрованного ключа, сделанным до APC. Если данные не изменились - расшифровка не удалась, и Vidar переходит к следующему кандидату. Если данные изменились, через второй форк выполняется дополнительная проверка: в памяти форкнутого процесса ищется последовательность байт "v20", и для каждой найденной записи (например, cookie или пароля) предпринимается попытка расшифровки AES-256-GCM с новым ключом. Успешная проверка тега GCM подтверждает корректность ключа.
В любом случае после считывания расшифрованного ключа Vidar повторно шифрует его в памяти браузера, внедряя ещё один APC с функцией CryptProtectMemory для сохранения исходного состояния браузера. Если ни один кандидат не дал успешного результата, стилер убивает все экземпляры браузера, запускает его заново и повторяет цикл сканирования, расшифровки и проверки.
Использование APC-инъекций вместо более традиционного подхода с записью кода через NtWriteVirtualMemory представляет собой любопытный тактический ход. Такие инъекции считаются менее распространёнными и могут проходить мимо некоторых систем обнаружения, хотя они хорошо известны антивирусным решениям и EDR-платформам. Однако, как показывает практика, каждый новый метод обхода ABE имеет свои преимущества и недостатки, и именно это подталкивает авторов инфостилеров к постоянному поиску новых способов.
Предоставленный анализ техники Vidar демонстрирует, что злоумышленники готовы вкладывать значительные усилия в обход современных механизмов защиты браузеров. При этом основная рекомендация для пользователей остаётся прежней - своевременно обновлять браузеры и операционные системы, а также использовать решения для защиты конечных точек, способные выявлять аномальное поведение процессов.
Индикаторы компрометации
SHA256
- 459daa809751e73f60fbbe4384a7d1653c36bb06945e4eb3635270924241100a