
Анализ, POC и цепочка эксплойта для arm64, использующая переполнение кучи DACL в Linux cifs.ko (CVE-2026-97555) с повышением привилегий до перезаписи учётных данных uid-0 на эталонной сборке.
English | 简体中文
Переполнение кучи в ядре при перезаписи DACL в клиенте SMB. id_mode_to_cifs_acl()
вычисляет размер выходного дескриптора безопасности на основе DACL, возвращённого сервером,
но replace_sids_and_copy_aces() перезаписывает каждый ACE с новым
SID владельца/группы (до 68 байт). Поэтому сервер, возвращающий DACL с короткими SID,
заставляет результат перезаписи превысить размер выделения и выйти за пределы
объекта kmalloc.
Этот репозиторий содержит анализ, POC, воспроизводящий межобъектную запись, и EXP, превращающий переполнение в управляемую сквозную цепочку (утечка кучи, возврат страничных таблиц, произвольная запись PTE, перезапись собственных учётных данных до uid 0) на эталонной сборке.
| Пункт | Значение |
|---|---|
| CVE | CVE-2026-97555 |
| Класс | Переполнение буфера в куче (запись, CWE-122) в id_mode_to_cifs_acl() |
| Компонент | cifs.ko (клиент SMB) |
| Затронуто | Linux cifs.ko без исправления (проверено на ядре Android на базе GKI 6.12) |
| Патч | upstream 0ee150794c75 (stable fb736ab4fb8c) |
| Эталонная сборка | Ядро Android 16 на базе GKI 6.12, arm64, страницы 4 КиБ, struct page размером 64 байта |
| POC | Межобъектная запись воспроизведена (SET_INFO дескриптора безопасности размером 696 байт) |
| EXP | Полная цепочка до шелла с uid 0 на эталонной сборке |
/proc/kpageflags от имени root. Получение возможности монтирования
является тестовой обвязкой, а не частью уязвимости, и выходит за рамки
этого репозитория.uid/suid/euid/fsuid собственных учётных данных;
поля capability и securebits не затрагиваются. Очистка — перезагрузка.src/exp.c (адреса символов, геометрия vmemmap, смещения BTF)
относятся к эталонной сборке. Пересчитайте их для любой другой
сборки перед запуском; см. scripts/derive_offsets.sh.id_mode_to_cifs_acl() вычисляет размер выделения из dacl_ptr->size или
num_aces * sizeof(struct smb_ace), затем replace_sids_and_copy_aces()
перестраивает каждый ACE с новым SID владельца/группы. Когда SID сервера
короткие (0-subauth), а новый SID составляет 68 байт, перезаписанный DACL оказывается длиннее
выделения, и build_sec_desc() пишет за пределы объекта slab.
Динамическая инструментация FORTIFY в ядре проверяет встроенные записи
заголовков ACE, но копирования SID проходят через внешнюю cifs_copy_sid()
и не инструментированы. DACL, выровненный так, что каждый заголовок ACE остаётся внутри
nsecdesclen - 28, достигает уязвимого копирования.
Раскладка сервера cross7 (7 ACE, исходные размеры 76/76/76/44/44/16/16) делает
выделение ровно 512 байт; копирование 68-байтного SID 7-го ACE начинается со
смещения 492 и пересекает границу объекта, а перезаписанный дескриптор
вырастает до 696 байт — на 184 байта за пределы объекта. Расширенный дескриптор
всё равно передаётся в SET_INFO, поэтому запись наблюдаема со стороны сервера.
fb736ab4fb8c.kpageflags, mount,
chown) и окно с SELinux в режиме permissive (или эквивалентное разрешение политики)
для upcall cifs idmap.src/poc_smb2_acl.py, смонтированный с
cifsacl/modefromsid через стандартный клиент ядра
(scripts/mount_cifs.sh). Само монтирование — привилегированная обвязка; то, как
тестировщик получает эту привилегию, выходит за рамки этого репозитория.cifs.idmap для целевых id: в эталонном окружении
нет upcall-помощника /sbin/cifs.idmap (src/poc.c и src/exp.c
предварительно загружают их; src/acl_key_preload.c делает это отдельно).# host
python3 src/poc_smb2_acl.py # ACL_LAYOUT=cross7 for the overflow
adb reverse tcp:1468 tcp:1468
# device (root)
sh /data/local/tmp/e2e/mount_cifs.sh 127.0.0.1 share /mnt/cifs
make && make push
/data/local/tmp/poc /mnt/cifs
Ожидаемые свидетельства: журнал сервера печатает
SET_INFO security: len=696 ... (cross7), то есть дескриптор длиннее
512-байтового выделения, а scripts/trace_acl02.sh показывает
вход/возврат id_mode_to_cifs_acl вокруг chown.
POC лишь запускает уязвимый путь; он не выполняет груминг, утечку или повышение привилегий. Триггер выполняет запись за границы ядра: сборки, чей layout срабатывает на динамических проверках FORTIFY, могут паниковать, поэтому запускайте его только на тестовом устройстве, которое можно перезагрузить.
EXP — это полная цепочка, от начала до конца на эталонной сборке:
cross7 перезаписывает datalen полезной нагрузки соседнего ключа;
keyctl_read выгружает 64 КиБ, включая живой массив pipe_buffer. Утёкший указатель anon_pipe_buf_ops даёт смещение KASLR,
а страница буфера P даёт реальную цель struct page.buffer[0] одного hit-канала
перезаписывается значениями page=P, len=0x1234, ops=default_pipe_buf_ops.P; обращение по адресу
base возвращает её как страницу таблицы страниц, что подтверждается kretprobe, доказывающим, что
__alloc_pages_noprof вернул P из __pte_alloc.P+16 (PTE[2]); её первые восемь байт — это
дескриптор self-map для самой P. Затем пользовательское пространство читает и пишет
страницу таблицы страниц через base+8 КиБ.KPF_SLAB отображаются через
свежие PTE и сканируются на предмет comm этой задачи; pid/tgid, окно линейного отображения
и cred->uid/euid перекрёстно проверяют попадание, а свежие, никогда не использовавшиеся
VA повторно проверяют физическую страницу.setresuid(2000) (ядро выделяет свежий cred), читает task->cred через self-map,
обнуляет только uid/suid/euid/fsuid, подтверждает getuid()==0, отключает
self-map и выполняет execve для /system/bin/sh. Оболочка работает с uid 0.make exp
make push
# on the device, as root:
sh /data/local/tmp/e2e/launch_exp.sh /mnt/cifs 10 1 r1 4000 400 2400 7 0 0 cred
# then read /data/local/tmp/e2e/e2e_status.txt and
# /data/local/tmp/e2e/root_shell.txt
Этап cred включается опционально через последний аргумент (cred); scout останавливается
после валидации только для чтения. EXP хранит свои PTE внутри процесса,
а очистка выполняется перезагрузкой.
Требования: Android NDK r29 (переопределяется через NDK=), GNU make, adb.