Skip to content

Latest commit

 

History

History
166 lines (107 loc) · 24.3 KB

File metadata and controls

166 lines (107 loc) · 24.3 KB

Copy Fail (CVE-2026-31431) - Порт на C

English (en)日本語 (ja)简体中文 (zh-cn)한국어 (ko)Русский (ru)

Кроссплатформенная реализация на C эксплойта Copy Fail для Linux (CVE-2026-31431). Уязвимость была раскрыта 29 апреля 2026 года исследователями Theori / Xint. Полное описание уязвимости, хронологию и процесс обнаружения Theori можно найти в официальном отчете на copy.fail.

Публично выпущенный Proof-of-Concept (PoC) представляет собой Python-скрипт размером 732 байта. Данный порт на C демонстрирует, что тот же самый эксплойт может быть написан на переносимом C, который компилируется под любую архитектуру, поддерживаемую nolibc, без использования специфичных для архитектуры шестнадцатеричных блоков (hex blobs) или встроенного ассемблера в исходном коде самого проекта.

Автор этого порта: Tony Gies tony.gies@crashunited.com. Обнаружение и исходное раскрытие: Theori / Xint.

Структура репозитория

copy-fail-c/
├── exploit.c           дроппер (вариант с мутацией бинарного файла)
├── exploit-passwd.c    дроппер (вариант с подменой UID в /etc/passwd)
├── vulnerable.c        неразрушающая проверка уязвимости
├── payload.c           сбрасываемая полезная нагрузка (setgid+setuid+execve sh)
├── utils.c, utils.h    общий примитив мутации page cache через AF_ALG/splice
├── Makefile            оркестрация сборки
├── nolibc/             код, заимствованный из torvalds/linux tools/include/nolibc
└── README.md           этот файл

После выполнения make:

├── payload             крошечный статический ELF, встроенный в дроппер в виде байтов
├── payload.o           полезная нагрузка, обернутая в перемещаемый .o с помощью `ld -r -b binary`
├── exploit             дроппер, вариант с мутацией бинарного файла
├── exploit-passwd      дроппер, вариант с подменой UID в /etc/passwd
└── vulnerable          неразрушающая проверка уязвимости

exploit.c открывает целевой бинарный файл только для чтения, затем для каждого 4-байтового окна встроенной полезной нагрузки запускает одну фиктивную операцию AEAD-расшифровки через AF_ALG, где зашифрованный ввод поставляется через splice() из страниц кэша (page-cache) целевого файла. Оптимизация "на месте" (in-place) шаблона authencesn рассматривает исходные страницы, переданные через splice, одновременно как ввод шифротекста и место назначения для открытого текста, поэтому обреченная на неудачу расшифровка уже перезаписывает страницу кэша к тому моменту, когда проверка подлинности отклоняет запрос. После 4 * N итераций закэшированный образ целевого файла побайтово заменяется полезной нагрузкой. Выполнение execve() для целевого файла загружает мутировавшие страницы; inode на диске по-прежнему имеет бит setuid root, поэтому ядро предоставляет root-привилегии и запускает полезную нагрузку.

payload.c — это обычный переносимый C: setgid(0); setuid(0); execve("/bin/sh", ...). nolibc предоставляет _start, механизмы системных вызовов и работу с регистрами, специфичную для каждой архитектуры.

Второй вариант, exploit-passwd.c, мутирует четыре байта в кэше страниц файла /etc/passwd вместо образа setuid-бинарника. Ему не нужна встроенная полезная нагрузка, и он работает на системах, где путь с мутацией бинарного файла заблокирован, но его поверхность для успешной эксплуатации (cashout) гораздо уже.

vulnerable.c не является эксплойтом. Он создаёт в рабочей директории локальный файл testfile со строкой init, после чего запускает тот же примитив patch_chunk() против собственного кэша страниц этого файла, пытаясь перезаписать байты строкой vulnerable. Если прочитанное содержимое совпадает, работающее ядро находится в зоне действия CVE-2026-31431. Inode на диске не модифицируется; testfile удаляется при выходе, и мутация кэша страниц исчезает вместе с ним. Запускается без привилегий. Возвращает код выхода 100 при наличии уязвимости, 0 если примитив выполнился, но мутация не сработала, 2 если семейство сокетов AF_ALG или шаблон authencesn недоступны и состояние патча нельзя определить, и 1 для других ошибок выполнения.

Сборка

По умолчанию (нативно для архитектуры хоста):

make

Кросс-компиляция под aarch64 (или любую другую архитектуру Linux, для которой установлен кросс-тулчейн):

make CC=aarch64-linux-gnu-gcc LD=aarch64-linux-gnu-ld

Архитектуры, поддерживаемые включенной (vendored) версией nolibc (в соответствии с апстримом): x86_64, i386, arm, aarch64, riscv32/64, mips, ppc, s390x, loongarch, m68k, sh, sparc. nolibc осуществляет диспетчеризацию на основе макросов архитектуры компилятора, поэтому достаточно выбрать правильные CC/LD.

Требования для сборки:

  • Компилятор C (cc, gcc или любой кросс-компилятор)
  • Линкер, поддерживающий ld -r -b binary (подходят как binutils ld, так и lld)
  • Заголовочные файлы ядра UAPI, предоставляющие linux/if_alg.h и <asm/unistd.h> (Debian/Ubuntu: linux-libc-dev; для кросс-компиляции: обычно подтягиваются пакетом кросс-тулчейна)

Наборы заголовков старше Linux 5.6 не содержат __kernel_old_time_t и struct __kernel_old_timespec, которые использует включённый (vendored) nolibc. compat.h (принудительно включаемый в сборку полезной нагрузки) предоставляет их при отсутствии, поэтому сборка проходит и со старым linux-libc-dev. На заголовках 5.6 и новее он не даёт никакого эффекта.

Зависимости от внешних библиотек отсутствуют. Полезная нагрузка (payload) собирается автономно (freestanding) с использованием nolibc; дроппер линкуется с системной libc только для использования fprintf и perror.

Архитектурные решения

Несколько небольших возможностей тулчейна берут на себя основную задачу по сохранению переносимости исходного кода и небольшого размера полезной нагрузки.

nolibc

nolibc/ — это крошечная замена libc от ядра, состоящая только из заголовочных файлов, скопированная из tools/include/nolibc/ репозитория torvalds/linux. Она предоставляет _start, переносимый макрос syscall() и встроенные обертки системных вызовов, причем соглашения о регистрах для конкретных архитектур закодированы в nolibc/arch-*.h. Сборка полезной нагрузки с флагами -nostdlib -static -ffreestanding -Inolibc создает крошечный статический ELF, который обращается к ядру напрямую, не подтягивая код запуска glibc, инициализацию TLS или механизмы stack-canary. Результат, после упаковки и удаления заголовков секций (то и другое описано ниже): ~720 байт на x86_64, ~1.2 КБ на aarch64 по сравнению с ~17 КБ для того же payload.c, слинкованного с musl-static, или ~700 КБ с glibc-static.

ld -r -b binary для встраивания

Makefile превращает собранный ELF-файл payload в payload.o с помощью команды ld -r -b binary -o payload.o payload. Линкер выводит входные байты "как есть" в качестве секции данных перемещаемого объектного файла и синтезирует три символа из имени входного файла:

_binary_payload_start    адрес первого байта полезной нагрузки
_binary_payload_end      адрес байта, следующего за последним байтом полезной нагрузки
_binary_payload_size     абсолютный символ, значением которого является размер в байтах

exploit.c объявляет первые два символа как extern const unsigned char[] и вычисляет размер как _binary_payload_end - _binary_payload_start.

-Wl,-N и жесткий max-page-size

Полезная нагрузка статически линкуется с опциями -Wl,-N -Wl,-z,max-page-size=0x10, что сворачивает секции .text/.rodata/.data в единый сегмент LOAD с выравниванием файла по 16 байтам вместо значения по умолчанию (выравнивание по страницам ядра, 4 КБ на сегмент). Из-за этого ld выдает предупреждение об «RWX permissions» (разрешениях на чтение, запись и выполнение), которое носит исключительно информационный характер — защита памяти во время выполнения не имеет значения для данной узконаправленной программы. Без этого флага тот же код линкуется в файл размером ~13 КБ на x86_64 (в основном из-за заполнения нулями между сегментами); с ним размер составляет ~1.3 КБ перед удалением заголовков секций, описанным ниже.

Удаление заголовков секций

После линковки objcopy --strip-section-headers удаляет из полезной нагрузки таблицу заголовков секций и .shstrtab. Загрузчик ELF в ядре отображает программу только по её заголовкам программы (program headers), поэтому эти байты никогда не загружаются во время выполнения, а поскольку payload встраивается «как есть», они вдобавок увеличивают объём сброса и число итераций patch_chunk. Их удаление уменьшает полезную нагрузку на x86_64 с ~1.3 КБ до 720 байт (с 322 четырёхбайтовых итераций до 180). Остальное срезают два флага времени линковки: -Wl,--build-id=none убирает заметку build-id, а -fcf-protection=none убирает заметку x86 CET там, где её поддерживает компилятор.

Для удаления требуется binutils >= 2.40. При кросс-сборке objcopy для целевой архитектуры передаётся через OBJCOPY= (например, OBJCOPY=aarch64-linux-gnu-objcopy); если objcopy не может выполнить операцию, сборка выводит примечание и сохраняет корректную, но более крупную полезную нагрузку.

Варианты эксплойта и их жизнеспособность

В этом репозитории представлены два варианта эксплойта, которые используют один и тот же примитив мутации кэша страниц через AF_ALG/splice, но добиваются выполнения кода от имени root (cashout) по-разному. Профили их надежности неодинаковы, и эта разница имеет значение при анализе реальных моделей угроз.

Вариант с мутацией бинарного файла (exploit)

Мутирует кэш страниц целевого setuid-бинарника, внедряя байты полезной нагрузки, а затем выполняет (exec) этот бинарник. Ядро предоставляет root-привилегии на основе нетронутого бита setuid файла на диске, загружает поврежденный образ из памяти и запускает полезную нагрузку.

Работает везде, где злоумышленник может выполнить open(target, O_RDONLY) для любого root-setuid бинарника в системе. В той или иной степени блокируется в средах, где setuid-бинарники скрыты за директориями с ограниченными правами на чтение, а также в системах, спроектированных без использования setuid.

Вариант с подменой UID в /etc/passwd (exploit-passwd)

Мутирует четыре байта в кэше страниц файла /etc/passwd, чтобы установить поле UID текущего пользователя в «0000». Файл /etc/passwd доступен для чтения всем (world-readable) в любой стандартной системе Linux, поэтому сама мутация работает универсально. Однако превращение этого в выполнение кода от имени root зависит от того, будет ли какой-либо процесс со стороны root разрешать имя пользователя через getpwnam/getpwuid и действовать на основе полученного uid без перекрестной проверки. Существует множество таких потребителей; многие из них в целях защиты производят перекрестную проверку с представлением ядра о uid вызывающего процесса или с владельцем файла на диске, что ломает эту цепочку эксплуатации.

Матрица жизнеспособности эксплуатации

Цель эксплуатации (Cashout) Требуется подготовка до root Примечания
Создание сессии WSL2 Нет Вызов setuid(getpwnam(default_user)->pw_uid) для каждой сессии в WSL не выполняет никаких проверок. Работает чисто.
util-linux su Нет Снисходительная обработка идентификатора вызывающего процесса.
shadow-utils su Да Проверка идентификатора вызывающего через getpwuid(getuid()) завершается неудачей, так как мутация отвязывает реальный uid.
sshd (по умолчанию StrictModes yes) Да (отключить StrictModes) StrictModes требует, чтобы домашняя директория принадлежала root или pw->pw_uid. В результате мутации pw_uid становится равным 0, тогда как владелец на диске остается прежним (исходный uid); из-за этого несоответствия аутентификация отклоняется.
Локальная доставка почты MTA (postfix, exim и т.д.) Зависит от системы Зависит от проверки прав доступа к домашней директории агентом доставки (MDA). Требует тестирования для каждого конкретного MTA.

Смена вектора атаки (Pivot) после неудачи su

exploit-passwd выполняет su <user> после мутации как самый простой вариант эксплуатации. Это срабатывает против util-linux su, но терпит неудачу против shadow-utils su с ошибкой "Cannot determine your user name." (Невозможно определить имя пользователя). В этот момент мутация кэша страниц все еще активна, и возможен переход к любому другому способу эксплуатации (например, использование демона, разрешающего пользователей через getpwnam без перекрестных проверок). Выполните echo 3 > /proc/sys/vm/drop_caches от имени root, чтобы очистить поврежденный кэш страниц по завершении тестирования.

Уязвимые версии ядра

Нижняя граница:  torvalds/linux 72548b093ee3   Август 2017 г., v4.14
                                              (Переработка AF_ALG iov_iter,
                                               которая представила примитив
                                               записи на страницу файла через splice
                                               в scatterlist AEAD)

Верхняя граница: torvalds/linux a664bf3d603d   Апрель 2026 г., mainline
                                              (Отменяет оптимизацию algif_aead
                                               "на месте" (in-place) 2017 года;
                                               разделяет исходный и целевой scatterlist,
                                               чтобы страницы кэша (page-cache)
                                               больше не могли быть целевым адресом
                                               для записи при криптографической операции)

В промежутке: ядра всех основных дистрибутивов, в которые не был бэкпортирован (backport) патч. Ubuntu, RHEL, SUSE, Amazon Linux и Debian — все они были подтверждены как уязвимые в своих стандартных ядрах для облачных образов на момент раскрытия информации. Бэкпорты на уровне дистрибутивов начали выпускаться примерно 29 апреля 2026 года, параллельно с публичным раскрытием. Чтобы проверить, находится ли целевое ядро в уязвимом диапазоне, убедитесь, присутствует ли коммит a664bf3d603d (или его специфичный для дистрибутива бэкпорт) в логах git ядра или в changelog дистрибутива.

Коммерческая поддержка

По вопросам платного аудита безопасности, заказного портирования или непубличных уведомлений о безопасности обращайтесь к автору через его консалтинговую компанию Crash United, LLC.

Контакты: tony.gies@crashunited.com · https://crashunited.com
GitHub: @tgies · X/Twitter: @me_irl

Лицензия и благодарности

Обнаружение и первоначальное раскрытие CVE-2026-31431: Theori / Xint. Публичный отчет: https://copy.fail/.

Данный порт на C: Tony Gies tony.gies@crashunited.com

nolibc/: заимствовано (vendored) из дерева исходного кода ядра Linux; имеет двойное лицензирование LGPL-2.1-or-later ИЛИ MIT (см. nolibc/nolibc.h и SPDX-заголовки отдельных файлов).

Исходные коды дроппера и полезной нагрузки в этом репозитории выпущены на тех же условиях двойного лицензирования LGPL-2.1-or-later ИЛИ MIT, что и дерево nolibc, от которого они зависят. Это сделано для того, чтобы сохранить тривиальную совместимость лицензий для всех, кто решит включить этот каталог целиком в свои собственные разработки.

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