Abusing Android Media Pipelines & Image Parsers

Tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

Delivery: Messaging Apps ➜ MediaStore ➜ Privileged Parsers

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.

$ file IMG-2025-02-10.jpeg
TIFF image data ...
$ exiftool IMG-2025-02-10.jpeg | grep "Opcode List"
Opcode List 1 : [opcode 23], [opcode 23], ...

Key takeaways

  • 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.).
  • 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.

Quram’s DNG Opcode Interpreter Bugs

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.

Failing plane bounds in DeltaPerColumn

  • Attackers set plane=5125 and planes=5123 even though Stage-3 images only expose planes 0–2 (RGB).
  • 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.
  • 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.

Turning increments into arbitrary writes

  • The exploit first corrupts Stage-3 QuramDngImage.bottom/right using 480 malformed DeltaPerColumn operations so future opcodes treat enormous coordinates as in-bounds.
  • 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.
  • Because the opcode parameters live inside the DNG metadata, the payload can encode hundreds of thousands of writes without touching process memory directly.

Heap Shaping Under Scudo

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):

  • QuramDngImage descriptors for Stage 1/2/3
  • QuramDngOpcodeTrimBounds and vendor Unknown opcodes (ID ≄14, including ID 23)

The exploit sequences allocations to deterministically place chunks:

  1. Stage-1 Unknown(23) opcodes (20,000 entries) spray 0x30 chunks that later get freed.
  2. Stage-2 frees those opcodes and places a new QuramDngImage inside the freed region.
  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.
  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.
  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.

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.

Reusing Vendor “Unknown” Opcodes as Data Blobs

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:

  • 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).
  • 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.

Crafting Bogus MapTable Objects & Bypassing ASLR

MapTable objects are larger than TrimBounds, but once the layout corruption lands, the parser happily reads extra parameters out-of-bounds:

  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.
  2. Patch the rest of the TrimBounds fields (top/left/width/planes) so the object behaves like a valid MapTable when executed later.
  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.
  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.

Triggering the JOP ➜ system() Transition

Once the gadget pointers and shell command are staged inside the opcode spray:

  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.
  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.
  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.

Notes on Allocator Variants

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.

References

Tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks