Forums d'entraide informatique - Astuces - Conseils
Des experts à votre écoute pour tous vos dysfonctionnements
Vous n'êtes pas identifié.
#1 31-08-2008 22:58:14
- Admin
- Administrateur
- Date d'inscription: 30-07-2008
- Messages: 683
Fiabilisation d'expoit Windows - de la faille à l'expoit
Fiabilisation
d'exploits Windows
DOSSIER
De la faille à l'exploit
Quiconque aura un jour essayé d'utiliser des exploits Windows disponibles sur Internet sera sans doute du même avis : qualité et fiabilité ne semblent pas être au cœur des préoccupations des concepteurs. Passons outre les soucis de portabilité du code entre les différents compilateurs et plateformes, rarement pris en compte, pour nous intéresser à des exploits un tant soit peu fonctionnels. Quelles sont les chances qu'un tel exploit donne le résultat escompté dans un environnement différent de celui mis en place pour le développement du code ? Généralement, elles sont faibles.
Les aspects de régionalisation des applicatifs ou du système d'exploitation sont rarement pris en compte, de même que le niveau d'application des correctifs. Or ces éléments peuvent avoir une influence sur les différents aspects constitutifs de l'exploit, pas tant sur le shellcode, que sur les différentes adresses nécessaires à la bonne exécution de notre code. Voyons comment s'affranchir de ces limitations, et évidemment comment se prémunir contre des exploits de plus en plus fiables.
L'exempte de MS05-039
La vulnérabilité MS05-039 [MS05-039], corrigée par Microsoft le 9 août 2005, impacte une opération de copie mémoire dans la bibliothèque dynamique umpnprngr.dil. Cette portion de code peut être atteinte par 4 fonctions exportées par l'interface RPC 8d9f4e40-a03d-11ce-8f69-08003e30051b [PNP], dont 2 (PhiP_ CueryResConfList et PhiP DetectResourceCorflict) présentent un chemin d'exécution menant à la faille particulièrement simple, accessible anonymement [MSRPC]. Ces informations en main, il ne reste plus qu'a construire une requête déclenchant le débordement et observer son impact sur le flux d'exécution, le contenu des registres et de la pile du processus hébergeant cette interface RPC. Le procédé ne sera pas détaillé ici, car dépassant largement le cadre de l'article.
La chaîne de caractère "BEBBCOCCDD..." apparaissant à plusieurs reprises a été soumise par nos soins et indique par conséquent une zone mémoire sous notre empire. A ce stade précis, nous avons le contrôle de EIP et donc du flux d'exécution. Il nous reste à déterminer quelle valeur affecter à ce registre afin de sauter dans notre zone mémoire, et donc potentiellement exécuter du code.
L'expérience vous apprendra qu'utiliser directement une adresse de la pile n'est généralement pas une bonne idée lorsque le processus est threadé, l'adresse de base de la pile ayant tendance à ne pas être bien stable. Ainsi, après un reboot et une deuxième tentative, notre buffet- se verra déplacé de 0x13ef914 en 0x51f914.
L'alternative réside dans la présence de l'adresse de notre zone mémoire à des emplacements référençables relativement au contenu des registres au moment de la prise de contrôle du flux d'exécution. La valeur à affecter au registre El P devra être l'adresse d'une suite d'instructions permettant de rediriger le flux d'exécution vers le contenu d'un des emplacements trouvés précédemment. Dans notre exemple, EBX contient l'adresse de notre buffer. Ainsi, il nous faudra affecter à EIP l'adresse d'une instruction iMP EBX, ou équivalente, afin d'arriver dans notre buffer.
Vous remarquerez aussi plusieurs références à notre chaîne dans la pile, adressables relativement à ESP. Ainsi pour [ESP+8] (troisième entrée dans la pile en partant du haut), une instruction semblable à JMP [ESP+8] fera tout aussi bien l'affaire. Une variation couramment utilisée pour cette dernière instruction est une suite d'instructions POP REG I, POP REG2 suivie d'un RET (quels que soient REG I et REG2, nous n'avons pas à nous préoccuper de leur contenu) : cela équivaut à rajouter 8 à ESP grâce aux deux POP, puis à sauter vers notre buffer grâce au RET, dans la mesure où notre adresse se trouve maintenant en haut de la pile.
Certaines des adresses possibles, utilisées dans la nature, sont présentées dans le tableau I.
Kostya Kortchinsky
Ingénieur chercheur - EADS/CCR kostya.kortchinsky@eads.net
TABLEAU 1 PnP MS05-039 Adresse Mnémoniques assembleur Module Commentaire
Quelques exploits publics Date pour la vulnérabilité Auteur
I I août 2005 H D Moore 0x767a38f6 pop edi, pop esi, retn umpnpmgr.dll Intégré à Metasploit
Il août 2005 m5l0nny 0x75021e3e call ebx ws2help.dll
12 août 2005 houseofdabus 0x767a1567 pop eax, pop esi, retn umpnpmgr.dll
0x767a366f jmp ebx umpnpmgr.dll Adresse non utilisée
17 août 2005 Variante IRCBot 0x01013c79 pop esi, pop ebx, retn services.exe Inspiré du précédent
Dépendances à l'environnement
Usuellement, de telles adresses se trouvent au sein des différents modules chargés par le processus : exécutables, bibliothèques dynamiques. En prospectant directement les fichiers à la recherche de séquences hexadécimales correspondant aux instructions souhaitées, puis en traduisant les adresses physiques concordantes en adresses virtuelles, on obtient le résultat escompté. Cette méthode est utilisée notamment par l'outil msfpescan du frarnework Metasploit [METASPL011].
Néanmoins, ces binaires sont amenés à évoluer avec les versions du système d'exploitation ou de l'applicatif concerné. Tout correctif pourra induire un déplacement (ou même la suppression) dans le code des séquences d'octets utilisables. Même entre deux versions identiques d'un binaire, l'adresse de base de chargement de ce dernier peut varier, entraînant une modification de l'adresse virtuelle des séquences en mémoire. Il en résulte une forte dépendance entre les adresses de retour et l'environnement dans lequel est exécuté le processus : version du système d'exploitation, version de l'application, régionalisation. Notez toutefois que les versions « poste de travail » ou
« serveur » d'un même Windows (et autres si elles existent) sont construites sur un même pool de binaires, ce qui n'engendre pas de différence d'un produit à un autre pour un même numéro de mouture.
Les résultats exposés ci-dessus ont le mérite de mettre en évidence la volatilité des adresses des bases de certaines DLL en fonction du langage : parfois identiques pour deux versions dissemblables, parfois différentes pour deux versions similaires. Afin de contourner ce facteur contraignant, nous pouvons envisager plusieurs possibilités : trouver des adresses communes aux différentes versions, identifier finement la régionalisation et les applicatifs, provoquer des fuites d'information, attaquer par force brute.
Adresses de retour communes
Bien des exploits se targuent d'être universels, à savoir de cibler indifféremment tout système vulnérable à la faille impliquée (enfin du moins une sous-partie bien délimitée). La raison à cela : l'utilisation d'adresses de retour communes à un grand nombre de plateformes.
43
TABLEAU 2 er 5.0.2191.1 Adresse de base 0x77e80000
Russe
Adresses de base de la bibliothèque Version de Windows 2000 kernel32.dl1
3e:
Aucun
Professionnel
Professionnel 3 Anglais 5.0.2195.5400 0x77e80000
Server 4 Japonais 5.0.2195.6688 0x77e50000
Advanced Server 4 Français 5.0.2195.6688 0x77e70000
Professionnel 4 avec URI Espagnol 5.0.2195,7006 0x79450000
Ces adresses existent à des degrés variables, les unes cibleront par exemple la famille des Windows 2000 anglais, tandis que les autres adresseront les Windows 2000 avec Service Pack 4 quelle que soit leur régionalisation.
Les constats concernant les systèmes d'exploitat on sont les suivants
MB 11 existe un certain nombre de DLL qui, au sein d'une même version majeure de Windows (par exemple 2000), n'ont pas été modifiées au cours des versions mineures (service packs) tout en conservant la même adresse de base. Malheureusement, cette adresse de base varie d'une régionalisation à une autre. C'est dans les zones mémoires occupées par ces binaires que l'on trouvera des adresses « universelles » pour une régionalisation donnée de l'OS. Dans l'exemple de MS05-039, les adresses de ws2hel p.d11 et Eimprpnigr.d11 ciblent les Windows 2000 anglophones quel que soit leur service pack (ainsi que quelques autres langues par effet de bord).
Il existe des EXE, modifiés au fil des service packs, qui gardent une adresse de base identique pour toutes les régionalisations. C'est dans ceux-ci qu'il faudra chercher des adresses « universelles » pour une version mineure donnée. Dans l'exemple de MS05-039, l'adresse de services. exe cible tous les Windows 2000 SP4 — dont, bien sûr, les francophones.
Les binaires combinant les attributs énoncés précédemment sont très rares, et même s'ils existent, leur utilisation est souvent cantonnée à des programmes obscurs. Afin de les mettre en exergue, un utilitaire tel que DlIVers [DLLVERS], et un grand nombre de plateformes de test, vous seront indispensables. L'idée est de lister tous les binaires d'une installation de Windows (on peut se limiter aux DLL et EXE du répertoire %Wi nOi r% I systern32 dans un premier temps), leur version et l'adresse de base de leur image en mémoire afin de détecter ceux communs. L'emploi d'outils de virtualisation tels VirtualPC ou VMware permet de faire cohabiter l'ensemble sur une même machine physique. Une alternative est la Metasploit Opcode Database [OPCODEDB], qui vous permettra de rechercher, dans une base de données pré-remplie, des adresses qui répondent à des critères que vous définirez. Les plateformes disponibles sont nombreuses, avec pour l'instant trois régionalisations, et les réponses sont pertinentes. il va sans dire que ces constats s'adaptent aussi aux appli-cations.
La bibliothèque msvcp56.dIt est utilisée, entre autres, par le service Message Queuing, affecté par une vulnérabilité de type stock overflow [MS05-017]. Elle est par conséquent l'endroit idéal où trouver une adresse de retour universelle pour cette faille précise. Quant aux autres, leur usage est plus sporadique. Attention ! Les adresses de base peuvent changer dans l'éventualité où deux bibliothèques seraient chargées à cheval sur des zones mémoires déjà mappées (typiquement une collision entre deux On). C'est un aspect à prendre en compte. Notez aussi que ce tableau ne recense pas les exécutables, qui peuvent aussi constituer une source d'adresses de valeur.
TABLEAU 3 versions de
Adresse de base
Bibliothèques dynamiques communes aux Windows 2000 utilisées précédemment
Module Version
ados 5.0.2920.0 0x80000000
bootvid.dll 5.0.2172.1 0x80010000
dbmsadsn.dll 1999.10.20.0 0x42bd0000
dbmssocn.dll 1999.10.20.0 0x73330000
dbmsspxn.dll 1999.10.20.0 Ox42be0000
gpkcsp.dll 5.0.2134.1 0x08000000
mcdsrv32.d11 5.0.2160,1 Ox800 I 0000
mspatcha.dll 5.1.2600.0 Ox600a0000
msvcirt.dll 6.1.8637.0 0x780a0000
msvcp50.d11 5.0.0.7051 Ox780c0000
slbcsp.dll 5.0.2134.1 0x08000000
slbkygen.dll 5.0.2144.1 0x08000000
sqlwid.d11 1999.10.20.0 0x412f0000
vcdex.dll 5.0.2134.1 OxOffb0000
vdmredir.dll 5.0.2134.1 OxOffa0000
Emulation de code
eEye a présenté une méthode plus évoluée de recherche d'adresses aux conférences BlackHat grâce à leur outil privé EEREAP (eEye Emulating Return Address Purveyor). En sauvegardant l'état du processus attaqué au moment de la prise de contrôle du flux d'exécution (mémoire, registres, drapeaux, etc.), l'outil tente de trouver les séquences d'octets qui, une fois exécutées, mènent à une zone mémoire sous leur contrôle, en les émulant. Ils peuvent alors découvrir des suites de plusieurs dizaines d'octets satisfaisant leurs critères et multiplier le nombre d'adresses de retour possibles par 4 ou 5. La plus grosse portion de code découverte dans leur cas d'étude contient 91 instructions.
Au delà des DLL...
Exécutables et bibliothèques dynamiques ne sont pas les seuls éléments à peupler l'espace mémoire d'un processus. On y trouve aussi quantité d'objets : des piles (stock), des tas (bec»), des fichiers mappés, des sections partagées, des blocs d'environnement (TEB et PEB), etc. Même si les données de certains sont très volatiles, elles sont pour d'autres beaucoup plus stables et peuvent se révéler éminemment intéressantes. Afin d'élargir le panel des résultats, la recherche d'adresses
devra s'étendre à la totalité de l'espace mémoire du processus à exploiter.
Grâce à un outil comme DumpOp [DUMPOP], nous pouvons rapidement parcourir les zones de mémoire commises (affublées du drapeau MEM COMMIT) pour un processus donné et lister les adresses qui satisfont nos critères. Il vous signalera également les adresses communes à différentes plateformes. Il fonctionne en allant lire l'espace mémoire d'un processus afin d'y repérer des séquences hexadécimales prédéfinies, et peut sauvegarder les résultats dans un fichier. Ce fichier pourra être ensuite chargé sur une autre plate-forme afin de ne conserver que les « collisions ».
TABLEAU 4
Adresses de retour potentielles communes aux versions de Windows 2000 utilisées précédemment
' Adresse Objet Instructions
0x00187533 unicode.nls jmp ebx
0x00 18759f unicode.nls call ebx
0x001875e3 unicode.nls call ebx
0x00 I 87839 unicode.nls jmp ebx
0x00 187a I f unicode.nls jmp ebx
0x00189b5f unicode.nls jmp ebx
0x00 18a485 unicode.nls jmp ebx
0x00 I ecad0 sortkey.n I s push ebx, retn
_
Ox00 I fOcd0 sortkey.nls call ebx
0x001f4c30 sortkey.nls jmp ebx
Les fichiers NLS (nécessaires au support natif de la langue), apparaissant dans le tableau ci-dessus, présentent trois avantages ils sont toujours mappés au même endroit pour chaque version majeure de Windows (NT 4.0, 2000, XP et 2003), ne subissent pas de modification avec les service packs, et contiennent une pléthore d'adresses de retour utilisables. Deux inconvénients cependant : les pages mémoires ne sont pas marquées exécutables, l'octet de poids fort de ces adresses est nul.
Le premier ne devrait pas poser de problème dans le cadre de cette étude (lors d'exécution de code sur la pile, elle aussi marquée comme non exécutable, vous trouverez de plus amples informations sur cela dans l'article [TINNES] de ce numéro); quant au second, il peut nuire à l'exploitation de failles fondées sur des opérations de traitement de chaînes de caractères ANSI, sauf si ce caractère est le dernier de la chaîne provoquant le débordement. Blaster, pour exploiter la vulnérabilité MS03- 036, utilisait comme adresse de retour 0x001B759t lorsqu'il ciblait les Windows 2000, adresse que vous retrouverez dans notre tableau.
Dans le cas de MS05-039, il s'agit d'une copie mémoire, la présence d'un caractère nul n'influant pas sur le débordement. Dans le cas de débordement dû au traitement de chaîne de caractères UNICODE, tel que pour MS05-046 [MS05-046], la présence de ce seul caractère nul ne gênera pas l'exploitation, puisque seul un couple d'octets nuls (qui plus est en position « paire ») marque la fin d'une chaîne.
Il est bien sûr possible de trouver d'autres adresses génériques, en fonction du processus étudié, des instructions recherchées. Plus on restreindra le nombre de cibles, plus la quantité d'adresses sera importante. On pourra alors se permettre davantage d'exigences telles que l'absence de certains octets dans les adresses, des adresses compatibles unicodes, etc.
Fuites d'information
Si la généricité « ultime » est complexe à atteindre, il peut être opportun d'obtenir un maximum d'informations sur te système à attaquer avant d'en exploiter une faille, afin de construire un code sur mesure. En environnement Windows, il est rare d'avoir la possibilité de lancer des attaques par force brute essayant exhaustivement l'ensemble des paramètres possibles, et la première attaque exécutée devra être la bonne.
Identifier finement la cible
La version majeure de l'OS est un minimum, son niveau de mise à jour est un plus, sa régionalisation se révélera à l'usage essentielle. Il existe bien entendu des méthodes génériques de prise d'empreinte d'un système : pile TCP/IP, bannières, SNMP, qui peuvent parfois suffire. Windows offre cependant des possibilités supplémentaires d'identification avancée, ne nécessitant souvent qu'un minimum de privilèges, à savoir une session dite « nulle ». La plupart de ces fonctionnalités sont accessibles grâce aux API standards Microsoft, documentées dans la bibliothèques MSDN. Nous signalerons en particulier
▪ NetQueryDisplayInformation : permet de récupérer des
informations sur les comptes, les ordinateurs et les groupes sur la machine distante
NetRenoteTOD: retourne les informations concernant la date et l'heure courantes sur la machine spécifiée
NetServerGetInfo : retourne des informations sur la machine spécifiée grâce au service Serveur : plate-forme, nom, version, type
▪ NetShareEnum : énumère les partages disponibles sur le poste distant
.11 NetSessionEnum : détaille les sessions en cours sur le poste distant
▪ NetUserEnum : liste les utilisateurs existant sur le poste distant
▪ NetlIkstaGetInfo : retourne des informations sur la machine
spécifiée grâce au service « Poste de Travail » : plate-forme,
nom, version, type.
Un exemple d'usage de ces fonctions est donné dans un article chinois [XFOCUS], et les renseignements récupérables par ce biais sont complets : seul le niveau de mise à jour échappera à notre activité de reconnaissance. Pour pallier cela, nous pouvons
passer par l'énumération des interfaces RPC exposées [5 EKI] car correctifs et service packs influent sur ces dernières, en retirant certaines, en exposant de nouvelles. Plusieurs indications figurent aussi à des endroits non accessibles directement à l'utilisateur, comme lors de l'établissement de sessions SMB. Les valeurs des champs Nati ve OS et Native LAN Manager des paquets réponses Sessi onSetupAndX donnent la version majeure de Windows. Si vous êtes en mesure de capturer du trafic émanant de la machine ciblée, les données transmises par les applications clientes telles le navigateur préciseront avantageusement le contexte.
Fuites d'adresses
Si nous arrivons à obtenir des pointeurs issus de la mémoire de l'objectif, nous pourrons peut-être arriver à nos fins en passant outre toute tentative d'identification du système ou de l'applicatif. En général, c'est toute la problématique du memory leak ou fuite de mémoire qui nous intéressera. En particulier, ce sont certaines propriétés de l'implémentation DCERPC de Microsoft qui nous captiveront. Par exemple, l'attribut [unique] du langage MIDL, utilisé pour définir les interfaces RPC, spécifie un pointeur unique : si cet attribut est accolé à l'attribut [out; qui identifie un paramètre retourné par l'appel RPC, bingo ! Nous aurons dans la réponse à notre appel un pointeur vers une zone
mémoire du processus. Dans le meilleur des cas, ce pointeur sera vers une zone mémoire sous notre contrôle. C'était le cas pour le service de Gestion de Licenses [MS05-0 IO] : la fonction RPC LlsrLocalServiceAddVi nous permettait de placer notre buffer en mémoire, tandis que la fonction Li srLotalSer s sel n fo GetW nous retournait une copie de ce buffer en la précédant d'un pointeur vers cette copie. Il ne restait alors plus qu'a déclencher le débordement sans autre considération, grâce à cette adresse fournie gracieusement par le service attaqué, pointant dans une zone mémoire sous notre contrôle.
Conctusion ou comment s'en prémunir ?
Vous l'aurez sans doute remarqué, les outils d'exploitation publics de vulnérabilités ainsi que les vers en vogue sont loin d'intégrer tous ces éléments à leur code. Il est donc primordial de prendre les devants afin d'élever la difficulté d'exploitation de votre parc informatique et vous prémunir des évolutions prochaines à attendre dans ce domaine. Les premières mesures à adopter relèvent de considérations sécuritaires générales
Filtrez autant que faire se peut les ports associés à SMB et MSRPC : I35/TCP, I37/UDP, I38/UDP, I391TCP, 445/TCP seront un bon début ;
Identification distante de
la régionalisation de Windows
A ce jour, il ne semble pas exister publiquement de méthode fiable pour déterminer à distance la nature de la régionalisation d'une installation d'un système d'exploitation Windows. Si avec NT 4.0, le choix était relativement réduit, il a explosé avec 2000 : versions japonaise, chinoise, turque, danoise,...
Heureusement, certaines chaînes de caractères changent en fonction de la langue et transparaissent dans les réponses à certaines requêtes RPC. Des personnes ont suggéré l'utilisation de letQueryni spl ayl nf ormati on ou NetUserErwir afin de récupérer les noms des utilisateurs administrateur et invité et les comparer à ceux recensés en fonction de la régionalisation. Les résultats sont plutôt décevants, d'autant plus que Windows XP refuse ces requêtes par défaut. L'alternative réside dans le champ Rerrark=des partages listés par la fonction Net Shd r
hautement dépendant de la régionalisation de l'OS. Un exemple d'implémentation est donné dans l'utilitaire FpLang [FPLANG]. Seuls XP SP2 et 2003 SPI rejettent par défaut ces appels distants.
Protection de l'espace d'adressage :
état de l'art sous Linux et OpenBSD
La protection de l'espace d'adressage d'un processus permet de rendre plus difficile l'exploitation de failles de sécurité. Plusieurs techniques existent, notamment mises en oeuvre au niveau du noyau Linux, qui se montrent très efficaces, pour un coût presque négligeable en performances et en temps d'administration. Sous Windows, par manque de contrôle sur les éléments de bas niveau du système, les techniques les plus avancées sont difficiles à mettre en oeuvre et les solutions actuelles, regroupées sous le nom de HIPS (Host instrusion prevention systems) n'ont pas la même efficacité.
1. Prévention générique d'exploitation de failles de sécurité
Introduction
Beaucoup savent qu'une simple erreur de programmation peut être exploitée par un attaquant pour exécuter du code arbitraire avec les privilèges du processus vulnérable. La possibilité d'exploiter certaines failles est souvent due à des détails d'implémentation de bas niveau, rarement connus du programmeur : par exemple le fait que des données soient mélangées à des structures de contrôle, que ce soit sur la pile (variables locales et adresse de retour sur architecture Intel), sur le tas (données et métadonnées de gestion du tas) ou dans une chaîne de format. De nombreux domaines, e priori orthogonaux, se trouvent mélangés. Qui aurait prévu que les choix a priori indépendants des encodages ASCII et des opcodes d'Intel ou l'adresse virtuelle choisie par le linker pour une fonction jouent un rôle dans la facilité d'exploitation d'une faille ? De ce fait, déterminer l'exploitabilité ou non d'une faille de sécurité est délicat. Il est cependant possible de rendre plus difficile, voire impossible, l'exploitation de certaines erreurs de programmation de manière générique, sans connaître la faille en question a priori.
Classification
Dans cet article, nous nous limiterons aux failles de sécurité dont l'exploitation se fait en contrôlant le flot d'exécution du processus. Cela exclut par exemple les erreurs logiques et un cas particulier de dépassement de tampon où l'on pourrait écraser une variable vitale gérant nos privilèges.
Les étapes de l'exploitation d'une faille (prenons ici pour simplifier un buffer overflow) sont généralement les suivantes
II Recherche d'une faille dans un programme
Communication avec le processus vulnérable afin de déclencher le dépassement de tampon
J Injection de code arbitraire dans l'espace d'adressage du processus (facultatif)
In Contrôle du flot d'exécution du programme (éventuellement pour le rediriger vers le code arbitraire préalablement injecté)
113 Détournement du processus pour réaliser diverses actions (installation de rootkit, récupération de certaines données, lancement d'un nouvel exploit)
Le séquencement de ces étapes n'est pas strict, on peut par exemple imaginer utiliser le contrôle du flot d'exécution du programme pour injecter du code arbitraire ou, ce qui arrive plus fréquemment, pour rendre des données injectées exécutables (nous reviendrons là-dessus).
Des mécanismes de prévention générique existent pour prévenir chacune de ces étapes
MI Détection d'erreurs de programmation par analyse statique (sparse, Coverity, Microsoft PREfix/PREfast, etc.)
la Empêcher que les conditions de la faille (par exemple l'overflow) se produisent à l'exécution (libsafe, BCC (bound checking GCC) et CASH)
Il Empêcher l'injection de code arbitraire dans l'espace
d'adressage d'un processus [0] (PaX, OpenBSD's WAX)
El Empêcher le contrôle du flot d'exécution par l'attaquant (SSP/ Propolice), obscurcissement de l'espace d'adressage (PaX, Ozone, VVhentrust)
II Empêcher un processus contrôlé par l'attaquant de faire certaines actions en limitant ses droits (Système de contrôle d'accès, SELinux, TrustedBSD, RSBAC, GrSecurity's RBAC, LIDS, McAfee Entercept, CISCO CSA).
Dans cet article, nous traiterons des étapes 3 et 4 sous Linux et OpenBSD. L'étape 5 sera toutefois abordée car les cinq étapes ci-dessus ne sont pas totalement disjointes, et la correspondance avec les étapes d'exploitation n'est pas stricte : par exemple, un système de contrôle d'accès peut participer à empêcher l'injection de code arbitraire dans l'espace d'adressage d'un processus (nous y reviendrons). Le cas de Windows sera à peine abordé, nous y reviendrons sans doute dans un prochain article.
2. Éléments de système
Nous allons décrire quelques éléments du fonctionnement d'un système d'exploitation sur architecture Intel.
Une grande partie de la sécurité d'un système d'exploitation moderne n'est possible que grâce à l'existence d'une MMU (Memory Management Unit) dans le processeur utilisé. Grâce à celle-ci
Il existe un niveau de privilège matériel qui sépare le noyau
Le noyau contrôle quels périphériques sont accessibles pour un processus en mode utilisateur (à l'aide du champ IDPL du registre EFLAGS et du bitmap de permissions enregistré dans le TSS). En règle générale, seul le noyau peut accéder directement aux périphériques et, en mode utilisateur, il faut utiliser des services du noyau (appels système) pour que le processus puisse exécuter des instructions (contrôlées) en mode noyau et accéder au matériel.
La mémoire d'un processus est virtualisée. Il se voit seul dans son espace d'adressage.
Ces trois éléments sont essentiels pour la sécurité car ils rendent possible une séparation des privilèges entre processus : le noyau étant un passage obligé pour la communication entre les processus et le matériel, il est le superviseur de la sécurité.
En effet, grâce à l'espace d'adressage virtuel, un processus doit passer par le noyau pour pouvoir par exemple modifier l'espace d'adressage d'un autre processus (ptrace ( ), création de zones de mémoire partagée..). C'est ce qui permet des modèles de séparation de privilège simples (par uid/gid) ou complexes (mandatory access contre/ par exemple).
Cette vision des choses est très importante lorsque l'on étudie les systèmes de contrôle d'accès (RSBAC, SELinux, TrustedBSD...), c'est-à-dire lorsque l'on suppose qu'un attaquant possède déjà le contrôle d'un processus : le fait qu'il soit ensuite obligatoire de passer par le noyau pour « sortir de ce processus » (en ouvrant des fichiers, en infectant l'espace d'adressage d'un autre processus, en ouvrant des connexions réseau...) est alors capital.
Segmentation et pagination
Dans cet article cependant, nous allons voir comment l'on peut réagir un niveau auparavant, afin d'empêcher le contrôle du processus par l'attaquant.
Ce qui nous intéresse surtout est le procédé de virtualisation de l'espace d'adressage et nous allons l'étudier rapidement dans le cadre de l'architecture Intel IA32 en mode protégé,
Un programme travaille avec des adresses dites « logiques » dans l'instruction rnov eax, dword ptr ss:Lesp+4], si esp vaut OxBFFFFF80, le mot contenu à l'adresse logique pointée par ss :OxBFFFFF84 est mis dans le registre EAX.
Afin de savoir quelle adresse (physique) il doit utiliser sur le bus mémoire, le processeur effectue deux transformations
II La première, appelée « la segmentation », transforme l'adresse logique en adresse linéaire (aussi appelée adresse virtuelle)
El La deuxième, appelée « pagination », transforme l'adresse linéaire en adresse physique.
Unité de segmentation
Nous l'avons vu, les adresses mémoire utilisées par un programmeur sont des adresses logiques. Celles-ci sont de la forme « selector:offset » où « selector » est un sélecteur de segment de 16 bits et offset un décalage de 32 bits. Les sélecteurs de segment sont en pratique rarement spécifiés, car par défaut, (c'est-à-dire sans préfixe segment override), le segment de code (cs) est utilisé pour tous les instruction fetch, le segment de pile (ss) pour tous les push, pop et références utilisant ESP ou EBP, le segment de données (da) pour toutes les références sauf celles liées à la pile ou aux chaînes, le sélecteur ES pour les instructions liées aux chaînes de caractères. Nous allons décrire, de manière peu détaillée (les lecteurs intéressés peuvent se reporter à la documentation [Intel]) le mécanisme de segmentation.
C'est dans la segmentation (et non pas dans la pagination) que l'on retrouve les fameux rings, c'est-à-dire les niveaux de privilèges Intel. Ils vont de 0 à 3, 0 étant le privilège le plus fort. En pratique les systèmes d'exploitation courants n'utilisent que les privilèges 0 (mode noyau) et 3 (mode utilisateur).
Considérons par exemple l'adresse logique précédente avec 7B pour valeur de SS (7B:OxBFFFF884). Le sélecteur de segment se décompose en trois champs (voir figure ci-dessous).
II Le champ Tl (table indicator) indique dans quelle table de descripteurs, G DT (global descriptor table) ou LDT (local descriptor table), le descripteur de segment correspondant doit être cherché.
H Le champ index indique l'index du descripteur de segment dans la G DT ou la LDT
Ei Le champ RPL indique le requested privilege level, i. e. le plus haut privilège nécessaire pour avoir accès au segment. Dans le cas de CS, ce champ indique le CPL (current privilege level, c'est-à-dire le niveau de privilège courant (typiquement 0 en mode kerne! et 3 en mode utilisateur).
Ainsi, notre sélecteur SS dont la valeur est Ox7B indique que le descripteur de segment à utiliser est le numéro 15 (Obl I II) dans la GDT et que le RPL est 3.
_3 --
Grâce au sélecteur de segment, le processeur repère un descripteur de segment. Selon l'indicateur de table (TI), le descripteur de segment est recherché dans la LDT ou dans la GDT (les adresses linéaires de ces tables sont situées dans les registres LDTR et GDTR). À l'aide du champ index, le processeur repère le descripteur de segment désiré dans la table. Ce descripteur de 8 octets contient de nombreux champs, entre autres un champ base et un champ limit : l'addition de ce champ base et de l'offset donne l'adresse linéaire, le champ lirnit permet de limiter la taille du segment. Parmi les autres champs importants, citons le champ DPL (descriptor privilege level) qui, combiné au CPL (derniers bits de CS) et au RPL du descripteur utilisé permet de vérifier les privilèges.
Par défaut, la majorité des systèmes d'exploitation actuels utilisent le Flat memory mode!, c'est-à-dire qu'ils utilisent zéro comme base pour les principaux segments (ceux dont les sélecteurs sont chargés dans es, ds, ss, es), au moins pour les segments utilisés en mode user, et OxFFFFF comme limite (ce qui donne une limite de 4 Go au segment). L'offset d'une adresse logique peut ainsi dans la majorité des cas être identifiée à l'adresse linéaire. Ceci explique les nombreuses confusions entre adresse linéaire et adresse logique. La segmentation joue en général un rôle peu important dans les systèmes d'exploitation modernes, son utilisation est cependant obligatoire sur processeur Intel, pour des raisons historiques. Nous verrons cependant plusieurs méthodes pour tirer parti de la segmentation. Vous pouvez explorer vos descripteurs de segments à l'aide du programme [DTDUMPER].
Unité de pagination
La pagination constitue la deuxième étape de la translation d'adresse : elle transforme les adresses linéaires en adresses physiques que le processeur envoie sur le bus mémoire. L'espace d'adressage linéaire est découpé en pages (en général de 4 Ko), la pagination fait la correspondance entre les pages et les cadres de pages (pages en mémoire physique). En pratique, sur la plupart des systèmes d'exploitation, c'est elle qui joue le rôle le plus important, elle permet de vérifier les droits d'accès aux pages (droit en écriture, privilège nécessaire pour accéder à la page)... et grâce à elle des mécanismes avancés sont possibles
MI Un cadre de page n'est pas forcément alloué en mémoire physique pour une page effectivement utilisable par le processus : l'utilisation de cette page provoquera une page fouit et le handler correspondant du noyau allouera le cadre de page correspondant en mémoire physique, en y plaçant éventuellement les données nécessaires
Swaping : des cadres de page peuvent être déchargés de la mémoire physique et placés sur le disque.
On demand paging : des fonctions telles que m'op() ou MapViewOffi le sont utilisées pour mapper un fichier en mémoire, les pages ne seront effectivement mappées qu'a l'utilisation.
MI Le copy on write : deux processus sans mémoire partagée peuvent partager le même cadre de page ; lorsque l'un d'eux écrira sur sa page, le cadre de page sera dupliqué. Ce procédé est particulièrement avantageux pour mapper des bibliothèques.
Il existe plusieurs types de pagination sur les processeurs Intel. Nous allons pour simplifier décrire ici rapidement le mode de pagination simple avec pages de 4 Ko (sans PSE, sans PAE, sans IA32e). Dans ce mode, la pagination s'effectue à deux niveaux : une adresse linéaire contient un index de Directory, un index de Table (de 10 bits chacun, qui permettent de repérer une entrée dans une table de 1024 entrées) et un offset de 12 bits qui permet de se repérer à un octet près dans une page de 4 Ko.
,,
_5_
Pagination: transformation d'une adresse linéaire en mémoire physique
Page Base Address
Index
Page Directory Page Table
Ent
Ent
•
Entry
. 'PT Entry
111 x
Offset
Mémoire physique
P
Page Fra
Le registre cri contient l'adresse physique du Page Directory, cette adresse, combinée au Directory Index nous fournit l'adresse d'un PDE (Page Directory Entry), qui lui-même permet de repérer une Page table qui, combinée au Table Index, fournit l'adresse du PTE (Page table entry) de la page. Celui-ci contient un champ Page base address de 20 bits qui permet de repérer le cadre de page (Page Frame) en mémoire physique ainsi que divers Pags qui indiquent notamment les droits de la page (Read/Write), ses privilèges (User/Superviser; notons qu'il n'y a ici que deux niveaux et non pas 4 Rings), si elle est présente en mémoire physique, si elle a été accédée...
Nous ne détaillerons pas les PDE qui sont similaires aux PTE (voir Figure). Remarquons qu'un PTE permet de marquer une page comme étant disponible en écriture ou non (W), mais qu'il n'existe pas de flag pour marquer une page disponible en exécution.
L'absence de ce flag pose de gros problèmes de sécurité (il fut ensuite réintroduit en fanfare sous la dénomination de flag NX, nous y reviendrons). Notons que comme nous ne perdons pas d'information, les 32 bits d'une adresse linéaire permettent bien d'adresser 4 Go de mémoire.
Espace d'adressage d'un processus
Voyons à titre d'exemple comment se crée l'espace d'adressage d'un processus sous Linux 2.4. On peut voir la partie utilisateur de l'espace d'adressage linéaire d'un processus en lisant le fichier /proc/pid/inaps. Sous Linux 2.4, tous les descripteurs de segment qui nous intéressent ont une base nulle; on peut donc ici directement confondre l'espace d'adressage logique (tel qu'il est vu par le processus) et l'espace d'adressage linéaire (après segmentation).
I cat /proc/self/maps
08048000-0004c000 r-xp 00000000 03:03 32672 /binicat [1]
0804c000-0804d500 rw-p 00003000 03:03 32672 ibin/cat [2]
0804d000-0806e000 rwxp 00000000 00:00 0 [3]
40000000-40016000 r-xp 00000000 03:03 175102 flib/Id-2.3.2.so
40015000-40017000 rw-p 00015000 03:03 179102 /lib/ld-2.3.2.so
40017000-40018000 rw-p 00000005 00:00 0
4001d000-40145000 r-xp 00000000 03:03 179607 /lib/libc-2.3.2.so
40145000-40144000 rw-p 05127000 03:03 179607 /libilitc-2,3.2.so
40144000-40150000 rw-p 00000000 00:00 0
40150005-4019f500 r--p 00000000 03:03 942989 /usrilibilacale/locale-archive
bff'e00B-c0099000 rwxp ffiff000 00:00 0 [pile]
L'espace d'adressage d'un processus est changé lors de l'appel système sys_execve 0 qui remplace le contexte d'exécution d'un processus (la création d'un nouveau processus est due à l'appel système sys_fork (a).
L'espace d'adressage linéaire au-dessus de 3 Go (0xC0000000) est réservé au noyau et est mappé dans chaque processus (c'est- à-dire que les pages situées au-dessus de OxC0000000 dans chaque processus pointent vers les mêmes cadres de page). Décrivons rapidement quelques étapes du remplacement du contexte d'exécution d'un processus réalisées par la fonction sys Execve O.
Les exécutables sous Linux utilisent généralement le format ELF. Un fichier ELF peut être vu de deux manières, selon le header considéré : le Program Header offre une vision adaptée à un chargeur de programme. alors que la section header table offre une vision adaptée à un linker statique.
Le Program Header décrit le fichier exécutable comme une suite
de segments. Les segments d'un programme sont des informations pour le chargeur de programme, ils n'ont absolument rien à voir avec la segmentation Intel. Voici le program header de « cat ».
$ readelf -I cat
Elf file type is EXEC (Executable file)
Entry point 0x8048b70
Phare are 7 program headers, starting at offset 52
Program leaders:
Type Offset VirtAddr RhysAddr FileSiz MemSiz Flg Align
P810 0x000034 0x08048034 0)(08048034 0x000e0 0x000e0 RE 0x4
INTERP 0x000114 0)(03048114 0x08048114 0x00013 0)(00013 0x1
[Requesting program interpreter: ilibild-linux.so.2]
LOAD LOAD DYNAMIC NOTE GliU_STACK 0x000000 0A8048000 0x08048000 0x003a3 0x0804ca30 0x0804ca30 0x003a7c 0x0804ca7c 0x0804ca7c 0x000128 0x08048128 0)(08048128 0x000000 0x03000000 0x00000000 0x03a2d 0x03a2d 0x001d0 0x0033c 0x000c8 0)(000c8 0x00020 0x00020 0x00000 0x00000 RE RW RW 0x1000 0x1000 0x4
RW 0x4 0x4
Section ta Segment mapping: Segment Sections,..
00
01 .interp
02 .interp .note.A13I-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r
.rel.dyn .rel.plt .finit .plt .text .fini .rodata
03 .data .eh_frame .dynamic ,ctors .dtors .jcr .got .bss
04 .dynamic
05 .note.A9I-tag
Voici quelques étapes du chargement d'un programme :
Il La fonction fl us h_ol d_exec ( ) du noyau libère les ressources utilisées par le précédent programme, en particulier toutes les régions mémoire user.
11 La pile est allouée, juste en dessous de la limite de l'espace utilisateur. Nous pouvons la voir ici de Oxbfffe000 à Oxc0000000.
la Les segments de type PT_LOAD sont chargés en mémoire. On peut voir ici qu'il y a une zone mémoire avec les permissions read et execute [i] et une zone mémoire avec des permissions read et write [2]. On retrouve bien ces zones dans notre fichier maps. Notons que le deuxième segment a une hieraSiz supérieure à sa Fi 1 eSiz. Cela est à l'origine de la création de la zone [3] qui contient les données non initialisées (section .bss) et qui sert de point de départ au tas (heap).
4 Grâce au Program Header (repéré par son type PT_INTERP), le
noyau localise le chargeur dynamique (1 d -1 i nux so), le charge
en mémoire et lui transfère l'exécution.
El Le chargeur dynamique utilise l'appel système sys_rnmap() pour charger les bibliothèques dynamiques (entrées DT NEEDED dans la table dynamique) en mémoire. Ces bibliothèques dynamiques sont elles-mêmes au format ELF, de type ET_ DYN.
6 Le chargeur dynamique réalise des relocations pour le programme principal et les bibliothèques chargées. C'est lui qui met éventuellement en place le mécanisme GOT/PLT.
la Le chargeur dynamique transfère l'exécution au point d'entrée du programme
Le format ELF est très complexe (il est par exemple beaucoup plus complexe que le format PE de Microsoft). Les lecteurs intéressés peuvent se reporter à la spécification TIS [E LF].
Le fichier maps de la page précédente correspond à un noyau 2.4. Avec un noyau 2.6 nous verrions quelques différences
Presque tout en haut de l'espace d'adressage linéaire (Oxffffe000), une page est réservée par le noyau. Elle contient une bibliothèque ELF appelée vDSO qui est utilisée pour réaliser des appels systèmes. Celle-ci est chargée automatiquement en mémoire par le kernel dans tous les processus. Selon le type de processeur, cette bibliothèque réalise des appels système en utilisant int 0x80/iret ou sysenter/sysexit.
IM Les adresses des bibliothèques dynamiques sont changées
car l'allocation se fait maintenant du haut vers le bas.
IM À partir du noyau 2.6.12 on peut constater une faible randomisation des adresses des bibliothèques dynamiques et de la pile (nous reviendrons dessus dans la partie consacrée à Exec-Shield).
aucun effet utile. Pour pallier ce défaut de l'architecture Intel, Solar Designer utilisa une technique relativement simple afin de rendre la pile non exécutable : en mode utilisateur, le descripteur de segment référencé par le registre cs possède une limite inférieure à l'adresse linéaire de la pile. Les autres descripteurs de segment sont inchangés.
Ainsi, lorsqu'un accès à la pile est réalisé relativement aux registres ds, sa et es, aucune différence n'apparaît. La base du descripteur de segment (0) est ajoutée à l'offset demandé, la somme ne dépassant pas la limite du segment (4 Go), aucune protection matérielle due à la segmentation ne vient perturber l'accès à la mémoire. En revanche, lorsque le processeur tente d'exécuter du code sur la pile, le descripteur de segment référencé par le sélecteur contenu dans le registre cs est utilisé. La somme de la base de ce descripteur (0) et de l'offset demandé dépasse la limite de ce segment (modifiée dans ce but), le processeur lève une exception de protection générale et le noyau Linux tue le processus.
eteeee.,'4e4e.,:e
Limite ds,ss,es
3. Prévention d'exécution de code arbitraire
Nous décrivons ici plusieurs techniques visant à empêcher la prise de contrôle du processus par un attaquant. Comme nous l'avons expliqué, nous nous limiterons aux prises de contrôles fortes qui permettent de détourner le flot d'exécution du programme. Cela est réalisé en
III Empêchant l'attaquant de détourner le flot d'exécution du programme
Il Empêchant l'attaquant d'injecter du code arbitraire.
Un attaquant peut simplement détourner le flot d'exécution du processus sans injecter de code arbitraire. En revanche le contrôle est souvent beaucoup plus simple et plus fort en injectant du code arbitraire puis en détournant le flot d'exécution du processus vers celui-ci. Les choses se compliquent si l'on considère que l'on peut détourner le flot d'exécution du programme vers du code existant dans l'espace d'adressage, de sorte à injecter du code arbitraire et à l'exécuter.
OpenWall
Le premier patch pour le noyau Linux destiné à rendre plus difficile l'exploitation d'erreurs de programmation fut OpenWall (sorti en 1998) [Openwall]. Bien avant l'ère des exploits de format strings (-2000) et avant la démocratisation des heap overflows, la plupart de ces erreurs de programmation avaient pour conséquence un dépassement de tampon sur la pile. La technique classique d'exploitation étant d'écraser une adresse retour afin d'exécuter du code injecté sur la pile, il semblait souhaitable de rendre la pile non exécutable.
On peut certes marquer la pile comme étant non exécutable (c'est-à-dire changer les données internes au noyau, les virtual memory areas), mais à cause de l'absence de flag marquant le droit d'exécution dans les PTE, cela n'impliquera pas de changement dans les structures utilisées par le processeur et n'aura donc
Kernel
physical memory mapping
Pile mode user
Lu
n c°
libc.so (non détaillé)
Id.so (non détaillé)
cat (bss restant ÷ heap)
/binicat (R/W)
/bin/cat (R/X)
Base ds,ss,es
Limite cs
Espace d'adressage linéaire sous OpenWall (sans ASCII armor)
Les limites de cette protection sont nombreuses. Il reste en effet possible d'injecter du code exécutable ailleurs que sur la pile et d'utiliser du code existant. Solar Designer fut également le premier à proposer d'utiliser la technique du return-to-libc afin de passer outre sa protection. Mais on peut également imaginer utiliser de nombreuses techniques plus avancées, par exemple des return-to-libc chaînés (voir l'article de Ratai VVojtczuk dans Phrack [Wojtczulq), afin de recopier son code exécutable dans une autre zone mémoire puis de l'exécuter.
Afin de déjouer ces techniques visant à exploiter du code existant dans l'espace d'adressage, Solar Designer a proposé l'une des premières techniques d'obfuscation de l'espace d'adressage, l'armure ASCII. Le principe est que les bibliothèques sont chargées à une adresse inférieure à Ox01010000 afin que leurs adresses contiennent toujours un zéro. Cela peut en effet
empêcher l'injection d'une adresse de retour cible (par exemple celle de system()) dans l'espace d'adressage d'un processus via des chaînes de caractères. Cette méthode n'est bien entendue utile que s'il est difficile pour un attaquant d'injecter des zéros et il est toujours possible d'utiliser du code existant dans les zones mémoires correspondants au fichier exécutable principal, qui lui ne se trouvera pas dans l'ASCII armer.
Malgré ses limites très importantes, ce patch extrêmement simple déjoue un grand nombre des exploits publics visant des stock overflows (ce qui n'a certes pas grand chose à voir avec la sécurité réelle, n'en déplaise aux éditeurs d'antivirus).
PaX
PaX [PaX] est né en 2000, il était alors plus une preuve de concept exploitant une des idées du projet PIeX86 (utiliser les TLB séparés pour pouvoir instrumenter séparément la lecture/écriture et l'exécution d'une page sur architecture Intel) qu'un véritable patch de sécurité. Il a évolué en introduisant successivement l'ASLR (Address Space Loyout Randomization) en juillet 2001 puis le VMA mirroring, (base de SEGMEXEC et RANDEXEC) en juillet 2002, ainsi qu'en démocratisant des méthodes de compilation permettant d'obtenir des exécutables ET DYN. Il supporte plus de dix architectures (Alpha, i386, ia64, Mips, Mips64, Parisc, PPC, PPC64, Sparc, Sparc64, x86_64) et sert de base aux distributions sécurisées Hardened Gentoo et Adamantix ainsi qu'au patch GrSecurity.
Nous l'avons vu, la pagination Intel fournit nativement un flag permettant de rendre ou non une page disponible en écriture dans les PTE. Idéalement, les protections d'une page devraient pouvoir être une combinaison arbitraire de PROT_READ, PROT_VVR1TE, PROT_EXEC (cette terminologie est celle utilisée notamment dans l'appel système mprotect() qui est utilisé pour changer les protections d'une zone mémoire), mais nous avons vu que seul PROT_WRITE avait un sens au niveau matériel.
Le premier rôle de PaX est d'émuler une sémantique de pages non exécutables sur les processeurs Intel qui ne le supportent pas, c'est-à-dire de donner une signification à PROT EXEC (cette fonctionnalité s'appelle NOEXEC). Il existe deux techniques : la première, PAGEEXEC, induit une surcharge, la deuxième, SEGMEXEC, n'introduit pas de surcharge réelle mais ne permet d'utiliser que 1,5 Go de l'espace d'adressage virtuel utilisateur. Depuis l'introduction des Pentium 4 5xxj (puis 6xx supportant l'EM64T ou x86_64) et des Athlons 64, les processeurs supportent nativement cette fonctionnalité (appelée souvent flag NX). Dans ce cas, PaX (PAGEEXEC) utilise la fonctionnalité native du processeur (ce qui malheureusement nécessite l'utilisation du mode PAE du processeur, fastidieux et plus lent).
PAGEEXEC
Cordialement
L'équipe Parisdepannage.fr
Hors ligne
2008 Parisdepannage |Plan du site|Forums |Blog|Lexique ![]()