Blog Haypo

Aller au contenu | Aller au menu | Aller à la recherche

samedi 26 janvier 2008

Failles de sécurité liées à Unicode

La complexité du jeu de caractères Unicode est source de nombreuses failles de sécurité. Cet article présente quelques failles récentes pour illustrer les problèmes qu'on peut rencontrer.

Écriture bidirectionnelle (RLO et LRO)

Le premier type que je veux présenter n'est pas un bug d'Unicode, mais une fonctionalité ! On peut changer l'ordre dans lequel est écrit le texte. Un dieu du CSS, Stu Nicholls, l'utilise pour afficher son adresse email en clair, alors qu'en fait elle est écrite à l'envers dans la souce HTML ! Le style CSS est « unicode-bidi:bidi-override; direction: rtl; ».

Sauf que des malins ont pensé à utiliser cette fonctionnalité pour tromper l'œil humain en cachant l'extension d'un nom de fichier. L'article Deceptive file names under Vista (septembre 2007) montre comment une programme Windows (.scr) est affiché comme une image JPEG (.jpg) dans Windows Vista. Windows XP ne supporte pas cette fonctionnalité et affiche donc les codes de contrôle Right-to-left override (RLO, U+202E) et Left-to-right override (LRO, U+202D), montrant alors la supercherie.

Halfwidth and Fullwidth Forms

Les failles de type « directory traversal » outrepassent les mesures de sécurité et permettent de lire un fichier arbitraire. En PHP, on trouve souvent des failles du type « index.php?page=../../../../etc/passwd ». Les webmestres se protègent en interdisant la chaîne « ../ » dans le nom du fichier utilisé. Quelques fois, il est possible d'outrepasser cette protection en spécifiant le chemin complet du fichier. Une variante est de jouer entre les caractères « / » et « \ » selon le système d'exploitation. Certains serveurs et/ou systèmes d'exploitations acceptent également « .../ » dans le nom du fichier.

Ce type de bug est aujourd'hui connu et corrigé dans la majorité des serveurs. Dumoins, c'est ce qu'on pensait jusqu'à cette annonce : Unicode encoding can be used to bypass intrusion detection systems (juin 2007). L'idée est d'utiliser les caractères halfwidth et fullwidth de la plage Unicode U+FF01-U+FFEE. Le soucis est que les URL sont normalisées après avoir été validées !

Exemple de normalisation (décomposition canonique) avec Python 2.5 :

>>> from unicodedata import normalize
>>> char=normalize('NFKC', u'\uFF0E'); print "%r (%s)" % (char, ord(char))
u'.' (46)
>>> char=normalize('NFKC', u'\uFF0F'); print "%r (%s)" % (char, ord(char))
u'/' (47)
>>> char=normalize('NFKC', u'\uFF3C'); print "%r (%s)" % (char, ord(char))
u'\\' (92)

Séquence UTF-8 invalide

Il faut savoir que 7 ans plus tôt, un bug similaire avait déjà été découvert dans Microsoft IIS (octobre 2000). Cette fois-ci, le problème était la normalisation de l'encodage UTF-8. IIS était trop laxiste : il acceptait les séquences invalides, c'est-à-dire lorsqu'un code a une séquence plus longue en octets que la taille normale. Exemple : le caractère point « . » (U+2E) s'encode « 0x2E » en UTF-8, mais peut également être encodé (0xC0, 0xAE) (forme invalide).

Note : Le langage Java utilise d'ailleurs une forme non standard d'UTF-8 : le caractère nul est encodé volontairement (0xC0, 0x80) pour éviter qu'une chaîne soit tronqué par une fonction C bas niveau (telle que strcpy).

Confusion entre octet et caractère

Depuis que le jeu de caractères ASCII a été inventé, il existe une confusion entre la notion d'octet et de caractère. C'est encore plus vrai avec les jeux de caractères ISO-8859. La très grande majorité des programmes mélangent allègrement octets et caractères sans se poser de question. D'une manière générale, ce n'est pas trop gênant. On retrouve cette problématique lorsqu'on manipule du HTML : si on tronque du texte HTML à une position donnée, il est possible qu'on coupe en plein milieu d'une balise ou d'un caractère écrit sous la forme « &nom; ». Exemple : « J'ai mangé ! » tronqué au 12e caractère donne « J'ai mang&ea ».

Exemple de vulnérabilité : WordPress Charset SQL Injection Vulnerability (décembre 2007). Le problème apparait lorsque la base de donnée utilise un jeu de caractère chinois : Big5 ou GBK. La fonction qui échappe les chaînes de caractères SQL utilise addslashes() qui travaille sur des octets et non pas des caractères. La séquence d'octets (0xB3, 0x27) est alors échappée en (0xB3, 0x5C, 0x27). Or 0xB35C est un caractère valide en Big5, et on obtient donc une apostrophe seule !

Exemple avec Python 2.5 :

>>> user='\xB3\x27'
>>> sql=user.replace("'", "\\'")
>>> print unicode(sql, "big5")
許'

Le problème de fond est que PHP ne supporte pas Unicode. Il va falloir attendre PHP6 qui est en cours de gestation. Notez que ce genre de bug touche également les programmes écrit en C, Java ou même en Python. Bien que Python propose le type unicode, il est rarement utilisé bien que complet. Le module re (expression régulières) supporte les expressions unicode. Python 3000 vise, entre autre, à encourager l'adoption d'Unicode comme type par défaut des chaînes de caractères.

Autres bugs des implémentations d'Unicode

La bibliothèque Qt de Trolltech calculait mal la longueur des chaînes UTF-8 (il manquait un "+1") : Bugzilla Bug 269001: CVE-2007-4137 QT off by one buffer overflow (rapport de bug avec patchs pour Qt3 et Qt4, août 2007).

La fonction repr() du langage Python n'allouait pas assez de mémoire pour les chaînes Unicode : buffer overrun in repr() for unicode strings (août 2006, lire aussi le CVE-2006-4980). La fonction repr() n'allouait que 6 octets par caractère en ne considérant que la forme « \uXXXX », or la forme « \Uxxxxxxxx » peut être nécessaire et consomme 10 octets par caractère.

Conclusion

Unicode regorge de fonctionnalités qui sont souvent méconnues. Mal utilisées ou utilisées à mauvais escient, ça peut faire très mal. Je pense qu'il manque des fonctionalités de sécurité dans les bibliothèques Unicode. Les encodages non standards doivent être rejettés ou une alerte doit être déclanchée. Le module mod_security d'Apache propose ce genre de fonctionnalité : voir SecFilterCheckUnicodeEncoding et @validateUtf8Encoding. Il faudrait pouvoir désactiver toutes les fonctionalités Unicode et n'activer que ce dont on n'a besoin pour éviter les effets de bord indésirables.

Jeu de caractères Unicode

Unicode est un hydre : lorsqu'on découvre Unicode, chaque point éclairci va faire surgir deux nouvelles questions ! Cet article tente de démystifier Unicode en présentant quelques particularités de ce jeu de caractères.

Unicode 5.0

Unicode est un jeu de caractères incluant tous les autres jeux de caractères existant : c'est un surensemble qui est donc forcément plus gros. La dernière version publiée date de juillet 2006 : la norme Unicode 5.0.

Nombre de caractères :

  • Caractères graphiques : 98.884 (lettres, marqueurs, chiffres, ponctuations, symboles et espaces)
  • Code de formatage : 140 (séparateurs de ligne et de paragraphe, sens du texte, ...)
  • Code de contrôle : 65 (héritage de l'ASCII)
    • U+00–U+1F
    • U+7F–U+9F
  • Caractères à usage privé : 137.468 (l'émetteur et le récepteur peuvent définir leurs propres codes)
    • U+E000–U+F8FF
    • U+F0000–U+FFFFD
    • U+100000–U+10FFFD
  • Codes surrogates : 2.048
    • U+D800–U+DBFF : Low surrogates
    • U+DC00–U+DFFF : High surrogates
    • Pour UTF-16, les codes U+10000 à U+10FFFF sont encodés sur deux mots de 16 bits :
      • On retire 0x10000 au code : 0x10000..0x10FFFF => 0x0000..0xFFFF
      • On découpe le résultat en deux valeurs de 10 bits chacune
      • Finalement, le premier mot est : 0xD800 + (code-0x10000) & 0x003FF (10 bits de poids faible)
      • Et le deuxième mot est : 0xDC00 + ((code-0x10000) >> 10) & 0x003FF (10 bits de poids fort)
    • Les surrogates sont donc des codes Unicode interdits pour éviter de confondre un code avec un mot d'UTF-16
  • Non caractères : 66
    • Codes terminés par 0xFFFE en hexadécial : U+FFFE, U+1FFFE, ... U+10FFFE (17 codes). Codes interdits pour que le code U+FEFF puisse servir comme marqueur d'endian pour les encodages UTF-16 et UTF-32.
    • Codes terminés par 0xFFFF en hexadécial : U+FFFF, U+1FFFF, ... U+10FFFF (17 codes). Peuvent être utilisé dans une implémentation d'Unicode comme « valeur maximale ».
    • U+FDD0–U+FDEF
  • Codes réservés : 875.441 (réservés pour des usages futurs)

Unicode 5.0 contient donc 1.114.112 codes caractères : codes 0x000000 à 0x10FFFF.

Les codes 0x0000 à 0xFFFF constituent ce qu'on appelle le « Plan multilingue de base » (abrégé BMP en anglais).

Sérialisation d'Unicode

Un octet ne pouvant contenir que 256 codes différents, Unicode a besoin d'un encodage sur plusieurs octets pour chaque code. Il existe de nombreux encodages :

  • UTF-7 : mots de 7 bits (le 8e bit peut être utilisé comme bit de parité)
  • UTF-8 : mots de 8 bits
  • UTF-EBCDIC : mots de 8 bits (compatible avec le jeu de caractère EBCDIC)
  • CESU-8 : mots de 8 bits (mélange entre UTF-8 et UTF-16)
  • UTF-16 : mots de 16 bits (UTF-16LE et UTF-16BE pour little endian et big endian)
  • UTF-32 : mots de 32 bits (UTF-32LE et UTF-32E)

Il existe également les algorithmes SCSU et BOCU qui servent à compresser un texte Unicode sérialisé.

Exemple avec Python 2.5 :

>>> u'é'.encode('utf8')
'\xc3\xa9'
>>> u'é'.encode('UTF-16LE')
'\xe9\x00'
>>> u'é'.encode('UTF-16BE')
'\x00\xe9'

Caractère U+AF05 :

>>> u'\uAF05'.encode('utf8')
'\xea\xbc\x85'
>>> u'\uAF05'.encode('UTF-16LE')
'\x05\xaf'
>>> u'\uAF05'.encode('UTF-16BE')
'\xaf\x05'

Encodage Punycode

L'algorithme Punycode, normalisé par la RFC 3492, sert à convertir un nom de domaine Unicode sous forme ASCII : lettres A à Z, chiffres 0 à 9 et le caractère « - ». L'algorithme est réversible : on peut convertir un punnycode en Unicode sans perte d'information. Cet encodage est surtout utilisé pour les noms de domaine Unicode (serveurs DNS) : les Internationalized Domain Names (IDN). Lire le billet de Stéphane Bortzmeyer sur les IDNA.

Exemple avec Python 2.5 :

>>> from encodings.idna import ToASCII, ToUnicode
>>> ToASCII(u'café')
'xn--caf-dma'
>>> print ToUnicode('xn--caf-dma')
café

Normalisation d'un caractère

On peut composer un caractère. Le caractère « ä » peut s'écrire « a » + umlaut (U+308). Il existe trois caractères correspondant dans la table Unicode :

  • U+61 : « a », lettre A
  • U+E4 : « ä », lettre A avec umlaut
  • U+308 : « ¨ », umlaut

Donc U+E4 et (U+61, U+308) sont équivalents. Il existe des outils pour normaliser selon les différentes formes :

  • NFD (Forme normale D) : décomposition canonique
  • NFC (Forme normale C) : décomposition canonique suivie d'une recomposition des caractères précomposés
  • NFKC (Forme normale KC) : décomposition de compatibilité, c'est-à-dire remplace les caractères de compatibilité par leurs équivalents
  • NFKD (Forme normale KD) : décomposition de compatibilité suivie d'une décomposition canonique

Exemple avec Python 2.5 (Décompose puis Compose) :

>>> from unicodedata import normalize
>>> list(normalize('NFD', u'ä'))
[u'a', u'\u0308']
>>> list(normalize('NFC', u'a\u0308'))
[u'\xe4']

Conclusion

Unicode est un sacré bordel ! Les gens habitués à manipuler ASCII et son jeu de caractère ridicule de 128 codes sont perdus. Même ceux habitués à ISO-8859-15 flambant neuf avec son euro bling-bling (€) sont perdus car ils connaissent presque les 256 codes par cœur. Unicode contient plus d'un million de codes qu'on peut écrire d'un millier de manières différentes !

Néanmoins, c'est un pari sur l'avenir car on peut enfin mélanger sans broncher des textes dans n'importe quel langue. D'ailleurs, on peut changer l'ordre d'affichage pour les écritures de droite à gauche et inversement. Les encodages UTF-8, UTF-16 et UTF-32 simplifient la détection de l'encodage et sont plus simples que leurs ancêtres (ex: Shift JIS et ses nombreux codes de contrôle). UTF-8 est le charset ultime pour l'interopérabilité car il n'a pas de problème d'endian, est simple à détecter et peu coûteux en place pour l'encodage d'ISO-8859-1.

vendredi 18 janvier 2008

Analyser un fichier binaire ou un programme inconnu

Cet article explique comment analyser un fichier d'origine peu sûre (ex: Internet) ou dont le format est inconnu (rétro-ingénierie). Il n'est sûrement pas exhaustif, mais liste divers outils bien pratiques pour ce genre de travail.

Avertissement

Lorsqu'on traite un fichier d'origine inconnue, il faut être sur ses gardes. Il se peut que le fichier attaque volontairement les outils d'analyse cités dans cet article. Les virus sont connus pour cracher les débogueurs et/ou changer leur propre comportement lorsqu'ils sont analysés. Travaillez sur une machine dédiée aux tests (ex: machine virtuelle), ou bien avec des privilèges minimaux (ex: machine coupée du réseau).

Détecter le type d'un fichier inconnu

Quand on reçoit un fichier binaire d'un type inconnu, le programme le plus utile est file. Il détermine le format du fichier à partir d'une importante base de signature. Il sait extraire certaines méta-données (dimension d'une image, version du format, etc.) et sait également faire la différence entre les sous-formats (tel que AVI ou WAVE pour le format RIFF, et Theora et Vorbis pour Ogg).

D'autres programmes peuvent servir pour identifier le format ou plutôt extraire les méta-données :

  • hachoir-metadata : supporte un grand nombre de formats de fichiers
  • extract : supporte un grand nombre de formats de fichiers
  • Kaa : images, sons et vidéos
  • identify : images, fait parti de l'excellente suite Image Magick

Analyse manuelle d'un fichier binaire

Le programme strings sert à extraire des chaînes de caractères d'un fichier binaire. Il vous faudra peut-être tester différentes options (encodages des chaînes) pour obtenir satisfaction. Souvent, strings donne beaucoup de faux positifs (la sortie est assez bruitée).

Un éditeur hexadécimal est toujours pratique pour rechercher visuellement des motifs, des chaînes de caractères, informations cachées, etc. J'utilise « hexdump -C fichier » ou bien khexedit (programme KDE).

Quand un fichier semble vraiment trop aléatoire, il se peut qu'il soit compressé et/ou chiffré. J'ai écrit un petit script « entropy.py » qui calcule l'entropie des symboles (mot de 8 bits) d'un fichier. Quelques exemples :

  • Programme EXE PE : 4,11 bits/symbole
  • Page HTML : 4,89 bits/symbole
  • Document PDF : 7,75 bits/symbole
  • Image JPEG et PNG : 7,87 et 7,82 bits/symbole
  • Archive gzip (.tar.gz) et bzip2 (.tar.bz2) : 7,99 bits/symbole

Au delà de 7,5 bits/symbole, il y a de fortes chances que le fichier contienne des champs compressés. C'est le cas dans les exemples, mais cet outil n'est qu'une mesure empirique.

Pour trouver les blocs compressés, une solution est de tenter de décompresser à partir du 1er octet, puis du 2e, etc. Le script « find_deflate.py » implémente justement cet algorithme, lent mais il fonctionne.

Enfin, l'outil hachoir-subfile permet de rechercher les fichiers contenu dans un fichier binaire en recherchant des motifs (marqueur de début, marqueur de fin) et en vérifiant que le fichier trouvé est valide (pour limiter les faux positifs). Il existe beaucoup d'outils similaires tels que Photorec et Scalpel, ou encore TestDisk et Sleuth Kit qui sont eux dédiés à l'analyse d'images de disque dur.

Analyse statique d'un programme

Ayant majoritairement travaillé sous Linux, je ne parlerai que des programmes ELF. L'outil objdump affiche de nombreuses informations sur un fichier ELF tel que ses sections, les symboles (objdump -T fichier) et sait désassembler du code. L'outil nm liste les symboles des bibliothèques statiques (extension du fichier « .a »). L'outil ldd liste les bibliothèques importés par un programme ou une bibliothèque avec le chemin complet qui sera utilisé. Enfin, elfsh est une suite complète d'outils pour l'analyse de fichier ELF.

Analyse dynamique

Analyser un programme sans l'exécuter ne permet que d'extraire de maigres informations. Il est toujours plus instructif de l'exécuter. Il existe de nombreux outils pour tracer un programme. strace affiche les appels systèmes. ltrace affiche les appels aux bibliothèques dynamiques, mais sait également tracer les appels systèmes. gdb est le grand classique parmis les débogueurs, boîte à tout faire.

Pendant l'exécution du programme, « lsof -p pid » affiche les fichiers qu'il a ouvert et « netstat » permet d'afficher les connexions réseaux.

Autres outils que je n'ai pas testé :

J'ai écrit un binding Python pour ptrace qui permet d'écrire facilement son propre outil d'audit à la manière de strace ou ltrace. Enfin, mon bref article Syscall contient mes divers notes sur les appels systèmes Linux.

Sites Internet

Pour en savoir plus sur le sujet, voici quelques sites très instructifs :

vendredi 4 janvier 2008

Évolutions des interfaces utilisateurs

Les périphériques de pointage les plus connus sont le clavier et la souris, tous deux inventés dans les années 1960. Depuis Windows 95, le clavier a gagné 3 « touches Windows », passage de 102 à 105 touches. La souris est passé de 2 boutons à 3 boutons, puis a gagné une roulette verticale. Certaines souris ont également une roulette horizontale et plus de 4 boutons. Il existe également les souris inversées : les trackballs, plus reposant pour le poignet mais moins précises (je trouve). Malgré tous ces changements, il n'y a rien de révolutionnaire. Voyons les interfaces multitouch, la Wii et autres innovations bien plus rigolotes :-)

Lire la suite

Publication de Fusil version 0.7

J'ai commencé à m'intéresser au fuzzing en avril 2007 alors que je testais la solidité de mon projet Hachoir. J'ai ensuite écrit divers scripts bash, puis Python, pour fuzzer d'autres programmes. Perfectionniste, j'ai commencé en novembre dernier à réécrire tous mes scripts depuis zéro en partant sur une nouvelle architecture basée sur un système multi-agents. Deux mois plus tard, j'ai fini de réécrire tous les scripts (Mplayer, ClamAV, Firefox, etc.). Pour marquer le coup, j'ai publié la version 0.7 du projet Fusil.

Nouveautés de la version 0.7

  • Projet Firefox utilisant un serveur HTTP écrit depuis zéro, un processus Firefox, et un agent qui envoie la touche F5 à Firefox
  • Début du portage sous Windows : Fusil devrait maintenant fonctionner sur Linux, *BSD et Windows
  • Corrections du système multi-agents et divers bugs mineurs

La complexité du scénario du projet Firefox montre que Fusil est maintenant prêt pour n'importe quel type de scénario :

  • Exécution d'un processus Firefox
  • Génération d'images invalides (ou n'importe quel type fichier, comme un animation Flash)
  • Exécution d'un serveur HTTP : port TCP 8080 en écoute, puis réponse aux diverses requêtes du navigateur web
  • Envoi de la touche F5 à Firefox pour rafraichir l'affichage

Chaque étape est activée à la fin de la précédente, ce qui se décrit simplement avec les événements Fusil (session_start, mangle_filenames, http_server_ready, ...).

Fusil gagne en fonctionnalités

L'idée de Fusil est de simplifier l'écriture d'un projet de fuzzing. Il suffit de décrire le scénario pour préparer et surveiller l'environnement, sans avoir à s'occuper des détails techniques (rediriger la sortie du processus, détecter un plantage, etc). Fusil va plus loin en offrant des fonctionnalités communes à tous les projets :

  • Reconnaissance des comportements anormaux : utilisation excessive du processeur (+75% durant 3 secondes), motif de texte dans la sortie standard ou logs Linux (« assertion », « segfault », « glibc detected », ...)
  • Recherche automatique des paramètres optimaux capables de planter une application (en particulier, le nombre d'erreur injectées dans un fichier valide)
  • Protection du système d'exploitation : limitation des ressources (mémoire / temps) des processus crées, copie partielle des variables d'environnement, etc.
  • Pause pour attendre que la charge de la machine retombe sous un certain seuil (CPU à 50%)
  • Création d'un dossier temporaire pour chaque session (exécution) du projet, sauvegardé en cas de succès
  • Fichier de log contenant l'ensemble des commandes permettant d'analyser un crash voir de rejouer le scénario manuellement

Dans sa version 0.7, Fusil offre des outils pour compiler du code C, créer et surveiller des processus, des clients et serveurs réseaux non blocants (en particulier, un serveur HTTP), etc.

mercredi 2 janvier 2008

Bonne année 2008