
CVE-2022-22629 Proof of Concept
Ce billet concerne la preuve de concept du bug WebGL qui a été corrigé dans les mises à jour de sécurité de Safari 15.4.

Si vous souhaitez apprendre en détail comment fonctionne WebGL, vous pouvez utiliser cette ressource officielle puisque nous allons discuter ici des détails spécifiques au bug :
https://webglfundamentals.org/webgl/lessons/webgl-how-it-works.html
Cette preuve de concept ne concerne pas la façon d'obtenir les primitives pour exploiter ce bug, nous allons simplement discuter de l'analyse de la vulnérabilité et de la façon de faire planter le contenu web : )
WebGL est une API JavaScript utilisée dans les navigateurs pour rendre des graphiques 2D et 3D. Elle utilise le composant ANGLE comme backend WebGL principal, améliorant considérablement la compatibilité avec d'autres navigateurs et permettant un WebGL 2.0 conforme dans WebKit. WebGL offre de nombreuses extensions permettant au développeur d'effectuer le rendu avec des optimisations. Nous allons donc examiner l'extension qui nous intéresse.

Cette API fournit donc plusieurs extensions pour différents usages. Le bug dont nous allons discuter se trouve dans EXT_.
Vous pouvez lire plus de détails sur les extensions ici :
https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Using_Extensions
Quoi qu'il en soit, passons au bug :)
L'extension WEBGL_multi_draw fait partie de l'API WebGL et permet de rendre plusieurs primitives en un seul appel de fonction. Cela peut améliorer les performances d'une application WebGL en réduisant les coûts de liaison dans le moteur de rendu et en accélérant le temps de traitement du thread GPU avec des données uniformes.
Cette extension expose les variantes d'appel de dessin
Multi*dans la fonctionnalitéEXT_multi_draw_arrays, en plus de la variable intégrée du shader de sommetsgl_DrawIDexposée parARB_shader_draw_parameterspour OpenGL.Ces fonctions se comportent de manière identique aux fonctions standards
DrawArrays()etDrawElements(), sauf qu'elles gèrent plusieurs listes de sommets en un seul appel. Leur objectif principal est de permettre à un seul appel de fonction de rendre plus d'une primitive, comme une bande de triangles, un éventail de triangles, etc.De plus, cette extension ajoute une variable intégrée supplémentaire,
gl_DrawID, au langage de shader. Cette variable contient l'index du dessin en cours de traitement par une varianteMulti*d'une commande de dessin.
Vous pouvez lire plus de détails sur l'extension ici :
https://registry.khronos.org/webgl/extensions/WEBGL_multi_draw/
Cette extension est similaire à l'API WebGL WebGLRenderingContext.drawArrays() et WebGLRenderingContext.drawElements(), etc., qui rendent des primitives à partir de données de tableaux. La seule différence est que nous pouvons effectuer le rendu en utilisant plusieurs tableaux grâce à cette extension.
Nous pouvons effectuer ces opérations à l'aide de cette extension : [1]
ext.multiDrawArraysWEBGL() rend plusieurs primitives à partir de données de tableaux (identique à plusieurs appels à drawArrays).ext.multiDrawElementsWEBGL() rend plusieurs primitives à partir de données de tableaux d'éléments (identique à plusieurs appels à drawElements).ext.multiDrawArraysInstancedWEBGL() rend plusieurs primitives à partir de données de tableaux (identique à plusieurs appels à drawArraysInstanced).ext.multiDrawElementsInstancedWEBGL() rend plusieurs primitives à partir de données de tableaux d'éléments (identique à plusieurs appels à drawElementsInstanced).
https://github.com/WebKit/WebKit/commit/c087ca07c9125093ca62e8e44024b738bb7d46dc
La vulnérabilité n'existe que de Safari 15.0 à 15.3 car cette extension n'est prise en charge que dans Safari 15 (iOS) dans le cas d'Apple.
Le correctif est très facile à comprendre et indique clairement qu'il s'agit d'un débordement de tampon de tas (heap buffer overflow) lors du rendu. Cette fonction est appelée ici dans WebGLMultiDraw.cpp pour toutes les opérations que nous avons mentionnées ci-dessus [1]. Comprenons ce qui se passe avant que cette fonction ne soit appelée. Nous pouvons examiner l'implémentation de l'une des fonctions ci-dessus :
void WebGLMultiDraw::multiDrawArraysWEBGL(GCGLenum mode, Int32List firstsList, GCGLuint firstsOffset, Int32List countsList, GCGLuint countsOffset, GCGLsizei drawcount)
{
if (!m_context || m_context->isContextLost())
return;
if (!validateDrawcount("multiDrawArraysWEBGL", drawcount)
|| !validateOffset("multiDrawArraysWEBGL", "firstsOffset out of bounds", firstsList.length(), firstsOffset, drawcount)
|| !validateOffset("multiDrawArraysWEBGL", "countsOffset out of bounds", countsList.length(), countsOffset, drawcount)) {
return;
}
m_context->graphicsContextGL()->multiDrawArraysANGLE(mode, makeSpanWithOffset(firstsList, firstsOffset), makeSpanWithOffset(countsList, countsOffset), drawcount);
}
En JS, les spécifications de la fonction sont définies comme ceci :
void ext.multiDrawArraysWEBGL(mode,
firstsList, firstsOffset,
countsList, countsOffset,
drawCount);
mode:
gl.POINTS : Dessine un point unique.gl.TRIANGLES : Dessine un triangle pour un groupe de trois sommets.firstsList:
firstsOffset:
countsList:
countsOffset:
drawCount:
En regardant l'implémentation, on voit que nous pouvons contrôler le décalage (offset) et le drawcount pour dessiner au-delà de la taille du tampon : )
Donc, notre POC se trouve ici ...

Comme vous pouvez le voir, notre page web a simplement planté car la fonction validateOffset() appelée dans multiDrawArraysWEBGL() ne vérifie pas si notre firstsOffset se trouve dans l'intervalle [0, size-drawcount] et nous dessinons au-delà de la taille du tampon réel. L'appel de fonction m_context->graphicsContextGL()->multiDrawArraysANGLE() est implémenté dans des composants tiers pour WebKit connus sous le nom d'ANGLE et ne comporte aucune vérification avant le rendu, donc nous plantons en écrivant une valeur erronée dans la région mémoire.
À titre d'information, le pseudo-code de multiDrawArraysANGLE() fonctionne comme ceci :
for(i=0; i<drawcount; i++) {
if (*(counts+i)>0) DrawArrays(mode, *(firsts+i), *(counts+i));
}
Et avec le correctif appliqué, il ne rendra rien et retournera false via l'appel de la fonction validateOffset() :

js) ...
PARS Defense Team
Nous recrutons de nouveaux membres d'équipe, contactez-nous : info[at]parsdefense.com