## https://sploitus.com/exploit?id=KITPLOIT:TOOLS-GITHUB-MURF-XD-CVE-2023-20768
# CVE-2023-20768 на Samsung Galaxy M32 — исследование достижимости
**Резюме.** CVE-2023-20768 — это путаница типов (CWE-843) в аллокаторе ION от MediaTek. Я проверил, реально ли она эксплуатируется на Samsung SM-M325F (Galaxy M32, Helio G80) с прошивкой от июля 2022 года. Уязвимый код присутствует, и я доказал, что он выполняется на этом устройстве, когда его запускает непривилегированный процесс. Превратить это в эксплойт мне не удалось. Путь ioctl ограничен проверками ввода, а путь, содержащий настоящее чтение за пределами буфера, обрабатывает только настоящие буферы ION, потому что вторая проверка указателя `dma_buf_ops` отклоняет всё, что я мог подделать. В этом отчёте описано, как я пришёл к такому выводу, включая один промежуточный вывод, который оказался ошибочным.
Уязвимость публичная и уже закрыта патчем. Все тесты проводились на моём собственном устройстве с root-доступом через Magisk.
## Почему именно это устройство
Ошибка находится в коде ION от MediaTek, а не в апстрим-Linux и не в том, что написала Samsung. MediaTek поставляет собственный форк аллокатора Android ION в BSP, который достаётся каждому вендору, делающему устройства на их чипах. В M32 стоит Helio G80, поэтому этот код туда попадает. Тот же телефон с SoC Exynos вообще не был бы затронут.
## Корневая причина
Я вытащил образы `vmlinux` 2022 (уязвимый) и 2023 (исправленный) и сравнил их в IDA. Изменились две функции ION:
Функция| 2022| 2023
---|---|---
`ion_drv_file_to_buffer`| `strstr(name, "dmabuf")`| `is_dma_buf_file()`
`_ion_ioctl`| `strcmp(name, "ion")`| `is_dma_buf_file()`
`is_dma_buf_file` не существует в образе 2022 года. Она появляется в версии 2023. То есть обе функции определяли, является ли `struct file` буфером dma_buf, по имени, а исправление заменило это настоящей проверкой типа. Если перепутать объект здесь, ядро читает не-dma_buf так, как будто это dma_buf.
## Как добраться до кода из пользовательского пространства
Из этих двух функций `_ion_ioctl` — та, до которой может добраться непривилегированный процесс:
root@kitploit:~
open("/dev/ion")
-> ion_ioctl (.unlocked_ioctl)
-> ION_IOC_CUSTOM (0xC0104906)
-> ion_custom_ioctl
-> _ion_ioctl
-> case 0: ION_SYS_CACHE_SYNC
-> find_vma(user_VA) (call site at _ion_ioctl+0x9f0)
-> strcmp(vma->vm_file...name, "ion")
Запрос — это `ion_custom_data { u32 cmd = 0; u64 arg; }`, указывающий на 120-байтовую структуру `ion_sys_data`:
`spoof.c` формирует такой запрос.
### Доказательство диспетчеризации
Я не мог просто трассировать это. Устройство блокирует `kprobe_events`, `set_ftrace_filter` и `function_graph` политикой SELinux от Samsung и усилением ядра, а printk'и ION спрятаны за debug-проверкой, поэтому dmesg молчит.
Поэтому вместо этого я использовал коды возврата как оракул. Четыре запроса — и по тому, что возвращается, видно, куда пошло выполнение:
Успех в случае C означает, что switch действительно диспетчеризует по `sys_cmd`. Различие между A и B означает, что VA обрабатывается, а это помещает выполнение внутрь `find_vma`. Это и есть уязвимый путь, достижимый без root.
## Где это остановилось
Дойти до проверки — не значит обойти её. Я проверил, что именно принимает `strcmp`:
* Настоящий буфер ION, отображённый через `ION_IOC_SHARE`, а затем `mmap`, проходит проверку и возвращает 0.
* memfd с именем `memfd:ion` не проходит, возвращается `-EFAULT`.
* Обычный файл, названный буквально `ion`, тоже не проходит, возвращается `-EFAULT`.
Значит, поле, сравниваемое по смещению `vm_file+0x60`, — это не имя файла. Оно внутреннее для dma_buf, почти наверняка `dma_buf->exp_name`, которое ION устанавливает в `"ion"`. Проверка некорректна по своей конструкции, но ничто, что я могу создать из пользовательского пространства, не может установить это поле.
Затем я фаззил этот путь: sync_type от 0 до 7, размеры {0, 1, 0x1000, 0x100000, 0xffffffff}, VA {настоящий ion-буфер, memfd, 0} — 120 случаев, плюс проверка с освобождённым handle на предмет use-after-free. Ни одного краха, устройство осталось в строю. Слишком большие размеры отсекаются до `find_vma` и возвращают 0. sync_type с 3 по 5 попадают в путь m4u и возвращают `-EPERM`. Всё, что больше 5, возвращает `-EINVAL`. Освобождённый handle возвращает `-EINVAL`, то есть ION его валидирует, и UAF здесь нет.
## Другая функция и ложный след
Настоящее чтение за пределами буфера находится в `ion_drv_file_to_buffer`. Она выполняет `ldr [private_data+0x28]`, читая `private_data` не-dma_buf так, будто это dma_buf. Там есть сравнение `ops == &ion_dma_buf_ops` (таблица по адресу `0xFFFFFF800A097F18`), но оно происходит _после_ этого чтения, поэтому не предотвращает его. Далее по цепочке `__do_dump_share_fd` читает поля `+0x28`, `+0x48`, `+0x50`, `+0xb8`, `+0xe4` из возвращённого буфера и печатает их, а `ldr x8, [buf+0x28]; ldr [x8+0x30]` — это разыменование дикого указателя для перепутанного объекта.
Триггер — `ion_dump_all_share_fds`, которая с помощью `iterate_fd` обходит файловые дескрипторы каждого процесса-клиента ION. Сначала я заключил, что она запускается только при чтении узла debugfs у ION, а в этом ядре `CONFIG_DEBUG_FS` не задан. Я проверил это тремя способами: `/proc/config.gz`, отсутствие debugfs в `/proc/filesystems` и `mount -t debugfs`, возвращающий ENODEV. Я списал этот путь как структурно недостижимый.
Это было ошибкой. `dump_header` — дамп памяти OOM-киллера — содержит прямой вызов `bl ion_mm_heap_memory_detail` по адресу `0xffffff8008204b9c`, а `dump_header` вызывается из `out_of_memory` и `oom_kill_process`. Никакой debugfs не нужен.
`memcg_oom.c` это подтверждает. Он создаёт cgroup в `/dev/memcg`, ограничивает и `memory.limit_in_bytes`, и `memory.memsw.limit_in_bytes` до 8MB (ограничение только первого позволяет потомку уйти в своп zram), и форкает дочерний процесс, который выделяет память, пока не умрёт. Затем dmesg показывает:
root@kitploit:~
dump_header <- oom_kill_process <- out_of_memory <- mem_cgroup_oom_synchronize
за которым следует полный вывод `ion_mm_heap_memory_detail` и `__do_dump_share_fd`, показывающий настоящие dma_buf-буферы gralloc. То есть уязвимая функция выполняется во время события, которое может вызвать любой непривилегированный процесс. Есть и третий триггер — `ShowStatus` из `hang_detect_dump_thread` в сторожевом таймере MediaTek.
## Почему это всё равно не работает
Я держал 32 memfd с именем `memfd:dmabuf` и вызвал тот же memcg OOM. Если бы один из моих попал в `ion_drv_file_to_buffer`, `strstr` прошла бы, `private_data` был бы NULL, и ядро напечатало бы `[ION]ion_drv_file_to_buffer warnning, dmabuf is NULL` с уровнем KERN_ERR. Эта строка ни разу не появилась. В дамп попали только клиенты графики и gralloc. Либо таблица fd обычного клиента `/dev/ion` — не то, что `iterate_fd` обходит в этом месте, либо memfd молча отбрасывается ещё до печати.
В любом случае OOM-дамп обрабатывает только легитимные dma_buf-буферы системы, которые проходят проверку без проблем. К тому же memfd — единственный тип fd, имя которого я могу контролировать настолько, чтобы оно содержало «dmabuf», а он не может вызвать сбой.
## Вердикт
Уязвимость присутствует. Уязвимый код достижим и реально выполняется в этой сборке, запускается без root. Превратить его в эксплойт из пользовательского пространства здесь нельзя. Путь ioctl ограничен валидацией handle, проверкой размера и `access_ok`. Путь дампа видит только настоящие буферы ION, а подделка блокируется проверкой `ops == &ion_dma_buf_ops`. Чтобы продвинуться дальше, нужен контролируемый не-ION объект, у которого `exp_name` равен `"ion"`, либо другой примитив вроде UAF в буфере ION или гонка TOCTOU.
## Файлы
* `spoof.c` — PoC для cache-sync ioctl и дифференциальный оракул
* `memcg_oom.c` — memcg OOM-триггер для пути дампа
* `trigger.c`, `oom_trigger.c` — более ранние попытки триггеров
* `boot_images/` — извлечённые образы ядра (2022 и 2023) и база IDA для сборки 2022 года