Obfusquer du code qui génère du code : Reflection.Emit, DynamicMethod et arbres d'expression
Quand votre application émet de l'IL à l'exécution — DynamicMethod, TypeBuilder, Expression.Compile — le renommage peut la casser d'une manière que le compilateur ne signale jamais. Voici exactement quels motifs de génération de code survivent à l'obfuscation et lesquels nécessitent des règles de conservation.
Les sérialiseurs, les ORM, les conteneurs d’injection de dépendances, les mappeurs et les moteurs de règles font tous la même astuce en coulisses : ils écrivent de l’IL à l’exécution au lieu d’appeler vos méthodes directement, car une méthode générée et spécialisée est bien plus rapide que la réflexion à chaque appel. .NET offre trois façons principales de le faire — DynamicMethod + ILGenerator, TypeBuilder via Reflection.Emit et Expression.Compile sur des arbres d’expression LINQ. Toutes trois interagissent avec l’obfuscation d’une manière que le compilateur ne peut pas signaler, car la casse survient à l’exécution, dans du code qui n’existait pas à la compilation. Voici le modèle précis de ce qui est sûr et de ce qui ne l’est pas.
La distinction clé : le générateur est publié, le code généré non
Deux morceaux de code distincts sont en jeu, et les confondre est là que commence la confusion.
Le code généré — le corps du DynamicMethod, le type de TypeBuilder, le délégué de l’expression compilée — est créé en mémoire à l’exécution et jamais écrit dans votre assembly, donc l’obfuscateur ne le voit pas : il n’est ni protégé ni endommagé. (Si le protéger importait, vous ne pourriez de toute façon pas ; il est éphémère.)
Le générateur — la méthode pleine d’appels à il.Emit(...), ou le code qui construit l’expression — est, lui, dans votre assembly. C’est de l’IL ordinaire : il est renommé, son flux de contrôle est aplati et ses chaînes sont chiffrées comme le reste. C’est très bien, à une réserve près : le générateur référence généralement vos types et membres, et la façon dont il les référence décide si l’obfuscation le casse.
Où ça casse : la recherche de membres par chaîne
Le générateur qui casse est celui qui résout par nom (chaîne) les membres vers lesquels il émet des appels. Prenons un DynamicMethod qui appelle un getter trouvé par nom :
MethodInfo getter = typeof(Order).GetMethod("get_Total")!; // string lookup
var dm = new DynamicMethod("ReadTotal", typeof(decimal), new[] { typeof(Order) });
var il = dm.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Callvirt, getter); // emits a call to whatever `getter` resolved to
il.Emit(OpCodes.Ret);
Après que l’obfuscation a renommé Order.Total (et son accesseur get_Total), la chaîne "get_Total" ne correspond plus à rien, donc GetMethod renvoie null et la ligne suivante lève NullReferenceException / ArgumentNullException — à l’exécution, sans le moindre indice à la compilation. Même panne pour le code TypeBuilder qui définit des substitutions par nom, et pour Expression.Property(p, "Total").
Le correctif tient en deux options. Conserver le nom des membres précis que le générateur recherche par chaîne (une règle de conservation ciblée, pas une exclusion globale). Ou, mieux, cesser d’utiliser des chaînes : capturez le membre via quelque chose que l’obfuscateur peut réécrire.
Ce qui survit : les références que l’obfuscateur peut voir
Quand le générateur capture un membre comme référence de métadonnées plutôt que comme chaîne, l’obfuscateur réécrit cette référence dans le cadre du renommage, et tout continue de fonctionner :
// Survives renaming — MethodInfo captured from a delegate, not a string.
MethodInfo getter = ((Func<Order, decimal>)(o => o.Total)).Method; // won't help: that's a lambda's Method
// Better: read the MemberInfo out of an expression tree
Expression<Func<Order, decimal>> e = o => o.Total;
var prop = (PropertyInfo)((MemberExpression)e.Body).Member; // real PropertyInfo, token-based
Les arbres d’expression compilés sont le cas sympathique : o => o.Total porte une référence PropertyInfo que l’obfuscateur renomme en même temps que la propriété, de sorte qu’Expression.Compile() se lie ensuite au bon membre. La règle empirique : les arbres d’expression et typeof(T).GetMethod(référence-dérivée-de-MethodInfo) sont sûrs face à la réécriture ; tout nom de membre sous forme de "chaîne" dans un générateur ne l’est pas.
Une liste de contrôle pratique
- Auditez vos points de génération de code à l’exécution à la recherche de littéraux de chaîne nommant vos propres membres ; ce sont les points de casse.
- Préférez les arbres d’expression ou la capture de délégué/
MethodInfoàGetMethod("Nom")quand vous le pouvez. - Pour les recherches par chaîne inévitables, ajoutez des règles de conservation ciblées pour exactement ces membres — et rappelez-vous que leurs corps restent obfusqués ; seul le nom survit.
- Testez explicitement les chemins de génération de code de la build obfusquée, car ni le compilateur ni un test de fumée qui saute le chemin généré ne détecteront une émission cassée.
Le faire avec Nebula
Nebula renomme, aplatit et chiffre les chaînes de vos générateurs comme de n’importe quel autre code, et son modèle de règles de conservation est assez précis pour préserver exactement les membres que votre code Reflection.Emit ou de construction d’expressions résout par nom, tout en obfusquant leurs corps. Combinez-le avec les conseils sur la réflexion et la sérialisation et vous pourrez publier du code obfusqué qui génère du code sans une seule surprise à l’exécution.
Commencez avec l’édition gratuite, ou consultez les offres.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.