Bon nombre de choses dans le post font appel à des comportements indéfinis ("undefined behavior") par le standard C. Le programme n'est pas portable et fonctionne uniquement grâce au bon vouloir de GCC et des particularitées de l'achitecture x86(_64). Le comportement dépend complètement de l'implémentation: le programme peut ne pas compiler, ou bien compiler et ne pas fonctionner, ou bien faire des choses logiques mais non définis par le standard ou encore faire des choses complètement illogiques.
Il serait bien de faire la liste des comportements indéfinis présent dans le post pour que ceux qui ne maîtrisent pas encore le langage C sachent que ce sont des choses qui peuvent marcher dans certains cas mais qui sont des mauvaises pratiques et qu'il ne vaut mieux pas compter dessus... Voici une liste non exhaustive des comportements indéfinis:
- Même si GCC accepte apparemment que l'on définisse la fonction main avec n'importe quel nombre d'arguments de n'importe quel type, le standard C définit qu'un programme "conformant" doit avoir une fonction main définit d'une des deux façon suivante:
int main(int argc, char *arg[]) { /* code */ }
/* ou bien: */
int main(void) { /* code */ }
Ou de manière équivalente: on peut par exemple changer les identifiants argc et argv ou utiliser le type char ** pour le second argument.
- La définition:
void __(){}
définit une fonction ne prennant aucun argument. C'est donc un comportement indéfinis que d'appeler cette fonction avec des arguments. Cette ligne:
__(0 ,p?"Success":"Fail", _);
a donc un comportement indéfinis.
- Dans le même genre, c'est un comportement indéfini que de mettre plus de spécification de conversation (e.g., "%s", "%d", ...) dans la chaîne format (i.e., le premier argument) de la fonction printf que le nombre d'arguments passés à printf (en plus du format). Exemple:
printf("3.14159%s !\b\b\b\b\b\brni\rHello %4x\n");
Ici il y a 2 spécifications de conversion, "%s" et "%4x", mais aucun argument (sans compter le format) n'est passé à la fonction.
- Comme précisé, le type int par défaut ne marche qu'en C89, les versions modernes du C (C99 & C11) ne le permettent plus.
- Tout ce qui concerne la manipulation de la pile est bien évidemment non standard et utilise des comportements indéfnis, puisque le standard C ne définit même pas ce qu'est une pile (stack) pour ne pas obliger les implémentations à en utiliser une. Le bon fonctionnement de ce code:
#include <stdio.h>
int f(char* a, int b, int c)
{
}
int main(void)
{
f(NULL, 10, 20)
printf("param1 : %d, param2 : %d \n");
}
tient uniquement au fait que GCC aligne la pile sur une "16-byte boundary", autrement cela ne fonctionnerait pas. D'ailleurs cela ne fonctionne pas de manière générale. Exemple, ce code ne fonctionne pas:
/*
* Compiler sous linux avec:
* gcc -std=c11 -pedantic -m32 -o main main.c
*/
#include <stdio.h>
int f(char* a, int b, int c, int d, int e) {}
int main(void)
{
f(NULL, 10, 20, 30, 40);
printf("param1 : %d, param2 : %d, param3: %d, param4: %d\n");
return 0;
}
(en 32 bits du moins). Pour le 64 bits, les arguments étant principalement passés par les registres ça fonctionne différemment mais il y a aussi des cas où cela ne fonctionne pas.
Pour ce qui est de l'obfuscation de code à l'aide de ces techniques je suis assez sceptique.
Pour l'obfuscation du code source C, personne n'en doutera c'est rudemment efficace. Le nombre de personnes qui comprendront le code est très réduit. Par contre, je ne pense pas qu'on puisse réussir à créer un vrai gros programme comme ça. Il y aura bien trop de bugs et la maintenance et l'ajout/modification de fonctionnalitées sera un vrai calvaire. De plus, si on ne veut pas que les gens voyent notre code source, on peut aussi ne pas le distribuer.
Enfin, pour le code object je ne trouve pas que ça obfusque quoi que ce soit. L'utilisation d'identifiants illisibles (e.g., "_", "__") ne change rien au code objet: il reste le même quel que soit le nom des identifiants. Ensuite pour ce qui est de ce genre de code:
f(NULL, 10, 20);
printf("param1 : %d, param2 : %d \n");
Au niveau de l'assembleur/code objet on voit très bien que des arguments sont push sur le stack sans que la fonction f n'en fasse rien et donc que c'est la fonction printf qui va les utiliser.