
Análisis de CVE del kernel de Android y PoC para una confusión de tipos en el asignador de ION de MediaTek, que abarca el diffing de la causa raíz, la activación sin privilegios y la evaluación de explotabilidad.
Resumen. CVE-2023-20768 es una confusión de tipos (CWE-843) en el asignador ION de MediaTek. Comprobé si realmente es explotable en un Samsung SM-M325F (Galaxy M32, Helio G80) con el firmware de julio de 2022. El código vulnerable está presente y demostré que se ejecuta en este dispositivo cuando lo activa un proceso sin privilegios. No pude convertirlo en un arma. La ruta de ioctl está limitada por la validación de entrada, y la ruta que contiene la auténtica lectura fuera de límites solo procesa buffers ION genuinos, porque una segunda comprobación sobre el puntero dma_buf_ops rechaza cualquier cosa que pudiera falsificar. Este informe cubre cómo llegué a esa conclusión, incluida una conclusión intermedia que resultó ser errónea.
El CVE es público y está parcheado. Todas las pruebas se realizaron en mi propio dispositivo, rooteado con Magisk.
El fallo está en el código ION de MediaTek, no en el Linux ascendente ni en nada escrito por Samsung. MediaTek distribuye su propia bifurcación del asignador ION de Android en el BSP que llega a todos los fabricantes que usan sus chips. El M32 usa un Helio G80, así que recibe ese código. El mismo teléfono con un SoC Exynos no se vería afectado en absoluto.
Extraje las imágenes vmlinux de 2022 (vulnerable) y 2023 (parcheada) y las comparé en IDA. Dos funciones de ION cambiaron:
| Función | 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 no existe en la imagen de 2022. Aparece en la de 2023. Así que ambas funciones decidían si un struct file era un dma_buf mirando un nombre, y el parche sustituyó eso por una comprobación de tipo real. Confundir un objeto aquí significa que el kernel lee un no-dma_buf como si lo fuera.
De las dos, _ion_ioctl es la que un proceso sin privilegios puede alcanzar:
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")
La solicitud es un ion_custom_data { u32 cmd = 0; u64 arg; } que apunta a un ion_sys_data de 120 bytes:
spoof.c construye esto.
No podía simplemente rastrearlo. El dispositivo bloquea kprobe_events, set_ftrace_filter y function_graph mediante la política SELinux y el endurecimiento del kernel de Samsung, y los printk de ION están detrás de compilación condicional de depuración, así que dmesg permanece silencioso.
Así que usé los códigos de retorno como oráculo. Cuatro solicitudes, y el patrón de lo que devuelve te dice dónde fue la ejecución:
Que C devuelva éxito significa que el switch realmente despacha según sys_cmd. Que A y B difieran significa que la VA se está procesando, lo que sitúa la ejecución dentro de find_vma. Esa es la ruta vulnerable, alcanzada sin root.
Alcanzar la comprobación no es lo mismo que superarla. Probé qué acepta realmente el strcmp:
ION_IOC_SHARE y luego mmap pasa, devuelve 0.memfd:ion falla, -EFAULT.ion también falla, -EFAULT.Así que el campo que se compara en vm_file+0x60 no es el nombre de archivo. Es interno a dma_buf, casi con seguridad dma_buf->exp_name, que ION establece como "ion". La comprobación es insegura por diseño, pero nada que yo pueda crear desde espacio de usuario puede establecer ese campo.
Luego fuzzée la ruta: sync_type de 0 a 7, tamaños {0, 1, 0x1000, 0x100000, 0xffffffff}, VAs {buffer ION real, memfd, 0}, 120 casos, más una sonda con handle liberado para un use-after-free. Sin caídas, el dispositivo siguió funcionando. Los tamaños demasiado grandes salen antes de find_vma y devuelven 0. Los sync types de 3 a 5 entran en la ruta m4u y devuelven -EPERM. Cualquier valor superior a 5 devuelve -EINVAL. Un handle liberado devuelve -EINVAL, así que ION lo valida y no hay UAF ahí.
La lectura real fuera de límites está en ion_drv_file_to_buffer. Hace ldr [private_data+0x28], leyendo el private_data de un no-dma_buf como si fuera un dma_buf. Hay una comparación ops == &ion_dma_buf_ops (la tabla está en 0xFFFFFF800A097F18), pero ocurre después de esa lectura, así que no la impide. Más abajo, __do_dump_share_fd lee campos en +0x28, +0x48, +0x50, +0xb8, +0xe4 del buffer devuelto y los imprime, y ldr x8, [buf+0x28]; ldr [x8+0x30] es un desreferencia de puntero salvaje para un objeto confundido.
El desencadenante es ion_dump_all_share_fds, que usa iterate_fd para recorrer los descriptores de archivo de cada proceso cliente de ION. Mi primera conclusión fue que esto solo se ejecuta cuando se lee un nodo debugfs de ION, y este kernel tiene CONFIG_DEBUG_FS desactivado. Lo verifiqué de tres maneras: /proc/config.gz, debugfs ausente de /proc/filesystems, y mount -t debugfs devolviendo ENODEV. Descarté la ruta como estructuralmente inalcanzable.
Eso era incorrecto. dump_header, el volcado de memoria del asesino OOM, contiene un bl ion_mm_heap_memory_detail directo en 0xffffff8008204b9c, y dump_header se llama desde out_of_memory y oom_kill_process. No se necesita debugfs.
memcg_oom.c lo confirma. Crea un cgroup bajo /dev/memcg, limita tanto memory.limit_in_bytes como memory.memsw.limit_in_bytes a 8MB (limitar solo el primero permite que el hijo escape al swap de zram), y hace fork de un hijo que asigna memoria hasta morir. dmesg muestra entonces:
dump_header <- oom_kill_process <- out_of_memory <- mem_cgroup_oom_synchronize
seguido de la salida completa de ion_mm_heap_memory_detail y __do_dump_share_fd resolviendo gralloc dma_bufs reales. Así que la función vulnerable se ejecuta, durante algo que cualquier proceso sin privilegios puede provocar. Hay un tercer desencadenante también, ShowStatus de hang_detect_dump_thread en el watchdog de MediaTek.
Mantuve 32 memfds llamados memfd:dmabuf y desencadené el mismo OOM de memcg. Si uno de los míos hubiera sido alimentado a ion_drv_file_to_buffer, el strstr pasaría, private_data sería NULL, y el kernel imprimiría [ION]ion_drv_file_to_buffer warnning, dmabuf is NULL en KERN_ERR. Esa línea nunca apareció. Solo se volcaron clientes de gráficos y gralloc. O bien la tabla de fds de un cliente normal de /dev/ion no es lo que iterate_fd recorre aquí, o memfd falla silenciosamente antes de la impresión.
De cualquier manera, el volcado de OOM solo maneja los dma_bufs legítimos del sistema, que pasan limpiamente. memfd también es el único tipo de fd cuyo nombre puedo controlar lo suficiente como para contener "dmabuf", y no puede fallar.
La vulnerabilidad está presente. El código vulnerable es alcanzable y de hecho se ejecuta en esta compilación, y se puede activar sin root. No es armable desde espacio de usuario aquí. La ruta de ioctl está limitada por la validación del handle, una comprobación de tamaño y access_ok. La ruta de volcado solo ve buffers ION reales, y la falsificación está bloqueada por ops == &ion_dma_buf_ops. Ir más allá requeriría un objeto no-ION controlable cuyo exp_name sea "ion", o una primitiva diferente como un UAF de buffer ION, o una carrera TOCTOU.
spoof.c — PoC del ioctl de cache-sync y el oráculo diferencialmemcg_oom.c — desencadenante de OOM por memcg para la ruta de volcadotrigger.c, oom_trigger.c — intentos de activación anterioresboot_images/ — imágenes del kernel extraídas (2022 y 2023) y la base de datos de IDA para la compilación de 2022| Desplazamiento | Campo |
|---|
+0x00 | sys_cmd = 0 |
+0x08 | handle de ION (asigna uno primero; heap_id_mask = 0x1 funciona) |
+0x10 | dirección virtual de usuario |
+0x18 | mitad baja = tamaño, mitad alta = sync_type en {0,1,2} |
| Caso | Solicitud | Resultado |
|---|
| A | sys_cmd=0, VA falsificada | -EFAULT |
| B | sys_cmd=0, VA = 0 | 0 |
| C | sys_cmd=4 | 0 |
| D | sys_cmd=99 | -EFAULT |