Abusando dos Pipelines de Mídia do Android e Analisadores de Imagem

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

Entrega: Apps de Mensageria ➜ MediaStore ➜ Parsers privilegiados

Builds OEM modernos executam regularmente indexadores de mídia privilegiados que reescaneiam MediaStore para recursos de “AI” ou compartilhamento. No firmware Samsung anterior ao patch de abril de 2025, com.samsung.ipservice carrega Quram (/system/lib64/libimagecodec.quram.so) e analisa automaticamente qualquer arquivo que o WhatsApp (ou outros apps) coloque em MediaStore. Na prática, um atacante pode enviar um DNG disfarçado como IMG-*.jpg, aguardar a vítima tocar em “download” (1-click), e o serviço privilegiado irá analisar o payload mesmo que o usuário nunca abra a galeria.

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

Principais conclusões

  • A entrega depende do reparseamento de mídia do sistema (não do cliente de chat) e, portanto, herda as permissões desse processo (acesso total de leitura/escrita à galeria, capacidade de adicionar nova mídia, etc.).
  • Qualquer parser de imagem acessível via MediaStore (vision widgets, wallpapers, recursos de resumo/AI, etc.) torna-se alcançável remotamente se o atacante conseguir convencer a vítima a salvar mídia.

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

Stacks de mensagens modernos também decodificam automaticamente áudio para transcrição/busca. No Pixel 9, Google Messages entrega áudio RCS/SMS recebido ao Dolby Unified Decoder (UDC) dentro de /vendor/lib64/libcodec2_soft_ddpdec.so antes do usuário abrir a mensagem, ampliando a superfície 0-click para codecs de mídia.

Principais restrições de parsing

  • Cada DD+ syncframe tem até 6 blocks; cada block pode copiar até 0x1FF bytes de skip data controlada pelo atacante para um skip buffer (≈ 0x1FF * 6 bytes por frame).
  • O skip buffer é escaneado por EMDF: syncword (0xX8) + emdf_container_length (16b) + campos de tamanho variável. emdf_payload_size é parseado com um loop variable_bits(8) sem limite.
  • Os bytes do payload EMDF são alocados dentro de um bump allocator por-frame customizado chamado “evo heap” e então copiados byte-a-byte a partir de um bit-reader limitado por emdf_container_length.

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

  • ddp_udc_int_evo_malloc alinha alloc_size+extra a 8 bytes via total_size += (8 - total_size) % total_size sem detecção de wrap. Valores próximos de 0xFFFFFFFFFFFFFFF9..FF encolhem para um total_size minúsculo em AArch64.
  • O loop de cópia continua a usar o logical payload_length de emdf_payload_size, de modo que bytes do atacante sobrescrevem dados do evo-heap além do chunk subdimensionado.
  • O comprimento do overflow é precisamente limitado por emdf_container_length escolhido pelo atacante; os bytes do overflow são dados do payload EMDF controlados pelo atacante. O slab allocator é reiniciado a cada syncframe, proporcionando adjacência previsível.

Primitivo de leitura secundário Se emdf_container_length > skipl, o parsing EMDF lê além dos skip bytes inicializados (OOB read). Isoladamente, ele leaks zeros/mídia conhecida, mas após corromper metadados de heap adjacentes ele pode ler de volta a região corrompida para validar o exploit.

Receita de exploração

  1. Forjar um EMDF com enorme emdf_payload_size (via variable_bits(8)) de modo que o padding do allocator dê wrap reduzindo o chunk.
  2. Definir emdf_container_length para o comprimento de overflow desejado (≤ orçamento total de skip data); colocar os bytes de overflow no payload EMDF.
  3. Moldar o evo heap por-frame para que a alocação pequena fique antes das estruturas alvo dentro do buffer estático do decoder (≈693 KB) ou do buffer dinâmico (≈86 KB) alocado uma vez por instância do decoder.
  4. Opcionalmente escolher emdf_container_length > skipl para ler de volta dados sobrescritos do skip buffer após a corrupção.

Quram’s DNG Opcode Interpreter Bugs

Arquivos DNG embutem três listas de opcodes aplicadas em diferentes estágios de decodificação. Quram replica a API da Adobe, mas seu handler de Stage-3 para DeltaPerColumn (opcode ID 11) confia em limites de plano fornecidos pelo atacante.

Limites de plano inválidos em DeltaPerColumn

  • Atacantes definem plane=5125 e planes=5123 mesmo que imagens Stage-3 exponham apenas planos 0–2 (RGB).
  • Quram calcula opcode_last_plane = image_planes + opcode_planes em vez de plane + count, e nunca verifica se o intervalo de planos resultante cabe dentro da imagem.
  • O loop, portanto, escreve um delta em raw_pixel_buffer[plane_index] com um offset totalmente controlado (ex.: plane 5125 ⇒ offset 5125 * 2 bytes/pixel = 0x2800). Cada opcode adiciona um valor float de 16 bits (0x6666) ao local alvo, gerando um primitivo preciso de adição OOB no heap.

Transformando incrementos em writes arbitrários

  • O exploit primeiro corrompe QuramDngImage.bottom/right do Stage-3 usando 480 operações DeltaPerColumn malformadas para que opcodes futuros tratem coordenadas enormes como in-bounds.
  • OpCodes MapTable (opcode 7) são então direcionados a esses limites falsos. Usando uma tabela de substituição de todos zeros ou um DeltaPerColumn com deltas -Inf, o atacante zera qualquer região, então aplica deltas adicionais para escrever valores exatos.
  • Como os parâmetros de opcode residem nos metadados DNG, o payload pode codificar centenas de milhares de writes sem tocar diretamente na memória do processo.

Modelagem de heap sob Scudo

Scudo agrupa alocações por tamanho. Quram por acaso aloca os seguintes objetos com tamanhos de chunk idênticos de 0x30 bytes, então eles caem na mesma região (espaçamento de 0x40 bytes no heap):

  • Descritores QuramDngImage para Stage 1/2/3
  • QuramDngOpcodeTrimBounds e opcodes vendor Unknown (ID ≥14, incluindo ID 23)

A sequência de alocações do exploit coloca chunks deterministically:

  1. Unknown(23) opcodes Stage-1 (20.000 entries) sprayam chunks 0x30 que depois são liberados.
  2. Stage-2 libera esses opcodes e coloca um novo QuramDngImage dentro da região liberada.
  3. 240 entries Unknown(23) Stage-2 são liberados, e Stage-3 aloca imediatamente seu QuramDngImage mais um novo raw pixel buffer do mesmo tamanho, reutilizando esses slots.
  4. Um TrimBounds craftado roda primeiro na lista 3 e aloca mais um raw pixel buffer antes de liberar o estado do Stage-2, garantindo adjacência “raw pixel buffer ➜ QuramDngImage”.
  5. 640 TrimBounds adicionais são marcados minVersion=1.4.0.1 para que o dispatcher os pule, mas seus objetos de backing continuam alocados e depois viram alvos primitivos.

Essa coreografia coloca o raw buffer do Stage-3 imediatamente antes do QuramDngImage do Stage-3, de modo que o overflow baseado em planos altera campos dentro do descritor em vez de travar estados aleatórios.

Reutilizando opcodes vendor “Unknown” como blobs de dados

A Samsung deixa o high bit set em IDs de opcode vendor-specific (ex.: ID 23), o que instrui o interpretador a alocar a estrutura mas pular a execução. O exploit abusa desses objetos dormentes como heaps controlados pelo atacante:

  • Entradas Unknown(23) das listas 1 e 2 servem como scratchpads contíguos para armazenar bytes do payload (cadeia JOP em offset 0xf000 e um comando shell em 0x10000 relativo ao raw buffer).
  • Como o interpretador ainda trata cada objeto como um opcode quando a lista 3 é processada, tomar o vtable de um objeto é suficiente para começar a executar dados do atacante.

Construindo objetos MapTable falsos & contornando ASLR

Objetos MapTable são maiores que TrimBounds, mas uma vez que a corrupção de layout acontece, o parser felizmente lê parâmetros extras fora dos limites:

  1. Use o linear write primitivo para sobrescrever parcialmente um ponteiro de vtable de TrimBounds com uma tabela de substituição MapTable forjada que mapeia os 2 bytes inferiores a partir da vtable de um TrimBounds vizinho para a vtable do MapTable. Apenas os bytes baixos diferem entre builds Quram suportados, então uma única tabela de lookup de 64K pode cobrir sete versões de firmware e cada slide ASLR de 4 KB.
  2. Corrija o restante dos campos de TrimBounds (top/left/width/planes) para que o objeto se comporte como um MapTable válido quando executado depois.
  3. Execute o opcode falso sobre memória zerada. Como o ponteiro da tabela de substituição referencia na realidade a vtable de outro opcode, os bytes de saída tornam-se leaked low-order addresses de libimagecodec.quram.so ou do seu GOT.
  4. Aplique passes MapTable adicionais para converter esses vazamentos de dois bytes em offsets para gadgets como __ink_jpeg_enc_process_image+64, QURAMWINK_Read_IO2+124, qpng_check_IHDR+624, e a entrada __system_property_get da libc. Os atacantes efetivamente reconstruem endereços completos dentro da sua região de opcode spray sem APIs nativas de disclosure de memória.

Disparo da transição JOP ➜ system()

Uma vez que os pointers de gadget e o comando shell estejam stageados dentro do spray de opcodes:

  1. Uma onda final de writes DeltaPerColumn adiciona 0x0100 ao offset 0x22 do QuramDngImage do Stage-3, deslocando seu ponteiro de raw buffer por 0x10000 de modo que agora refira a string de comando do atacante.
  2. O interpretador começa a executar o tail de 1040 opcodes Unknown(23). A primeira entry corrompida tem sua vtable substituída pela tabela forjada em offset 0xf000, de modo que QuramDngOpcode::aboutToApply resolve qpng_read_data (a 4ª entrada) a partir da tabela falsa.
  3. Os gadgets encadeados realizam: carregar o ponteiro QuramDngImage, somar 0x20 para apontar ao ponteiro do raw buffer, desreferenciar, copiar o resultado para x19/x0, então pular através de slots GOT reescritos para system. Como o ponteiro do raw buffer agora aponta para a string do atacante, o gadget final executa system(<shell command>) dentro de com.samsung.ipservice.

Notas sobre variantes de alocador

Existem duas famílias de payloads: uma ajustada para jemalloc e outra para scudo. Elas diferem em como os blocos de opcode são ordenados para alcançar adjacência, mas compartilham os mesmos primitivos lógicos (DeltaPerColumn bug ➜ MapTable zero/write ➜ vtable falsa ➜ JOP). A quarentena desabilitada do Scudo torna a reutilização da freelist de 0x30 bytes determinística, enquanto jemalloc depende de controle de size-class via tile/subIFD sizing.

References

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks