Sploitus

Exploit for Type Confusion in Google Android

githubexploit Β· 2026-08-05

Exploit Code

README107 lines
## https://sploitus.com/exploit?id=C1C09006-5FC5-5696-969E-A039990A5524
# CVE-2023-20768 on the Samsung Galaxy M32 β€” a reachability study

**Summary.** CVE-2023-20768 is a type confusion (CWE-843) in MediaTek's ION allocator. I checked whether it is actually exploitable on a Samsung SM-M325F (Galaxy M32, Helio G80) running the July 2022 firmware. The vulnerable code is present, and I proved that it runs on this device when triggered by an unprivileged process. I was not able to weaponize it. The ioctl path is bounded by input validation, and the path that contains the real out-of-bounds read only ever processes genuine ION buffers, because a second check on the `dma_buf_ops` pointer rejects anything I could forge. This writeup covers how I got to that conclusion, including one intermediate conclusion that turned out to be wrong.

The CVE is public and patched. All testing was done on my own device, rooted with Magisk.

## Why this device

The bug is in MediaTek's ION code, not in upstream Linux or in anything Samsung wrote. MediaTek ships its own fork of the Android ION allocator in the BSP that goes to every vendor building on their chips. The M32 uses a Helio G80, so it gets that code. The same phone with an Exynos SoC would not be affected at all.

## Root cause

I pulled the 2022 (vulnerable) and 2023 (patched) `vmlinux` images and diffed them in IDA. Two ION functions changed:

| Function | 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` does not exist in the 2022 image. It appears in the 2023 one. So both functions were deciding whether a `struct file` was a dma_buf by looking at a name, and the fix replaced that with a real type check. Confusing an object here means the kernel reads a non-dma_buf as if it were one.

## Reaching the code from userspace

Of the two, `_ion_ioctl` is the one an unprivileged process can reach:

```
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")
```

The request is an `ion_custom_data { u32 cmd = 0; u64 arg; }` pointing at a 120 byte `ion_sys_data`:

| Offset | Field |
| --- | --- |
| `+0x00` | `sys_cmd` = 0 |
| `+0x08` | ion handle (allocate one first, `heap_id_mask = 0x1` works) |
| `+0x10` | user virtual address |
| `+0x18` | low half = size, high half = sync_type in {0,1,2} |

`spoof.c` builds this.

### Proving it dispatches

I could not just trace it. The device blocks `kprobe_events`, `set_ftrace_filter` and `function_graph` through Samsung's SELinux policy and kernel hardening, and ION's printks are debug-gated so dmesg stays quiet.

So I used return codes as an oracle instead. Four requests, and the pattern of what comes back tells you where execution went:

| Case | Request | Result |
| --- | --- | --- |
| A | `sys_cmd=0`, spoofed VA | `-EFAULT` |
| B | `sys_cmd=0`, VA = 0 | `0` |
| C | `sys_cmd=4` | `0` |
| D | `sys_cmd=99` | `-EFAULT` |

C returning success means the switch really is dispatching on `sys_cmd`. A and B differing means the VA is being processed, which puts execution inside `find_vma`. That is the vulnerable path, reached without root.

## Where it stopped

Reaching the check is not the same as beating it. I tested what the `strcmp` actually accepts:

- A real ION buffer mapped via `ION_IOC_SHARE` then `mmap` passes, returns 0.
- A memfd named `memfd:ion` fails, `-EFAULT`.
- An ordinary file literally named `ion` also fails, `-EFAULT`.

So the field being compared at `vm_file+0x60` is not the filename. It is internal to dma_buf, almost certainly `dma_buf->exp_name`, which ION sets to `"ion"`. The check is unsound by design, but nothing I can create from userspace can set that field.

I then fuzzed the path: sync_type 0 through 7, sizes {0, 1, 0x1000, 0x100000, 0xffffffff}, VAs {real ion buffer, memfd, 0}, 120 cases, plus a freed-handle probe for a use-after-free. No crash, device stayed up. Oversized sizes bail out before `find_vma` and return 0. sync types 3 to 5 hit the m4u path and return `-EPERM`. Anything above 5 returns `-EINVAL`. A freed handle returns `-EINVAL`, so ION validates it and there is no UAF there.

## The other function, and a wrong turn

The actual out-of-bounds read is in `ion_drv_file_to_buffer`. It does `ldr [private_data+0x28]`, reading a non-dma_buf's `private_data` as if it were a dma_buf. There is an `ops == &ion_dma_buf_ops` comparison (the table is at `0xFFFFFF800A097F18`), but it happens *after* that read, so it does not prevent it. Downstream, `__do_dump_share_fd` reads fields at `+0x28`, `+0x48`, `+0x50`, `+0xb8`, `+0xe4` off the returned buffer and prints them, and `ldr x8, [buf+0x28]; ldr [x8+0x30]` is a wild pointer deref for a confused object.

The trigger is `ion_dump_all_share_fds`, which uses `iterate_fd` to walk every ION client process's file descriptors. My first conclusion was that this only runs when you read an ION debugfs node, and this kernel has `CONFIG_DEBUG_FS` unset. I verified that three ways: `/proc/config.gz`, debugfs missing from `/proc/filesystems`, and `mount -t debugfs` returning ENODEV. I wrote the path off as structurally unreachable.

That was wrong. `dump_header`, the OOM killer's memory dump, contains a direct `bl ion_mm_heap_memory_detail` at `0xffffff8008204b9c`, and `dump_header` is called from `out_of_memory` and `oom_kill_process`. No debugfs needed.

`memcg_oom.c` confirms it. It creates a cgroup under `/dev/memcg`, caps both `memory.limit_in_bytes` and `memory.memsw.limit_in_bytes` at 8MB (capping only the first lets the child escape into zram swap), and forks a child that allocates until it dies. dmesg then shows:

```
dump_header <- oom_kill_process <- out_of_memory <- mem_cgroup_oom_synchronize
```

followed by the full `ion_mm_heap_memory_detail` and `__do_dump_share_fd` output resolving real gralloc dma_bufs. So the vulnerable function executes, during something any unprivileged process can cause. There is a third trigger too, `ShowStatus` from `hang_detect_dump_thread` in the MediaTek watchdog.

## Why it still does not work

I held 32 memfds named `memfd:dmabuf` and triggered the same memcg OOM. If one of mine had been fed to `ion_drv_file_to_buffer`, the `strstr` would pass, `private_data` would be NULL, and the kernel would print `[ION]ion_drv_file_to_buffer warnning, dmabuf is NULL` at KERN_ERR. That line never appeared. Only graphics and gralloc clients got dumped. Either a plain `/dev/ion` client's fd table is not what `iterate_fd` walks here, or memfd fails silently before the print.

Either way the OOM dump only ever handles the system's legitimate dma_bufs, which pass cleanly. memfd is also the only fd type whose name I can control enough to contain "dmabuf", and it cannot fault.

## Verdict

The vulnerability is present. The vulnerable code is reachable and does execute on this build, triggerable without root. It is not weaponizable from userspace here. The ioctl path is bounded by handle validation, a size check and `access_ok`. The dump path only sees real ION buffers, and forgery is blocked by `ops == &ion_dma_buf_ops`. Getting further would need a controllable non-ION object whose `exp_name` is `"ion"`, or a different primitive such as an ION buffer UAF, or a TOCTOU race.

## Files

- `spoof.c` β€” cache-sync ioctl PoC and the differential oracle
- `memcg_oom.c` β€” memcg OOM trigger for the dump path
- `trigger.c`, `oom_trigger.c` β€” earlier trigger attempts
- `boot_images/` β€” extracted kernel images (2022 and 2023) and the IDA database for the 2022 build