Construire une keep-list Nebula : quoi exclure du renommage (et pourquoi)
Tout obfuscateur vous laisse exclure des noms du renommage, et le premier réflexe quand quelque chose casse est d'en exclure trop. Une keep-list est une frontière conçue et minimale, pas un fourre-tout pour tout ce qui a cassé. Voici la discipline : ce qui doit vraiment rester non renommé (API publique, cibles de réflexion, DTO sérialisés, classes d'options, scan de DI, XAML, points d'entrée d'un hôte), comment l'exprimer dans nebula.config.json, comment resserrer chaque entrée au strict minimum, et comment prouver le résultat dans Glass.NET.
Tout obfuscateur vous offre un moyen de dire « ne renomme pas ceci ». La tentation, la première fois que quelque chose casse, est d’y recourir généreusement : excluez le type, excluez l’espace de noms, excluez tout ce qui semble fragile, et passez à la suite. Ça marche, et ça démantèle votre protection en silence. Une keep-list n’est pas un fourre-tout pour tout ce qui a cassé ; c’est une frontière conçue et minimale entre les noms que votre programme résout comme des chaînes à l’exécution et tout le reste. Cet article porte sur la discipline qui consiste à bâtir cette frontière et à la garder petite : ce qui doit vraiment rester non renommé, comment le dire dans la configuration, comment resserrer chaque entrée au strict minimum, et comment prouver le résultat dans un décompilateur.
Si vous voulez le catalogue de ce qui casse et pourquoi, c’est obfuscation, réflexion et pièges de sérialisation. Voici la pièce de processus qui l’accompagne : l’article des pièges nomme le symptôme ; celui-ci explique comment écrire et maintenir le jeu de règles qui le prévient, sans exclure de trop et donner la protection.
Pourquoi la keep-list est un coût, pas un filet de sécurité
Partez du bon modèle mental : chaque nom de votre keep-list est un nom que vous remettez à un attaquant en clair. Le renommage est la protection la moins chère et la plus large dont vous disposez : il transforme ValidateLicenseKey en a dans tout l’assembly. Chaque entrée keep y recreuse un trou lisible. Gardez un espace de noms entier « par précaution » et vous aurez laissé chaque nom de type, de méthode et de champ de cet espace reposer dans les métadonnées, prêt à être lu par quiconque a un décompilateur.
La règle directrice est donc le minimalisme : gardez un nom seulement quand quelque chose le résout par sa chaîne à l’exécution, et gardez seulement ce nom — pas le type autour, pas l’espace de noms autour du type. Une keep-list bâtie sur cette règle est courte, et le court est tout l’objectif.
Ce qui doit vraiment rester
Au fil des applications .NET réelles, les mêmes catégories reviennent. Chacune est un endroit où un nom est lu comme une chaîne à l’exécution — par votre propre code, un framework, un sérialiseur ou un hôte — si bien que le renommer change un contrat plutôt que de simplement cacher un identifiant.
La surface d’API publique d’une bibliothèque livrée. Si d’autres compilent contre votre assembly, chaque nom de type et de membre public fait partie de votre contrat ; renommez-le et leur code ne se lie plus. C’est la seule catégorie que vous ne devriez presque jamais écrire à la main : activez preservePublicApi et l’obfuscateur garde exactement la surface visible de l’extérieur tout en renommant tout l’interne. Le cas de la bibliothèque entière est couvert dans protéger un SDK ou une bibliothèque .NET livrée.
Les types et membres atteints par réflexion. Tout ce qui est trouvé via Type.GetType("..."), GetMethod("..."), GetProperty("...") ou Activator.CreateInstance est résolu par une chaîne que le renommage ne touche pas. Gardez ces membres précis — pas, par réflexe, tout leur type déclarant.
Les noms de membre des DTO sérialisés. System.Text.Json, Newtonsoft.Json et les sérialiseurs XML construisent leur format de transport à partir des noms de propriété. Renommez Invoice.Total et le JSON devient silencieusement {"a":…}, cassant chaque consommateur et chaque document stocké. Gardez-les — ou, bien mieux, épinglez-les avec [JsonPropertyName("total")] pour que le nom de transport cesse de dépendre du nom C#, comme dans épingler un contrat JSON avec JsonPropertyName.
Les classes d’options liées depuis la configuration. Configure<TOptions> fait correspondre les clés de configuration aux noms de propriété via le binder. C’est la catégorie la plus souvent oubliée, car les classes d’options sont généralement des POCO internal que preservePublicApi ne couvre pas — elles se renomment donc en silence et retombent sur les valeurs par défaut en production. Gardez leurs noms de propriété liés.
Les types scannés par convention de DI. Si un conteneur enregistre des services en scannant un motif de nom — chaque type dont le nom finit par Service, par exemple — le renommage efface la convention et rien ne s’enregistre. Préférez corriger l’enregistrement pour qu’il soit explicite ou basé sur un marqueur ; là où vous ne le pouvez pas, gardez les noms que le scan apparie. Le traitement complet est dans obfusquer le scan d’assembly et l’injection de dépendances.
Les types référencés par nom dans XAML ou le markup. Un {Binding CustomerName}, un x:Class, une référence de type dans un DataTemplate, un convertisseur nommé dans le markup : tous résolvent des chaînes à l’exécution. Gardez les noms de propriété et de type que le markup nomme.
Les points d’entrée qu’un hôte charge par nom. Un plugin que l’hôte active par nom de type, une classe Startup qu’un hôte recherche, un module qu’un framework charge depuis une chaîne de manifeste : si quelque chose hors de votre assembly nomme le point d’entrée, gardez-le.
L’exprimer en règles
Nebula.NET lit un nebula.config.json avec un jeu de règles de renommage évalué de haut en bas — le premier motif qui correspond l’emporte —, alors placez vos règles keep étroites au-dessus du rename large qui ramasse le reste. Voici une keep-list pour une bibliothèque livrée découpée en une surface publique, un espace de noms de contrats sérialisés, un espace d’options et un moteur interne :
{
"schemaVersion": 1,
"inputs": [ "bin/Release/net8.0/Acme.Sdk.dll" ],
"outputDirectory": "protected",
"preservePublicApi": true,
"encryptStrings": true,
"controlFlowObfuscation": true,
"renaming": {
"rules": [
{ "pattern": "Acme.Sdk.Contracts.*", "action": "keep" },
{ "pattern": "Acme.Sdk.Options.*", "action": "keep", "members": "public" },
{ "pattern": "Acme.Sdk.*", "action": "rename" }
]
}
}
preservePublicApi prend en charge l’API visible de l’extérieur, il n’y a donc pas de règle pour elle. Les deux règles keep épinglent les contrats sérialisés et les options liées ; le qualificateur members: "public" resserre le keep des options aux noms de propriété publique et laisse les champs privés se renommer. Tout le reste de l’assembly retombe sur le rename qui ramasse le reste.
Pour une seule cible de réflexion, un motif de configuration est un instrument brutal : un attribut sur le membre exact est plus précis et vit à côté du code qui en dépend :
public sealed class ReportEngine
{
[Obfuscation(Feature = "renaming", Exclude = true)] // résolu par nom via réflexion
public ReportResult Run(string reportName) { /* ... */ }
private decimal Accumulate(ReadOnlySpan<Line> lines) { /* ... */ } // renommé — personne ne le recherche
}
L’attribut garde le nom d’une méthode ; Accumulate et le reste de la classe se renomment normalement. Recourez aux attributs pour les keeps individuels et chirurgicaux, et aux motifs de configuration pour les espaces de noms entiers qui sont un contrat par conception.
Resserrez chaque entrée
L’écart entre une keep-list faible et une forte est presque entièrement affaire de resserrement. Trois gestes font l’essentiel du travail.
Gardez le nom, protégez quand même le corps. Exclure un membre du renommage n’est pas l’exclure de la protection. Un DTO ou une classe d’options gardés reçoivent toujours leurs corps de méthode aplatis en flux de contrôle, leurs chaînes chiffrées et leurs constantes masquées : vous avez renoncé à une transformation sur un nom, pas au traitement entier sur le code. Alors n’hésitez jamais à garder un nom qu’il faut garder ; le coût n’est que la lisibilité de ce seul nom.
Gardez les membres, renommez l’interne. Un type lié par sérialisation a besoin que l’on garde ses noms de propriété publique, pas ses champs de stockage privés, ses constructeurs ou ses méthodes utilitaires. Limitez le keep aux membres qu’un sérialiseur ou un binder lit réellement et laissez le reste se renommer — le qualificateur members: "public" ci-dessus, ou un attribut sur les seuls membres exposés.
Gardez la feuille, pas la branche. La surexclusion la plus courante est de garder un espace de noms entier parce qu’on fait de la réflexion sur un type qu’il contient, ou un type entier parce qu’on en fait sur une méthode. Ramenez cela au seul nom qui est résolu comme une chaîne. Une méthode gardée dans une classe de trente membres en laisse vingt-neuf renommés ; un espace de noms gardé n’en laisse aucun.
Partout où la plateforme le permet, supprimez entièrement la dépendance par nom plutôt que de garder autour : [JsonPropertyName] au lieu de garder les noms de DTO, enregistrement explicite de DI au lieu de scan par convention, bindings compilés (x:Bind) au lieu de chemins en chaîne. Chaque dépendance que vous convertissez d’un nom vers un type est une entrée keep que vous pouvez supprimer.
Prouvez le résultat
Une keep-list n’est correcte que si deux choses sont vraies à la fois, et vous ne pouvez pas dire laquelle en lisant la configuration. Compilez l’assembly protégé et vérifiez les deux dans un décompilateur. Glass.NET est le compagnon gratuit et en analyse statique seule pour cela : il lit les métadonnées et l’IL et n’exécute jamais le binaire.
D’abord, confirmez que rien qui devait être caché n’a survécu : parcourez les types internes, vérifiez qu’ils se lisent comme a, b, c, que les noms de méthode de votre moteur ont disparu et qu’aucune règle keep n’a apparié par accident plus que vous ne vouliez. Ensuite, confirmez que rien qui doit survivre n’a été renommé : vérifiez que votre surface publique, vos propriétés de DTO gardées et vos noms d’options sont intacts. Garder de trop se manifeste par des internes lisibles ; garder de trop peu, par un contrat renommé. La procédure complète est dans votre obfuscation a-t-elle vraiment fonctionné ?.
Exécutez ensuite le test que la revue de configuration ne peut remplacer : lancez votre suite d’intégration contre la build protégée — sérialisez un DTO et faites un aller-retour, exercez un chemin de réflexion, liez une section de configuration. Les ruptures par résolution de noms sont invisibles à la compilation et évidentes à l’instant où le chemin s’exécute, si bien qu’un test qui traverse l’une de ces coutures attrape immédiatement un keep trop étroit.
Gardez-la minimale dans le temps
Une keep-list n’est pas un fichier que l’on configure et que l’on oublie. Chaque nouveau DTO sérialisé, chaque nouveau type sur lequel on fait de la réflexion, chaque nouvelle classe d’options est une entrée candidate — et chaque refactoring qui supprime une dépendance par nom est une entrée que vous pouvez supprimer. Revoyez-la comme du code : quand vous ajoutez un keep, écrivez à côté pourquoi — quel sérialiseur, quel appel de réflexion, quel binding — pour que la personne suivante puisse dire s’il mérite encore sa place. Une keep-list dont chaque ligne a une raison est une keep-list que vous pouvez garder courte, et une keep-list courte est tout l’enjeu : le plus petit ensemble possible de noms laissés en clair, avec Nebula.NET qui renomme, chiffre et protège tout le reste.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.