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 지원하기
- 구독 계획 확인하기!
- **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.
전달: 메시징 앱 ➜ 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_malloc는total_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하지만, 인접 힙 메타데이터를 손상한 후에는 손상된 영역을 다시 읽어 익스플로잇을 검증할 수 있다.
익스플로잇 레시피
variable_bits(8)를 이용해 매우 큰emdf_payload_size를 갖는 EMDF를 제작하여 할당기 패딩이 래핑되어 작은 청크로 들어가도록 한다.emdf_container_length을 원하는 오버플로 길이(≤ 총 skip 데이터 예산)로 설정하고 오버플로 바이트를 EMDF 페이로드에 넣는다.- 프레임별 evo heap을 조작하여 작은 할당이 디코더의 static 버퍼(≈693 KB) 또는 디코더 인스턴스 당 한 번 할당되는 dynamic 버퍼(≈86 KB) 내의 목표 구조 앞에 위치하도록 만든다.
- 선택적으로
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=5125와planes=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-3QuramDngImage.bottom/right를 손상시켜 이후 opcode들이 거대한 좌표를 in-bounds로 처리하게 만든다. MapTableopcode(opcode 7)는 그런 가짜 경계를 대상으로 삼는다. 전체가 0인 치환 테이블 또는-Inf델타를 가진DeltaPerColumn을 사용해 공격자는 임의 영역을 0으로 만든 다음 추가 델타를 적용해 정확한 값을 쓴다.- opcode 매개변수가 DNG 메타데이터 내부에 존재하기 때문에 페이로드는 프로세스 메모리를 직접 건드리지 않고도 수십만 건의 쓰기를 인코딩할 수 있다.
Scudo 하의 힙 셰이핑
Scudo는 크기별로 할당을 버킷화한다. Quram은 우연히 다음 객체들을 동일한 0x30바이트 청크 크기로 할당하여 동일 영역에 배치된다(힙 상에서 0x40바이트 간격):
QuramDngImagedescriptors for Stage 1/2/3QuramDngOpcodeTrimBoundsand vendorUnknownopcodes (ID ≥14, including ID 23)
익스플로잇은 청크를 결정론적으로 배치하기 위해 할당을 순차적으로 조작한다:
- Stage-1
Unknown(23)opcode(20,000개 항목)가 0x30 청크를 스프레이하고 이후 해제된다. - Stage-2는 해당 opcode들을 해제하고 해제된 영역 안에 새로운
QuramDngImage를 배치한다. - 240개의 Stage-2
Unknown(23)항목이 해제되고, Stage-3는 즉시 자신의QuramDngImage와 동일 크기의 새로운 raw pixel buffer를 할당해 그 자리를 재사용한다. - 조작된
TrimBoundsopcode가 리스트 3에서 먼저 실행되어 Stage-2 상태를 해제하기 전에 또 다른 raw pixel buffer를 할당함으로써 “raw pixel buffer ➜ QuramDngImage“의 인접성을 보장한다. - 추가 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로 기꺼이 읽는다:
- 선형 쓰기 프리미티브를 사용해
TrimBounds의 vtable 포인터 일부를 이웃한TrimBoundsvtable의 하위 2바이트를MapTablevtable로 매핑하는 조작된MapTable치환 테이블로 덮어쓴다. 지원되는 Quram 빌드 간에는 하위 바이트만 다르므로 단일 64K 룩업 테이블로 7개 펌웨어 버전과 모든 4 KB ASLR 슬라이드를 처리할 수 있다. TrimBounds필드 나머지(top/left/width/planes)를 패치해 객체가 나중에 실행될 때 유효한MapTable처럼 동작하게 한다.- 제로 처리된 메모리에서 가짜 opcode를 실행한다. 치환 테이블 포인터가 실제로 다른 opcode의 vtable을 참조하기 때문에 출력 바이트는
libimagecodec.quram.so또는 그 GOT의 저차 주소들이 leaked 된다. - 추가
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 스프레이 내부에 준비되면:
- 마지막
DeltaPerColumn쓰기 물결이 Stage-3QuramDngImage의 오프셋 0x22에0x0100을 더하여 raw 버퍼 포인터를 0x10000만큼 이동시키고 이제 공격자 명령 문자열을 참조하게 한다. - 인터프리터는 1040개의
Unknown(23)opcode의 꼬리를 실행하기 시작한다. 첫 번째 손상된 항목은 vtable이 오프셋 0xf000의 위조 테이블로 교체되어QuramDngOpcode::aboutToApply가 가짜 테이블의 4번째 항목인qpng_read_data를 해석한다. - 연결된 가젯들은 다음을 수행한다:
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를 제어한다.
참고자료
- Project Zero – A look at an Android ITW DNG exploit
- Project Zero – Pixel 0-click: CVE-2025-54957 in Dolby UDC
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 지원하기
- 구독 계획 확인하기!
- **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.


