
MediaTek ION 할당자 타입 혼동에 대한 Android 커널 CVE 분석 및 PoC로, 근본 원인 diffing, 비특권 트리거, 익스플로잇 가능성 평가를 다룹니다.
요약. CVE-2023-20768은 MediaTek ION 할당기의 타입 혼동(CWE-843) 취약점입니다. 저는 2022년 7월 펌웨어를 실행하는 삼성 SM-M325F(갤럭시 M32, Helio G80)에서 이 취약점이 실제로 악용 가능한지 확인했습니다. 취약한 코드는 존재하며, 권한이 없는 프로세스가 트리거하면 이 기기에서 실행된다는 것을 입증했습니다. 다만 무기화에는 실패했습니다. ioctl 경로는 입력 검증에 의해 제한되고, 실제 out-of-bounds 읽기가 있는 경로는 dma_buf_ops 포인터에 대한 두 번째 검사가 제가 위조할 수 있는 모든 것을 거부하기 때문에 진짜 ION 버퍼만 처리합니다. 이 문서는 그 결론에 도달한 과정을 다루며, 그중 하나는 결국 틀린 것으로 판명된 중간 결론도 포함합니다.
이 CVE는 공개되었고 패치되었습니다. 모든 테스트는 Magisk로 루팅한 제 기기에서 수행되었습니다.
이 버그는 upstream Linux나 삼성이 작성한 코드가 아니라 MediaTek의 ION 코드에 있습니다. MediaTek은 자사 칩 기반으로 제품을 만드는 모든 벤더에게 제공하는 BSP에 Android ION 할당기의 자체 포크를 포함합니다. M32는 Helio G80을 사용하므로 해당 코드가 적용됩니다. Exynos SoC를 탑재한 같은 폰은 전혀 영향을 받지 않습니다.
2022년(취약) 및 2023년(패치) vmlinux 이미지를 추출하여 IDA에서 diff했습니다. 두 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입니다:
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")
요청은 120바이트 ion_sys_data를 가리키는 ion_custom_data { u32 cmd = 0; u64 arg; }입니다:
| 오프셋 | 필드 |
|---|
spoof.c가 이를 구성합니다.
단순히 추적(trace)할 수는 없었습니다. 이 기기는 삼성의 SELinux 정책과 커널 하드닝을 통해 kprobe_events, set_ftrace_filter, function_graph를 차단하며, ION의 printk는 디버그 게이트가 적용되어 dmesg는 조용히 유지됩니다.
그래서 대신 리턴 코드를 오라클로 사용했습니다. 네 가지 요청을 보내고, 돌아오는 결과의 패턴을 보면 실행이 어디로 흘렀는지 알 수 있습니다:
C가 성공을 반환한다는 것은 switch가 실제로 sys_cmd를 기준으로 디스패치한다는 뜻입니다. A와 B의 결과가 다르다는 것은 VA가 처리되고 있다는 뜻이며, 이는 실행이 find_vma 내부로 들어갔다는 것을 의미합니다. 이것이 루트 없이 도달하는 취약 경로입니다.
검사에 도달하는 것과 검사를 우회하는 것은 다릅니다. strcmp가 실제로 무엇을 통과시키는지 테스트했습니다:
ION_IOC_SHARE로 매핑한 후 mmap한 실제 ION 버퍼는 통과하며 0을 반환합니다.memfd:ion인 memfd는 실패하며 -EFAULT를 반환합니다.ion인 일반 파일도 실패하며 -EFAULT를 반환합니다.따라서 vm_file+0x60에서 비교되는 필드는 파일 이름이 아닙니다. 이 필드는 dma_buf 내부의 것으로, 거의 확실히 ION이 "ion"으로 설정하는 dma_buf->exp_name입니다. 이 검사는 설계상 불완전하지만, 사용자 공간에서 제가 만들 수 있는 어떤 것으로도 이 필드를 설정할 수 없습니다.
그런 다음 이 경로를 퍼징했습니다: sync_type 07, 크기 {0, 1, 0x1000, 0x100000, 0xffffffff}, VA {실제 ion 버퍼, memfd, 0}로 총 120개 사례와 use-after-free를 확인하기 위한 해제된 핸들 프로브까지 테스트했습니다. 크래시는 없었고 기기는 계속 정상 작동했습니다. 과도한 크기는 5는 m4u 경로에 도달하여 find_vma 이전에 빠져나가 0을 반환합니다. sync 타입 3-EPERM을 반환합니다. 5보다 큰 값은 -EINVAL을 반환합니다. 해제된 핸들은 -EINVAL을 반환하므로 ION이 이를 검증하며, 여기에는 UAF가 없습니다.
실제 out-of-bounds 읽기는 ion_drv_file_to_buffer에 있습니다. 이 함수는 ldr [private_data+0x28]을 수행하여 dma_buf가 아닌 객체의 private_data를 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 클라이언트 프로세스의 파일 디스크립터를 순회합니다. 제 첫 번째 결론은 이것이 ION debugfs 노드를 읽을 때만 실행되고, 이 커널에는 CONFIG_DEBUG_FS가 설정되어 있지 않다는 것이었습니다. 세 가지 방법으로 확인했습니다: /proc/config.gz, /proc/filesystems에 debugfs가 없는 것, mount -t debugfs가 ENODEV를 반환하는 것. 그래서 이 경로를 구조적으로 도달 불가능하다고 판단했습니다.
그것은 틀렸습니다. OOM 킬러의 메모리 덤프인 dump_header에는 0xffffff8008204b9c 위치에 직접적인 bl ion_mm_heap_memory_detail 호출이 포함되어 있으며, dump_header는 out_of_memory와 oom_kill_process에서 호출됩니다. debugfs는 필요 없습니다.
memcg_oom.c가 이를 확인해 줍니다. 이 코드는 /dev/memcg 아래에 cgroup을 만들고 memory.limit_in_bytes와 memory.memsw.limit_in_bytes를 모두 8MB로 제한하며(첫 번째만 제한하면 자식이 zram 스왑으로 빠져나갈 수 있음), 죽을 때까지 메모리를 할당하는 자식 프로세스를 포크합니다. 그러면 dmesg에 다음이 표시됩니다:
dump_header <- oom_kill_process <- out_of_memory <- mem_cgroup_oom_synchronize
그 뒤에는 실제 gralloc dma_buf를 해석한 전체 ion_mm_heap_memory_detail 및 __do_dump_share_fd 출력이 이어집니다. 따라서 권한이 없는 프로세스가 유발할 수 있는 상황에서 취약 함수가 실행됩니다. 세 번째 트리거도 있습니다. MediaTek 워치독의 hang_detect_dump_thread에서 오는 ShowStatus입니다.
이름이 memfd:dmabuf인 memfd 32개를 보유한 상태에서 동일한 memcg OOM을 트리거했습니다. 제 memfd 중 하나가 ion_drv_file_to_buffer에 전달되었다면 strstr은 통과하고 private_data는 NULL이 되어 커널이 KERN_ERR 레벨로 [ION]ion_drv_file_to_buffer warnning, dmabuf is NULL을 출력했을 것입니다. 그 줄은 결코 나타나지 않았습니다. 그래픽 및 gralloc 클라이언트만 덤프되었습니다. 일반 /dev/ion 클라이언트의 fd 테이블이 여기서 iterate_fd가 순회하는 대상이 아니거나, memfd가 출력 이전에 조용히 실패하는 것입니다.
어느 쪽이든 OOM 덤프는 시스템의 정상적인 dma_buf만 처리하며, 이들은 문제없이 통과합니다. 또한 memfd는 이름에 "dmabuf"가 포함되도록 제가 충분히 제어할 수 있는 유일한 fd 유형이며, memfd는 폴트를 일으킬 수 없습니다.
취약점은 존재합니다. 취약한 코드는 이 빌드에서 도달 가능하고 실제로 실행되며, 루트 없이 트리거할 수 있습니다. 그러나 이 환경에서는 사용자 공간에서 무기화할 수 없습니다. ioctl 경로는 핸들 검증, 크기 검사 및 access_ok에 의해 제한됩니다. 덤프 경로는 실제 ION 버퍼만 볼 수 있으며, 위조는 ops == &ion_dma_buf_ops에 의해 차단됩니다. 더 나아가려면 exp_name이 "ion"인 제어 가능한 비-ION 객체, ION 버퍼 UAF와 같은 다른 프리미티브, 또는 TOCTOU 경합이 필요합니다.
spoof.c — cache-sync ioctl PoC 및 차등 오라클memcg_oom.c — 덤프 경로용 memcg OOM 트리거trigger.c, oom_trigger.c — 초기 트리거 시도boot_images/ — 추출된 커널 이미지(2022년, 2023년) 및 2022년 빌드용 IDA 데이터베이스+0x00 | sys_cmd = 0 |
+0x08 | ion 핸들(먼저 하나를 할당할 것, heap_id_mask = 0x1로 충분함) |
+0x10 | 사용자 가상 주소 |
+0x18 | 하위 절반 = 크기, 상위 절반 = sync_type({0,1,2}) |
| 사례 | 요청 | 결과 |
|---|
| A | sys_cmd=0, 스푸핑된 VA | -EFAULT |
| B | sys_cmd=0, VA = 0 | 0 |
| C | sys_cmd=4 | 0 |
| D | sys_cmd=99 | -EFAULT |