Android 미디어 파이프라인 및 이미지 파서 악용

Tip

AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE) Azure 해킹 배우기 및 연습하기: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks 지원하기

전달: 메시징 앱 ➜ MediaStore ➜ 특권 파서

현대 OEM 빌드는 “AI” 또는 공유 기능을 위해 정기적으로 MediaStore를 재검색하는 권한 있는 미디어 인덱서를 실행합니다. 2025년 4월 패치 이전의 Samsung 펌웨어에서 com.samsung.ipservice는 Quram (/system/lib64/libimagecodec.quram.so)을 로드하고 WhatsApp(또는 다른 앱)이 MediaStore에 저장한 모든 파일을 자동으로 파싱합니다. 실제로 공격자는 IMG-*.jpg로 위장한 DNG를 전송하고 피해자가 “download” (1-click)를 누르기를 기다린 다음, 사용자가 갤러리를 열지 않아도 그 특권 서비스가 페이로드를 파싱합니다.

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

핵심 요지

  • 전달은 시스템 미디어 재파싱(채팅 클라이언트가 아니라)에 의존하므로 해당 프로세스의 권한(갤러리에 대한 전체 읽기/쓰기 접근, 새 미디어 추가 능력 등)을 계승한다.
  • 공격자가 대상에게 미디어 저장을 유도하면 MediaStore를 통해 접근 가능한 모든 이미지 파서(vision widgets, 배경화면, AI 이력서 기능 등)가 원격으로 도달 가능해진다.

0-click DD+/EAC-3 디코딩 경로 (Google Messages ➜ mediacodec sandbox)

현대 메시징 스택은 전사/검색을 위해 audio를 자동 디코딩하기도 한다. Pixel 9에서는 Google Messages가 수신된 RCS/SMS 오디오를 사용자가 메시지를 열기 전에 /vendor/lib64/libcodec2_soft_ddpdec.so 내부의 **Dolby Unified Decoder (UDC)**에 전달하여 0-click 공격 표면을 미디어 코덱으로 확장한다.

주요 파싱 제약

  • 각 DD+ syncframe은 최대 6개의 블록을 가지며; 각 블록은 최대 0x1FF 바이트의 공격자가 제어하는 skip data를 skip buffer로 복사할 수 있다(프레임 당 ≈ 0x1FF * 6 바이트).
  • skip buffer는 EMDF를 찾기 위해 스캔된다: syncword (0xX8) + emdf_container_length (16비트) + 가변 길이 필드. emdf_payload_size는 제한 없는 variable_bits(8) 루프로 파싱된다.
  • EMDF 페이로드 바이트는 프레임별 커스텀 “evo heap” bump 할당기에서 할당된 후 emdf_container_length로 제한된 비트 리더로부터 바이트 단위로 복사된다.

정수 오버플로 → 힙 오버플로 원시(primitive) (CVE-2025-54957)

  • ddp_udc_int_evo_malloctotal_size += (8 - total_size) % total_size를 통해 alloc_size+extra를 8바이트 정렬하지만 wrap detection 없이 수행된다. 0xFFFFFFFFFFFFFFF9..FF 근처 값들은 AArch64에서 작은 total_size로 축소된다.
  • 복사 루프는 여전히 emdf_payload_size에서 온 논리적 payload_length를 사용하므로, 공격자 바이트가 축소된 청크를 넘어 evo-heap 데이터를 덮어쓴다.
  • 오버플로 길이는 공격자가 선택한 emdf_container_length로 정확히 제한되며; 오버플로 바이트는 공격자가 제어하는 EMDF 페이로드 데이터다. 슬랩 할당기는 각 syncframe마다 리셋되어 인접성(adjacency)을 예측 가능하게 만든다.

Secondary read primitive 만약 emdf_container_length > skipl라면 EMDF 파싱은 초기화된 skip 바이트를 넘어 읽는다(OOB read). 단독으로는 zeros/known media를 leaks하지만, 인접 힙 메타데이터를 손상한 후에는 손상된 영역을 다시 읽어 익스플로잇을 검증할 수 있다.

익스플로잇 레시피

  1. variable_bits(8)를 이용해 매우 큰 emdf_payload_size를 갖는 EMDF를 제작하여 할당기 패딩이 래핑되어 작은 청크로 들어가도록 한다.
  2. emdf_container_length을 원하는 오버플로 길이(≤ 총 skip 데이터 예산)로 설정하고 오버플로 바이트를 EMDF 페이로드에 넣는다.
  3. 프레임별 evo heap을 조작하여 작은 할당이 디코더의 static 버퍼(≈693 KB) 또는 디코더 인스턴스 당 한 번 할당되는 dynamic 버퍼(≈86 KB) 내의 목표 구조 앞에 위치하도록 만든다.
  4. 선택적으로 emdf_container_length > skipl을 선택해 손상 후 skip 버퍼에서 덮어쓴 데이터를 다시 읽을 수 있다.

Quram의 DNG Opcode 인터프리터 버그

DNG 파일은 서로 다른 디코드 단계에서 적용되는 세 개의 opcode 리스트를 포함한다. Quram은 Adobe의 API를 복제했지만, Stage-3의 DeltaPerColumn (opcode ID 11) 핸들러는 공격자가 제공한 plane 경계를 신뢰한다.

DeltaPerColumn의 실패한 plane 경계

  • 공격자는 Stage-3 이미지가 plane 0–2(RGB)만 노출함에도 불구하고 plane=5125planes=5123을 설정한다.
  • Quram은 opcode_last_plane = image_planes + opcode_planes를 계산하며 plane + count 대신 사용하고, 결과 plane 범위가 이미지 안에 맞는지 검사하지 않는다.
  • 따라서 루프는 완전히 제어 가능한 오프셋으로 raw_pixel_buffer[plane_index]에 델타를 작성한다(예: plane 5125 ⇒ 오프셋 5125 * 2 bytes/pixel = 0x2800). 각 opcode는 대상 위치에 16비트 플로트 값(0x6666)을 더해 정확한 힙 OOB add primitive를 만든다.

증분을 임의 쓰기로 변환하기

  • 익스플로잇은 먼저 480개의 잘못된 DeltaPerColumn 연산을 사용해 Stage-3 QuramDngImage.bottom/right를 손상시켜 이후 opcode들이 거대한 좌표를 in-bounds로 처리하게 만든다.
  • MapTable opcode(opcode 7)는 그런 가짜 경계를 대상으로 삼는다. 전체가 0인 치환 테이블 또는 -Inf 델타를 가진 DeltaPerColumn을 사용해 공격자는 임의 영역을 0으로 만든 다음 추가 델타를 적용해 정확한 값을 쓴다.
  • opcode 매개변수가 DNG 메타데이터 내부에 존재하기 때문에 페이로드는 프로세스 메모리를 직접 건드리지 않고도 수십만 건의 쓰기를 인코딩할 수 있다.

Scudo 하의 힙 셰이핑

Scudo는 크기별로 할당을 버킷화한다. Quram은 우연히 다음 객체들을 동일한 0x30바이트 청크 크기로 할당하여 동일 영역에 배치된다(힙 상에서 0x40바이트 간격):

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

익스플로잇은 청크를 결정론적으로 배치하기 위해 할당을 순차적으로 조작한다:

  1. Stage-1 Unknown(23) opcode(20,000개 항목)가 0x30 청크를 스프레이하고 이후 해제된다.
  2. Stage-2는 해당 opcode들을 해제하고 해제된 영역 안에 새로운 QuramDngImage를 배치한다.
  3. 240개의 Stage-2 Unknown(23) 항목이 해제되고, Stage-3는 즉시 자신의 QuramDngImage와 동일 크기의 새로운 raw pixel buffer를 할당해 그 자리를 재사용한다.
  4. 조작된 TrimBounds opcode가 리스트 3에서 먼저 실행되어 Stage-2 상태를 해제하기 전에 또 다른 raw pixel buffer를 할당함으로써 “raw pixel buffer ➜ QuramDngImage“의 인접성을 보장한다.
  5. 추가 640개의 TrimBounds 항목은 minVersion=1.4.0.1로 표시되어 dispatcher가 건너뛰지만, 해당 객체들의 실제 할당은 유지되어 나중에 primitive 대상이 된다.

이 연출은 Stage-3 raw 버퍼를 Stage-3 QuramDngImage 바로 앞에 배치하므로, plane 기반 오버플로가 무작위 상태를 충돌시키지 않고 디스크립터 내부의 필드를 뒤집는다.

Vendor “Unknown” Opcode를 데이터 블롭으로 재사용

Samsung은 공급업체 전용 opcode ID(예: ID 23)에서 상위 비트를 설정해 인터프리터에게 구조체를 allocate하되 실행은 건너뛰도록 한다. 익스플로잇은 그런 휴면 객체들을 공격자가 제어하는 힙으로 악용한다:

  • Opcode 리스트 1과 2의 Unknown(23) 항목들은 페이로드 바이트를 저장하기 위한 연속 스크래치패드로 사용된다(raw 버퍼 기준 오프셋 0xf000의 JOP 체인과 0x10000의 쉘 명령 등).
  • 인터프리터는 리스트 3 처리 시에도 각 객체를 opcode로 취급하기 때문에, 나중에 한 객체의 vtable을 장악하는 것만으로 공격자 데이터를 실행할 수 있다.

가짜 MapTable 객체 제작 및 ASLR 우회

MapTable 객체는 TrimBounds보다 크지만, 레이아웃 손상이 일어나면 파서가 추가 매개변수를 OOB로 기꺼이 읽는다:

  1. 선형 쓰기 프리미티브를 사용해 TrimBounds의 vtable 포인터 일부를 이웃한 TrimBounds vtable의 하위 2바이트를 MapTable vtable로 매핑하는 조작된 MapTable 치환 테이블로 덮어쓴다. 지원되는 Quram 빌드 간에는 하위 바이트만 다르므로 단일 64K 룩업 테이블로 7개 펌웨어 버전과 모든 4 KB ASLR 슬라이드를 처리할 수 있다.
  2. TrimBounds 필드 나머지(top/left/width/planes)를 패치해 객체가 나중에 실행될 때 유효한 MapTable처럼 동작하게 한다.
  3. 제로 처리된 메모리에서 가짜 opcode를 실행한다. 치환 테이블 포인터가 실제로 다른 opcode의 vtable을 참조하기 때문에 출력 바이트는 libimagecodec.quram.so 또는 그 GOT의 저차 주소들이 leaked 된다.
  4. 추가 MapTable 패스를 적용해 그 2바이트 leaks를 __ink_jpeg_enc_process_image+64, QURAMWINK_Read_IO2+124, qpng_check_IHDR+624, libc의 __system_property_get 진입점 같은 가젯을 향한 오프셋으로 변환한다. 공격자는 네이티브 메모리 유출 API 없이도 스프레이된 opcode 영역 안에서 전체 주소를 효과적으로 재구성한다.

JOP ➜ system() 전환 트리거

가젯 포인터와 쉘 명령이 opcode 스프레이 내부에 준비되면:

  1. 마지막 DeltaPerColumn 쓰기 물결이 Stage-3 QuramDngImage의 오프셋 0x22에 0x0100을 더하여 raw 버퍼 포인터를 0x10000만큼 이동시키고 이제 공격자 명령 문자열을 참조하게 한다.
  2. 인터프리터는 1040개의 Unknown(23) opcode의 꼬리를 실행하기 시작한다. 첫 번째 손상된 항목은 vtable이 오프셋 0xf000의 위조 테이블로 교체되어 QuramDngOpcode::aboutToApply가 가짜 테이블의 4번째 항목인 qpng_read_data를 해석한다.
  3. 연결된 가젯들은 다음을 수행한다: QuramDngImage 포인터를 로드하고 raw 버퍼 포인터를 가리키도록 0x20을 더하고, 역참조하여 결과를 x19/x0에 복사한 다음 system으로 재작성된 GOT 슬롯을 통해 점프한다. raw 버퍼 포인터가 이제 공격자 문자열과 같기 때문에 최종 가젯은 com.samsung.ipservice 내에서 system(<shell command>)를 실행한다.

할당기(allocator) 변형에 대한 주석

두 가지 페이로드 계열이 존재한다: jemalloc용으로 조정된 것과 scudo용. 이들은 인접성을 달성하기 위해 opcode 블록의 순서를 다르게 하지만 동일한 논리적 프리미티브(DeltaPerColumn 버그 ➜ MapTable zero/write ➜ bogus vtable ➜ JOP)를 공유한다. Scudo의 quarantine 비활성화는 0x30바이트 freelist 재사용을 결정론적으로 만들고, jemalloc은 tile/subIFD 크기 조정으로 size-class를 제어한다.

참고자료

Tip

AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE) Azure 해킹 배우기 및 연습하기: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks 지원하기