
Obfusque le C/C++ via des passes LLVM : chiffrement des chaînes, aplatissement du flux de contrôle, réécriture MBA et anti-analyse pour déjouer la rétro-ingénierie.
██╗ ███████╗███████╗████████╗
██║ ██╔════╝██╔════╝╚══██╔══╝
██║ █████╗ █████╗ ██║
██║ ██╔══╝ ██╔══╝ ██║
███████╗███████╗███████╗ ██║
╚══════╝╚══════╝╚══════╝ ╚═╝
██████╗ ██████╗ ███████╗██╗ ██╗███████╗ ██████╗ █████╗ ████████╗ ██████╗ ██████╗
██╔═══██╗██╔══██╗██╔════╝██║ ██║██╔════╝██╔════╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗
██║ ██║██████╔╝█████╗ ██║ ██║███████╗██║ ███████║ ██║ ██║ ██║██████╔╝
██║ ██║██╔══██╗██╔══╝ ██║ ██║╚════██║██║ ██╔══██║ ██║ ██║ ██║██╔══██╗
╚██████╔╝██████╔╝██║ ╚██████╔╝███████║╚██████╗██║ ██║ ██║ ╚██████╔╝██║ ██║
╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
A simple obfuscator for C/C++ x64 and x86 code
C'est conçu comme un fork de LLVM, vous avez donc besoin du code source réel pour l'obfusquer. Ce n'est pas un obfuscateur d'exécutables arbitraires. C'est une version modifiée du compilateur.
Si vous voulez voir les résultats, j'ai créé et obfusqué 2 crackmes avec. Vous pouvez les récupérer dans les releases aux côtés du binaire clang précompilé.
Tout cela a été compilé avec le flag -O3 et toutes les captures d'écran proviennent d'IDA Pro 9.4. Je l'ai aussi testé avec Binary Ninja et Ghidra, les résultats étaient soit identiques, soit pires.
Chiffre les chaînes au moment de la compilation et insère une fonction de déchiffrement à chaque utilisation de la chaîne chiffrée. Cela désactive complètement la possibilité de rechercher des chaînes dans le binaire. Et chaque chaîne possède sa propre clé unique codée en dur dans la fonction de déchiffrement, ce qui rend leur extraction et leur déchiffrement bien plus difficiles. Ces fonctions de déchiffrement seront également inlinées. (non inlinées ci-dessous uniquement pour la démonstration)
![]() Avant le pass |
![]() Après |
Remplace les opérations arithmétiques par leurs équivalents MBA. Il est pratiquement impossible de voir ce que faisait l'opération d'origine sans d'abord la passer dans un déobfuscateur MBA. Bien sûr, cette obfuscation est plutôt faible car le MBA est la plus vieille astuce du genre, il existe donc de nombreux outils pour y faire face, par exemple CoBRA, qui déobfusquera avec succès ceci en l'expression d'origine :
./cobra-cli --mba "((x^y) - (((x^y)&0xFF)&0xA) + 10) * ((x^y) - ((x^y)|0xA) + 10) + ((x^y) - (((x^y)&0xFF)|0xFFFFFFF5) - 11) * (~(x^y) - (~(x^y)|0xA) + 10)" --bitwidth 32
10 * (x ^ y)
C'est pourquoi j'ai créé le pass AAMBA.
![]() Avant le pass |
![]() Après |
Remplace les opérandes des opérations binaires par ADC(X, 255) - 255 - CF et SBB(X, 255) + 255 + CF. Bien sûr, cela s'évalue toujours à X, mais cela rend l'expression dépendante du carry flag. À moins que le décompilateur ne suive l'état de CF (ce qui est parfois impossible), il sera très perturbé et ne pourra pas replier ces expressions. Cela s'associe très bien avec le pass MBA précédent, obfusquant encore davantage l'arithmétique. Comme vous pouvez le voir ci-dessous, le décompilateur a créé des variables supplémentaires et utilise beaucoup d'appels à __PAIR64__ et __CFADD__, ce qui rend beaucoup plus difficile (mais pas impossible) de coller cela dans des outils comme CoBRA ; le plugin gooMBA d'IDA n'aide pas non plus à simplifier cela. De plus, le décompilateur d'IDA suit le carry flag dans une certaine mesure, mais en combinant cela avec l'obfuscation du flux de contrôle, le suivi devient pratiquement impossible sans exécution, car vous ne savez pas quelle était l'opération précédente : peut-être qu'elle a mis CF à 1, peut-être pas, donc les passes ultérieures s'ajouteront encore à celle-ci.
![]() Avant le pass |
![]() Après |
Collecte tous les blocs d'une fonction et en fait une seule machine à états géante, il crée une table de saut au début de la fonction et y place tous les pointeurs de blocs. Ensuite, au lieu d'un saut normal à la fin de chaque bloc, tout est routé via le dispatcher qui utilise des sauts indirects, ceux-ci étant quasiment impossibles à résoudre statiquement sans aucune exécution. Il rétrograde également les registres vers la pile, donc si vous coupez un bloc en deux, toutes les variables du bloc précédent seront sur la pile, ce qui signifie qu'il y aura BEAUCOUP de variables dans chaque expression. Si vous passez les opérations d'un seul bloc dans CoBRA, il ne pourra pas beaucoup le désobfusquer car il y aura trop de variables inconnues.
Crée un ensemble de blocs factices contenant de l'assembleur invalide. Cela perturbe énormément les désassembleurs, car si le désassembleur rencontre un octet techniquement invalide qui ne sera jamais exécuté, il essaiera quand même de lui donner un sens. Ainsi, si l'octet est incomplet, il créera une instruction à partir des octets qui se trouvent après lui, les consommant essentiellement. Cela crée une désynchronisation détruisant chaque instruction suivante. Sur les binaires Windows, IDA pourra s'en remettre quelque peu ; dans de rares cas, il pourra générer un graphe et décompiler ce qu'il peut (bien que ce sera cassé et incomplet), tandis que sur les binaires Linux, cela casse complètement la vue graphe et désactive la décompilation. De plus, il insérera un minuteur RDTSC et d'autres vérifications anti-débogage ; s'il détecte un débogueur, vous plantez.
![]() Windows |
![]() Linux |