Ce que l'obfuscation casse : réflexion, sérialisation et ce qu'il faut exclure
La première fois que vous obfusquez une app qui marchait, elle peut casser de façons surprenantes : le JSON sort faux, la réflexion renvoie null, le data binding s'arrête. La cause est toujours la même : quelque chose a résolu par son nom un membre renommé. Voici exactement ce qui casse, pourquoi, et comment exclure les bonnes choses sans affaiblir la protection.
Vous ajoutez un obfuscateur à une app .NET qui marchait, vous compilez, vous exécutez — et quelque chose qui marchait hier est cassé. Le JSON que renvoie votre API a des champs nommés a et b. Un appel à Type.GetMethod("Process") renvoie null. Votre fenêtre WPF ne lie plus rien. L”injection de dépendances ne trouve pas un service. Rien de tout cela n”est un bug de l”obfuscateur ; c”est l”erreur d”obfuscation la plus courante, et elle a toujours la même cause racine : quelque chose a résolu par son nom un membre renommé à l”exécution.
Pourquoi les noms sont la ligne de faille
Le renommage — le cœur de la plupart des obfuscations — transforme CustomerName en a, CalculateTotal en b. Le code compilé qui appelle ces membres est réécrit pour correspondre, donc les appels directs continuent de marcher ; le compilateur les a déjà résolus en jetons, pas en noms. Le danger est tout chemin de code qui va chercher un membre par son nom de chaîne à l”exécution, car cette recherche demande toujours le nom d”origine, qui n”existe plus dans l”assembly.
C”est tout le mode de défaillance. Tout ce qui suit en est une instance précise.
Les victimes habituelles
Sérialisation. Un sérialiseur mappe entre noms de membres et un format de fil. Renommez les membres et la sortie se renomme avec eux :
public class Invoice { public decimal Total { get; set; } public string Customer { get; set; } }
JsonSerializer.Serialize(new Invoice { Total = 42, Customer = "Acme" });
// Expected: {"Total":42,"Customer":"Acme"}
// After renaming: {"a":42,"b":"Acme"} ← your API contract just changed
Pire, la désérialisation de données précédemment stockées échoue maintenant à lier, parce que le JSON dit Total et la propriété s”appelle a. Tout type qui franchit une frontière de sérialisation — DTOs d”API, fichiers de configuration, documents enregistrés, charges de messages — est à risque.
Réflexion. Toute recherche par nom renvoie le mauvais membre ou rien :
var mi = typeof(Report).GetMethod("Generate"); // null — it's called 'a' now
typeof(Plugin).GetProperty("Name")?.GetValue(obj); // null
Activator.CreateInstance(Type.GetType("MyApp.Widgets.Chart")); // type name changed → null
Enum.Parse<Status>("Active"); // enum member renamed → throws
Le data binding est de la réflexion déguisée. {Binding CustomerName} de WPF/XAML, @bind de Blazor et les colonnes automatiques de DataGrid résolvent les noms de propriété comme des chaînes à l”exécution ; renommez la propriété et le binding n”affiche plus rien en silence.
Frameworks par convention. La DI qui mappe IFooService → FooService par nom, EF Core mappant une propriété à une colonne par nom, AutoMapper appariant des membres par nom, le model binding de MVC : tout ce dont la « magie » est la correspondance de noms casse quand les noms changent.
La règle : excluez le contrat, pas la logique
Le correctif n”est pas d”obfusquer moins ; c”est d”exclure du renommage exactement les noms résolus par nom, et rien de plus. En pratique, c”est un petit ensemble identifiable :
- Les types sérialisés (DTOs, configuration, contrats de messages).
- Les membres atteints par réflexion, y compris tout ce qu”un framework résout par nom.
- Les types/membres référencés depuis XAML ou un autre balisage.
- La surface d”API publique d”une bibliothèque contre laquelle d”autres compilent.
Vous marquez ceux-ci pour sauter le renommage, avec un attribut ou une règle de configuration. Nebula.NET accepte des règles d”exclusion de renommage par espace de noms, type ou attribut, donc vous gardez une frontière nette : votre espace de noms Contracts conserve ses noms, le reste de l”assembly est renommé librement.
// Exclude a serialized contract from RENAMING, keep everything else renamed.
[Obfuscation(Feature = "renaming", Exclude = true, ApplyToMembers = true)]
public class Invoice
{
public decimal Total { get; set; }
public string Customer { get; set; }
}
Le correctif plus robuste : découpler le nom du fil
Exclure des types fonctionne, mais pour la sérialisation il existe un meilleur motif qui supprime entièrement la fragilité : figez les noms sérialisés explicitement pour qu”ils ne dépendent jamais du nom du membre.
public class Invoice
{
[JsonPropertyName("total")] public decimal Total { get; set; }
[JsonPropertyName("customer")] public string Customer { get; set; }
}
Maintenant le JSON dit total et customer quel que soit le nom de la propriété en IL ; renommez Total en a et le format de fil est inchangé. Cela découple votre contrat de votre code, ce qui est de la bonne ingénierie indépendamment de l”obfuscation (un simple refactor de renommage C# n”altérera pas non plus votre API en silence). Le même raisonnement s”applique ailleurs : référencez les valeurs d”enum par le type d”enum, pas Enum.Parse sur une chaîne ; résolvez la DI par enregistrement explicite, pas par convention ; préférez les bindings compilés (x:Bind dans UWP/WinUI) aux chemins de chaîne là où la plateforme l”offre.
L’exclusion est au niveau du nom, pas de la protection
La nuance la plus importante : exclure un type du renommage ne le laisse pas sans protection. Le renommage est une transformation parmi plusieurs. Un DTO dont vous conservez les noms de propriété peut toujours avoir ses corps de méthode aplatis dans le flux de contrôle, ses littéraux de chaîne chiffrés et ses constantes masquées. Vous dites à l”obfuscateur « ne change pas ces noms », pas « ne protège pas ce code ». Le coût de faire les exclusions correctement est donc essentiellement nul en matière de protection : vous gardez une couverture complète sur la logique et renoncez au renommage uniquement sur la poignée de noms que le runtime recherche en tant que chaînes.
Un flux de travail fiable
Obfusquez, puis exécutez votre vraie suite de tests contre la build obfusquée — pas seulement contre la non-obfusquée. Les cassures de résolution de noms sont invisibles à la compilation et évidentes dès qu”un aller-retour de sérialisation ou un chemin de réflexion s”exécute, donc un test d”intégration qui sérialise un DTO, appelle une API ou exerce un plugin les attrapera immédiatement. Corrigez chacune en excluant ce contrat précis (ou en figeant ses noms de fil), jamais en désactivant le renommage en bloc. Faites-le une fois, et « l”obfuscation a cassé mon app » devient une étape de configuration unique plutôt qu”un mystère récurrent : votre logique reste difficile à lire, et les noms dont dépend votre programme restent exactement là où il les attend.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.