Wykorzystywanie potoków multimedialnych Androida i parserów obrazów

Tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks

Dostarczanie: aplikacje komunikacyjne ➜ MediaStore ➜ uprzywilejowane parsery

Nowoczesne buildy OEM regularnie uruchamiają uprzywilejowane indeksatory mediów, które ponownie skanują MediaStore pod kątem funkcji “AI” lub udostępniania. Na firmware Samsung przed łatką z kwietnia 2025 com.samsung.ipservice ładuje Quram (/system/lib64/libimagecodec.quram.so) i automatycznie parsuje każdy plik, który WhatsApp (lub inne aplikacje) umieszcza w MediaStore. W praktyce atakujący może wysłać DNG podszyty pod IMG-*.jpg, poczekać aż ofiara stuknie “download” (1-click), a uprzywilejowana usługa sparsuje payload nawet jeśli użytkownik nigdy nie otworzy galerii.

$ 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], ...

Kluczowe wnioski

  • Dostarczanie opiera się na systemowym ponownym parsowaniu mediów (nie kliencie czatu) i w związku z tym dziedziczy uprawnienia tego procesu (pełny dostęp do odczytu/zapisu galerii, możliwość umieszczania nowych mediów itp.).
  • Każdy parser obrazów dostępny przez MediaStore (vision widgets, wallpapers, AI résumé features itp.) staje się zdalnie osiągalny, jeśli atakujący przekona cel do zapisania mediów.

0-click DD+/EAC-3 decoding path (Google Messages ➜ mediacodec sandbox)

Nowoczesne stosy messagingowe także automatycznie dekodują audio do transkrypcji/wyszukiwania. Na Pixel 9 Google Messages przekaże przychodzące audio RCS/SMS do Dolby Unified Decoder (UDC) w /vendor/lib64/libcodec2_soft_ddpdec.so zanim użytkownik otworzy wiadomość, rozszerzając 0-click powierzchnię do kodeków multimedialnych.

Kluczowe ograniczenia parsowania

  • Każdy DD+ syncframe ma do 6 bloków; każdy blok może skopiować do 0x1FF bajtów kontrolowanych przez atakującego skip data do skip buffer (≈ 0x1FF * 6 bajtów na ramkę).
  • Skip buffer jest skanowany w poszukiwaniu EMDF: syncword (0xX8) + emdf_container_length (16b) + pola o zmiennej długości. emdf_payload_size jest parsowane pętlą variable_bits(8) bez ograniczeń.
  • Bajty ładunku EMDF są alokowane wewnątrz niestandardowego per-frame “evo heap” bump allocator i następnie kopiowane bajt-po-bajcie z bit-readera ograniczonego przez emdf_container_length.

Integer-overflow → heap-overflow primitive (CVE-2025-54957)

  • ddp_udc_int_evo_malloc wyrównuje alloc_size+extra do 8 bajtów przez total_size += (8 - total_size) % total_size bez wykrywania przepełnienia. Wartości bliskie 0xFFFFFFFFFFFFFFF9..FF kurczą się do bardzo małego total_size na AArch64.
  • Pętla kopiująca nadal używa logicznej payload_length z emdf_payload_size, więc bajty atakującego nadpisują dane evo-heap poza zbyt małym chunkiem.
  • Długość overflow jest precyzyjnie ograniczona przez kontrolowany przez atakującego emdf_container_length; bajty overflow są kontrolowanymi danymi ładunku EMDF. Slab allocator jest resetowany dla każdej syncframe, co daje przewidywalną sąsiedztwo.

Drugorzędny prymityw odczytu Jeśli emdf_container_length > skipl, parsowanie EMDF odczytuje poza zainicjalizowane bajty skip (OOB read). Samo w sobie leaks zera/znane media, ale po uszkodzeniu sąsiednich metadanych sterty może odczytać z powrotem uszkodzony region, by zweryfikować exploit.

Przepis eksploatacji

  1. Stwórz EMDF z ogromnym emdf_payload_size (przez variable_bits(8)), tak aby padding alokatora zawinął się do małego chanka.
  2. Ustaw emdf_container_length na pożądaną długość overflow (≤ całkowity budżet skip data); umieść bajty overflow w ładunku EMDF.
  3. Ukształtuj per-frame evo heap tak, aby mała alokacja znalazła się przed celowymi strukturami wewnątrz statycznego bufora dekodera (≈693 KB) lub dynamicznego bufora (≈86 KB) alokowanego raz na instancję dekodera.
  4. Opcjonalnie wybierz emdf_container_length > skipl, aby odczytać z powrotem nadpisane dane ze skip buffer po korupcji.

Quram’s DNG Opcode Interpreter Bugs

Pliki DNG osadzają trzy listy opcode’ów stosowanych na różnych etapach dekodowania. Quram kopiuje API Adobe, ale jego handler Stage-3 dla DeltaPerColumn (opcode ID 11) ufa dostarczonym przez atakującego ograniczeniom płaszczyzny.

Failing plane bounds in DeltaPerColumn

  • Atakujący ustawiają plane=5125 i planes=5123, mimo że obrazy Stage-3 eksponują tylko płaszczyzny 0–2 (RGB).
  • Quram oblicza opcode_last_plane = image_planes + opcode_planes zamiast plane + count, i nigdy nie sprawdza, czy wynikowy zakres płaszczyzn mieści się w obrazie.
  • Pętla więc zapisuje delta do raw_pixel_buffer[plane_index] z całkowicie kontrolowanym offsetem (np. plane 5125 ⇒ offset 5125 * 2 bytes/pixel = 0x2800). Każdy opcode dodaje wartość 16-bit float (0x6666) do wskazanego miejsca, dając precyzyjny heap OOB add prymityw.

Turning increments into arbitrary writes

  • Exploit najpierw uszkadza Stage-3 QuramDngImage.bottom/right używając 480 sfałszowanych operacji DeltaPerColumn, tak by przyszłe opcode’y traktowały ogromne współrzędne jako mieszczące się w granicach.
  • Opcode’y MapTable (opcode 7) są następnie skierowane na te fałszywe granice. Używając tabeli substytucji wypełnionej zerami lub DeltaPerColumn z delta = -Inf, atakujący zeruje dowolny region, a potem stosuje dodatkowe delty, by zapisać dokładne wartości.
  • Ponieważ parametry opcode’ów żyją w metadanych DNG, ładunek może zakodować setki tysięcy zapisów bez bezpośredniego dotykania pamięci procesu.

Heap Shaping Under Scudo

Scudo grupuje alokacje według rozmiaru. Quram ma alokacje następujących obiektów o identycznym rozmiarze chunk 0x30, więc trafiają w to samo miejsce (odstęp 0x40 na stercie):

  • QuramDngImage deskryptory dla Stage 1/2/3
  • QuramDngOpcodeTrimBounds oraz vendor Unknown opcodes (ID ≥14, w tym ID 23)

Exploit sekwencjonuje alokacje, aby deterministycznie umieścić chunki:

  1. Stage-1 Unknown(23) opcode’y (20,000 wpisów) spraye 0x30 chunki, które potem są zwalniane.
  2. Stage-2 zwalnia te opcode’y i umieszcza nowy QuramDngImage w zwolnionym regionie.
  3. 240 Stage-2 Unknown(23) wpisów jest zwalnianych, a Stage-3 natychmiast alokuje swój QuramDngImage plus nowy raw pixel buffer tej samej wielkości, ponownie używając tych miejsc.
  4. Sfałszowany opcode TrimBounds uruchamia się pierwszy w liście 3 i alokuje kolejny raw pixel buffer zanim Stage-2 stan zostanie zwolniony, gwarantując sąsiedztwo “raw pixel buffer ➜ QuramDngImage”.
  5. Kolejne 640 TrimBounds wpisów oznaczono jako minVersion=1.4.0.1, więc dispatcher je pomija, lecz ich obiekty pozostają alokowane i później stają się celami prymitywów.

Ta choreografia umieszcza Stage-3 raw buffer natychmiast przed Stage-3 QuramDngImage, tak że overflow oparty na płaszczyznach odwraca pola wewnątrz deskryptora, zamiast losowo przerywać stan.

Reusing Vendor “Unknown” Opcodes as Data Blobs

Samsung zostawia ustawiony high bit w vendor-specific ID opcode’ów (np. ID 23), co instruuje interpreter, by alokował strukturę, ale pominął jej wykonanie. Exploit nadużywa tych uśpionych obiektów jako stert kontrolowanych przez atakującego:

  • Lista opcode’ów 1 i 2 Unknown(23) służy jako sąsiadujące scratchpady do przechowywania bajtów payloadu (JOP chain na offsetcie 0xf000 i polecenie shell na 0x10000 względem raw buffer).
  • Ponieważ interpreter nadal traktuje każdy obiekt jako opcode gdy lista 3 jest przetwarzana, przejęcie vtable jednego obiektu później wystarcza, żeby zacząć wykonywać dane atakującego.

Crafting Bogus MapTable Objects & Bypassing ASLR

Obiekty MapTable są większe niż TrimBounds, ale gdy korupcja layoutu wystąpi, parser chętnie czyta dodatkowe parametry poza granicami:

  1. Użyj liniowego prymitywu zapisu, by częściowo nadpisać wskaźnik vtable TrimBounds spreparowaną tabelą substytucji MapTable, która mapuje niższe 2 bajty z sąsiedniego vtable TrimBounds do vtable MapTable. Tylko niskie bajty różnią się między wspieranymi buildami Quram, więc pojedyncza 64K tabela lookup obsłuży siedem wersji firmware i każdy 4 KB ASLR slide.
  2. Załatuj resztę pól TrimBounds (top/left/width/planes), tak by obiekt zachowywał się jak ważny MapTable przy późniejszym wykonaniu.
  3. Wykonaj fałszywy opcode nad wyzerowaną pamięcią. Ponieważ wskaźnik tabeli substytucji faktycznie referuje do vtable innego opcode’a, wyjściowe bajty stają się leaked low-order addresses z libimagecodec.quram.so lub z jego GOT.
  4. Zastosuj dodatkowe przebiegi MapTable, by przekształcić te dwubajtowe leaks w przesunięcia w kierunku gadgetów takich jak __ink_jpeg_enc_process_image+64, QURAMWINK_Read_IO2+124, qpng_check_IHDR+624 oraz libc __system_property_get. Atakujący efektywnie odbudowują pełne adresy wewnątrz swojej spraye’owanej regionu opcode’ów bez natywnych API do ujawniania pamięci.

Triggering the JOP ➜ system() Transition

Gdy wskaźniki gadgetów i polecenie shell są ustawione wewnątrz opcode spray:

  1. Ostateczna fala zapisów DeltaPerColumn dodaje 0x0100 do offsetu 0x22 w Stage-3 QuramDngImage, przesuwając jego wskaźnik raw buffer o 0x10000, tak że teraz wskazuje na string polecenia atakującego.
  2. Interpreter zaczyna wykonywać tail 1040 Unknown(23) opcode’ów. Pierwszy uszkodzony wpis ma vtable zastąpiony spreparowaną tabelą na offsetcie 0xf000, więc QuramDngOpcode::aboutToApply rozwiązuje qpng_read_data (4. wpis) z fałszywej tabeli.
  3. Połączone gadgety wykonują: ładują wskaźnik QuramDngImage, dodają 0x20 by pokazać na wskaźnik raw buffer, dereferencują go, kopiują wynik do x19/x0, a następnie skaczą przez GOT sloty przepisane na system. Ponieważ wskaźnik raw buffer teraz równa się stringowi atakującego, finalny gadget wykonuje system(<polecenie shell>) wewnątrz com.samsung.ipservice.

Notes on Allocator Variants

Istnieją dwie rodziny payloadów: jedna dostrojona pod jemalloc, druga pod scudo. Różnią się kolejnością bloków opcode, by osiągnąć sąsiedztwo, ale dzielą te same logiczne prymtywy (DeltaPerColumn bug ➜ MapTable zero/write ➜ bogus vtable ➜ JOP). Wyłączona kwarantanna Scudo sprawia, że reuse freelistu 0x30 bajtów jest deterministyczny, podczas gdy jemalloc polega na kontroli klas rozmiarów przez tile/subIFD sizing.

References

Tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks