29 марта 2024 года в рассылке Openwall Open Source Security (oss-security) было опубликовано сообщение об обнаружении вредоносного бэкдора в XZ, которое всколыхнуло сообщества ИБ-экспертов, разработчиков ПО с открытым исходным кодом и пользователей Linux. XZ — это утилита для сжатия данных, встроенная во многие популярные дистрибутивы Linux.
Бэкдор в этой библиотеке ориентирован на то, что ее может использовать sshd — серверный процесс OpenSSH. В некоторых дистрибутивах Linux, таких как Ubuntu, Debian и RedHat/Fedora, для доступа к функциям systemd применяется модифицированный OpenSSH, зависящий от этой библиотеки (отметим, что эта проблема не коснулось дистрибутивов Arch Linux и Gentoo). Злоумышленники стремились внедрить в sshd возможность удаленного выполнения кода, доступную только им.
В отличие от ранее обнаруженных атак на Node.js, PyPI, FDroid и ядро Linux, в которых злоумышленники внедрялись в цепочку поставок посредством микроскопических вредоносных исправлений, поддельных пакетов и пакетов с тайпсквоттинговыми именами, этот инцидент носил многоэтапный характер. Атакующие почти преуспели в попытке скомпрометировать SSH-серверы по всему миру.
Бэкдор в библиотеке liblzma был реализован на двух уровнях. В исходный код инфраструктуры сборки для получения финальных пакетов злоумышленники добавили файл build-to-host.m4, который извлекал скрипт следующего этапа, спрятанный в файле тестового примера (bad-3-corrupt_lzma2.xz). Этот скрипт, в свою очередь, извлекал вредоносный бинарный компонент из другого тестового файла (good-large_compressed.lzma), который в процессе компиляции прилинковывался к легитимной библиотеке и с ней попадал в репозитории Linux. В результате основные вендоры начали распространять вредоносный компонент в составе бета-версий и экспериментальных сборок. Случай с компрометацией XZ был зарегистрирован под номером CVE-2024-3094 с максимальным уровнем серьезности по десятибальной шкале.
Оглавление
Хронология событий
19.01.2024 Сайт XZ перемещен на GitHub новым мейнтейнером (jiaT75).
15.02.2024 В .gitignore добавлен файл build-to-host.m4.
23.02.2024 Добавлены два «тестовых файла», содержащих этапы вредоносного скрипта.
24.02.2024 Релиз XZ 5.6.0.
26.02.2024 Коммит в CMakeLists.txt, нарушающий работу функции безопасности Landlock.
04.03.2024 Бэкдор приводит к проблемам с Valgrind.
09.03.2024 Обновлены два «тестовых файла», изменены функции CRC, «исправлена» проблема с Valgrind.
09.03.2024 Релиз XZ 5.6.1.
28.03.2024 Обнаружен баг, Debian и RedHat уведомлены.
28.03.2024 Debian откатывает XZ 5.6.1 до версии 5.4.5-0.2.
29.03.2024 В списке рассылки OSS-security появляется публикация.
29.03.2024 RedHat подтверждает, что XZ с бэкдором поставлялся в составе Fedora Rawhide и Fedora Linux 40 beta.
30.03.2024 Debian прекращает распространение своих сборок и начинает процесс их пересборки.
02.04.2024 Главный разработчик XZ признает инцидент с бэкдором.
Исходные дистрибутивы с бэкдором
xz-5.6.0
MD5
c518d573a716b2b2bc2413e6c9b5dbde
SHA1
e7bbec6f99b6b06c46420d4b6e5b6daa86948d3b
SHA256
0f5c81f14171b74fcc9777d302304d964e63ffc2d7b634ef023a7249d9b5d875
xz-5.6.1
MD5
5aeddab53ee2cbd694f901a080f84bf1
SHA1
675fd58f48dba5eceaf8bfc259d0ea1aab7ad0a7
SHA256
2398f4a8e53345325f44bdd9f0cc7401bd9025d736c6d43b372f4dea77bf75b8
Анализ первичного заражения
В GitHub-репозитории XZ содержится набор файлов для тестирования работоспособности кода упаковщика/распаковщика. Злоумышленник с учетной записью Цзя Тан (Jia Tan) или jiaT75 закоммитил два на первый взгляд безобидных тестовых файла, которые послужили основой для внедрения бэкдора.
Этими файлами были:
bad-3-corrupt_lzma2.xz (86fc2c94f8fa3938e3261d0b9eb4836be289f8ae)
good-large_compressed.lzma (50941ad9fd99db6fca5debc3c89b3e899a9527d7)
Эти файлы содержали шелл-скрипты и сам объектный файл бэкдора. Злоумышленник скрыл их внутри искаженных данных, из которых мог при необходимости их извлечь.
Этап 1 — модифицированный скрипт build-to-host
Когда очередной релиз XZ готов, его исходные файлы начинают распространяться через официальный GitHub-репозиторий проекта. Изначально эти релизы в репозитории не представляли опасности, поскольку встроенные вредоносные тестовые файлы никогда не выполнялись. Судя по всему, злоумышленник добавлял вредоносный код, инициирующий заражение, только в случае загрузки релиза с сайта https://xz[.]tukaani.org, который находился под контролем Цзя Тана.
Этот URL-адрес используется большинством дистрибутивов, и если загрузить с него пакет, в нем будет файл build-to-host.m4 с вредоносным кодом.
Файл build-to-host.m4 (c86c8f8a69c07fbec8dd650c6604bf0c9876261f) выполняется в процессе сборки. Содержащийся в нем код исправляет и распаковывает первый файл, добавленный в папку tests.
Строка кода, изображенная на скриншоте выше, заменяет «битые» данные из файла bad-3-corrupt_lzma2.xz с помощью команды tr и передает результат в команду xz -d, которая распаковывает данные. Распакованные данные содержат шелл-скрипт, который будет выполнен позже с помощью командной оболочки /bin/bash, вызываемой упомянутым файлом .m4.
Этап 2 — внедренный шелл-скрипт
Вредоносный скрипт, внедряемый вредоносным файлом .m4, проверяет: а) запущен ли он на машине под управлением Linux, б) запущен ли он при этом внутри нужного процесса сборки.
Для выполнения следующего этапа шелл-скрипт использует файл good-large_compressed.lzma, который и в самом деле корректно упакован с помощью XZ, но содержит «мусорные» данные в составе распакованных.
Процедура удаления мусорных данных выглядит следующим образом: функция eval выполняет последовательность head-команд, при этом каждая из них либо игнорирует следующие 1024 байта, либо извлекает следующие 2048 или 724 байта.
В общей сложности с помощью этих команд извлекается 33 492 байта (2048*16 + 724 байта). После этого команда tail сохраняет последние 31 265 байт файла, игнорируя все остальное.
Затем команда tr производит простую подстановку в выходных данных с целью их деобфускации. Вторая команда XZ распаковывает преобразованные байты в необработанный поток lzma, после чего результат передается в командную оболочку.
Этап 3 — извлечение бэкдора
Шелл-скрипт последнего этапа тщательно проверяет, соответствует ли окружение ожидаемым условиям: в чатсности, настроен ли проект на использование IFUNC (это мы подробнее рассмотрим это в следующих разделах).
Также предусмотрен ряд других проверок, к примеру, будет ли использоваться компилятор GCC и содержит ли проект файлы, которые в дальнейшем понадобятся скрипту.
На этом этапе извлекается сам бинарный код бэкдора — объектный файл, запрятанный в уже упомянутом файле good-large_compressed.lzma, но по другому смещению.
За извлечение отвечает следующий код:
В процессе извлечения задействуется последовательность команд, где результат каждой команды служит входными данными для последующей. Ниже представлена отвечающая за это строка кода в отформатированном виде:
Изначально файл good-large_compressed.lzma извлекается с помощью самой утилиты XZ. На последующих этапах с помощью функции eval $i выполняется цепочка вызовов команды head (как и во время извлечения на третьем этапе).
Затем для расшифровки бинарных данных используется самописный RC4-подобный алгоритм, результатом работы которого становится еще один сжатый файл. Он также распаковывается с помощью утилиты XZ. Затем скрипт удаляет несколько байтов из начала распакованных данных, используя предустановленные значения, и сохраняет результат на диск как файл liblzma_la-crc64-fast.o — файл бэкдора, задействуемый в процессе линковки финальной библиотеки liblzma.so.
В завершение скрипт модифицирует функцию is_arch_extension_supported из файла crc_x86_clmul.h в liblzma, заменяя вызов функции __get_cpuid на _get_cpuid, удалив один символ подчеркивания.
Благодаря этой модификации бэкдор теперь может быть слинкован в библиотеку (мы подробно рассмотрим это в следующем разделе). Всю цепочку заражения сборки можно представить в виде следующей схемы:
Анализ бинарного файла бэкдора
Сценарий скрытой загрузки
В оригинальном коде XZ для вычисления CRC указанных данных используются две специальные функции: lzma_crc32 и lzma_crc64. Обе эти функции хранятся в таблице символов ELF с типом IFUNC, предоставляемым библиотекой GNU C (GLIBC). IFUNC позволяет разработчикам динамически выбирать подходящую функцию. Выбор происходит, когда динамический компоновщик загружает разделяемую библиотеку.
Таким образом XZ определяет, нужно ли использовать оптимизированную версию функции lzma_crcX, которая задействует наборы инструкций современных процессоров (CLMUL, SSSE3 и SSE4.1). Чтобы удостовериться, что они поддерживаются процессором, нужно вызывать инструкцию cpuid с помощью обертки/intrinsic-функции __get_cpuid, предоставляемой GLIBC. Как раз в этот момент загружается бэкдор.
Бэкдор сохраняется в виде объектного файла, его основная задача — прилинковаться к основному исполняемому файлу во время компиляции. Для этого в объектном файле предусмотрена функция __get_cpuid. Так как внедренные шелл-скрипты удаляют один знак подчеркивания из функции в оригинальном исходном коде, вместо нее вызывается _get_cpuid, версия функции с бэкдором.
Анализ кода бэкдора
Изначальный код бэкдора вызывается дважды, поскольку в lzma_crc32 и lzma_crc64 используется одна и та же модифицированная функция (_get_cpuid). Для контроля создан простой счетчик, проверяющий, выполнялся ли уже код. Вредоносная активность начинается в момент, когда IFUNC для lzma_crc64 вызывает _get_cpuid, проверяет, что счетчик равен единице, то есть к функции уже обращались, и делает последний шаг, необходимый для перенаправления потока исполнения к истинной точке входа вредоносной программы.
Перед тем как инициализировать вредоносный код, бэкдор сначала инициализирует несколько структур, содержащих основную информацию о текущем запущенном процессе. В первую очередь он находит адрес глобальной таблицы смещений (Global Offset Table, GOT) при помощи жестко прописанных смещений и использует эту информацию для поиска указателя cpuid внутри него.
В GOT хранятся смещения символов, в том числе обертки cpuid. Затем бэкдор подменяет указатели на функцию main вредоносной программы и вызывает ее, как если бы это был вызов cpuid.
Основное поведение
Основная цель бэкдора — успешно перехватить исполнение определенных функций, которые позволят ему отслеживать каждое соединение с зараженной машиной. К таким функциям относятся:
Целевая функция
Описание
RSA_public_decrypt
Эта функция реализована в библиотеке libcrypto и используется для расшифровки зашифрованного текста, подписанного закрытым ключом.
EVP_PKEY_set1_RSA
Эта функция реализована в библиотеке libcrypto и используется для установки RSA-ключа для данного контекста.
RSA_get0_key
Эта функция реализована в библиотеке libcrypto и используется для получения ключа RSA.
Однако организовать перехват исполнения для всех этих функций — не самая простая задача для бэкдора. Ему потребуется обращаться к внутренним структурам и манипулировать структурами, специфичными для процессов, чтобы достичь цели. И все это должно происходить незаметно, поэтому вредоносный код содержит множество проверок, чтобы затруднить анализ.
Проверка среды выполнения
Бэкдор в первую очередь извлекает различную информацию об окружении и проверяет, соответствует ли текущий процесс, в котором запущен бэкдор, критериям выполнения:
Текущий процесс — /usr/bin/sshd.
Отсутствие «аварийного выключателя».
Бэкдор извлекает имя текущего процесса из argv[0], а также анализирует все переменные окружения процесса на предмет присутствия во внутренней таблице строк. Если найдено хоть одно совпадение или процесс не является sshd, вредоносная программа прекращает свое выполнение.
Единственная переменная внутри таблицы — это yolAbejyiejuvnup=Evjtgvsh5okmkAvj, которая в данном контексте служит в роли аварийного выключателя.
Структура «префиксное дерево» (trie)
Одной из отличительных особенностей бэкдора является использование единой структуры префиксное дерево для операций со строками. Вместо того чтобы напрямую сравнивать строки или использовать хеши строк для поиска определенной константы (например, имени библиотечной функции), код выполняет поиск по дереву и проверяет, равен ли результат определенной численной константе. Например, возвращенное значение 0x300 соответствует магическому числу заголовка ELF файла, а возвращенное значение 0x9F8 — функции system. Префиксное дерево используется не только для сравнения. Например, некоторые функции, использующие указатели на строки (например, ssh-2.0), применяют префиксное дерево для поиска строк в бинарном файле хоста. Это позволяет скрыть подозрительные строки в теле бэкдора.
В реализованном префиксном дереве используются битовые маски по 16 байт, первая половина которых соответствует диапазону входных байтов 0x00–0x3F, а вторая — диапазону 0x40–0x7F. Под узлы листа дерева выделяется по два байта, три бита из которых служат флагами (направление, завершение), а остальные зарезервированы под значение (или расположение следующего узла).
Резолвер символов
Существует как минимум три процедуры, связанные с резолвингом символов, которые используются бэкдором для поиска структуры символов ELF, содержащей такую информацию, как символическое имя и его смещение. Все функции резолвера символов получают ключ, который следует искать в префиксном дереве.
Одна из функций резолвера бэкдора перебирает все символы и проверяет, какой из них содержит нужный ключ. Если таковой находится, она возвращает структуру Elf64_Sym, которая впоследствии потребуется для наполнения внутренней структуры бэкдора, содержащей все необходимые указатели функций. Схожий процесс встречается в атаках на Windows, построенных на основе получения API по хешу его имени.
Например, бэкдор ищет различные функции из библиотеки libcrypto (OpenSSL), поскольку те используются в последующих процедурах шифрования. Он также отслеживает, сколько функций ему удалось найти и отрезолвить. Так он определяет, правильно ли он работает или ему следует прекратить.
Еще один интересный резолвер символов эксплуатирует функцию lzma_alloc, которая является частью библиотеки liblzma. Эта функция помогает разработчикам эффективно выделять память с помощью стандартного (malloc) или собственного аллокатора. В случае с бэкдором XZ эта функция задействуется для использования поддельного аллокатора. В действительности он функционирует как еще один резолвер символов. Параметр, отвечающий за размер выделяемой памяти, на самом деле является ключом символа внутри префиксного дерева. Эта уловка призвана усложнить анализ бэкдора.
Бэкдор динамически выполняет резолвинг своих символов в процессе выполнения в хаотичном порядке, вероятно для затруднения анализа и отвлечения внимания. Резолвингу подвергаются различные символы/функции, начиная от легитимных функций OpenSSL и заканчивая такими функциями, как system, используемыми для выполнения команд на машине.
Перехват Symbind
Как уже упоминалось ранее, основная цель инициализации бэкдора — успешно реализовать перехват ислолнения функций. Для этого бэкдор использует rtdl-audit, функцию динамического компоновщика (ld-linux), позволяющую создавать свои разделяемые библиотеки, чтобы получать уведомления об определенных событиях, происходящих в компоновщике, например, о разрешении символов. В типичном сценарии разработчик создает разделяемую библиотеку по руководству rtdl-audit. Однако бэкдор XZ предпочитает модифицировать в процессе выполнения уже зарегистрированные (стандартные) интерфейсы, загруженные в память, тем самым перехватывая процедуру резолвинга символов.
Созданная злоумышленниками структура audit_iface, хранящаяся в глобальной переменной dl_audit в области памяти динамического компоновщика, содержит адрес обратного вызова symbind64, который вызывается динамическим компоновщиком. Она передает всю информацию о символе управляющему бэкдору, который затем используется для получения вредоносного адреса для целевых функций, что и позволяет реализовать перехват исполнения.
Адреса глобальных переменных dl_audit и dl_naudit, где хранится количество доступных интерфейсов аудита, можно получить путем дизассемблирования функций dl_main и dl_audit_symbind_alt. Бэкдор содержит простенький дизассемблер для декодирования инструкций. Он активно использует его, особенно при поиске специфических значений, таких как адреса *audit.
Адрес переменной dl_naudit обнаруживается по одной из инструкций mov в коде функции dl_main, которая к ней обращается. Затем бэкдор проверяет, совпадает ли полученный адрес в памяти с адресом, к которому обращалась функция dl_audit_symbind_alt Это позволяет бэкдору удостовериться, что найден правильный адрес. Выяснив адрес dl_naudit, он может легко вычислить, где находится dl_audit, поскольку обе функции хранятся в памяти рядом друг с другом.
Заключение
В этой статье мы рассмотрели весь процесс внедрения бэкдора в liblzma (XZ) и погрузились в детальный анализ бинарного кода бэкдора, вплоть до достижения его начальной цели — создания перехватчика исполнения авторизации клиента SSH-сервера.
Совершенно очевидно, что этот бэкдор устроен весьма сложно и использует изощренные методы, препятствующие его обнаружению. К ним относятся многоэтапное внедрение в репозиторий XZ, а также сложный код, содержащийся в самом бинарном файле.
Нам предстоит еще многое узнать о внутреннем устройстве бэкдора, поэтому эта статья будет первой из цикла статей о бэкдоре для XZ.
Продукты «Лаборатории Касперского» обнаруживает и определяет вредоносные объекты, связанные с этой атакой, как HEUR:Trojan.Script.XZ и Trojan.Shell.XZ. Кроме того, Kaspersky Endpoint Security для Linux обнаруживает вредоносный код в памяти процесса SSHD как MEM:Trojan.Linux.XZ (в рамках проверки важных областей).
Индикаторы компрометации
Правила YARA
rule liblzma_get_cpuid_function {
meta:
description = «Rule to find the malicious get_cpuid function CVE-2024-3094»
author = «Kaspersky Lab»
strings:
$a = { F3 0F 1E FA 55 48 89 F5 4C 89 CE 53 89 FB 81 E7 00 00 00 80 48 83 EC 28 48 89 54 24 18 48 89 4C 24 10 4C 89 44 24 08 E8 ?? ?? ?? ?? 85 C0 74 27 39 D8 72 23 4C 8B 44 24 08 48 8B 4C 24 10 45 31 C9 48 89 EE 48 8B 54 24 18 89 DF E8 ?? ?? ?? ?? B8 01 00 00 00 EB 02 31 C0 48 83 C4 28 5B 5D C3 }
condition:
$a
}
Известные библиотеки с бэкдором
Debian Sid liblzma.so.5.6.0
4f0cf1d2a2d44b75079b3ea5ed28fe54
72e8163734d586b6360b24167a3aff2a3c961efb
319feb5a9cddd81955d915b5632b4a5f8f9080281fb46e2f6d69d53f693c23ae
Debian Sid liblzma.so.5.6.1
53d82bb511b71a5d4794cf2d8a2072c1
8a75968834fc11ba774d7bbdc566d272ff45476c
605861f833fc181c7cdcabd5577ddb8989bea332648a8f498b4eef89b8f85ad4
Связанные файлы
d302c6cb2fa1c03c710fa5285651530f, liblzma.so.5
4f0cf1d2a2d44b75079b3ea5ed28fe54, liblzma.so.5.6.0
153df9727a2729879a26c1995007ffbc, liblzma.so.5.6.0.patch
53d82bb511b71a5d4794cf2d8a2072c1, liblzma.so.5.6.1
212ffa0b24bb7d749532425a46764433, liblzma_la-crc64-fast.o
Проанализированные артефакты
86fc2c94f8fa3938e3261d0b9eb4836be289f8ae, bad-3-corrupt_lzma2.xz
c86c8f8a69c07fbec8dd650c6604bf0c9876261f, build-to-host.m4
50941ad9fd99db6fca5debc3c89b3e899a9527d7, good-large_compressed.lzma
Securelist