Introduction au reverse engineering
1. Quâest-ce que le « reverse engineering »
Le « reverse » nâest pas une activitĂ© strictement informatique : on peut penser Ă la formule du Coca-Cola qui a Ă©tĂ© analysĂ©e au spectrographe de masse, aux capsules de cafĂ© dites « compatibles », Ă la fabrication dâaccessoires pour lâiPhone 5 (dont Apple nâa pas publiĂ© les spĂ©cifications), etc.
Ă la limite de lâinformatique, il existe Ă©galement une forte activitĂ© autour du « reverse » de cartes Ă©lectroniques, de bitstreams FPGA, voire de composants Ă©lectroniques. Cette activitĂ© existe depuis fort longtemps, par exemple dans le domaine de la tĂ©lĂ©vision sur abonnement (Pay TV), mais elle devient de plus en plus prĂ©sente dans les confĂ©rences de sĂ©curitĂ©, Ă mesure que la sĂ©curitĂ© descend dans les couches matĂ©rielles via des TPM et autres processeurs « ad-hoc ». Portes dâhĂŽtel, bus automobiles, microcode des cartes Wi-Fi, tĂ©lĂ©phones par satellite, femtocells, routeurs et autres Ă©quipements embarquĂ©s deviennent des objets dâĂ©tude pour les « hackers ». Ă titre anecdotique, on peut par exemple citer le projet 3DBrew [1] qui compte « libĂ©rer » la console Nintendo 3DS en dĂ©capsulant le processeur Nintendo pour en extraire les clĂ©s privĂ©es par microscopie Ă©lectronique.
Dans le domaine de la sécurité informatique, le « reverse » est souvent considéré comme le « Graal » des compétences, à la limite de la sorcellerie. Pénétrons donc dans le territoire des arts obscurs.
2. LâĂ©pine lĂ©gislative
La question rĂ©currente liĂ©e Ă la pratique du « reverse » en France est lĂ©gale : que risque-t-on Ă sâafficher publiquement « reverser » ? Le Malleus Maleficarum nous indique quâil faut transpercer chaque grain de beautĂ© du suspect Ă lâaide dâune aiguille : si la plaie ne saigne pas, nous sommes en prĂ©sence dâun sorcier. Et je ne vous parle pas des techniques dĂ©ployĂ©es par lâANSSI âŠ
« Je ne suis pas juriste », mais il me semble que la rĂ©ponse Ă la question lĂ©gislative nâa aucune importance. Si vous ĂȘtes du cĂŽtĂ© lumineux de la Force â par exemple, que vous analysez des virus pour dĂ©sinfecter un parc dâentreprise (voire que vous Ă©ditez un antivirus français) â ni lâauteur du virus, ni le procureur de la RĂ©publique ne vous inquiĂ©teront pour ce fait.
Si par mĂ©garde vous cheminez sur les sentiers obscurs du cracking et de keygening, vous passerez sous les fourches caudines de lâautoritĂ© publique pour un motif quelconque. Nâoublions pas que dans la plus cĂ©lĂšbre affaire judiciaire de « reverse » française â la sociĂ©tĂ© Tegam contre Guillermito â ce dernier a Ă©tĂ© condamnĂ© du fait quâil nâavait pas acquis de licence pour le logiciel objet de son Ă©tudeâŠ
Le point juridique étant évacué, entrons désormais dans le vif du sujet.
3. « Reverse engineering » vs. « cracking »
Le vrai « reverse » est une discipline noble et exigeante. Elle consiste Ă comprendre entiĂšrement un logiciel au point dâen produire une copie interopĂ©rable. Les exemples sont nombreux : projet Samba (dont la version 4 peut se substituer Ă un contrĂŽleur de domaine Microsoft Active Directory), nombreux pilotes Linux et codecs, algorithme RC4 (republiĂ© en 1994 sous le nom de ARC4 pour « Alleged RC4 »), etc.
Il sâagit dâune activitĂ© trĂšs diffĂ©rente de la recherche et de lâexploitation de failles logicielles, ou du « dĂ©plombage » de logiciels commerciaux. Il est regrettable de recevoir de nombreux CV mentionnant la compĂ©tence « reverse engineering », alors que le candidat ne sait que suivre un flot dâexĂ©cution Ă la main dans son dĂ©bogueur (technique dite du « F8 »), jusquâĂ trouver lâinstruction « JNZ » qui va contourner la quasi-totalitĂ© des systĂšmes de licence Ă©crits « Ă la main ».
4. Bestiaire Monstrueux
MĂȘme en se limitant au logiciel informatique, la notion de « reverse » couvre un pĂ©rimĂštre excessivement large : du code embarquĂ© dans un microcontrĂŽleur Ă une application Java, en passant par les « bonnes vieilles » applications en C.
Avant de traiter du cĆur du sujet â Ă savoir les langages compilĂ©s (C/C++/Objective-C/âŠ), feuilletons le reste du bestiaire que tout apprenti doit connaĂźtre.
4.1 Assembleur
Les applications les plus bas niveaux (BIOS, firmwares, systĂšmes dâexploitation, jeux pour consoles archaĂŻques, applications MS-DOS, etc.) sont gĂ©nĂ©ralement largement Ă©crites en assembleur. Dans ce cas, il nây a guĂšre dâalternatives : il est strictement nĂ©cessaire de connaĂźtre trĂšs finement lâarchitecture de la plateforme matĂ©rielle cible, ainsi que les instructions du processeur rĂ©servĂ©es aux opĂ©rations bas niveau (initialisation matĂ©rielle, etc.).
Ce cas est gĂ©nĂ©ralement rĂ©servĂ© Ă des spĂ©cialistes et ne sera pas couvert dans la suite de lâarticle.
4.2 Bytecode
PlutĂŽt que de livrer des applications compilĂ©es pour un processeur existant, certains environnements de dĂ©veloppement compilent dans un langage intermĂ©diaire de haut niveau, appelĂ© « bytecode ». Un environnement dâexĂ©cution (la « machine virtuelle ») doit ĂȘtre fourni pour chaque plateforme matĂ©rielle sur laquelle doit sâexĂ©cuter ce langage intermĂ©diaire.
Le lecteur Ă©veillĂ© pense immĂ©diatement aux environnements Java et .NET, mais lâutilisation de « bytecode » est beaucoup plus rĂ©pandue : on peut citer Flash ActionScript, Visual Basic 6 et son P-Code, les langages de script comme Python (fichiers PYC/PYO), mais aussi ⊠les protections logicielles.
On peut disserter sans fin des avantages et des inconvĂ©nients dâun « bytecode » par rapport Ă du code « natif » dâun point de vue gĂ©nie logiciel. Ce qui est sĂ»r, câest que la quasi-totalitĂ© des « bytecodes » qui nâont pas Ă©tĂ© conçus dans une optique de protection logicielle se dĂ©compilent sans aucune difficultĂ© sous forme de code source original. Je vous renvoie Ă vos outils favoris pour cette opĂ©ration (JD-GUI pour Java, Reflector pour .NET, Uncompyle2 pour Python, etc.).
4.3 Cas des protections logicielles
La protection logicielle est au reverse engineering ce que la nĂ©cromancie est Ă la divination : ce sont deux Ă©coles qui procĂšdent selon les mĂȘmes principes, mais que tout oppose. Et tandis que tout le monde consulte son voyant, peu nombreux sont ceux qui admettent lever les morts.
NĂ©anmoins, il ne faut pas se mentir : lâĂ©tude des protections logicielles constitue la meilleure Ă©cole pour apprendre rapidement le reverse engineering. Sites de « crackmes », tutoriels, outils, tout est lĂ pour qui veut brĂ»ler les Ă©tapes. Plus rapide, plus sĂ©duisant, mais pas plus puissant est le « cracking ». Seule la maĂźtrise de lâalgorithmique et de la thĂ©orie de la compilation permettra dâatteindre la transcendance.
Les protections logicielles sont gĂ©nĂ©ralement mises en Ćuvre pour protĂ©ger la gestion des licences logicielles, dâoĂč le caractĂšre sulfureux de leur analyse. Il faut noter toutefois quâil existe dâautres cas dâusage, comme par exemple :
La protection dâalgorithmes contre lâanalyse par des clients lĂ©gitimes du logiciel (ex. Skype) ;
La protection de codes malveillants contre lâanalyse par les Ă©diteurs antivirus (ex. rootkit TDL-4) ;
La protection de « cracks » contre lâanalyse par les Ă©diteurs (ex. crack pour Windows Vista simulant un BIOS OEM [2]).
Ces derniers cas lĂ©gitiment grandement lâactivitĂ© de reverse engineering des protections logicielles. Vous pouvez donc poursuivre la lecture de cet article sans craindre pour votre Ăąme.
Une « bonne » protection doit rĂ©sister aussi bien Ă lâanalyse statique quâĂ lâanalyse dynamique de la cible. Lâobjet de cet article introductif nâest pas dâĂ©numĂ©rer toutes les techniques mises au point dans le jeu du chat et de la souris entre « crackers » et Ă©diteurs de protection, mais il existe grosso modo deux grandes classes de protections logicielles utilisĂ©es actuellement :
Les protections externes (dites « packers »). Ces protections visent à « enrober » le logiciel original dans une couche de protection. Le logiciel est chiffrĂ© contre lâanalyse statique. Il est dĂ©chiffrĂ© en mĂ©moire Ă lâexĂ©cution, mais des protections anti-dĂ©bogage et anti-dump permettent dâĂ©viter une rĂ©cupĂ©ration trop facile du code.
Les protections internes (dites « obfuscateurs »). Le principe est de rĂ©Ă©crire le code assembleur du logiciel afin de le rendre inintelligible aux Moldus. Plusieurs techniques ont Ă©tĂ© expĂ©rimentĂ©es : transformation du code assembleur en bytecode (vulnĂ©rable Ă un « class break » si la machine virtuelle est analysĂ©e), insertion de code mort ou complexe Ă Ă©valuer statiquement, rĂ©Ă©criture du graphe de contrĂŽle (« code flattening »), etc. Il sâagit dâun domaine de recherche trĂšs actif, y compris dans le monde acadĂ©mique.
à titre anecdotique, les systÚmes de contrÎle de licence sont à classer dans plusieurs catégories :
Les protections matĂ©rielles (dongles) : trĂšs puissantes, mais assez rares aujourdâhui, sauf pour du logiciel trĂšs haut de gamme. Le principe consiste Ă dĂ©porter une partie des traitements dans un composant Ă©lectronique (clĂ© USB « intelligente ») supposĂ© inviolable. Plus le traitement dĂ©portĂ© est complexe, plus il sera difficile Ă comprendre en boĂźte noire. Un logiciel qui se contenterait de vĂ©rifier la prĂ©sence du susdit dongle sans jamais sâen servir serait bien sĂ»r condamnĂ© Ă finir sur Astalavista.
NumĂ©ro de sĂ©rie ou fichier de licence : la cryptographie moderne autorise des schĂ©mas thĂ©oriquement inviolables, si lâimplĂ©mentation est correcte. Mais mĂȘme un systĂšme inviolable peut ĂȘtre piraté⊠en remplaçant la clĂ© publique de lâĂ©diteur dans le binaire !
Activation en ligne : le logiciel vĂ©rifie quâil dispose du droit de sâexĂ©cuter auprĂšs dâun serveur tiers. Dans les cas les plus Ă©laborĂ©s, une partie des traitements est effectuĂ©e en ligne â mais cette situation nâest pas toujours acceptable par le client.
5. Les Piliers du Temple
Entrons maintenant dans le vif du sujet : quelles sont les compĂ©tences requises pour sâattaquer Ă une application « native », compilĂ©e depuis un langage relativement courant (tel que le C) ?
De mon expérience, elles sont au nombre de quatre.
5.1 ConnaĂźtre lâassembleur cible
Il nâest absolument pas nĂ©cessaire de connaĂźtre par cĆur le manuel de rĂ©fĂ©rence du processeur ! Ă titre dâexemple, le jeu dâinstructions des processeurs x86 et x64 contient une majoritĂ© dâinstructions en virgule flottante, rarement rencontrĂ©es et dont la sĂ©mantique est impossible Ă mĂ©moriser.
Par ailleurs, les compilateurs nâutilisent quâun sous-ensemble rĂ©duit du jeu dâinstructions, comme on le verra plus tard.
Il est par contre de bon ton de connaĂźtre les spĂ©cifiĂ©s du processeur cible. Sur architectures x86 et x64, ce sont par exemple la segmentation et les registres implicites. Sur architecture SPARC ce sont les registres glissants et les « delay slots » (quâon retrouve Ă©galement sur MIPS). Il existe des architectures encore plus exotiques telles que les processeurs VLIW (Very Long Instruction Word).
5.2 Savoir développer
Nâoublions pas que le reverse engineering logiciel consiste Ă comprendre ce quâa Ă©crit le dĂ©veloppeur dâorigine. Il est donc fortement recommandĂ© de savoir soi-mĂȘme dĂ©velopper dans le langage cibleâŠ
Il est amusant de constater quâil existe des modes dans les « design patterns ». Si le code Sendmail est truffĂ© de constructions setjmp/longjmp/goto dĂ©sormais obsolĂštes, le tout nouveau C++11 autorise bien dâautres acrobatiesâŠ
Par ailleurs, chaque dĂ©veloppeur a ses lubies. Il ne sâagit donc pas de savoir programmer, mais dâavoir une idĂ©e de comment programment les autres⊠La lecture rĂ©guliĂšre de code issu de projets open source â ou la consultation de forums dâaide aux dĂ©veloppeurs â permet de se faire une idĂ©e de lâextrĂȘme libertĂ© que confĂšre le code.
5.3 Connaissance du compilateur
Nâoublions pas que le code assembleur nâest quâune libre interprĂ©tation du code de haut niveau Ă©crit par lâhumain. Le compilateur a la charge de produire un code fonctionnellement identique, mais qui peut ĂȘtre structurellement trĂšs diffĂ©rent. Nous reviendrons plus tard sur les optimisations proposĂ©es par les compilateurs, mais si vous ĂȘtes du genre Ă encore penser que vous pouvez faire mieux quâun compilateur moderne « Ă la main », arrĂȘtez-vous et lisez immĂ©diatement la formation de Rolf Rolles intitulĂ©e « Binary Literacy â Optimizations and OOP » [3].
La diversitĂ© des compilateurs sâest considĂ©rablement rĂ©duite ces derniers temps (il devient rare de rencontrer les compilateurs de Borland, Watcom ou Metrowerks). Les deux survivants sont GCC et Visual Studio â avec LLVM en challenger, et une mention spĂ©ciale pour le compilateur Intel (ICC), qui est capable de produire du code incroyablement optimisĂ© â et donc totalement illisible.
MĂȘme sâil ne reste que deux compilateurs, il nâen reste pas moins que la liste des options de compilation proposĂ©es par GCC laisse rĂȘveur [4]. Or, le niveau dâoptimisation, les options finline*, funroll* ou fomit-frame-pointer vont avoir des effets considĂ©rables sur le code gĂ©nĂ©rĂ©.
5.4 Reconnaissance de forme
Câest probablement la qualitĂ© essentielle du bon reverser. AprĂšs des milliers dâheures dâapprentissage, son cerveau reconnaĂźt immĂ©diatement toutes les structures « classiques », de la boucle « for »⊠à lâimplĂ©mentation DES en boĂźte blanche.
Je ne prĂ©tends absolument pas jouer dans cette catĂ©gorie â câest dâailleurs pour cela quâon ne mâa confiĂ© que lâintroduction â mais je crois quâil mâa Ă©tĂ© donnĂ© dâapprocher des gens rĂ©ellement hors du commun dans le domaine. Et je peux vous dire que câest beau comme un concert de Rammstein.
Ce qui nous amĂšne Ă la conclusion essentielle : seul un conditionnement du cerveau Ă lâĂąge oĂč sa plastique est maximale permet de produire un reverser dâexception. Si les Chinois mettent en place des camps dâentraĂźnement, on peut commencer Ă craindre pour notre propriĂ©tĂ© intellectuelle. Et si la sĂ©curitĂ© informatique espĂšre encore recruter dans 10 ans, il serait temps de mettre http://crackmes.de/ au programme du collĂšge.
6. Another World
6.1 Analyse statique ou analyse dynamique ?
Les puristes vous diront que le vrai « reverser » ne fait que de lâanalyse statique, câest-Ă -dire de la lecture de code mort (Ă©ventuellement couplĂ©e Ă un peu dâexĂ©cution symbolique). Il est vrai que cette approche est parfois la seule envisageable, par exemple lorsque la plateforme matĂ©rielle nâest pas disponible ou dĂ©bogable (ex. systĂšmes SCADA).
NĂ©anmoins, je ne serai pas aussi intransigeant et je vous prĂ©senterai lâautre approche moins Ă©lĂ©gante, mais plus « quick win » : il sâagit de lâanalyse dynamique.
En effet, lâobservation du logiciel en cours dâexĂ©cution permet dâidentifier rapidement ses fonctions essentielles en « boĂźte noire ». Des outils comme Process Monitor, APIMonitor ou WinAPIOverride32 sâavĂšrent indispensables.
6.2 DĂ©marche pour lâanalyse statique
Une dĂ©marche de reverse efficace ne commence pas bille en tĂȘte par la lecture du code assembleur. Dâautres Ă©lĂ©ments plus facilement observables donnent rapidement des pistes essentielles.
Sections
Cela peut sembler une Ă©vidence, mais si votre cible contient 3 sections nommĂ©es UPX0, UPX1 et UPX2, vous gagnerez un temps prĂ©cieux Ă utiliser la commande upx -d plutĂŽt que dâexĂ©cuter pas-Ă -pas le programme depuis le point dâentrĂ©e [5]âŠ
Il faut Ă©galement prĂȘter attention au dĂ©veloppeur malicieux, qui utilise une version modifiĂ©e du logiciel UPX pour induire en erreur lâanalyste. Câest pourquoi des outils dâidentification (tels que PEiD) proposent de rechercher les signatures applicatives des « packers » les plus courants.
Imports/exports
La liste des fonctions importĂ©es â lorsquâelle est disponible â permet rapidement de se faire une idĂ©e des parties essentielles du programme : cryptographie, gĂ©nĂ©ration dâalĂ©a, accĂšs au rĂ©seau, etc.
Dans lâhypothĂšse oĂč lâapplication cible prend une « empreinte » de la plateforme matĂ©rielle pour gĂ©nĂ©rer un numĂ©ro dâinstallation unique par exemple, identifier les fonctions GetAdaptersAddresses() ou GetVolumeInformation() dans les imports donne des points dâentrĂ©e intĂ©ressantsâŠ
Constantes
La plupart des algorithmes reposent sur des constantes « bien connues » : la chaßne de caractÚres KGS!@#$% pour la génération des hashes LM, les constantes 0x67452301 0xEFCDAB89 0x98BADCFE 0x10325476 pour MD5, etc.
Dâautre part, de nombreuses implĂ©mentations « optimisĂ©es » reposent Ă©galement sur des tableaux prĂ©-calculĂ©s : câest le cas pour DES, tous les algorithmes de CRC, etc.
Enfin, la plupart des algorithmes disposent dâune structure identifiable (nombre de tours, ordre des dĂ©calages, etc.).
ChaĂźnes de caractĂšres
Les chaĂźnes de caractĂšres sont des constantes particuliĂšres. Il nâexiste pas de mĂ©thode scientifique pour explorer les chaĂźnes de caractĂšres, mais lâĆil humain repĂšre rapidement les chaĂźnes « intĂ©ressantes » : chaĂźnes encodĂ©es en Base64 ou en ROT13, chaĂźnes donnant des indications dâusage (comme User-Agent : Mozilla/4.0 ou BEGIN RSA PRIVATE KEY), copyrights, rĂ©fĂ©rences au code source, messages de dĂ©bogage, etc.
BibliothĂšques et code open source
Force est de constater que les dĂ©veloppeurs adorent la rĂ©utilisation de code, comme lâa encore dĂ©montrĂ© la rĂ©cente faille dans libupnp. OpenSSL, Boost, ZLib et LibPNG font Ă©galement partie des suspects habituels. La prĂ©sentation dâAntony Desnos sur les applications Android laisse rĂȘveur, sachant que certaines applications se composent Ă 95% de code publicitaire !
Il est donc strictement indispensable dâĂ©laguer tout le code provenant des librairies standards fournies avec le compilateur, et tout le code issu des projets open source, avant de se concentrer sur la partie essentielle du code.
Il nâexiste pas dâapproche universelle dans le domaine, mais il existe au moins deux pistes intĂ©ressantes : la gĂ©nĂ©ration de signatures pour IDA avec lâoutil FLAIR [6], et lâoutil BinDiff [7]. Ce dernier Ă©tant basĂ© sur la comparaison de graphes, il est indĂ©pendant du langage assembleur : il est donc thĂ©oriquement possible de compiler OpenSSL sur Linux/x86, puis dâidentifier les fonctions correspondantes dans un binaire Android/ARM par exemple.
Il existe des projets visant Ă mutualiser lâĂ©change de signatures entre « reversers », comme par exemple CrowdRE [8].
Métadonnées
Selon les technologies utilisĂ©es, le compilateur peut intĂ©grer dans le binaire des mĂ©tadonnĂ©es parfois trĂšs intĂ©ressantes : informations de dĂ©bogage, donnĂ©es RTTI pour le C++ (voire Ă ce sujet lâarticle de Jean-Philippe LUYTEN dans MISC n°61), stubs des interfaces MS-RPC (cf. plugin mIDA), etc.
Traces et modes de débogage
De (trop) nombreux logiciels disposent de fonctions de journalisation qui peuvent ĂȘtre rĂ©activĂ©es par une configuration spĂ©cifique (clĂ© de base de registre, variable dâenvironnement, conjonction astrale, etc.). La sortie de cette journalisation peut sâeffectuer dans un fichier texte, un fichier au format Windows ETL, un dĂ©bogueur attachĂ© au processus, etc.
Non seulement les chaĂźnes de caractĂšres associĂ©es Ă ces fonctions vont livrer des informations prĂ©cieuses pour lâanalyse statique (comme les types ou les noms des champs dans les structures de donnĂ©es), mais ces traces vont Ă©galement considĂ©rablement accĂ©lĂ©rer lâanalyse dynamique.
Je crois quâon sous-estime grandement la valeur des versions « Checked Build » de Windows disponibles aux abonnĂ©s MSDNâŠ
6.3 Théorie de la compilation
Passons en revue quelques techniques « classiques » dâoptimisation quâil est de bon ton de connaĂźtre.
Alimentation du « pipeline » et prédiction de branchement
Les processeurs modernes pratiquent la divination : ils exĂ©cutent des instructions au-delĂ de la valeur courante du pointeur dâinstruction (exĂ©cution spĂ©culative), dans lâordre qui leur paraĂźt le plus efficace (rĂ©-ordonnancement). Cette optimisation est trĂšs efficace sur des instructions arithmĂ©tiques indĂ©pendantes - dâautant que le compilateur va entremĂȘler les instructions (scheduling) et allouer les registres en consĂ©quence - mais se heurte au problĂšme des sauts conditionnels.
La majoritĂ© des sauts conditionnels Ă©tant corrĂ©lĂ©s Ă lâimplĂ©mentation dâune boucle, le processeur applique lâheuristique suivante : les sauts en arriĂšre sont gĂ©nĂ©ralement pris, tandis que les sauts en avant ne sont gĂ©nĂ©ralement pas pris. Si le processeur sâest trompĂ© dans sa prĂ©diction, il doit annuler le commencement dâexĂ©cution de toutes les instructions qui ne seront finalement pas exĂ©cutĂ©es, ce qui est trĂšs coĂ»teux.
Le compilateur connaĂźt cette heuristique, ce qui lui permet dâoptimiser les boucles et les tests conditionnels.
Sur architectures Intel x86 et x64, il est possible de contrĂŽler la prĂ©diction de branchement via des registres de configuration, voire dâenregistrer tous les branchements dans un buffer circulaire (Branch Trace Store) Ă des fins de profiling.
Sur architecture ARM, toutes les instructions sont conditionnelles, ce qui permet des optimisations intéressantes, comme la suppression des sauts conditionnels pour les assignations simples (de type opérateur ternaire).
Le Pentium Pro (architecture P6) a introduit lâinstruction dâassignation conditionnelle CMOV, qui permet le mĂȘme type dâoptimisation. Il faut toutefois noter que le gain en performance nâest pas automatique, et que dâautres astuces (comme lâinstruction SBB, qui prend en compte la valeur du « Carry Flag ») permettaient dĂ©jĂ des optimisations.
Le déroulement de boucle
La meilleure solution pour Ă©viter le coĂ»t des sauts conditionnels⊠câest de les supprimer purement et simplement !
Si le compilateur connaĂźt Ă lâavance le nombre dâitĂ©rations dans une boucle, et que le code gĂ©nĂ©rĂ© peut tenir entiĂšrement dans une page de code (soit 4 Ko sur architectures x86/x64), alors le compilateur copie/colle le code de la boucle autant de fois que nĂ©cessaire.
Cette construction est trĂšs courante dans les sĂ©quences dâinitialisation dâalgorithmes cryptographiques. Le code gĂ©nĂ©rĂ© semble anormalement long de prime abord, mais sâanalyse trĂšs rapidement.
Lâalignement
Par conception des microcircuits, il est beaucoup plus efficace de travailler Ă la taille native des mots du processeur. Par exemple, une implĂ©mentation naĂŻve de memcpy() pourrait ĂȘtre :
for (i=0 ; i < len ; i++) dst[i] = src[i] ;
Aucune librairie C ne propose une implĂ©mentation aussi mĂ©diocre : travailler octet par octet sur un bus de 32 bits, câest diviser par 4 la performance. Une meilleure implĂ©mentation de memcpy() serait la suivante :
En amont, sâassurer que dst et src sont allouĂ©s Ă des adresses multiples de 4. Si les deux tableaux sont alignĂ©s sur 4 Ko et occupent donc un nombre minimal de pages en mĂ©moire, câest encore mieux.
Copier len / 4 mots de 32 bits.
Copier les len % 4 octets restants.
Câest en substance lâimplĂ©mentation quâon trouve dans la librairie C de Windows XP (MSVCRT.DLL).
Bien entendu, des algorithmes plus complexes sâoptimisent encore mieux. Je vous invite par exemple Ă consulter lâimplĂ©mentation de la fonction strlen() dans la librairie C du projet GNU (mais pas celle de BSD, qui est naĂŻve).
Si vous ĂȘtes sensible Ă la beautĂ© du code, voici lâimplĂ©mentation rĂ©elle de strlen() sur Mac OS 10.7 (64 bits), telle que reprĂ©sentĂ©e par lâoutil otool :
_strlen:
pxor %xmm0,%xmm0
movl %edi,%ecx
movq %rdi,%rdx
andq $0xf0,%rdi
orl $0xff,%eax
pcmpeqb (%rdi),%xmm0
andl $0x0f,%ecx
shll %cl,%eax
pmovmskb %xmm0,%ecx
andl %eax,%ecx
je 0x000a250b
00000000000a2501:
bsfl %ecx,%eax
subq %rdx,%rdi
addq %rdi,%rax
ret
00000000000a250b:
pxor %xmm0,%xmm0
addq $0x10,%rdi
00000000000a2513:
movdqa (%rdi),%xmm1
addq $0x10,%rdi
pcmpeqb %xmm0,%xmm1
pmovmskb %xmm1,%ecx
testl %ecx,%ecx
je 0x000a2513
subq $0x10,%rdi
jmp 0x000a2501
« Inlining »
Comme les sauts conditionnels, lâappel de fonction est une opĂ©ration excessivement coĂ»teuse, surtout si la destination ne se trouve pas dans la mĂȘme page de code.
On notera au passage que le noyau Windows â ainsi que le logiciel Chrome [9] â font lâobjet dâune optimisation post-compilation, qui consiste Ă regrouper le code le plus souvent exĂ©cutĂ© dans quelques pages mĂ©moire, plutĂŽt que de le rĂ©partir dans tout le binaire (optimisation dite « OMAP » [10]). Il est mĂȘme optimal dâaligner le code sur la taille des lignes du cache dâinstruction, soit 16 octets sur architectures x86 et x64.
Afin de limiter les appels de fonction, le compilateur peut dĂ©cider dâinclure le code de la sous-routine directement sur son lieu dâappel, comme dans les exemples ci-dessous.
strcpy()
strcmp()
Notez lâutilisation des instructions SCAS/MOVS/CMPS associĂ©es au prĂ©fixe REP, plutĂŽt quâune construction de boucle beaucoup moins performante.
Les instructions interdites
Certaines instructions sont peu ou pas utilisĂ©es par les compilateurs. Il sâagit dâune optimisation liĂ©e au temps dâexĂ©cution de ces instructions. Par exemple, lâinstruction LOOP nĂ©cessite 6 cycles dâhorloge en cas de branchement sur processeur 80486. La sĂ©quence DEC ECX / JNZ xxx ne nĂ©cessite que 1 + 3 cycles pour la mĂȘme opĂ©ration.
A contrario, lâinstruction LEA est couramment utilisĂ©e pour effectuer des additions entre constantes et registres, alors que ça nâest pas sa fonction premiĂšre.
La situation se complique encore, car le nombre de cycles par instruction est trĂšs variable dâun processeur Ă lâautre. Câest pourquoi en fonction du processeur cible que vous spĂ©cifierez au compilateur Intel ICC, vous obtiendrez un code sensiblement diffĂ©rentâŠ
Multiplication et division
La multiplication et la division par une puissance de 2 sâimplĂ©mentent par un simple dĂ©calage de bits : ce sont des opĂ©rations simples. Les mĂȘmes opĂ©rations avec des opĂ©randes arbitraires sont des opĂ©rations excessivement coĂ»teuses. Ă titre dâexemple, voici les durĂ©es dâexĂ©cution (en nombre de cycles dâhorloge) de quelques instructions sur un processeur 80486 :
Opération
Opérande
Cycles
Multiplication
IMUL <reg32>
12 Ă 42
MUL <reg32>
13 Ă 42
Division
DIV <reg32>
40
IDIV <reg32>
43
DĂ©calage
SHL <reg>, <imm8>
2
Addition
ADD <reg>, <reg>
1
Pour multiplier X par 12, le compilateur va donc dĂ©couper lâopĂ©ration de la maniĂšre suivante : (X*8) + (X*4).
Pour diviser un nombre X sur 32 bits par 17, le compilateur peut ĂȘtre tentĂ© dâutiliser la multiplication rĂ©ciproque.
Selon lâalgorithme dâEuclide Ă©tendu, lâinverse de 17 modulo 2^32 est 4042322161. Il suffit donc de multiplier X par ce nombre (modulo 2^32) pour obtenir le rĂ©sultat de la division.
6.4 Pratique de la décompilation
La compilation en langage assembleur provoque la perte dâinformations essentielles, telles que le type de donnĂ©es. La dĂ©compilation (retour au code source dâorigine) sâavĂšre donc ĂȘtre un problĂšme difficile. Ce domaine a stimulĂ© de nombreux travaux universitaires, qui sont pour la plupart restĂ©s du niveau de la « preuve de concept ».
Loin du Latex et autres CiteSeer, un maĂźtre du reverse engineering - auteur du logiciel IDA Pro â sâest un jour attaquĂ© au problĂšme. Combinant lâabondante thĂ©orie sur le sujet avec sa pratique de la chose et de nombreuses heuristiques par compilateur, il fabriqua par un matin blĂȘme la Pierre Philosophale de la dĂ©compilation : Hex-Rays Decompiler [11].
Aujourdâhui, cet outil intĂ©grĂ© Ă IDA Pro dĂ©compile de maniĂšre tout Ă fait correcte les assembleurs x86 et ARM, supporte lâenrichissement manuel du listing, mais permet Ă©galement le dĂ©bogage au niveau du code source reconstituĂ©. Il justifie donc largement son coĂ»t dâacquisition, relativement modique pour une entreprise.
Source : https://connect.ed-diamond.com/MISC/MISCHS-007/Introduction-au-reverse-engineering
DerniĂšre mise Ă jour
Cet article vous a-t-il été utile ?