Инциденты

Бэкдор в XZ: анализ перехватчика

Часть 1. История с бэкдором в XZ — первоначальный анализ
Часть 2. Инцидент с XZ Utils: как дошли до жизни такой (социальная инженерия в инциденте с проектом XZ)
Часть 3. Бэкдор в XZ: анализ перехватчика

В нашей первой статье по бэкдору в XZ мы проанализировали его код с момента первоначального заражения и до момента создания перехватчика. Как мы отметили в материале, изначальная задача злоумышленников заключалась в перехвате функций, отвечающих за манипуляции с RSA-ключами. В этой статье мы сосредоточимся на поведении бэкдора уже внутри ПО OpenSSH, а именно в переносимой версии OpenSSH 9.7p1, самой актуальной на текущий момент.

Для лучшего понимания контекста рекомендуем ознакомиться со статьей на ресурсе Baeldung, посвященной методам аутентификации в SSH, и с материалом в блоге JFrog о разделении привилегий в SSH.

Основные выводы

Наш анализ выявил следующие интересные функциональные особенности бэкдора:

  • Злоумышленник реализовал защиту от повторного воспроизведения (anti-replay), чтобы третьи лица, которым удалось перехватить и записать передаваемые бэкдором данные на одном из зараженных серверов, не смогли атаковать другие.
  • Автор бэкдора применил ранее неизвестную технику стеганографии в коде x86, позволяющую спрятать в нем произвольное сообщение, — оригинальный способ скрыть открытый ключ шифрования.
  • Бэкдор скрывает логи несанкционированных подключений к SSH-серверу посредством перехвата функции логирования.
  • Бэкдор перехватывает функцию аутентификации по паролю, позволяя злоумышленнику входить на зараженный сервер с любым именем пользователя и паролем без дополнительных проверок. То же самое происходит и с функцией аутентификации по открытому ключу.
  • Бэкдор позволяет злоумышленнику удаленно выполнять любые системные команды на зараженном сервере.

Детальный анализ

Бэкдор пытается перехватить три функции. Его главная цель — RSA_public_decrypt, второстепенная — RSA_get0_key. Третьей функции, EVP_PKEY_set1_RSA, в рассматриваемой версии SSH-сервера нет. Возможно, это артефакт, оставшийся от инструмента для генерации вредоносных открытых ключей (например, эта функция встречается в отдельной утилите ssh-keygen из пакета OpenSSH), либо эта функция использовалась в какой-то редкой или устаревшей версии SSH-сервера.

Две целевые функции, которые присутствуют в последней версии сервера SSH, вызываются в том случае, если в качестве метода аутентификации используется RSA-сертификат. Проверив валидность указателя на ключ, перехватчик целевой функции передает его в общий обработчик (вызываемой всеми перехватчиками), который анализирует RSA-ключ и извлекает информацию из его части. Основная функция полезной нагрузки бэкдора срабатывает только один раз за сеанс preauth клиента, когда выполняются проверки аутентификации по открытому ключу.

Функция перехвата RSA_public_decrypt

Функция перехвата RSA_public_decrypt

Для взаимодействия с зараженным сервером злоумышленнику нужно создать специальный ключ RSA. Ключ будет выступать в роли контейнера для передачи команд злоумышленника через SSH-соединения с использованием сертификатов.

Ключ RSA представлен структурой в библиотеке OpenSSL и состоит из E (экспоненты) и N (модуля). Бэкдор извлекает и обрабатывает модуль RSA-ключа, что означает, что вредоносная полезная нагрузка упаковывается внутри значения N криптосистемы RSA.

Чтобы модуль RSA-ключа правильно обрабатывался бэкдором, он должен соответствовать следующему формату:

Структура данных модуля ключа RSA

Структура данных модуля ключа RSA

В заголовке полезной нагрузки есть три поля (PartialCommand1, PartialCommand2 и PartialCommand3 на схеме выше), которые используются для определения типа команды и проверки магического числа. Тип команды рассчитывается по следующей формуле: PartialCommand3 + (PartialCommand2 * PartialCommand1), результатом которой является значение от 0 до 3:

Вычисление типа команды

Вычисление типа команды

Если проверка пройдена успешно, код приступит к расшифровке полезной нагрузки и проверке ее подписи.

Извлечение зашифрованного открытого ключа ED448 — стеганография на базе кода x86

Для расшифровки и проверки данных полезной нагрузки бэкдор использует открытый ключ ED448, который извлекается из бинарного кода.

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

Информация об открытом ключе была внедрена частями в бинарный код отдельных валидных инструкций. Метод восстановления ключа чем-то напоминает сканирование «гаджетов» в сценариях эксплуатации бинарного кода по методу возвратно-ориентированного программирования (ROP). Но здесь «гаджетами» на самом деле являются инструкции «регистр-регистр» (например, mov rdi, rbx), каждая из которых содержит по одному биту информации со значением 1 или 0.

Для восстановления ключа некоторые функции (обычно в начале выполнения) вызывают алгоритм «восстановления ключа» с определенными аргументами.

Вызов функции частичного восстановления ключа

Вызов функции частичного восстановления ключа

Аргументы, используемые этим алгоритмом:

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

Декодирование инструкций «регистр-регистр»

Алгоритм восстановления ключа сканирует определенные функции бэкдора от начала до конца в поисках инструкций «регистр-регистр», перечисленных на картинке выше. Когда алгоритм находит такую инструкцию, он декодирует значение индекса бита, чтобы извлечь правильные индекс байта и устанавливаемый бит.

Фрагмент кода восстановления зашифрованного ключа

Фрагмент кода восстановления зашифрованного ключа

Значение индекса в битовом поле распаковывается для определения целевого индекса в байтовом буфере. Затем бэкдор прибавляет (битовая операция ИЛИ) бит к текущему значению по этому индексу. Поскольку буфер зашифрованного открытого ключа инициализируется нулями, алгоритм восстановления меняет определенные биты внутри него на единицы, если инструкция «регистр-регистр» соответствует заданным критериям (см. изображение выше). В противном случае он пропускает его и оставляет 0. После этого значение индекса бита увеличивается на 1.

Алгоритм определяет, нужно ли устанавливать бит, для каждой инструкции по отдельности, даже если эти инструкции после дизассемблирования выглядят одинаково. Это связано с тем, что некоторые инструкции могут иметь одинаковые ассемблерные мнемоники, но различные коды операций. Например инструкцию xor eax, eax можно закодировать как кодами 31 C0 так и 33 C0. Эту избыточность кодирования некоторых инструкций и использовали авторы бэкдора для создания данного вида стеганографии.

Алгоритм восстановления открытого ключа

Алгоритм восстановления открытого ключа

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

Автоматизация восстановления ключа

Автоматизация восстановления ключа

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

Расшифровка полезной нагрузки и проверка подписи

Открытый ключ ED448 зашифрован по алгоритму ChaCha20. Ключ и одноразовый код (nonce) генерируются путем шифрования буфера, состоящего из нулей, тем же алгоритмом ChaCha20 с использованием нулей в качестве ключа и одноразового кода.

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

Схема расшифровки полезной нагрузки бэкдора и проверки

Схема расшифровки полезной нагрузки бэкдора и проверки

Проверка подписи полезной нагрузки

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

Проверка подписи полезной нагрузки

Проверка подписи полезной нагрузки

При создании и проверке подписи полезной нагрузки в число подписанных данных включается хэш SHA-256 открытого ключа сервера (этот ключ пересылается сервером клиенту при первоначальном SSH-рукопожатии сразу после установления соединения). Таким образом бэкдор проверяет, соответствует ли подписанная полезная нагрузка текущему серверу. Этот дополнительный шаг сделан для предотвращения атак повторного воспроизведения — чтобы некое третье лицо не могло перехватить данные, передаваемые бэкдором, и отправить тот же запрос на другой зараженный сервер.

Схема противодействия атакам повторного воспроизведения

Схема противодействия атакам повторного воспроизведения

Если все проверки пройдены, код приступает к анализу аргументов указанной команды бэкдора. Бэкдор может выполнять команды в двух режимах: в экземпляре процесса Monitor SSH сервера с правами root или в экземпляре процесса NetworkProcessing, работающего с ограниченными правами пользователя, обычно имеющего имя sshd. Однако операции в режиме без прав root, похоже, мало интересуют злоумышленника: в обработчике этого режима содержатся лишь обрывки неработающего экспериментального кода, поэтому мы опишем поведение бэкдора в режиме root.

Команды бэкдора

Команда определяется по результатам расчета полей заголовка в модуле предъявленного клиентом открытого RSA ключа. Основные команды бэкдора, по сути, позволяют злоумышленникам войти на сервер с правами обычного или root-пользователя и выполнить произвольные системные команды. В этом разделе мы опишем, что делает каждая команда.

Обход SSH-аутентификации

Команды 0 и 1 активируют вход атакующего на SSH-сервер под именем произвольного пользователя, в том числе (опционально) пользователя root, даже если он был запрещен в настройках сервера. Кроме того, эти команды при необходимости запрещают использование сервером подключаемых модулей аутентификации (PAM). Далее, в зависимости от команды, перехватывается одна из двух функций OpenSSH: mm_answer_authpassword, если получена команда 1, или mm_answer_keyallowed, если получена команда 0.

Основной код команды 1

Основной код команды 1

Перехват функции mm_answer_authpassword позволяет злоумышленнику входить на зараженный сервер с любым именем пользователя и паролем без дополнительных проверок. О функции mm_answer_keyallowed мы подробнее расскажем позже.

Удаленное выполнение команд через функцию system

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

Фрагмент кода удаленного выполнения команды

Фрагмент кода удаленного выполнения команды

Последняя команда, с номером 3, просто прекращает работу бэкдора, завершая сеанс предварительной аутентификации и процесс.

По завершении основной функции перехвата бэкдор заменяет входящий открытый RSA-ключ пустым, чтобы предотвратить регистрацию в логах любых вредоносных данных от злоумышленника.

Перехват mm_answer_keyallowed

По команде 0 бэкдор перехватывает функцию mm_answer_keyallowed. Эта функция вызывается привилегированным процессом Monitor, когда непривилегированный sshd-процесс NetworkProcessing получает метод publickey запроса ssh-userauth, что позволяет бэкдору обрабатывать дополнительные команды на этом этапе.

В этом методе обработки выполняются аналогичные действия: извлекается модуль RSA-ключа, присланного в запросе, расшифровывается и проверяется его подпись, как мы описывали ранее. Доступны следующие подкоманды:

  • Подкоманда 1:
    • Перехватывает функцию mm_answer_authpassword, что дает возможность входить в систему с любым паролем.
  • Подкоманда 2:
    • Перехватывает функцию mm_answer_keyverify. Этот перехват, по сути, позволяет входящему ключу пройти процесс аутентификации без каких-либо проверок и успешно завершить авторизацию с недействительным поддельным ключом.
  • Подкоманда 3:
    • Аналогично вышеописанной основной команде 2 эта подкоманда позволяет вызывать функцию system с произвольными аргументами и возможностью предварительного выбора идентификатора группы или пользователя.

Возможности скрытия событий в логах

Бэкдор также может скрывать события в логах, связанные с несанкционированными подключениями к SSH-серверу. С этой целью реализован перехват функции ведения логов и опциональный вызов setlogmask(0x80000000) из libc, после которого все сообщения, отправляемые в системный лог, будут игнорироваться. Аргумент 0x80000000 фактически устанавливает для маски лога значение 0, поскольку нулевой аргумент маски является специальным зарезервированным значением.

Перехватчик фильтрует сообщения логов, выполняя их поиск по префиксному дереву, описанному в нашей первой статье о бэкдоре XZ. Его дальнейшие действия зависят от найденного целевого сообщения (если таковое имеется):

Фрагмент кода фильтрации логов

Фрагмент кода фильтрации логов

Доступные фильтры логов:

Сообщение Как обрабатывается
Connection closed by X (Соединение было закрыто X) Если маска системного лога libc была ранее обнулена, временно восстанавливает ее значение по умолчанию 255, что разрешает все сообщения в системном логе и позволяет зарегистрировать это сообщение.
Снова отключает сообщения в системном логе путем обнуления маски лога.
Accepted password for X (Принят пароль для X)
Accepted publickey for X (Принят открытый ключ для X)
Заменяет эти сообщения об успешном подключении на сообщения о неудачных попытках аутентификации. Также временно включает, а затем отключает маску системного лога, если она была ранее обнулена.
Все остальные сообщения в логах Отфильтровываются (не выводятся).

Заключение

После трех публикаций о XZ-бэкдоре мы можем констатировать, что перед нами действительно сложная угроза со множеством особенностей, в том числе уникальных. Например, сведения об открытом ключе встроены непосредственно в бинарный код, что затрудняет его восстановление, а первоначальное заражение полагалось на тщательно спланированную долгосрочную кампанию социальной инженерии.

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

Решения «Лаборатории Касперского» обнаруживают и определяют вредоносные объекты, связанные с этой атакой, как HEUR:Trojan.Script.XZ и Trojan.Shell.XZ. Кроме того, Kaspersky Endpoint Security для Linux обнаруживает вредоносный код в памяти процесса sshd как MEM:Trojan.Linux.XZ (в рамках задачи сканирования критических областей).

Бэкдор в XZ: анализ перехватчика

Ваш e-mail не будет опубликован. Обязательные поля помечены *

 

Отчеты

CloudSorcerer: новая APT-угроза, нацеленная на российские государственные организации

«Лаборатория Касперского» обнаружила новую APT-угрозу CloudSorcerer, нацеленную на российские государственные организации и использующую облачные службы в качестве командных серверов аналогично APT CloudWizard.

StripedFly: двуликий и незаметный

Разбираем фреймворк StripedFly для целевых атак, использовавший собственную версию эксплойта EternalBlue и успешно прикрывавшийся майнером.

Подпишитесь на еженедельную рассылку

Самая актуальная аналитика – в вашем почтовом ящике