Le code automodifiable peut impliquer le remplacement d'instructions existantes ou la génération d'un nouveau code lors de l'exécution et le transfert du contrôle à ce code.
L'automodification peut être utilisée comme alternative à la méthode de « définition d'indicateurs » et de branchement conditionnel du programme, utilisée principalement pour réduire le nombre de fois où une condition doit être testée.
Cette méthode est fréquemment utilisée pour invoquer conditionnellement du code de test/débogage sans nécessiter de surcharge de calcul supplémentaire pour chaque cycle d'entrée/sortie .
Les modifications peuvent être effectuées :
- Uniquement lors de l'initialisation – en fonction des paramètres d'entrée (ce processus est plus communément décrit comme la « configuration » logicielle et s'apparente, en termes matériels, au réglage des cavaliers sur les circuits imprimés ). La modification des pointeurs d'entrée du programme est une méthode indirecte équivalente d'auto-modification, mais elle nécessite la coexistence d'un ou plusieurs chemins d'exécution alternatifs, ce qui augmente la taille du programme .
- tout au long de l'exécution (« à la volée ») – en fonction des états particuliers du programme atteints au cours de l'exécution
Dans les deux cas, les modifications peuvent être effectuées directement sur les instructions du code machine elles-mêmes, en superposant de nouvelles instructions aux instructions existantes (par exemple, en transformant une instruction de comparaison et de branchement en un branchement inconditionnel ou en une instruction NOP ).
Dans l' architecture IBM System/360 et ses successeurs jusqu'à z/Architecture , une instruction EXECUTE (EX) superpose logiquement le deuxième octet de son instruction cible avec les 8 bits de poids faible du registre 1. Cela donne l'effet d'une auto-modification, bien que l'instruction réelle en mémoire ne soit pas altérée.
de code source suivie d'une « mini-compilation » ou d'une interprétation dynamique (voir instruction eval )Langage d'assemblage
L'implémentation de code automodifiable est relativement simple en langage assembleur . Les instructions peuvent être créées dynamiquement en mémoire (ou superposées à du code existant dans un espace de stockage de programme non protégé) , dans une séquence équivalente à celle qu'un compilateur standard pourrait générer comme code objet . Avec les processeurs modernes, des effets de bord indésirables sur le cache du processeur peuvent survenir et doivent être pris en compte. Cette méthode était fréquemment utilisée pour tester les conditions de « première exécution », comme dans cet exemple d'assembleur IBM/360 commenté . Elle utilise la superposition d'instructions pour réduire la longueur du chemin d'exécution d' un facteur surcharge liée à la superposition).
SUBRTN NOP OUVERT POUR LA PREMIÈRE FOIS ICI ? * Le NOP est x'4700' < Adresse_d'ouverture> OI SUBRTN+1,X'F0' OUI, CHANGER NOP EN BRANCHE INCONDITIONNELLE (47F0...) OUVRIR L'ENTRÉE ET OUVRIR LE FICHIER D'ENTRÉE, CAR C'EST LA PREMIÈRE FOIS QUE VOUS L'UTILISEZ. OUVERT SAISIE DES DONNÉES TRAITEMENT NORMAL REPREND ICI ...Une autre solution consisterait à tester un indicateur à chaque itération. Le branchement inconditionnel est légèrement plus rapide qu'une instruction de comparaison et réduit la longueur totale du chemin d'exécution. Dans les systèmes d'exploitation plus récents, pour les programmes résidant en mémoire protégée , cette technique n'était plus utilisable ; il fallait alors modifier le pointeur vers la sous-routine . Ce pointeur résidait en mémoire dynamique et pouvait être modifié à volonté après le premier passage pour éviter le branchement inconditionnel OPEN(le chargement préalable d'un pointeur au lieu d'un branchement direct et d'une liaison à la sous-routine ajouterait N instructions à la longueur du chemin , mais la suppression du branchement inconditionnel permettrait une réduction correspondante de N ).
Voici un exemple en langage assembleur Zilog Z80 . Le code incrémente un registre Bdans l'intervalle [0, 5]. L' CPinstruction de comparaison est modifiée à chaque itération de la boucle.
Langues de haut niveau
Certains langages compilés autorisent explicitement le code automodifiable. Par exemple, le ALTERverbe `branch` en COBOL peut être implémenté comme une instruction de branchement modifiée lors de son exécution. Certaines techniques de programmation par lots font appel à du code automodifiable. Clipper et SPITBOL offrent également des fonctionnalités d'automodification explicite. Le compilateur Algol des systèmes B6700 proposait une interface avec le système d'exploitation permettant au code en cours d'exécution de transmettre une chaîne de caractères ou un fichier disque nommé au compilateur Algol, qui pouvait ensuite invoquer la nouvelle version d'une procédure.
Avec les langages interprétés, le « code machine » est le texte source et peut être modifié à la volée : en SNOBOL, les instructions exécutées sont les éléments d’un tableau de texte. D’autres langages, comme Perl et Python , permettent aux programmes de créer du code à l’exécution et de l’exécuter à l’aide d’une fonction `eval` , mais n’autorisent pas la modification du code existant. L’illusion de modification (même si aucun code machine n’est réellement écrasé) est obtenue en modifiant les pointeurs de fonction, comme dans cet exemple JavaScript :
Le langage de programmation Push est un système de programmation génétique conçu spécifiquement pour créer des programmes auto-modifiables. Bien qu'il ne s'agisse pas d'un langage de haut niveau, il n'est pas aussi bas niveau que le langage assembleur.
Modification composée
Avant l'avènement des systèmes à fenêtres multiples, les systèmes en ligne de commande pouvaient proposer un système de menus permettant de modifier un script de commandes en cours d'exécution. Supposons qu'un fichier batch MS-DOS MENU.BAT contienne ce qui suit :
:commencer AFFICHERMENU.EXE
Lors de l'exécution de MENU.BAT depuis la ligne de commande, SHOWMENU affiche un menu à l'écran, avec des informations d'aide, des exemples d'utilisation, etc. L'utilisateur effectue ensuite une sélection nécessitant l'exécution d'une commande SOMENAME : SHOWMENU se termine après avoir réécrit le fichier MENU.BAT pour contenir
:commencer AFFICHERMENU.EXE APPELLE QUELQU'UN NOM .BAT ALLER AU DÉBUT
Comme l'interpréteur de commandes ne compile pas le fichier script avant de l'exécuter, ni ne le charge intégralement en mémoire avant son exécution, et ne s'appuie pas non plus sur le contenu d'un tampon d'enregistrement, lorsque SHOWMENU se termine, il recherche une nouvelle commande à exécuter (l'appel du fichier script SOMENAME , situé dans un répertoire et via un protocole connu de SHOWMENU). Une fois cette commande terminée, il retourne au début du fichier script et réactive SHOWMENU, prêt pour la sélection suivante. Si l'option choisie dans le menu est « Quitter », le fichier est réécrit à son état initial. Bien que cet état initial ne nécessite pas l'étiquette, celle-ci, ou une quantité de texte équivalente, est indispensable car l'interpréteur de commandes se souvient de la position en octets de la commande suivante au moment de son exécution. Le fichier réécrit doit donc conserver l'alignement pour que le point de départ de la commande suivante corresponde bien au début de celle-ci.
Outre la commodité d'un système de menus (et d'éventuelles fonctionnalités auxiliaires), ce schéma signifie que le système SHOWMENU.EXE n'est pas en mémoire lorsque la commande sélectionnée est activée, un avantage significatif lorsque la mémoire est limitée.
Tables de contrôle
Les interpréteurs de tables de contrôle peuvent être considérés comme étant, dans un certain sens, « auto-modifiés » par les valeurs de données extraites des entrées de la table (plutôt que spécifiquement codées manuellement dans des instructions conditionnelles de la forme IF inputx = 'yyy').
Programmes de la chaîne
Certaines méthodes d'accès IBM utilisaient traditionnellement des programmes de canal auto-modifiables , dans lesquels une valeur, telle qu'une adresse de disque, est lue dans une zone référencée par un programme de canal, où elle est utilisée par une commande de canal ultérieure pour accéder au disque.
Histoire
L' IBM SSEC , présenté en janvier 1948, pouvait modifier ses instructions ou les traiter comme des données. Cependant, cette capacité fut rarement utilisée en pratique. Aux débuts de l'informatique, le code automodifiable était souvent employé pour réduire l'utilisation de la mémoire limitée, améliorer les performances, ou les deux. Il servait aussi parfois à implémenter les appels et retours de sous-programmes lorsque le jeu d'instructions ne proposait que de simples instructions de branchement ou de saut pour modifier le flux de contrôle . Cet usage reste pertinent dans certaines architectures ultra- RISC , du moins théoriquement ; voir par exemple l'ordinateur à jeu d'instructions unique . L'architecture MIX de Donald Knuth utilisait également du code automodifiable pour implémenter les appels de sous-programmes.
Usage
Le code automodificateur peut être utilisé à diverses fins :
- Optimisation semi-automatique d'une boucle dépendante de l'état.
- Optimisation dynamique du code sur place pour améliorer la vitesse en fonction de l'environnement de charge.
- La génération de code à l'exécution , ou la spécialisation d'un algorithme à l'exécution ou au chargement (pratique, par exemple, dans le domaine du graphisme en temps réel), comme un utilitaire de tri général – préparer le code pour effectuer la comparaison de clés décrite dans un appel spécifique.
- Modification de l'état intégré d'un objet , ou simulation de la construction de haut niveau des fermetures .
- Correction de l'appel d'adresse de sous-routine ( pointeur ), généralement effectuée au moment du chargement/initialisation des bibliothèques dynamiques , ou bien à chaque invocation, en corrigeant les références internes de la sous-routine à ses paramètres afin d'utiliser leurs adresses réelles (c'est-à-dire une auto-modification indirecte).
- Les systèmes informatiques évolutionnaires tels que la neuroévolution , la programmation génétique et d'autres algorithmes évolutionnaires .
- Dissimulation du code pour empêcher la rétro-ingénierie (par l'utilisation d'un désassembleur ou d'un débogueur ) ou pour échapper à la détection par les logiciels antivirus/antispyware, etc.
- Remplir toute la mémoire (dans certaines architectures) avec un motif roulant d' opcodes répétitifs , pour effacer tous les programmes et les données, ou pour effectuer un rodage du matériel ou réaliser des tests de RAM .
- Compression du code à décompresser et à exécuter lors de l'exécution, par exemple lorsque la mémoire ou l'espace disque est limité.
- Certains jeux d'instructions très limités n'offrent d'autre choix que d'utiliser du code automodificateur pour réaliser certaines fonctions. Par exemple, un ordinateur à jeu d'instructions unique (OISC) qui utilise uniquement l'« instruction » de soustraction et de branchement si négatif ne peut effectuer une copie indirecte (équivalente à une opération similaire
*a = **ben langage C ) sans recourir à du code automodificateur. - Démarrage . Les premiers micro-ordinateurs utilisaient souvent du code automodifiable dans leurs chargeurs de démarrage. Comme le chargeur de démarrage était saisi via le panneau avant à chaque mise sous tension, il importait peu qu'il se modifie de lui-même. Cependant, même aujourd'hui, de nombreux chargeurs de démarrage sont auto-relocalisables , et certains sont même automodifiables.
- Modification des instructions pour la tolérance aux pannes.
Optimisation d'une boucle dépendante de l'état
répéter N fois { si l'ÉTAT est 1 augmenter A de un autre diminuer A de un faire quelque chose avec A }Dans ce cas, un code automodificateur consisterait simplement à réécrire la boucle comme ceci :
répéter N fois { augmenter A de un } faire quelque chose avec A lorsque l'ÉTAT doit changer { Remplacez le code d'opération « augmenter » ci-dessus par le code d'opération correspondant à la diminution, ou inversement. } }Notez que le remplacement à deux états du code d'opération peut être facilement écrit comme « xor var à l'adresse avec la valeur opcodeOf(Inc) xor opcodeOf(dec)».
Le choix de cette solution doit dépendre de la valeur de la rétro-ingénierie et le piratage de logiciels . Dans les années 1980, il était utilisé pour dissimuler les instructions de protection contre la copie dans les programmes sur disquette destinés à des systèmes tels que les compatibles IBM PC et l'Apple II . Par exemple, sur un IBM PC, l' instruction d'accès au lecteur de disquettesint 0x13 n'apparaissait pas dans l'image mémoire du programme exécutable, mais était écrite dans son image mémoire après le lancement du programme.
Le code automodifiable est parfois utilisé par des programmes qui souhaitent rester invisibles, comme les virus informatiques et certains shellcodes . Les virus et shellcodes qui utilisent du code automodifiable le font généralement en combinaison avec du code polymorphe . La modification d'une portion de code en cours d'exécution est également utilisée dans certaines attaques, telles que les débordements de tampon .
Systèmes d'apprentissage automatique autoréférentiels
Les systèmes d'apprentissage automatique traditionnels possèdent un algorithme d'apprentissage fixe et préprogrammé pour ajuster leurs paramètres . Cependant, depuis les années 1980, Jürgen Schmidhuber a publié plusieurs systèmes auto-modificateurs capables de modifier leur propre algorithme d'apprentissage. Ils évitent le risque de réécritures catastrophiques en s'assurant que les modifications ne soient conservées que si elles sont utiles selon une fonction d' évaluation , d'erreur ou de récompense définie par l'utilisateur .
Systèmes d'exploitation
Le noyau Linux utilise notamment beaucoup de code automodifiable ; ceci afin de pouvoir distribuer une seule image binaire pour chaque architecture majeure (par exemple IA-32 , x86-64 , ARM 32 bits , ARM64 …) tout en adaptant le code du noyau en mémoire lors du démarrage en fonction du modèle de processeur détecté, par exemple pour tirer parti des nouvelles instructions du processeur ou contourner des bogues matériels. Dans une moindre mesure, le noyau DR-DOS optimise également ses sections critiques en termes de vitesse au moment du chargement, en fonction de la génération du processeur sous-jacent.
Quoi qu’il en soit, à un niveau méta , les programmes peuvent toujours modifier leur propre comportement en changeant des données stockées ailleurs (voir métaprogrammation ) ou via l’utilisation du polymorphisme .
Noyau de synthèse de Massalin
Le noyau Synthesis présenté dans la thèse de doctorat d' Alexia Massalin est un noyau Unix minimal qui adopte une approche structurée , voire orientée objet , du code auto-modifiable, où du code est créé pour chaque quaject , comme un descripteur de fichier. La génération de code pour des tâches spécifiques permet au noyau Synthesis (à l'instar d'un interpréteur JIT) d'appliquer un certain nombre d' optimisations telles que le repliement des constantes ou l'élimination des sous-expressions communes .
Le noyau Synthesis était très rapide, mais entièrement écrit en assembleur. Le manque de portabilité qui en résultait a empêché l'adoption des idées d'optimisation de Massalin par un noyau de production. Cependant, la structure de ces techniques suggère qu'elles pourraient être implémentées dans un langage de plus haut niveau , quoique plus complexe que les langages de niveau intermédiaire existants. Un tel langage et un tel compilateur permettraient de développer des systèmes d'exploitation et des applications plus rapides.
Paul Haeberli et Bruce Karsh se sont opposés à la « marginalisation » du code auto-modificateur et de l’optimisation en général, au profit de la réduction des coûts de développement.
Interaction entre le cache et le code auto-modificateur
Sur les architectures sans cache de données et d'instructions couplé (par exemple, certains cœurs SPARC , ARM et MIPS ), la synchronisation du cache doit être effectuée explicitement par le code de modification (vider le cache de données et invalider le cache d'instructions pour la zone mémoire modifiée).
Dans certains cas, de courts segments de code auto-modificateur s'exécutent plus lentement sur les processeurs modernes. Cela s'explique par le fait qu'un processeur moderne tente généralement de conserver des blocs de code dans sa mémoire cache. Chaque fois que le programme réécrit une partie de lui-même, cette partie doit être rechargée dans le cache, ce qui entraîne un léger délai si le pointeur d'instruction est modifiée, le processeur ne le détectera pas et exécutera le code tel qu'il était avant la modification. Voir la file d'attente de préchargement (PIQ). Les processeurs PC doivent gérer correctement le code automodifiable pour des raisons de rétrocompatibilité, mais ils sont loin d'être performants dans ce domaine.systèmes d'exploitation veillent à corriger ces vulnérabilités dès qu'elles sont connues. Le problème n'est généralement pas que les programmes se modifient intentionnellement, mais qu'ils puissent être modifiés de manière malveillante par une faille de sécurité .
Un mécanisme de prévention des modifications de code malveillantes repose sur une fonctionnalité du système d'exploitation appelée W^X (pour « écriture XOR exécution »). Ce mécanisme interdit à un programme de rendre une même page mémoire à la fois accessible en écriture et exécutable. Certains systèmes empêchent qu'une page accessible en écriture ne devienne exécutable, même si l'autorisation d'écriture est supprimée. D'autres systèmes offrent une sorte de « porte dérobée », autorisant plusieurs mappages d'une même page mémoire avec des permissions différentes. Une méthode relativement portable pour contourner W^X consiste à créer un fichier disposant de toutes les permissions, puis à mapper ce fichier en mémoire à deux reprises. Sous Linux, il est possible d'utiliser une option de mémoire partagée SysV non documentée pour obtenir de la mémoire partagée exécutable sans avoir à créer de fichier.Des chemins d'exécution rapides peuvent être établis pour un programme, réduisant ainsi certaines branches conditionnelles autrement répétitives .
Inconvénients
Le code automodifiable est plus difficile à lire et à maintenir car les instructions figurant dans le listing du programme source ne sont pas nécessairement celles qui seront exécutées. L'automodification consistant en la substitution de pointeurs de fonction peut être moins obscure s'il est clair que les noms des fonctions à appeler sont des espaces réservés pour des fonctions qui seront identifiées ultérieurement.
Le code automodificateur peut être réécrit comme un code qui teste un indicateur et effectue des branchements vers des séquences alternatives en fonction du résultat du test, mais le code automodificateur s'exécute généralement plus rapidement.
Le code auto-modifiable entre en conflit avec l'authentification du code et peut nécessiter des exceptions aux politiques exigeant que tout le code exécuté sur un système soit signé.
Le code modifié doit être stocké séparément de sa forme originale, ce qui entre en conflit avec les solutions de gestion de la mémoire qui, normalement, suppriment le code de la RAM et le rechargent à partir du fichier exécutable selon les besoins.
Sur les processeurs modernes dotés d'un pipeline d'instructions , un code qui se modifie fréquemment peut s'exécuter plus lentement s'il modifie des instructions déjà chargées en mémoire. Sur certains de ces processeurs, la seule façon de garantir la bonne exécution des instructions modifiées est de vider le pipeline et de relire de nombreuses instructions.
Le code automodifiable ne peut pas être utilisé du tout dans certains environnements, tels que les suivants :
- Un logiciel d'application fonctionnant sous un système d'exploitation doté d'une sécurité W^X stricte ne peut pas exécuter d'instructions dans les pages auxquelles il est autorisé à écrire ; seul le système d'exploitation est autorisé à la fois à écrire des instructions en mémoire et à exécuter ultérieurement ces instructions.
- De nombreux microcontrôleurs d'architecture Harvard ne peuvent pas exécuter d'instructions dans une mémoire en lecture-écriture, mais seulement des instructions dans une mémoire dans laquelle ils ne peuvent pas écrire, comme la ROM ou la mémoire flash non auto-programmable .
- Une application multithread peut comporter plusieurs threads exécutant la même section de code auto-modifiable, ce qui peut entraîner des erreurs de calcul et des défaillances de l'application.