
Análise de CVE do kernel Android e PoC para uma confusão de tipo no alocador ION da MediaTek, abrangendo diffing de causa raiz, gatilho sem privilégios e avaliação de explorabilidade.
Resumo. CVE-2023-20768 é uma confusão de tipo (CWE-843) no alocador ION da MediaTek. Verifiquei se ela é realmente explorável em um Samsung SM-M325F (Galaxy M32, Helio G80) rodando o firmware de julho de 2022. O código vulnerável está presente, e provei que ele executa neste dispositivo quando acionado por um processo sem privilégios. Não consegui transformá-la em uma arma. O caminho do ioctl é limitado por validação de entrada, e o caminho que contém a verdadeira leitura fora dos limites só processa buffers ION genuínos, porque uma segunda verificação no ponteiro dma_buf_ops rejeita qualquer coisa que eu pudesse forjar. Este relatório cobre como cheguei a essa conclusão, incluindo uma conclusão intermediária que se revelou errada.
O CVE é público e já foi corrigido. Todos os testes foram feitos no meu próprio dispositivo, com root via Magisk.
O bug está no código ION da MediaTek, não no Linux upstream nem em nada escrito pela Samsung. A MediaTek distribui seu próprio fork do alocador ION do Android no BSP que vai para todos os fornecedores que usam seus chips. O M32 usa um Helio G80, então recebe esse código. O mesmo telefone com um SoC Exynos não seria afetado de forma alguma.
Extraí as imagens vmlinux de 2022 (vulnerável) e 2023 (corrigida) e as comparei no IDA. Duas funções do ION mudaram:
| Função | 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 não existe na imagem de 2022. Ela aparece na de 2023. Então ambas as funções decidiam se um struct file era um dma_buf olhando para um nome, e a correção substituiu isso por uma verificação de tipo real. Confundir um objeto aqui significa que o kernel lê um não-dma_buf como se fosse um.
Das duas, _ion_ioctl é a que um processo sem privilégios consegue alcançar:
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")
A requisição é uma ion_custom_data { u32 cmd = 0; u64 arg; } apontando para um ion_sys_data de 120 bytes:
spoof.c constrói isso.
Não pude simplesmente rastreá-lo. O dispositivo bloqueia kprobe_events, set_ftrace_filter e function_graph por meio da política SELinux da Samsung e do endurecimento do kernel, e os printks do ION são limitados por debug, então o dmesg permanece silencioso.
Então usei códigos de retorno como um oráculo. Quatro requisições, e o padrão do que retorna indica para onde a execução foi:
O caso C retornando sucesso significa que o switch realmente está despachando com base em sys_cmd. A e B diferirem significa que o VA está sendo processado, o que coloca a execução dentro de find_vma. Esse é o caminho vulnerável, alcançado sem root.
Alcançar a verificação não é o mesmo que vencê-la. Testei o que o strcmp realmente aceita:
ION_IOC_SHARE e depois mmap passa, retorna 0.memfd:ion falha, -EFAULT.ion também falha, -EFAULT.Portanto, o campo comparado em vm_file+0x60 não é o nome do arquivo. Ele é interno ao dma_buf, quase certamente dma_buf->exp_name, que o ION define como "ion". A verificação é insegura por design, mas nada que eu possa criar a partir do userspace consegue definir esse campo.
Então fiz fuzzing no caminho: sync_type de 0 a 7, tamanhos {0, 1, 0x1000, 0x100000, 0xffffffff}, VAs {buffer ION real, memfd, 0}, 120 casos, além de uma sonda de handle liberado para um use-after-free. Sem crash, o dispositivo permaneceu de pé. Tamanhos acima do limite saem antes de find_vma e retornam 0. Os sync types 3 a 5 caem no caminho do m4u e retornam -EPERM. Qualquer valor acima de 5 retorna -EINVAL. Um handle liberado retorna -EINVAL, então o ION o valida e não há UAF ali.
A verdadeira leitura fora dos limites está em ion_drv_file_to_buffer. Ela faz ldr [private_data+0x28], lendo o private_data de um não-dma_buf como se fosse um dma_buf. Há uma comparação ops == &ion_dma_buf_ops (a tabela está em 0xFFFFFF800A097F18), mas ela ocorre depois dessa leitura, então não a impede. Mais adiante, __do_dump_share_fd lê campos em +0x28, +0x48, +0x50, +0xb8, +0xe4 do buffer retornado e os imprime, e ldr x8, [buf+0x28]; ldr [x8+0x30] é um desreferenciamento de ponteiro selvagem para um objeto confundido.
O gatilho é ion_dump_all_share_fds, que usa iterate_fd para percorrer os descritores de arquivo de cada processo cliente do ION. Minha primeira conclusão foi que isso só roda quando você lê um nó do debugfs do ION, e este kernel tem CONFIG_DEBUG_FS desativado. Verifiquei isso de três maneiras: /proc/config.gz, debugfs ausente de /proc/filesystems e mount -t debugfs retornando ENODEV. Descartei o caminho como estruturalmente inalcançável.
Isso estava errado. dump_header, o despejo de memória do OOM killer, contém um bl ion_mm_heap_memory_detail direto em 0xffffff8008204b9c, e dump_header é chamado a partir de out_of_memory e oom_kill_process. Nenhum debugfs é necessário.
memcg_oom.c confirma isso. Ele cria um cgroup sob /dev/memcg, limita tanto memory.limit_in_bytes quanto memory.memsw.limit_in_bytes a 8MB (limitar apenas o primeiro permite que o filho escape para o swap zram), e faz um fork de um filho que aloca até morrer. O dmesg então mostra:
dump_header <- oom_kill_process <- out_of_memory <- mem_cgroup_oom_synchronize
seguido pela saída completa de ion_mm_heap_memory_detail e __do_dump_share_fd resolvendo dma_bufs gralloc reais. Portanto, a função vulnerável executa durante algo que qualquer processo sem privilégios pode causar. Há também um terceiro gatilho, ShowStatus de hang_detect_dump_thread no watchdog da MediaTek.
Segurei 32 memfds chamados memfd:dmabuf e acionei o mesmo OOM do memcg. Se algum dos meus tivesse sido passado para ion_drv_file_to_buffer, o strstr passaria, private_data seria NULL, e o kernel imprimiria [ION]ion_drv_file_to_buffer warnning, dmabuf is NULL em KERN_ERR. Essa linha nunca apareceu. Apenas clientes de gráficos e gralloc foram despejados. Ou a tabela de fd de um cliente comum de /dev/ion não é o que iterate_fd percorre aqui, ou o memfd falha silenciosamente antes do print.
De qualquer forma, o despejo do OOM só lida com os dma_bufs legítimos do sistema, que passam limpos. memfd também é o único tipo de fd cujo nome consigo controlar o suficiente para conter "dmabuf", e ele não consegue falhar.
A vulnerabilidade está presente. O código vulnerável é alcançável e de fato executa nesta compilação, acionável sem root. Ele não é transformável em arma a partir do userspace aqui. O caminho do ioctl é limitado pela validação do handle, uma verificação de tamanho e access_ok. O caminho do despejo só vê buffers ION reais, e a falsificação é bloqueada por ops == &ion_dma_buf_ops. Ir mais longe exigiria um objeto não-ION controlável cujo exp_name seja "ion", ou uma primitiva diferente, como um UAF de buffer ION, ou uma corrida TOCTOU.
spoof.c — PoC do ioctl de cache-sync e o oráculo diferencialmemcg_oom.c — gatilho de OOM do memcg para o caminho do despejotrigger.c, oom_trigger.c — tentativas anteriores de gatilhoboot_images/ — imagens de kernel extraídas (2022 e 2023) e o banco de dados do IDA para a compilação de 2022| Offset | Campo |
|---|
+0x00 | sys_cmd = 0 |
+0x08 | handle do ION (aloque um primeiro, heap_id_mask = 0x1 funciona) |
+0x10 | endereço virtual do usuário |
+0x18 | metade baixa = tamanho, metade alta = sync_type em {0,1,2} |
| Caso | Requisição | Resultado |
|---|
| A | sys_cmd=0, VA forjado | -EFAULT |
| B | sys_cmd=0, VA = 0 | 0 |
| C | sys_cmd=4 | 0 |
| D | sys_cmd=99 | -EFAULT |