Language: Français
10-17, 15:00–15:50 (America/New_York), Track 2 (206a)
Qu'il s'agisse d'applications, de systèmes d'exploitation, de bases de données, etc., tout ce qui lit, écrit ou manipule des données doit utiliser un encodage. De nos jours, il s'agit le plus souvent d'UTF-8 par défaut, parfois d'UTF-16, les deux étant des normes Unicode. Les auditeurs et les chercheurs en sécurité manipulent souvent des données ou des protocoles, mais qu'en est-il de la manipulation de l'encodage sous-jacent ?
Unicode est devenu l'encodage par excellence, remplaçant des centaines d'anciennes normes. À première vue, on pourrait penser qu'il s'agit d'une simplification. Il n'en est rien. Tous ces anciens encodages étaient très basiques, alors qu'Unicode est d'une complexité inouïe, que l'on ne peut imaginer qu'en lisant les spécifications.
Au fil des ans, la méconnaissance de l'Unicode et sa complexité ont entraîné de nombreux problèmes et erreurs de mise en œuvre. La version 16.0 de la norme Unicode compte 1140 pages, et il y a plus de 60 UAX (Unicode Standard Annexes), UTS (Unicode Technical Standards), UTR (Unicode Technical Reports), chacun d'entre eux étant comparable à un RFC de l'IETF. Au cours des trois dernières années, j'ai analysé une quinzaine de langages de programmation, dont aucun n'implémente 100 % de la norme Unicode.
Tous les logiciels qui vous entourent utilisent probablement l'Unicode, mais aucun d'entre eux ne l'implémente complètement et tous sont probablement différents. Qu'est-ce qui pourrait mal se passer ?
Présentation
Comme la plupart des gens ne connaissent que le nom d'Unicode et ne connaissent pas vraiment son fonctionnement, je commencerai par un aperçu rapide des principales caractéristiques : code points, code planes, encodages (UTF-8, UTF-16, UTF-32), composition, jonction, propriétés. Le but est que tout le monde ait les clés pour comprendre les concepts suivants.
- Ce qu'est l'Unicode, ce qu'il contient, et une idée du nombre de caractères et de l'évolution dans le temps,
- Ce que sont les code points, comment ils sont représentés, exemple de différents types de caractères.
- Montrer la distribution des caractères dans les 17 code planes
- Montrer que les mécanismes d'encodage de l'UTF-8, de l'UTF-16 et de l'UTF-32 sont très différents et expliquer le rôle du BOM.
- Le rôle de la composition et la manière dont le même caractère peut être représenté de plusieurs façons, ce qui donne une représentation canoniquement équivalente.
- Le rôle de la jonction et comment il est possible de créer des millions de caractères à partir de 154k caractères.
- Versions : Unicode a des versions comme un logiciel, donc la spécification change avec le temps et un langage de programmation ou un logiciel donné peut utiliser une version différente d'Unicode pour différentes parties.
Problèmes de sécurité
Ensuite, je présenterai les problèmes de sécurité qui peuvent découler des mécanismes de base : homoglyphes, caractères invisibles, équivalence des diacritiques, substituts, taille(s) des chaînes (octets, unités, graphèmes), transformation de la casse (majuscules, minuscules, correspondance sensible au contexte, correspondance sensible à la langue), inversion des chaînes, classes d'expressions régulières et quantificateurs, normalisation.
En plus de ces questions, je présenterai des mises en garde concernant JavaScript, PHP, Ruby, Java, Python, Perl, PowerShell, Rust, Go, etc.
Parmi les problèmes de sécurité, je commencerai par les problèmes visuels :
- De nombreux caractères sont visuellement similaires (homoglyphes) et peuvent être utilisés pour des attaques par hameçonnage (!= typosquattage) ou pour introduire des portes dérobées dans le code source.
- Les caractères invisibles sont également pratiques pour introduire des portes dérobées dans le code source.
J'aborderai ensuite les questions d'équivalence :
- Les caractères composés avec diacritiques et les caractères précomposés sont équivalents mais pas égaux.
- Les caractères de substitution ont le même résultat que les diacritiques, mais ils peuvent être utilisés plus facilement pour contourner les restrictions sur les plages de caractères, mais l'utilisation de l'UTF-16 est moins commune parmi les languages de programmation et ce cas d'usage est un peu une niche.
Viennent ensuite les problèmes liés à la manipulation des chaînes de caractères :
- Avec l'ASCII, nous étions habitués à ce que 1 caractère = 1 octet et c'est tout. Mais avec Unicode, la taille en octets, le nombre d'unités de code et la taille en graphèmes ne sont souvent pas les mêmes.
- La taille en octets est le nombre d'octets que prend la chaîne dans un encodage donné. La taille varie entre UTF-8, UTF-16 et UTF-32.
- Le nombre d'unités de code, qui est parfois confondu avec la « taille en caractères », est le nombre de séquence Unicode utilisés y compris les « caractères » de contrôle.
- La taille en graphèmes est le nombre de « caractères » qu'un être humain peut voir, sans compter les caractères invisibles et les caractères de contrôle.
- Presque tous les langages de programmation manquent de méthodes pour faire cela ou ont des noms de fonctions contre-intuitifs. Cela peut conduire à des accès à l'index erronés ou à une troncature de la chaîne de caractères.
- Les transformations dans le monde Unicode sont loin d'être bijectives…
- Plusieurs lettres peuvent avoir la même sortie pour une transformation de casse donnée (n ➡️ 1)
- Une lettre peut devenir plusieurs lettres après une transformation de casse (1 ➡️ n)
- La transformation de casse est contextuelle, une même entrée peut donner lieu à des sorties différentes en fonction des autres éléments placés autour d'elle (1 ➡️ n)
- La transformation de cas est localisée, de sorte que la même entrée peut donner lieu à des résultats différents en fonction des paramètres régionaux (1 ➡️ n).
- … de sorte que le correspondance de casse pourrait générer : la modification de la longueur de la chaîne, la création de collisions, la non-concordance des entrées, le contournement des mesures de sécurité.
- Même quelque chose qui nous semble simple, comme l'inversion d'une chaîne de caractères, est étonnamment complexe et conduit à de nombreuses surprises, et encore une fois, peu de langages de programmation permettent de le faire correctement et facilement.
Je poursuivrai avec les problèmes causés par la prise en charge de l'Unicode dans les moteurs RegExp :
- On ne s'attend pas à ce que les expressions régulières posent des problèmes avec Unicode. Mais parmi tous les langages de programmation, certains supportent l'Unicode en natif, d'autres ont besoin d'un drapeau spécial, d'autres encore ne le supportent pas du tout. Il n'est pratiquement jamais possible de connaître la version d'Unicode supportée par un moteur de regexp de manière non heuristique, certains supportent les classes de caractères POSIX, d'autres non, et il en va de même pour les classes personnalisées. Et les classes POSIX seront-elles limitées à la plage ASCII ou Unicode ? Tout cela, et bien plus encore, est toujours implicite et largement non documenté. Il est impossible de s'attendre à ce que les développeurs utilisent correctement les regexps s'il est impossible de savoir comment le moteur se comporte en premier lieu.
Je finirai la partie sur les problèmes en parlant de la normalisation, qui peut être à la fois une solution et un danger.
- Tous les comportements précédents sont difficiles à gérer, c'est pourquoi la normalisation tente de simplifier cela. Mais il existe plusieurs formes de normalisation (NFD, NFC, NFKD et NFKC) et il convient d'utiliser la bonne au bon moment. Si elle est mal utilisée, la normalisation elle-même peut conduire à des vulnérabilités un peu comme les XSS mutés avec les WAFs ou les DOM sandboxes (DOMpurify, chromium JS sandbox).
Attaques au niveau applicatif
Enfin, je montrerai quelques exemples d'attaques utilisant ces vecteurs Unicode : prise de contrôle d'un compte via la réinitialisation du mot de passe en utilisant la collision de la transformation de la casse, division de l'hôte en utilisant la normalisation, comment le mauvais enchaînement des étapes de sécurité peut conduire à leur contournement pour aboutir à un XSS, le piégeage du code source en utilisant des caractères invisibles, redirections ouvertes en utilisant le repliement de casse et les plages de regexp.
I'm a pentester & security researcher, so I'm mostly focus on offensive security. Outside penetration tests (where I enjoy web the most), I spent a lot of time in R&D, where a majority of this time investment was spent on one topic: Unicode. So Unicode is, by far, the topic I know best.
Some people may know me for my Github activity: writing tools, contributing to open-source software a lot as well as security resources, maintaining packages at BlackArch, etc.