|
| 1 | +# Abusing Android Media Pipelines & Image Parsers |
| 2 | + |
| 3 | +{{#include ../../banners/hacktricks-training.md}} |
| 4 | + |
| 5 | +## Delivery: Messaging Apps ➜ MediaStore ➜ Privileged Parsers |
| 6 | + |
| 7 | +Modern OEM builds regularly run privileged media indexers that rescan `MediaStore` for "AI" or sharing features. On Samsung firmware prior to the April 2025 patch, `com.samsung.ipservice` loads Quram (`/system/lib64/libimagecodec.quram.so`) and automatically parses any file WhatsApp (or other apps) drops into `MediaStore`. In practice an attacker can send a DNG disguised as `IMG-*.jpg`, wait for the victim to tap "download" (1-click), and the privileged service will parse the payload even if the user never opens the gallery. |
| 8 | + |
| 9 | +```bash |
| 10 | +$ file IMG-2025-02-10.jpeg |
| 11 | +TIFF image data ... |
| 12 | +$ exiftool IMG-2025-02-10.jpeg | grep "Opcode List" |
| 13 | +Opcode List 1 : [opcode 23], [opcode 23], ... |
| 14 | +``` |
| 15 | + |
| 16 | +**Key takeaways** |
| 17 | +- Delivery relies on system media re-parsing (not the chat client) and thus inherits that process' permissions (full read/write access to the gallery, ability to drop new media, etc.). |
| 18 | +- Any image parser reachable through `MediaStore` (vision widgets, wallpapers, AI résumé features, etc.) becomes remotely reachable if the attacker can convince a target to save media. |
| 19 | + |
| 20 | +## Quram's DNG Opcode Interpreter Bugs |
| 21 | + |
| 22 | +DNG files embed three opcode lists applied at different decode stages. Quram copies Adobe's API, but its Stage-3 handler for `DeltaPerColumn` (opcode ID 11) trusts attacker-supplied plane bounds. |
| 23 | + |
| 24 | +### Failing plane bounds in `DeltaPerColumn` |
| 25 | +- Attackers set `plane=5125` and `planes=5123` even though Stage-3 images only expose planes 0–2 (RGB). |
| 26 | +- Quram computes `opcode_last_plane = image_planes + opcode_planes` instead of `plane + count`, and never checks whether the resulting plane range fits inside the image. |
| 27 | +- The loop therefore writes a delta to `raw_pixel_buffer[plane_index]` with a fully controlled offset (e.g., plane 5125 ⇒ offset `5125 * 2 bytes/pixel = 0x2800`). Each opcode adds a 16-bit float value (0x6666) to the targeted location, yielding a precise heap OOB add primitive. |
| 28 | + |
| 29 | +### Turning increments into arbitrary writes |
| 30 | +- The exploit first corrupts Stage-3 `QuramDngImage.bottom/right` using 480 malformed `DeltaPerColumn` operations so future opcodes treat enormous coordinates as in-bounds. |
| 31 | +- `MapTable` opcodes (opcode 7) are then aimed at those fake bounds. Using a substitution table of all zeros or a `DeltaPerColumn` with `-Inf` deltas, the attacker zeroes any region, then applies additional deltas to write exact values. |
| 32 | +- Because the opcode parameters live inside the DNG metadata, the payload can encode hundreds of thousands of writes without touching process memory directly. |
| 33 | + |
| 34 | +## Heap Shaping Under Scudo |
| 35 | + |
| 36 | +Scudo buckets allocations by size. Quram happens to allocate the following objects with identical 0x30-byte chunk sizes, so they land in the same region (0x40-byte spacing on the heap): |
| 37 | +- `QuramDngImage` descriptors for Stage 1/2/3 |
| 38 | +- `QuramDngOpcodeTrimBounds` and vendor `Unknown` opcodes (ID ≥14, including ID 23) |
| 39 | + |
| 40 | +The exploit sequences allocations to deterministically place chunks: |
| 41 | +1. Stage-1 `Unknown(23)` opcodes (20,000 entries) spray 0x30 chunks that later get freed. |
| 42 | +2. Stage-2 frees those opcodes and places a new `QuramDngImage` inside the freed region. |
| 43 | +3. 240 Stage-2 `Unknown(23)` entries are freed, and Stage-3 immediately allocates its `QuramDngImage` plus a new raw pixel buffer of the same size, reusing those spots. |
| 44 | +4. A crafted `TrimBounds` opcode runs first in list 3 and allocates yet another raw pixel buffer before freeing Stage-2 state, guaranteeing "raw pixel buffer ➜ QuramDngImage" adjacency. |
| 45 | +5. 640 additional `TrimBounds` entries are marked `minVersion=1.4.0.1` so the dispatcher skips them, but their backing objects stay allocated and later become primitive targets. |
| 46 | + |
| 47 | +This choreography puts the Stage-3 raw buffer immediately before the Stage-3 `QuramDngImage`, so the plane-based overflow flips fields inside the descriptor rather than crashing random state. |
| 48 | + |
| 49 | +## Reusing Vendor "Unknown" Opcodes as Data Blobs |
| 50 | + |
| 51 | +Samsung leaves the high bit set in vendor-specific opcode IDs (e.g., ID 23), which instructs the interpreter to *allocate* the structure but skip execution. The exploit abuses those dormant objects as attacker-controlled heaps: |
| 52 | +- Opcode list 1 and 2 `Unknown(23)` entries serve as contiguous scratchpads for storing payload bytes (JOP chain at offset 0xf000 and a shell command at 0x10000 relative to the raw buffer). |
| 53 | +- Because the interpreter still treats each object as an opcode when list 3 is processed, commandeering one object's vtable later is enough to start executing attacker data. |
| 54 | + |
| 55 | +## Crafting Bogus `MapTable` Objects & Bypassing ASLR |
| 56 | + |
| 57 | +`MapTable` objects are larger than `TrimBounds`, but once the layout corruption lands, the parser happily reads extra parameters out-of-bounds: |
| 58 | +1. Use the linear write primitive to partially overwrite a `TrimBounds` vtable pointer with a crafted `MapTable` substitution table that maps lower 2 bytes from a neighbouring `TrimBounds` vtable to the `MapTable` vtable. Only the low bytes differ between supported Quram builds, so a single 64K lookup table can handle seven firmware versions and every 4 KB ASLR slide. |
| 59 | +2. Patch the rest of the `TrimBounds` fields (top/left/width/planes) so the object behaves like a valid `MapTable` when executed later. |
| 60 | +3. Execute the fake opcode over zeroed memory. Because the substitution table pointer actually references another opcode's vtable, the output bytes become *leaked* low-order addresses from `libimagecodec.quram.so` or its GOT. |
| 61 | +4. Apply additional `MapTable` passes to convert those two-byte leaks into offsets toward gadgets such as `__ink_jpeg_enc_process_image+64`, `QURAMWINK_Read_IO2+124`, `qpng_check_IHDR+624`, and libc's `__system_property_get` entry. The attackers effectively rebuild full addresses inside their sprayed opcode region without native memory disclosure APIs. |
| 62 | + |
| 63 | +## Triggering the JOP ➜ `system()` Transition |
| 64 | + |
| 65 | +Once the gadget pointers and shell command are staged inside the opcode spray: |
| 66 | +1. A final wave of `DeltaPerColumn` writes adds `0x0100` to offset 0x22 of the Stage-3 `QuramDngImage`, shifting its raw buffer pointer by 0x10000 so it now references the attacker command string. |
| 67 | +2. The interpreter starts executing the tail of 1040 `Unknown(23)` opcodes. The first corrupted entry has its vtable replaced with the forged table at offset 0xf000, so `QuramDngOpcode::aboutToApply` resolves `qpng_read_data` (the 4th entry) out of the fake table. |
| 68 | +3. The chained gadgets perform: load the `QuramDngImage` pointer, add 0x20 to point at the raw buffer pointer, dereference it, copy the result into `x19/x0`, then jump through GOT slots rewritten to `system`. Because the raw buffer pointer now equals the attacker string, the final gadget executes `system(<shell command>)` inside `com.samsung.ipservice`. |
| 69 | + |
| 70 | +## Notes on Allocator Variants |
| 71 | + |
| 72 | +Two payload families exist: one tuned for jemalloc, another for scudo. They differ in how opcode blocks are ordered to achieve adjacency but share the same logical primitives (DeltaPerColumn bug ➜ MapTable zero/write ➜ bogus vtable ➜ JOP). Scudo's disabled quarantine makes 0x30-byte freelist reuse deterministic, while jemalloc relies on size-class control via tile/subIFD sizing. |
| 73 | + |
| 74 | +## References |
| 75 | + |
| 76 | +- [Project Zero – A look at an Android ITW DNG exploit](https://projectzero.google/2025/12/android-itw-dng.html) |
| 77 | + |
| 78 | +{{#include ../../banners/hacktricks-training.md}} |
0 commit comments