Оглавление
Введение
APT Lazarus и ее подгруппа BlueNoroff — это весьма изощренные и разносторонние группы злоумышленников, говорящих на корейском языке. Мы внимательно следим за их активностью и часто наблюдаем, как они используют в своих атаках собственное вредоносное ПО: полнофункциональный бэкдор под названием Manuscrypt. Согласно нашим данным, Lazarus применяет этот бэкдор как минимум с 2013 года, и мы зафиксировали его использование в более чем 50 уникальных кампаниях, направленных на правительственные учреждения, дипломатические ведомства, финансовые институты, подрядчиков в сфере ВПК, криптовалютную индустрию, IT- и телекоммуникационные компании, разработчиков игр, СМИ, казино, университеты и даже на исследователей в области безопасности и так далее.
13 мая 2024 года мы обнаружили, что наш продукт потребительского класса Kaspersky Total Security распознал проникновение Manuscrypt на персональный компьютер человека, проживающего в России. Поскольку злоумышленники из Lazarus редко атакуют физических лиц, это вызвало у нас интерес, и мы решили присмотреться повнимательнее. Выяснилось, что до обнаружения Manuscrypt наши технологии также зафиксировали эксплуатацию уязвимости веб-браузера Google Chrome с сайта detankzone[.]com. Своим внешним видом этот сайт напоминал профессионально оформленную страницу многопользовательской танковой игры в жанре MOBA, в которой были реализованы инструменты децентрализованного финансирования (DeFi) на базе так называемых «невзаимозаменяемых токенов», более известных как NFT. Посетителям сайта предлагалось скачать пробную версию игры. Но это была лишь видимость. На сайте скрытно размещался скрипт, эксплуатировавший уязвимость нулевого дня веб-браузера Google Chrome, в результате чего злоумышленники получали полный контроль над компьютером жертвы. Для заражения достаточно было просто посетить сайт — игра служила только для отвлечения внимания.
Нам удалось извлечь первый этап атаки — эксплойт, позволяющий удаленно выполнить код в процессе Google Chrome. Убедившись, что эксплойт использует уязвимость нулевого дня, присутствующую в последней версии Google Chrome, мы в тот же день сообщили о своих выводах в Google. Через два дня компания Google выпустила обновление и поблагодарила нас за обнаружение этой атаки.
После уведомления Google об обнаруженной уязвимости мы следовали принципам ответственного раскрытия информации об уязвимостях и воздержались от публикации подробностей, чтобы дать пользователям достаточно времени для применения исправления. Такой подход также направлен на предотвращение дальнейшей эксплуатации этой уязвимости злоумышленниками. Компания Google предприняла дополнительные меры, заблокировав detankzone[.]com и другие сайты, связанные с этой кампанией, чтобы любой потенциальный посетитель, даже не будучи пользователем наших продуктов, был предупрежден об их вредоносном характере.
Мы с пониманием отнеслись к просьбе Google о временном неразглашении информации об уязвимости, однако 28 мая 2024 года компания Microsoft опубликовала в своем блоге запись под названием «Moonstone Sleet Emerges as New North Korean Threat Actor with New Bag of Tricks» («Moonstone Sleet — новая группа киберзлоумышленников из Северной Кореи с новым арсеналом уловок»), в которой частично раскрывались обнаруженные нами факты. Судя по записям в блоге, специалисты Microsoft также следили за этой кампанией и связанными с ней веб-сайтами с февраля 2024 года. Однако их аналитики упустили из виду ключевой элемент вредоносной кампании — наличие эксплойта для браузера, использующего уязвимость нулевого дня. В этом отчете мы подробно рассмотрим уязвимости, которыми воспользовались злоумышленники, а также игру, которую они использовали в качестве приманки (спойлер: нам даже пришлось разработать собственный сервер для этой онлайн-игры).
Эксплойт
Сайт, который злоумышленники использовали в качестве прикрытия для своей кампании, был разработан на TypeScript/React, а один из его файлов
index.tsx содержал небольшой фрагмент кода, который загружал и выполнял эксплойт для Google Chrome.
Эксплойт содержит код для двух уязвимостей: первая используется для чтения памяти процесса Chrome и записи в нее с помощью JavaScript, а вторая — для обхода недавно представленной песочницы V8.
Первая уязвимость (CVE-2024-4947)
Сердцем каждого браузера является движок JavaScript. Движок JavaScript в Google Chrome называется V8, это собственный движок Google с открытым исходным кодом. Для снижения потребления памяти и достижения максимальной скорости в V8 используется довольно сложная последовательность этапов компиляции JavaScript, состоящая на данный момент из одного интерпретатора и трех JIT-компиляторов.
Приступая к выполнению JavaScript, V8 сначала компилирует скрипт в байт-код и выполняет его с помощью интерпретатора под названием Ignition. Ignition — это регистровая виртуальная машина с несколькими сотнями инструкций. Во время выполнения байт-кода V8 следит за поведением программы и может выполнять JIT-компиляцию некоторых функций для повышения производительности. Самый лучший и быстрый код создается высокооптимизированным компилятором TurboFan. Но у него есть один недостаток: генерация такого кода занимает слишком много времени. И все же разница в производительности между Ignition и TurboFan была настолько существенной, что в 2021 году был представлен новый неоптимизирующий JIT-компилятор под названием Sparkplug, который практически мгновенно компилирует байт-код в эквивалентный машинный код. Код, сгенерированный Sparkplug, работает быстрее интерпретатора, но разрыв в производительности между кодом, сгенерированным Sparkplug и TurboFan, остается значительным. В связи с этим в Chrome 117 (вышел в IV квартале 2023 года) разработчики представили новый оптимизирующий компилятор Maglev, цель которого — быстро генерировать достаточно качественный код, выполняя оптимизацию исключительно на основе обратной связи от интерпретатора. CVE-2024-4947 (проблема 340221135) — уязвимость в этом новом компиляторе.
Чтобы лучше разобраться в сути этой уязвимости и понять, как она была использована, рассмотрим код, с помощью которого злоумышленники привели ее в действие.
import * as moduleImport from ‘export var exportedVar = 23;’;
function trigger() {
moduleImport.exportedVar;
const emptyArray = [1, 2];
emptyArray.pop();
emptyArray.pop();
const arrHolder = {xxarr: doubleArray, xxab: fakeArrayBuffer};
function f() {
try {
moduleImport.exportedVar = 3.79837e-312;
} catch (e) { return false; }
return true;
}
while (!f()) { }
weakRef = new WeakRef(moduleImport);
return {emptyArray, arrHolder};
}
Код, используемый злоумышленниками для запуска CVE-2024-4947
Мы видим, что сначала происходит обращение к экспортированной переменной
exportedVar модуля moduleImport, затем создаются массив emptyArray и словарь arrHolder, но, похоже, никакой реальной работы с ними не происходит, они просто возвращаются функцией trigger. И тут появляется кое-что интересное — функция f выполняется до тех пор, пока не будет возвращено значение true. Однако эта функция возвращает true только в том случае, если она может установить для экспортируемой переменной moduleImport.exportedVar значение 3.79837e-312. Если же из-за этого возникает исключение, то функция f возвращает false. Как может быть, что выполнение одного и того же выражения moduleImport.exportedVar = 3.79837e-312; должно всегда возвращать false, пока не вернется true?LdaImmutableCurrentContextSlot [53]
Star1
LdaConstant [0]
SetNamedProperty r1, [1], [0] // moduleImport.exportedVar = 3.79837e-312;
Байт-код, созданный интерпретатором Ignition для выражения moduleImport.exportedVar = 3.79837e-312;
Если мы взглянем на байт-код, сгенерированный для этого выражения Ignition, и на код обработчика инструкции
SetNamedProperty, который должен присвоить этой переменной значение 3.79837e-312, то увидим, что он всегда будет выдавать исключение, поскольку, согласно спецификации ECMAScript, сохранение в объект модуля всегда приводит к ошибке в JavaScript.mov rax, 309000D616Dh // указатель объекта JS для moduleImport
mov edi, [rax+3]
add rdi, r14
mov rax, 309001870B5h // указатель объекта JS для 3.79837e-312
mov [rdi-1], eax
JIT-код, сгенерированный Maglev для выражения moduleImport.exportedVar = 3.79837e-312;
Но если дать этому байт-коду возможность выполниться достаточное количество раз, после чего V8 решит скомпилировать его с помощью компилятора Maglev, то мы увидим, что полученный машинный код не выдает исключение, а действительно устанавливает это свойство где-то в объекте
moduleImport. Это происходит из-за отсутствия проверки на сохранение в экспорте модуля — уязвимость CVE-2024-4947 (исправление можно найти здесь). Как злоумышленники используют это в своих целях? Чтобы ответить на этот вопрос, нужно понимать, как объекты JavaScript представлены в памяти.
Все объекты JS начинаются с указателя на специальный объект
Map (также известный как HiddenClass), который хранит метаинформацию об объекте и описывает его структуру. Он содержит тип объекта (хранится по смещению +8), количество свойств и пр.
Модуль
moduleImport представлен в памяти в виде объекта JSReceiver, который является наиболее общим объектом JS и используется для типов, для которых можно определить свойства. Он содержит указатель на массив свойств (PropertyArray), который по сути является обычным объектом JS типа FixedArray со своим собственным объектом Map. Если бы moduleImport в выражении moduleImport.exportedVar = 3.79837e-312; был не модулем, а обычным объектом, то код установил бы в этом массиве свойство #0, записав его по смещению +8, но, поскольку это модуль, а также имеется ошибка, код устанавливает это свойство, записывая его по смещению +0, при этом объект Map перезаписывается предоставленным объектом.
Поскольку 3.79837e-312 — это число с плавающей запятой, оно преобразуется в 64-битное значение (в соответствии со стандартом IEEE 754) и сохраняется в объекте JS
HeapNumber по смещению +4. Этот факт позволяет злоумышленникам установить свой собственный тип для объекта PropertyArray и вызвать путаницу типов. Установка для типа значения 0xB2 заставляет V8 рассматривать PropertyArray как PropertyDictionary, что приводит к повреждению памяти, поскольку объекты PropertyArray и PropertyDictionary имеют разные размеры и поле kLengthAndHashOffset PropertyDictionary выходит за границы PropertyArray.
Теперь злоумышленникам нужно добиться подходящей структуры памяти и нарушить работу чего-то полезного. Для этого они дефрагментируют кучу и выполняют действия, описанные в функции
trigger.
Эта функция работает следующим образом.
Происходит обращение к экспортированной переменной moduleImport.exportedVar для выделения памяти под массив PropertyArray модуля moduleImport.
Создается массив emptyArray с двумя элементами.
В результате удаления элементов из этого массива для объекта, используемого для хранения элементов, происходит перераспределение памяти, а для длины emptyArray устанавливается значение 0. Это важный шаг, поскольку для перезаписи длины массива emptyArray хэшем PropertyDictionary длина и хэш должны быть равны 0.
Функция trigger создает словарь arrHolder с двумя объектами. Это действие выполняется после создания emptyArray, чтобы обеспечить доступ к указателям на эти два объекта и произвести их перезапись при нарушении длины emptyArray. Первый объект, xxarr: doubleArray, используется для построения примитива для получения адресов объектов JS. Второй объект, xxab: fakeArrayBuffer, используется для построения примитива для получения доступа на чтение и запись ко всему адресному пространству процесса Chrome.
Затем функция trigger выполняет функцию f до тех пор, пока та не будет скомпилирована Maglev, и переписывает тип PropertyArray, чтобы тот обрабатывался движком как объект PropertyDictionary.
Выполнение команды new WeakRef(moduleImport) приводит к вычислению хэша PropertyDictionary, после чего длина emptyArray перезаписывается значением этого хэша.
Функция trigger возвращает emptyArray и arrHolder, содержащие объекты, которые могут быть перезаписаны emptyArray.
После этого эксплойт снова использует Maglev, а точнее тот факт, что он оптимизирует код на основе обратной связи, собранной интерпретатором. Эксплойт использует Maglev для компиляции функции, которая загружает значение типа
double из массива, полученного с помощью arrHolder.xxarr. После компиляции этой функции злоумышленники могут перезаписать указатель на массив, полученный с помощью arrHolder.xxarr, через emptyArray[5] и использовать эту функцию для получения адресов объектов JS. Аналогичным образом злоумышленники используют arrHolder.xxab для компиляции функции, которая устанавливает определенные свойства и перезаписывает длину другого объекта типа ArrayBuffer вместе с указателем на его данные (backing_store_ptr). Это возможно, когда указатель на объект, доступный через arrHolder.xxab, заменяется через emptyArray[6] указателем на ArrayBuffer. В результате злоумышленники получают доступ на чтение и запись ко всему адресному пространству процесса Chrome.
Вторая уязвимость (обход песочницы V8)
На данный момент злоумышленники могут считывать память и записывать в нее данные из JavaScript, но им нужна дополнительная уязвимость, чтобы обойти недавно появившуюся песочницу (кучи) V8. Эта песочница является программной, и ее основная идея заключается в изоляции памяти (кучи) V8 таким образом, чтобы злоумышленники не могли получить доступ к другим частям памяти и выполнить код. Как это работает? Вы, наверное, заметили, что все указатели в предыдущем разделе имеют длину 32 бита. Это не потому, что речь идет о 32-битном процессе. Сам процесс 64-битный, но указатели имеют длину 32 бита, потому что V8 использует сжатие указателей. Указатели хранятся не как единое целое, а только в виде своих младших частей, или же их можно представить как 32-битное смещение от некоторого «базового» адреса. Старшая часть («базовый» адрес) хранится в регистрах процессора и добавляется кодом. В этом случае злоумышленники не могут получить реальные указатели из изолированной памяти и не могут получить адреса для стека и страниц JIT-кода.
Для обхода песочницы V8 злоумышленники воспользовались интересной, но весьма распространенной уязвимостью, связанной с интерпретаторами, — ранее мы уже встречали ее разновидности в различных реализациях виртуальных машин. В V8 регулярные выражения реализованы с помощью интерпретатора Irregexp с собственным набором операционных кодов. Виртуальная машина Irregexp полностью отличается от Ignition, но она тоже регистровая.
RegisterT& operator[](size_t index) { return registers_[index]; }
BYTECODE(PUSH_REGISTER) {
ADVANCE(PUSH_REGISTER);
if (!backtrack_stack.push(registers[LoadPacked24Unsigned(insn)])) {
return MaybeThrowStackOverflow(isolate, call_origin);
}
DISPATCH();
}
BYTECODE(SET_REGISTER) {
ADVANCE(SET_REGISTER);
registers[LoadPacked24Unsigned(insn)] = Load32Aligned(pc + 4);
DISPATCH();
}
Примеры уязвимого кода в обработчиках инструкций виртуальной машины Irregexp
Уязвимость связана с тем, что виртуальная машина имеет фиксированное количество регистров и отдельный массив для их хранения, однако индексы регистров декодируются из тел инструкций и не проверяются, что позволяет атакующим получить доступ к памяти за пределами массива регистров.
PUSH_REGISTER r(REGISTERS_COUNT + idx)
POP_REGISTER r(0)
PUSH_REGISTER r(REGISTERS_COUNT + idx + 1)
POP_REGISTER r(1)
// Перезапись указателя output_registers
SET_REGISTER r(REGISTERS_COUNT), holderAddressLow
SET_REGISTER r(REGISTERS_COUNT + 1), holderAddressHigh
// Перезапись output_register_count
SET_REGISTER r(REGISTERS_COUNT + 2), 2
// MemCopy(output_registers_, registers_.data(), output_register_count_ * sizeof(RegisterT));
SUCCEED
Вредоносный байт-код виртуальной машины Irregexp для чтения памяти за пределами массива регистров
По случайному совпадению указатели на
output_registers и output_register_count расположены рядом с массивом регистров. Это дает злоумышленникам возможность считывать из памяти и записывать в нее данные за пределами песочницы V8 с помощью операционного кода SUCCEED. Атакующие используют это, чтобы перезаписать JIT-код шелл-кодом и выполнить его.
Эта проблема (330404819) была зарегистрирована и исправлена в марте 2024 года. Неизвестно, было ли наличие этих ошибок стечением обстоятельств и злоумышленники обнаружили эти ошибки первыми и использовали их как уязвимость нулевого дня или же они изначально использовались как уязвимость первого дня.
Шелл-код
На этом этапе атакующим требуются дополнительные уязвимости, чтобы выйти за пределы процесса Chrome и получить полный доступ к системе. Для этого они запускают валидатор в виде шелл-кода, который собирает как можно больше информации и отправляет ее на сервер, где решается, предоставлять ли следующий этап (еще один эксплойт) или нет. Решение принимается на основе следующей информации: данные CPUID (производитель, название процессора и т. д.), работает ли он на виртуальной машине или нет, версия и сборка ОС, количество процессоров, счетчик тактов, тип ОС, запущен ли процесс отладки, путь процесса, информация о версии файла системных модулей, информация о версии файла исполняемого процесса и таблица SMBIOS.
Когда мы проанализировали атаку, злоумышленники уже удалили эксплойт с сайта-приманки, что не позволило нам с легкостью получить следующий этап атаки. Несмотря на то что в нашем распоряжении есть технологии, которые помогли обнаружить и устранить огромное количество уязвимостей нулевого дня, связанных с повышением привилегий, которые использовались продвинутыми злоумышленниками в различных вредоносных кампаниях, в данном конкретном случае нам пришлось бы ждать очередной атаки, чтобы извлечь следующий этап. Мы решили не ждать и дать Google возможность устранить первоначальный эксплойт, используемый для удаленного выполнения кода в браузере Google Chrome.
Социальная активность
Что не перестает нас поражать, так это количество усилий, прикладываемых APT-группой Lazarus к своим кампаниям по социальной инженерии. В течение нескольких месяцев злоумышленники расширяли свое присутствие в социальных сетях, регулярно публикуя посты в X (бывший Twitter) с нескольких учетных записей, продвигая свою игру с помощью контента, созданного генеративным ИИ и графическими дизайнерами.
Одна из тактик злоумышленников заключалась в том, чтобы установить контакт с инфлюэнсерами из мира криптовалют, убедить их прорекламировать свой вредоносный сайт и, скорее всего, скомпрометировать их.
Активность атакующих не ограничивалась X, они также использовали профессионально разработанные веб-сайты с дополнительным вредоносным ПО, премиум-аккаунты в LinkedIn и фишинговые рассылки.
Игра
Эта атака особенно заинтересовала нас еще и тем, что вредоносный веб-сайт, атакующий своих посетителей с помощью эксплойта нулевого дня для Google Chrome, предлагал им загрузить и опробовать бета-версию компьютерной игры. Мы большие поклонники компьютерных игр и сразу захотели попробовать. Может, злоумышленники разработали настоящую игру для этой кампании? Неужели это первая компьютерная игра, разработанная киберпреступниками? Мы скачали
detankzone.zip, и он показался нам вполне легитимным: архив объемом 400 МБ содержал корректную структуру файлов игры, разработанной на Unity. Мы распаковали ресурсы игры и обнаружили логотипы DeTankZone, элементы HUD, текстуры 3D-моделей. Артефакты отладки указывали на то, что игра была скомпилирована злоумышленниками. Мы решили запустить игру.
После вступительного ролика с логотипом игры нас ожидало типичное для онлайн-игр стартовое меню с просьбой ввести учетные данные. Мы попытались войти в игру, используя несколько распространенных логинов и паролей, попытались зарегистрировать собственный аккаунт в игре и на сайте — все безрезультатно.
Неужели это все, что может предложить эта игра? Мы приступили к обратному инжинирингу кода, и выяснилось, что помимо стартового меню в игре есть и другие материалы. Мы обнаружили код, отвечающий за связь с игровым сервером, и занялись его обратным инжинирингом. В игре был задан сервер по адресу api.detankzone[.]com, и он явно не работал. Однако нам очень хотелось оценить эту игру, и что же мы сделали? Мы решили разработать собственный игровой сервер.
Во-первых, мы выяснили, что игра использует протокол Socket.IO для связи с сервером, и поэтому выбрали библиотеку
python-socketio для разработки собственного сервера. Затем мы нашли функцию со списком всех поддерживаемых названий команд (названий событий) и проанализировали, как те были обфусцированы. В результате обратного инжиниринга нам удалось выяснить, как были закодированы данные: оказалось, что это JSON, зашифрованный с помощью AES256 и закодированный посредством Base64. Для ключа AES используется строка Full Stack IT Service 198703Game, а для IV — строка MatGoGameProject. Мы рассчитывали, что эта информация может раскрыть личности разработчиков игры, но поиск в Google не дал никаких результатов. В итоге мы проанализировали формат данных для нескольких команд, реализовали их на нашем сервере и заменили URL-адрес на адрес этого сервера. После всего этого мы смогли войти в игру и поиграть с ботами!
Да, это оказалась настоящая игра! Мы немного поиграли в нее и даже получили удовольствие. Игра напомнила нам некоторые shareware-игры начала 2000-х годов, так что она определенно оправдала затраченные усилия. Текстуры выглядят немного неаккуратно, а сама игра отдаленно напоминает описанную в популярном самоучителе по Unity, но если бы Lazarus сами разработали эту игру, то установили бы новую планку для проведения кибератак, на которую пришлось бы равняться остальным злоумышленникам. Впрочем, Lazarus остаются верны себе: как выяснилось, исходный код этой игры был украден у оригинальных разработчиков.
Оригинальная игра
Мы нашли легитимную игру, которая послужила прототипом для версии злоумышленников — она называется DeFiTankLand (DFTL). Изучение Telegram-чата разработчиков помогло нам составить хронологию атаки. 20 февраля 2024 года злоумышленники запустили кампанию, рекламируя свою игру на сайте X. Спустя две недели, 2 марта 2024 года, цена валюты DefitankLand, монеты DFTL2, упала, и разработчики игры объявили в своем Telegram, что их холодный кошелек был взломан и монеты DFTL2 на сумму 20 000 долларов были украдены. Разработчики обвиняют в этом инсайдера. Мы не знаем, замешан ли тут инсайдер, однако подозреваем, что это дело рук Lazarus, и прежде чем украсть монеты, они сначала украли исходный код игры, изменили все логотипы и ссылки на DefitankLand и использовали полученный результат, чтобы сделать свою кампанию более правдоподобной.
Выводы
Lazarus — одна из самых активных и изощренных APT-групп, для которой получение финансовой выгоды остается одним из главных приоритетов. За прошедшие годы мы разоблачили множество их атак на сферу криптовалют, и очевидно, что эти атаки не прекратятся. Тактика злоумышленников эволюционирует, и они постоянно придумывают новые и более сложные схемы социальной инженерии. Злоумышленники из Lazarus уже успешно применяют генеративный ИИ, и мы прогнозируем еще более изощренные атаки с его использованием. Атаки Lazarus особенно опасны тем, что эта группа нередко использует эксплойты нулевого дня. Простой переход по ссылке в социальной сети или в электронном письме может привести к полной компрометации персонального компьютера или корпоративной сети.
Исторически сложилось, что половина ошибок, обнаруженных или эксплуатируемых в Google Chrome и других веб-браузерах, затрагивала их компиляторы. Значительные изменения в кодовой базе веб-браузера и внедрение новых JIT-компиляторов неизбежно приводят к появлению немалого количества новых уязвимостей. Что же делать пользователям? Несмотря на то что в Google Chrome продолжают добавлять новые JIT-компиляторы, существует еще и Microsoft Edge, который может работать вообще без JIT. Впрочем, справедливости ради стоит отметить, что недавно появившаяся песочница V8 может оказаться весьма успешной в борьбе с эксплуатацией ошибок в компиляторах. Когда эта технология станет более совершенной, возможно, эксплуатировать уязвимости Google Chrome с JIT будет так же сложно, как эксплуатировать уязвимости Microsoft Edge без него.
Индикаторы компрометации
Эксплойт
B2DC7AEC2C6D2FFA28219AC288E4750C
E5DA4AB6366C5690DFD1BB386C7FE0C78F6ED54F
7353AB9670133468081305BD442F7691CF2F2C1136F09D9508400546C417833A
Игра
8312E556C4EEC999204368D69BA91BF4
7F28AD5EE9966410B15CA85B7FACB70088A17C5F
59A37D7D2BF4CFFE31407EDD286A811D9600B68FE757829E30DA4394AB65A4CC
Домены
detankzone[.]com
ccwaterfall[.]com
Securelist