Masquage d'information ou les conséquences d'une incohérence subtile de spécification

Date :29 Septembre 2006

Publication: Article

Cet article analyse les conséquences des différentes méthodes de représentation des chaines de caractères dans l'espace de nommage Microsoft Windows. En particulier, sont présentées les différences entre l'API native et l'API standard pour l'accès à la base de registre et la possibilité de masquer des clés de registre à l'API standard. Plus généralement, il est possible de masquer n'importe quel objet nommé à l'API standard, mais il existe de nombreux outils utilisant l'API native permettant de les manipuler. Ce masquage n'est donc qu'un masquage de premier niveau.
 

Représentation d’une chaine de caractères

Dans les programmes, il existe plusieurs représentations des chaines de caractères. Les 2 représentations qui nous intéressent dans cet article sont les chaines de caractères dites "comptées" et les chaines de caractères dites "zero terminated".

Les chaines "comptées"

Une chaine de caractères "comptée" est représentée en mémoire par ses différents caractères précédés du nombre de caractères de cette chaine. C’est cette représentation qui est utilisée dans le langage de programmation "Pascal" ou "Java".

Ainsi la chaine de caractères "Cert-IST" sera représentée comme suit :

 8

 ‘C’

 ‘e’

 ‘r’

 ‘t’

 ‘-’

 ‘I’

 ‘S’

 ‘T ’

Dans cet exemple, le 8 représente le nombre de caractères de la chaine "Cert-IST". Au total, le stockage de la chaine de caractères nécessite 9 octets.

Les chaines "zero terminated"

Une chaine de caractères "zero terminated" est représentée en mémoire par ses différents caractères suivi d’un octet spécial marquant la fin de la chaine. Cet octet a pour valeur 0. C’est cette représentation qui est utilisée dans le langage de programmation "C" et "C++".

Ainsi la chaine de caractères "Cert-IST" sera représentée comme suit :

 ‘C’

 ‘e’

 ‘r’

 ‘t’

 ‘-’

 ‘I’

 ‘S’

 ‘T’

 0

Au total, le stockage de la chaine de caractères nécessite 9 octets.

Espace de nommage

A priori, ces différentes méthodes de représentation sont de la mécanique interne et ne devraient pas avoir d’influence sur le comportement des programmes.

Toutefois, l’ensemble des noms (appelé aussi espace de nommage) représentable avec la méthode "comptée" n’est pas identique à l’ensemble des noms représentable avec la méthode "zero terminated".

Ainsi, dans l’exemple suivant, la chaine représentée est valide en utilisant la méthode comptée, mais ne possède pas de représentation valide avec la méthode "zero terminated" (à cause de l’octet 0 en milieu de chaine de caractère).

 9

 ‘C’

 ‘e’

 ‘r’

 ‘t’

 0

 ‘I’

 ‘S’

 ‘T’


Conséquences

S’il est possible de manipuler un objet nommé avec une API capable d’utiliser l’une ou l’autre des méthodes de représentation des chaines de caractères, certains noms seront visibles en utilisant une chaine "comptée" et non visibles en utilisant une chaine "zero terminated".

Ceci est la conséquence directe du fait que les espaces de nommage des chaines "comptées" et "zero terminated" sont différents.

Accès à la base de registre

L’API standard

Microsoft propose une API standard et parfaitement documentée pour accéder à la base de registre. Cette API contient par exemple la fonction "RegCreateKey()" permettant d’ouvrir ou de créer une clé de registre. Cette fonction est implémentée dans la DLL système "AdvApi32.dll" et attend comme paramètre (entre autres) le nom de la clé de registre à ouvrir ou à créer. Ce nom de clé est une chaine de caractères du type "zero terminated".

L’API native

Il existe une autre API non ou mal documentée par Microsoft. Cette API est appelée l’API native car, en fin de compte, tous les appels systèmes effectués en utilisant l’API standard finissent par être convertis en appels à l’API native qui est l’API privilégiée par Microsoft pour accéder à la base de registre.

Ainsi, et pour rejoindre l’exemple précédent, il existe une fonction "NtCreateKey()" permettant de créer une clé de registre. Cette fonction est implémentée dans la DLL système "NTDLL.dll" et attend comme paramètre (entre autres) le nom de la clé de registre à créer. Par contre, ce nom de clé est une chaine de caractères du type "comptée".

API native ou API standard ?

Pratiquement, tout ce qu’il est possible de faire avec l’API native est faisable avec l’API standard à quelques exceptions mineures près. Ainsi, il est possible par exemple de spécifier si l’ouverture d’un fichier est sensible aux différences majuscules/minuscules avec l’API native, mais pas avec l’API standard.

Par contre, il n’y a pas de privilèges supplémentaires ni de "super pouvoirs cachés" à utiliser l’API native, c’est pourquoi il faut préférer l’utilisation de l’API standard.

Conséquence

La conséquence de ceci est qu’il est possible de créer une clé de registre au moyen de l’API native et possédant un nom particulier qui ne pourra pas être ouverte en utilisant l’API standard. C’est ainsi qu’il est possible de "cacher" des clés de registres à l’API standard.

En règle générale, tous les programmes utilisent l’API standard parce que c’est l’API qui est documentée et supportée par Microsoft. Ainsi le programme "regedit.exe" qui est le programme de manipulation de la base de registres utilise cette API standard et n’est donc pas capable de voir les clés de registre "cachées".

Conclusions

Généralisation de la méthode

Le paragraphe précédent s’appuie sur les accès à la base de registre, mais il existe d’autres objets nommés qui possèdent les mêmes caractéristiques au niveau des chaines de caractères. Il s’agit par exemple des objets "process" et "thread", des fichiers et répertoires, des tubes nommés, des fichiers "mailslot", des pilotes, des clés de registres, des objets "atomes" et "événements", des variables d’environnement et de localisation et probablement d’autres encore.

Utilisation de la méthode

Une fois que l’on connait ce principe de noms cachés (clé de registres, fichier ou toute autre chose nommée), qu’est-il possible de faire ?

En fait, cette méthode servira surtout à cacher des choses. Ainsi un programme pourra t’il écrire dans un fichier qui ne pourra pas être ouvert par le programme "notepad.exe" ou encore il sera possible de créer tout une branche dans la base de registres qui ne pourra pas être parcourue par le programme "regedit.exe".

Toutefois, il ne faut pas se tromper, cette méthode permettant de cacher de l’information est une méthode facile à détecter et il est très facile de trouver sur Internet des outils et programmes permettant de manipuler ces objets aux noms "bizarres".

Pourquoi ce problème ?

L’existence de ces différences minimes au niveau de l’espace de nommage des noms montrent qu’il est parfaitement possible d’avoir des spécifications légèrement différentes et que l’on pense identiques mais dont les effets de bords sont non négligeables avec le temps et l’analyse. En effet, il faut avoir un esprit retord pour imaginer et trouver une différence entre les chaines "zero terminated" et "comptées".

Il est important lors de la spécification des interfaces de bien faire en sorte de ne pas introduire ce genre de problème.

Une explication possible à ceci pourrait être le fait que la personne qui a spécifié les interfaces de l’API native connaissait plus particulièrement le langage "Pascal" et la personne qui a spécifié l’interface de l’API standard connaissant plus particulièrement le langage C. Ces 2 personnes ont fait leurs spécifications de leur côté et quand il a fallut faire communiquer les 2 interfaces entre elles, une méthode qui satisfait les 2 API a été trouvée et le trou dans l’espace de nommage n’a pas été vu.

Pour plus d'informations :