Malicious debugger ! (1/3)

Wed 03 October 2007 by alex

Pourquoi debugger des programmes ? En général, on se lance dans ces opérations quand on est développeur et qu’on recherche la cause d’un plantage dans son programme. Alors, pourquoi se préoccuper de debuggage quand on traite de sécurité ? Simplement parce que quiconque sait debugger peut faire quasiment tout sur un système d’exploitation. En effet, cela demande de toucher au cœur même du système, et donc de connaître tous ses principes de fonctionnement.Dans cette série de trois billets, nous présentons les méthodes classiques de debuggage sous Linux et Windows, puis nous développons quelques exemples de ce qu’ilest possible de faire grâce à cela.

Avant de nous lancer dans le debuggage à proprement parler, revenons sur quelques notions fondamentales liées aux systèmes d’exploitation (Operating System ou OS).

Le processus

La première notion importante est celle de processus. Il s’agit d’un environnement d’exécution, composé de données et d’instructions agissant sur ces données. Un processus n’existe qu’en mémoire. Il est crée par le noyau du système, souvent en chargeant un programme depuis un support physique (comme un disque dur). Le rôle du noyau est alors de lire le fichier, et de le copier en mémoire, en l’adaptant, pour ensuite lui passer la main.

La mémoire

Cela nous conduit à la deuxième notion importante, celle de mémoire. Il faut distinguer la mémoire physique (les barrettes de RAM) de la représentation qu’a un processus de la mémoire (dite mémoire virtuelle). Grâce à des mécanismes qui sortent du cadre de ce billet, un processus sur une architecture n bits a l’impression de disposer de 2^n adresses. En fait, ces adresses sont structurées différemment selon les OS, le format du binaire et quelques autres facteurs. Malgré les apparences, ces mécanismes permettent de faire des optimisations très efficaces en mémoire.

Si on prend le cas d’une bibliothèque utilisée par plusieurs processus, chacun aura l’impression de l’avoir à lui… et ce sera le cas d’une certaine manière, en fonction de la mémoire considérée. En mémoire virtuelle, propre à chaque processus, cette bibliothèque sera chargée dans l’espace mémoire du processus (l’adresse pourra d’ailleurs différer entre les processus). Néanmoins, en mémoire physique, la bibliothèque ne sera présente qu’une fois : la pagination et la segmentation font en sorte que des adresses virtuelles différentes pointent vers les mêmes adresses physiques. Ainsi, la bibliothèque est partagée par tous les processus qui s’y réfèrent, mais ils ne s’en rendent pas compte, ce qui permet de préserver de la RAM. Néanmoins la mémoire n’est pas occupée que par des processus. En fait, tels que nous les avons décrits, nous avons omis jusqu’à présent une partie qui est aussi partagée par tous les processus : le noyau. Le noyau d’un OS est un programme chargé de gérer le matériel, l’allocation de la mémoire physique, l’organisation de la mémoire virtuelle, l’ordonnancement des tâches et de nombreux autres aspects essentiels au bon fonctionnement du système.

Les niveaux de privilèges

Du fait de la criticité des tâches qui sont les siennes, le noyau est un composant très sensible, et la moindre erreur conduit généralement à un crash complet. Pour le protéger l’architecture x86 prévoit différents niveaux d’exécution, appelés ring : le ring 0 correspond au maximum de privilèges (i.e. le noyau), et le ring 3 au minimum (pour les processus, dits processus utilisateurs dont nous avons parlé).

Le debuggage

Maintenant qu’un processus utilisateur est en mémoire d’où il s’exécute, on souhaite examiner les opérations qu’il réalise ou les données qu’il manipule : c’est là qu’intervient le debuggage. En réalité, les capacités de debuggage ne dépendent pas de l’OS, mais de l’architecture sous-jacente. Pratiquement, on trouve deux fonctionnalités communes à toutes les architectures :

  1. le mode pas-à-pas (single step) : dans ce mode, le processeur n’exécute qu’une instruction, puis s’arrête tant qu’on ne lui dit pas d’exécuter la suivante ;
  2. les points d’arrêt (breakpoint) (bp) : parfois, on ne souhaite pas exécuter un processus en mode pas-à-pas pour se concentrer sur une partie du code. Pour cela, on place un point d’arrêt dans le programme à l’adresse souhaitée. Le processeur exécute toutes les instructions, jusqu’à tomber sur le bp, et là, il s’arrête, comme en mode pas-à-pas. Il existe 2 types de points d’arrêt : logiciels ou matériels. Les bp logiciels sont représentés par l’instruction int 3 (\xCC). Quand on souhaite en placer un dans un programme, on écrit l’opcode \xCC à l’adresse voulue, puis le processeur lève une exception indiquant qu’il vient de rencontrer le bp quand il exécute l’instruction. Pour cette raison, on peut en placer autant qu’on veut dans un processus. Inversement, les bp matériels sont limités (4 sur les processeurs IA 32) car ils sont liés à des registres, dits registres de debug qui contiennent l’adresse à surveiller. Tous les debuggers s’appuient sur ces deux fonctionnalités. Ce qui les différenciera sera la manière de les gérer, et donc la manière dont les OS les supportent.

Prochainement sur vos écrans

Dans les billets suivant, nous montrerons les techniques de debuggage disponibles sur les OS Linux et MS Windows, en architecture x86. Nous nous restreindrons au debuggage en espace utilisateur (ring3), mais nous verrons que cela permet déjà de faire beaucoup de choses.