daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

abusing-android-media-pipelines-image-parsers.md (14926B)


      1 ---
      2 title: "Abusing Android Media Pipelines & Image Parsers"
      3 section: "Mobile"
      4 sectionSlug: "mobile-pentesting"
      5 sourcePath: "src/mobile-pentesting/android-app-pentesting/abusing-android-media-pipelines-image-parsers.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/mobile-pentesting/android-app-pentesting/abusing-android-media-pipelines-image-parsers.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # Abusing Android Media Pipelines & Image Parsers
     14 
     15 ## Delivery: Messaging Apps ➜ MediaStore ➜ Privileged Parsers
     16 
     17 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.<sup>[[1]](#references)</sup>
     18 
     19 ```bash
     20 $ file IMG-2025-02-10.jpeg
     21 TIFF image data ...
     22 $ exiftool IMG-2025-02-10.jpeg | grep "Opcode List"
     23 Opcode List 1 : [opcode 23], [opcode 23], ...
     24 ```
     25 
     26 **Key takeaways**
     27 
     28 - 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.).
     29 - 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.
     30 
     31 ## 0-click DD+/EAC-3 decoding path (Google Messages ➜ mediacodec sandbox)
     32 
     33 Modern messaging stacks also auto-decode *audio* for transcription/search. On Pixel 9, **Google Messages** will hand incoming RCS/SMS audio to the **Dolby Unified Decoder (UDC)** inside `/vendor/lib64/libcodec2_soft_ddpdec.so` **before** the user opens the message, expanding the 0-click surface to media codecs.<sup>[[2]](#references)</sup>
     34 
     35 **Key parse constraints**
     36 - Each DD+ syncframe has up to 6 blocks; each block can copy up to `0x1FF` bytes of attacker-controlled *skip data* into a skip buffer (≈ `0x1FF * 6` bytes per frame).
     37 - The skip buffer is scanned for **EMDF**: `syncword (0xX8)` + `emdf_container_length` (16b) + variable-length fields. `emdf_payload_size` is parsed with an unbounded `variable_bits(8)` loop.
     38 - EMDF payload bytes are allocated inside a custom per-frame **“evo heap”** bump allocator and then copied byte-by-byte from a bit-reader bounded by `emdf_container_length`.
     39 
     40 **Integer-overflow → heap-overflow primitive (CVE-2025-54957)**
     41 - `ddp_udc_int_evo_malloc` aligns `alloc_size+extra` to 8 bytes via `total_size += (8 - total_size) % total_size` **without wrap detection**. Values near `0xFFFFFFFFFFFFFFF9..FF` shrink to tiny `total_size` on AArch64.
     42 - The copy loop still uses the *logical* `payload_length` from `emdf_payload_size`, so attacker bytes overwrite evo-heap data past the undersized chunk.
     43 - Overflow length is precisely capped by attacker-chosen `emdf_container_length`; overflow bytes are attacker-controlled EMDF payload data. The slab allocator is reset every syncframe, giving predictable adjacency.
     44 
     45 **Secondary read primitive**
     46 If `emdf_container_length > skipl`, EMDF parsing reads past initialized skip bytes (OOB read). Alone it leaks zeros/known media, but after corrupting adjacent heap metadata it can read back the corrupted region to validate the exploit.
     47 
     48 **Exploitation recipe**
     49 1. Craft EMDF with huge `emdf_payload_size` (via `variable_bits(8)`) so allocator padding wraps into a small chunk.
     50 2. Set `emdf_container_length` to the desired overflow length (≤ total skip data budget); place overflow bytes in the EMDF payload.
     51 3. Shape the per-frame evo heap so the small allocation sits before target structures inside the decoder’s static buffer (≈693 KB) or dynamic buffer (≈86 KB) allocated once per decoder instance.
     52 4. Optionally choose `emdf_container_length > skipl` to read back overwritten data from the skip buffer after corruption.
     53 
     54 ## AOSP DNG `MapTable`: wrapping plane intervals
     55 
     56 A separate bug pattern appeared in AOSP's DNG SDK. `dng_opcode_MapTable::ProcessArea` bounded its plane loop with `plane < Plane() + Planes()`. Because both DNG fields are attacker-controlled `uint32` values, the computed exclusive end can wrap below the starting plane. The public fix avoids ever materializing that end and compares the distance travelled from the start instead.<sup>[[3]](#references)</sup>
     57 
     58 ```cpp
     59 const uint32 planeStart = fAreaSpec.Plane();
     60 const uint32 planeCount = fAreaSpec.Planes();
     61 const uint32 bufferPlanes = buffer.Planes();
     62 for (uint32 plane = planeStart;
     63      plane < bufferPlanes && plane - planeStart < planeCount;
     64      ++plane) {
     65     // map this plane
     66 }
     67 ```
     68 
     69 When auditing other opcodes, search for the same `start + count`, `offset + length`, and `firstPlane + planes` idioms before clipping against image/tile bounds. A comparison against the destination size does **not** repair an already-wrapped end. See the generic [integer overflow/underflow patterns](https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/binary-exploitation/integer-overflow-and-underflow.md) for safe checked-arithmetic alternatives.<sup>[[3]](#references)</sup>
     70 
     71 ## Quram's DNG Opcode Interpreter Bugs
     72 
     73 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.<sup>[[1]](#references)</sup>
     74 
     75 ### Failing plane bounds in `DeltaPerColumn`
     76 - Attackers set `plane=5125` and `planes=5123` even though Stage-3 images only expose planes 0–2 (RGB).
     77 - 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.
     78 - 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.
     79 
     80 ### Turning increments into arbitrary writes
     81 - The exploit first corrupts Stage-3 `QuramDngImage.bottom/right` using 480 malformed `DeltaPerColumn` operations so future opcodes treat enormous coordinates as in-bounds.
     82 - `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.
     83 - Because the opcode parameters live inside the DNG metadata, the payload can encode hundreds of thousands of writes without touching process memory directly.
     84 
     85 ## Heap Shaping Under Scudo
     86 
     87 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):<sup>[[1]](#references)</sup>
     88 - `QuramDngImage` descriptors for Stage 1/2/3
     89 - `QuramDngOpcodeTrimBounds` and vendor `Unknown` opcodes (ID ≥14, including ID 23)
     90 
     91 The exploit sequences allocations to deterministically place chunks:
     92 1. Stage-1 `Unknown(23)` opcodes (20,000 entries) spray 0x30 chunks that later get freed.
     93 2. Stage-2 frees those opcodes and places a new `QuramDngImage` inside the freed region.
     94 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.
     95 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.
     96 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.
     97 
     98 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.
     99 
    100 ## Reusing Vendor "Unknown" Opcodes as Data Blobs
    101 
    102 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:<sup>[[1]](#references)</sup>
    103 - 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).
    104 - 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.
    105 
    106 ## Crafting Bogus `MapTable` Objects & Bypassing ASLR
    107 
    108 `MapTable` objects are larger than `TrimBounds`, but once the layout corruption lands, the parser happily reads extra parameters out-of-bounds:<sup>[[1]](#references)</sup>
    109 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.
    110 2. Patch the rest of the `TrimBounds` fields (top/left/width/planes) so the object behaves like a valid `MapTable` when executed later.
    111 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.
    112 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.
    113 
    114 ## Triggering the JOP ➜ `system()` Transition
    115 
    116 Once the gadget pointers and shell command are staged inside the opcode spray:<sup>[[1]](#references)</sup>
    117 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.
    118 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.
    119 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`.
    120 
    121 ## Notes on Allocator Variants
    122 
    123 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.<sup>[[1]](#references)</sup>
    124 
    125 ## Lab Workflow: Prove Reachability Before Fuzzing
    126 
    127 Do not infer the decoder from the filename, advertised MIME type, or the app that received the attachment. Replay the **real ingress** (message auto-download, notification preview, gallery rescan, share sheet, etc.), then attribute the decode to a process, SELinux domain, and mapped library. The DNG sample above kept a JPEG-looking name, while the audio path executed in the `mediacodec` sandbox; this distinction decides both exploit impact and the next sandbox boundary.<sup>[[1]](#references)[[2]](#references)</sup>
    128 
    129 ```bash
    130 adb logcat -c
    131 # Deliver/save the seed through the feature being tested, then:
    132 adb logcat -b crash -d -v threadtime > crash.txt
    133 adb shell 'ps -AZ | grep -E "ipservice|media\.codec|mediacodec"'
    134 PID=$(adb shell pidof com.samsung.ipservice | tr -d '\r')
    135 adb shell su -c "grep -E 'quram|codec|skia|hwui' /proc/$PID/maps" # rooted lab device
    136 ```
    137 
    138 Test the same bytes with the original extension, a misleading common extension, and the MIME type used by the delivery app. Compare **save-only**, thumbnail/listing, notification rendering, and explicit-open paths. A crash that only appears on explicit open is not a zero-click primitive; a background crash in a privileged OEM indexer is not equivalent to a crash in the sender app.<sup>[[1]](#references)[[2]](#references)</sup>
    139 
    140 ### Exercising proprietary Skia codecs
    141 
    142 [`SkCodecFuzzer`](https://github.com/googleprojectzero/SkCodecFuzzer) links against a firmware's `libhwui.so` (or older `libskia.so`) and reaches the same `SkCodec`/`SkAndroidCodec` path used by `BitmapFactory`. Build against the exact firmware libraries: OEM codec availability and ABI are build-specific. Its guard-page allocator (`libdislocator`) makes short heap overreads/writes easier to detect and `-l` records heap operations; always reproduce interesting cases with `-d` as well because the default Android allocator can produce a different layout.<sup>[[4]](#references)</sup>
    143 
    144 ```bash
    145 adb push loader seed.img /data/local/tmp/
    146 adb shell 'LIBC_HOOKS_ENABLE=1 /data/local/tmp/loader -l -i /data/local/tmp/seed.img'
    147 adb shell '/data/local/tmp/loader -d -i /data/local/tmp/seed.img'
    148 ```
    149 
    150 For coverage fuzzing, keep separate corpora per firmware/format and preserve parse-valid structure (container directories, tile offsets, opcode-list lengths, checksums) while mutating semantic dimensions and ranges. Deduplicate first with the guarded allocator, then confirm on the target build with its production allocator before treating a crash as reachable or exploitable.<sup>[[4]](#references)</sup>
    151 
    152 
    153 ## References
    154 
    155 - [1] [Project Zero – A look at an Android ITW DNG exploit](https://projectzero.google/2025/12/android-itw-dng.html)
    156 - [2] [Project Zero – Pixel 0-click: CVE-2025-54957 in Dolby UDC](https://projectzero.google/2026/01/pixel-0-click-part-1.html)
    157 - [3] [AOSP DNG SDK – Handle underflows in dng_opcode_MapTable](https://android.googlesource.com/platform/external/dng_sdk/+/80a267ed1ac714acff455e85ae28c1732777d5b6)
    158 - [4] [Project Zero – Android Skia Image Fuzzing Harness](https://github.com/googleprojectzero/SkCodecFuzzer)