Les "Voies de l''Informatique" sont parfois plein de "codes impénétrables"
Bonjour à toutes et à tous,
En ces temps particulièrement délicats de la cybersécurité, les « méchants » ont souvent recours à des techniques utilisant du « Code Impénétrable » :
Le code impénétrable d'un programme informatique est un code dont la compréhension est très difficile pour un humain tout en restant parfaitement compilable par un ordinateur.
Le procédé par lequel du code est rendu impénétrable est appelé "brouillage" ou "offuscation" (habituellement anglicisé donne : obfuscation). Cette technique de programmation – qui va à l'encontre des usages habituels – a ses raisons parfaitement défendables.
Un moyen de protéger les investissements de développement d'un logiciel :
- Techniques de génération de code objet rendant plus difficile la rétro-ingénierie.
- Remplacement d'identificateurs et de labels mnémoniques par des labels non signifiants.
==> Obscurcissement de code objet
Un exercice de style et/ou un loisir pour certains programmeurs esthètes et/ou épicuriens du code :
- Ecriture de code source dont la fonction est difficile à déterminer.
- Usage d'une syntaxe illisible, de particularités sémantiques peu connues ou peu recommandables du langage utilisé ou d'algorithmes peu naturels.
==> Obscurcissement du code source
Ces méthodes ne sont pas nouvelles et certaines sont bien connues (à condition de s’y être « frotté » auparavant).
Grattons un peu plus ... avec
De l'utilisation d’un langage générant de l’assembleur
Entre 1960 et 1970, la plupart des constructeurs d’informatique utilisaient pour leurs développements des langages de type « PL » (pour Programming Language) : PL/360, PL/M, PL/C, PL/S, PL/1600… qui traduisaient un code source semi-évolué en assembleur.
Les applications n'étant vendables à l'époque que si leur code source était disponible, c'est ce code assembleur généré qui était mis à disposition des clients. Ainsi, une instruction comme A=B+31; devenait :
LR 4,B
ADD 4,=31
ST 4,A
Version que le client était moins enclin à modifier, d'autant que la version suivante l'aurait obligé à recommencer toutes ses modifications.
Le but premier besoin était moins de masquer le code source que de limiter les modifications des programmes par les clients, la maintenance à distance par les éditeurs étant plus difficile pour les programmes modifiés.
En langage « Assembleur », on trouve des programmes éliminant tous les commentaires d'un programme-source et remplaçant toutes les étiquettes (par exemple "BOUCLE", "REPRISE", "ERREUR" par des termes moins parlants "YETI00001", "YETI00002", "YETI00003", voire des mélanges de "O", de "0", de "I" et de "1" les rendant pénibles à distinguer pour un lecteur humain sans pénaliser le moins du monde le programme assembleur).
Une variante, plus perverse encore, consiste à remplacer les commentaires par des informations qui n'apportent absolument rien.
Exemple : LR 28,=X’0F’ (chargement de ‘X0F’ dans le registre 28)
En "Turbo-Pascal 3" (1985), il existait un programme supprimant tous les blancs non significatifs d'un code source, tassant celui-ci à raison de 255 caractères par ligne, et remplaçant toutes les variables par des combinaisons de « l » (L minuscule) de « 1 » (chiffre 1), de « O » (O majuscule) et de 0 (chiffre 0).
La nécessité de ces offuscateurs venait du fait que « Turbo-Pascal 3 » ne permettait pas de distribuer des sous-programmes autrement que sous forme de code source.
Exemple : A := B + C devenait l0O1l10O:=O1ll10O0+O0O1l00ll.
En langage « Batch » (avec un bon vieux MS-DOS de « dessous les fagots »)
Il était très rudimentaire avec un côté un peu spartiate : pour qui ne le comprenait pas ou ne le maîtrisait pas, le langage « Batch » permettait de complexifier le code avec une compréhension difficile d’accès.
Exemple d'un morceau de code d' "Akhenaton" (qui masque la payload virale par les spartiates instructions "Batch") :
Set ¯=Set%þ%
%¯% ð=If Exist%þ%
%¯% =Echo%þ%
...//...
% % þAkhenatonþ>>%2
%ð% î%2 %Þ% F%þ%
Ren %2 î%2%þ%
% % @%% Off>%2%þ%
Find "þ"<%ê%>>%2
% % %ð% î%2 %Þ% E>>%2%þ%
Mais certains langages ont joué de l'obscurcissement en fonction des versions. Prenons le cas de mon ami le "Fortran" (The Return).
L'obscurcissement d'un code "Fortran", utilisant le format fixe utilisé avant "Fortran 90", peut être obtenu en supprimant toutes les espaces (y compris entre un mot-clé du langage et un nom de variable) et en utilisant des noms différents pour une même variable dans différentes parties du code par le biais des "EQUIVALENCES" ou des "COMMON".
Mais le format libre utilisé depuis "Fortran 90" ne permet plus cette suppression de toutes les espaces : les instructions "EQUIVALENCE" et "COMMON" sont considérées comme obsolètes depuis la norme "Fortran 2018" (la dernière mise à jour du langage Fortran est "Fortran 2023" et ça ... décape !!).
C'est bien joli et intéressant tout cela ... mais quels sont les autres obscurcissements connus ?
L’obscurcissement algorithmique
La division en sous-chaînes égales d'une chaîne de longueur "n" prouve que "n" est non premier (si n ≠ 2).
L'usage d'une expression rationnelle pour un tel test est donc possible, bien que peu naturel et plutôt lent.
Une preuve de concept a été écrite en langage "Perl" (pour celles et ceux que cela intéresse : "https://archive.wikiwix.com/cache/url=http%3A%2F%2Fwww.itworld.com%2Fnl%2Fperl%2F01182001%2F")
Une forme particulière d'obscurcissement est un programme multilingue qui fait la même chose quand il est exécuté par différents interpréteurs ou compilateurs :
il existe ainsi un programme qui s'exécute indifféremment interprété par Perl et compilé en C, et dont cela constitue d'ailleurs le seul intérêt.
L’obscurcissement utilise bien souvent des recoins peu utilisés d'un langage ou des « undocumented rat’s holes » et peut parfois contribuer à la détection de bugs.
Pour information, il existe des concours annuels d’obscurcissement (il existe bien des concours internationaux d'Excel ...) et c'est "rudement" intéressant (attention, ça envoie du bit et de l'octet).
en Langage C avec l’IOCCC (International Obsfucated C Code Contest);
en Perl avec « The Perl Journal », dont les fameux « JAPH » ( Just Another Perl Hacker).
La particularité des « JAPH » repose sur un court script Perl qui affiche le texte « Just Another Perl Hacker » (originellement, c’est un exercice de virtuosité intellectuelle utilisé comme signature de courriel par certains hackers - au sens noble du terme - du langage Perl).
Exemple d'un "JAPH" (sans code impénétrable) :
Exemple d'un "JAPH" avec un code ... opaque :
Et si l'on veut décoder ce "JAPH" à partir d'une chaîne littérale (la "Croisière s'amuse") :
Ou en utilisant seulement de la ponctuation, sans caractères alphanumériques (là, il faut être un peu "Dingo" ou "Dumbo" ou les deux ensemble ou prendre des trucs bizarres le soir ... en rédigeant cet article) :
Mais si vous le voulez en mots-clés purement "Perl", je peux aussi vous le produire (je vous fais grâce de son rendu en hexadécimal ;-)).
Vous comprendrez que cela peut représenter un assombrissement ou avoir une valeur pédagogique (et andragogique) en démontrant de manière élégante une fonctionnalité particulière du langage Perl.
D’ailleurs, la communauté « Perl » a étendu le champ des « JAPH » avec « Pugs » (https://fr.wikipedia.org/wiki/Pugs) et « Parrot » (https://fr.wikipedia.org/wiki/Parrot_(machine_virtuelle).
Jusqu'à présent, l'on retient que des langages comme « C », « PERL » ou encore « APL » se prêtent très bien à l’obscurcissement.
Dans le non moins célèbre magazine « The Humble Programmer », un certain « Edsger Dijkstra » a eu des mots très durs et violents envers les langages riches se prêtant à de telles techniques et se fait ainsi l’avocat des langages plus modestes.
A l'inverse, l'élégant « Larry Wall » (inventeur et concepteur du langage PERL) affirme en revanche "qu’un langage qui essaierait d’interdire de mauvaises pratiques telles que l’obscurcissement empêcherait aussi d’écrire des programmes légitimes tels que les « unilignes »".
Les unilignes ... autres acteurs différents de l'obscurcissement
Un code « uniligne » (« one liner » en anglais) est un programme informatique « jetable », peu banal, écrit pour exécuter une tâche ponctuelle, dans un langage de script (Perl, Ruby, Python, etc. ) et surtout tenant sur une seule ligne.
Les particularités de l’uniligne :
« Do What I Mean (DWIM) ».
« Don’t Repeat Yourself (DRY) » propres à certains langages.
Attention aux langages imposant plusieurs phases avant l’exécution, la déclaration explicite de variables, de fonctions, ou de classes : ils ne sont pas éligibles aux unilignes.
Parce qu'un uniligne est rarement destiné à être réutilisé, les considérations de lisibilité par un tiers sont secondaires. Certains unilignes sont des assombrissements délibérés.
En revanche, ils peuvent être pédagogiques (ou andragogiques) en démontrant de manière concise certains idiotismes du langage. Il faut néanmoins savoir que certains idiotismes deviennent inapplicables ou dangereux dans le cadre de programmes avec beaucoup de lignes de code.
L’uniligne a cependant une vraie histoire ... vraie :
Le mot "unilign" (one liner) est apparu pour la première fois dans le livre de "Alfred V. Aho, Peter J. Weinberger et Brian W. Kernighan", The AWK Programming Language (excellent ouvrage réédité) et utilisé dans le système d'exploitation Unix.
Les auteurs expliquent comment leur travail quotidien sur le système "Unix" fut à l'origine de ce paradigme (extraits et traduits par mes soins) :
« La version de 1977 d'Unix possédait très peu de variables internes et de fonctions prédéfinies. Elle est conçue pour écrire des programmes courts […] Notre modèle était qu'un appel de fonction serait long d'une ou deux lignes, exécutées sitôt tapées. […] Nous, en tant qu'auteurs, « savions » comment le langage était censé être utilisé, et c'est pour cela que nous n'écrivions que des unilignes. »
Ceci explique bien cela ...
En « AWK » :
Afficher le nombre total de lignes lues en entrée : END { Print NR }
Afficher la dixième ligne d’un fichier : NR == 10
Afficher le dernier champ de chaque ligne : { print $NF }
En « PERL » :
Inverser tous les octets d’un fichier : perl -0777e ‘print scalar reverse <>’ nomdufichier
Les unilignes sont souvent utilisés pour démontrer l'expressivité de certains langages de programmation, grâce au talent du programmeur, et de nombreux concours sont organisés pour savoir qui fera le plus exceptionnel.
D'ailleurs, le Le lauréat du dernier concours « IOCCC » a écrit ceci :
main(int c,char**v){return!m(v[1],v[2]);}m(char*s,char*t){return*t-42?*s?63==*t|*s==*t&&m(s+1,t+1):!*t:m(s,t+1)||*s&&m(s+1,t);}
Un peu imbuvable de prime abord (je vous l’accorde volontiers), mais ce programme permet de vérifier si une chaîne de caractères correspond à un motif, avec les conventions respectées de recherches d'Unix :
* pour une suite de caractères quelconques de longueur quelconque,
? pour un seul caractère…
Le code de retour est « 0 » pour "vrai" et « 1 » si non.
À noter que la correspondance motif se fait sur toute la chaîne, et non sur un sous-ensemble de celle-ci.
Dans la vraie vie, ça donne quoi comme résultat, si on exécute ce programme ?
$ prog foo 'f??'; echo $? → renvoie 0 (correspondance)
$ prog 'chaine quelconque' '??ai*e**n?*'; echo $? → renvoie 0 (correspondance)
$ prog 'chaine quelconque' 'ch*o?'; echo $? → renvoie 1 (non-correspondance)
Et si on prend une pincée de langage "Haskell" :
import IO
import List
main = (mapM_ putStrLn . sort . lines) =<< getContents
Il y a deux déclarations import ce qui porte en réalité le nombre de lignes du programme à trois.
En réalité, cet uniligne trie les lignes qu'il prend en entrée selon l'ordre lexicographique du code ASCII.
On continue la tambouille (grosse cuillère de « JAVA » et le fameux « Hello World ») :
public class OneLine { public static void main(String[] args) { System.out.println("Hello World!"); } }
Et pour le dessert, une touche de vanille des îles, en « Tcl-Tk » avec le calcul d’une factorielle:
proc ! x {expr {$x<2? 1: $x*[! [incr x -1]]}}
De part cet article, l'on voit "aisément" que le programmeur / développeur / codeur peut avoir "plusieurs tours dans son sac" en matière d'assombrissement, d'opacité et d'obscurcissement ou d'obsfucation.
A consommer avec beaucoup de modération car on pourrait par la suite ajouter une petite couche de chiffrement, histoire de pimenter un peu la vie ... peut-être une autre fois pour la suite de la recette.
Bien cordialement,
Yanolezard.
Bien cordialement, Yanolezard