Lire des stack traces obscurcies : cartes de renommage et désobscurcissement des plantages
L'obscurcissement par renommage transforme vos stack traces de production en un mur de a, b, c — sauf si vous conservez la carte de renommage. Voici comment fonctionne la carte, comment restaurer une trace lisible à partir d'un plantage obscurci, et comment garder la carte en sécurité sans la distribuer.
Le renommage est la première chose que font la plupart des obscurcisseurs .NET et le gain le moins cher qu”ils offrent : Billing.InvoiceService.Charge devient a.b.c, et un décompilateur ne livre plus votre architecture gratuitement à un lecteur. Le piège apparaît des semaines plus tard, la première fois qu”un vrai plantage revient du terrain. La stack trace est là, la ligne est juste — mais chaque frame affiche a.b(c), et vous ne pouvez pas savoir ce qui a cassé. L”instinct est de désactiver le renommage. Le bon geste est de conserver la carte de renommage et d”apprendre à lire à travers elle.
Pourquoi la trace est brouillée mais pas perdue
Une stack trace .NET est construite au moment du lancement à partir de métadonnées qui sont dans l”assemblage distribué : les tokens de méthode sur la pile d”appels, résolus en leurs noms. L”obscurcissement a réécrit ces noms avant la distribution, donc la trace rapporte fidèlement les noms réellement présents dans le binaire — les obscurcis. Rien n”est corrompu. Le type d”exception, le message, l”ordre des appels et (si vous avez conservé les symboles) les numéros de ligne sont tous authentiques. La seule chose qui manque est le dictionnaire qui remappe les noms distribués vers les vôtres.
Ce dictionnaire est la carte de renommage, et un bon obscurcisseur en émet une par build. Elle n”est pas distribuée avec l”app — cela remettrait à un attaquant l”inverse exact de l”obscurcissement. C”est un artefact de build que vous archivez à côté des PDB de cette version.
Ce que contient réellement une entrée de carte
Il est tentant d”imaginer la carte comme une table plate de ancienNom → nouveauNom, mais cela s”effondre aussitôt, car les obscurcisseurs réutilisent les noms courts de façon agressive. a pourrait être cent méthodes sans rapport ; b, une douzaine de types. La portée est ce qui les sépare, donc chaque entrée de carte est entièrement qualifiée — type déclarant plus signature complète — pas seulement un nom de feuille. Conceptuellement, quelques lignes ressemblent à ceci :
# original (fully qualified) => obfuscated
Billing.InvoiceService::Charge(Customer, Money) => a.b::c(a.d, a.e)
Billing.InvoiceService::.ctor(IClock) => a.b::.ctor(a.f)
Core.Money::FromMinor(Int64, String) => a.e::a(System.Int64, System.String)
Le fichier réel est généralement du XML ou un format propre à l”outil, mais la forme est la même : un élément d”origine identifié par espace de noms, type, membre et signature, apparié à sa forme obscurcie. Comme la clé est le frame complet, A::a(int) et B::a(string) sont des lignes distinctes même si les deux feuilles sont a. Un désobscurcisseur qui ne fait correspondre que le nom court traduira mal ; un qui fait correspondre le frame qualifié est exact.
Désobscurcir une trace, frame par frame
Voici une trace obscurcie telle qu”elle pourrait arriver dans un rapport de plantage :
System.InvalidOperationException: Sequence contains no elements
at a.e.a(Int64 A_0, String A_1)
at a.b.c(a.d A_0, a.e A_1)
at a.g.b(a.d A_0)
at X.<>c__DisplayClass4_0.a()
La relire avec la carte de ce build transforme chaque frame en son identité d”origine :
System.InvalidOperationException: Sequence contains no elements
at Core.Money.FromMinor(Int64 minor, String currency)
at Billing.InvoiceService.Charge(Customer customer, Money amount)
at Billing.BatchRunner.Run(Customer customer)
at Billing.BatchRunner.<Run>b__4_0() // lambda in Run
Deux détails importent ici. D”abord, les frames générés par le compilateur — la closure <>c__DisplayClass, la lambda b__ — sont généralement laissés intacts par le renommage parce que c”est le compilateur, et non vous, qui les a nommés ; une bonne carte résout tout de même la méthode conteneur pour que vous sachiez que la lambda vivait dans Run. (Si cette partie vous est inconnue, comment les lambdas deviennent des display classes en couvre la forme.) Ensuite, les noms de paramètres comme A_0 apparaissent parce que l”obscurcisseur a supprimé les originaux ; si vous les voulez de retour, conservez les noms de paramètres dans la carte ou désactivez le renommage des paramètres pour les frames qui vous importent.
La traduction elle-même est mécanique : analysez la trace en frames, et pour chaque frame cherchez (type déclarant, membre, signature) dans la carte et substituez l”original. La seule vraie subtilité est la correspondance — vous devez normaliser la signature obscurcie exactement comme la carte l”a stockée (types de paramètres entièrement qualifiés, y compris les noms de type déjà obscurcis), sinon la recherche échoue.
Garder la carte en sécurité et retrouvable
Une carte n”est utile que si, des mois après la distribution d”un build, vous pouvez retrouver la carte exacte de ce build — et vous seul le pouvez. Trois pratiques rendent cela fiable :
- Archivez une carte par build, indexée par version et module. Rangez-la à côté des PDB comme artefact de build, nommée par version d”assemblage et, idéalement, le
Mviddu module (le GUID gravé dans chaque module compilé). Quand un plantage arrive, le rapport vous donne la version ; cela sélectionne la carte sans deviner. - Ne laissez jamais la carte près du client. Elle n”a pas sa place dans l”installeur, le répertoire de l”app, un paquet NuGet, ni un serveur de symboles public. Traitez-la comme une clé de signature : stockage interne, accès contrôlé. Une carte distribuée est une app désobscurcie.
- Désobscurcissez côté serveur, à la réception. Branchez la traduction là où arrivent les rapports de plantage pour que les traces obscurcies entrantes soient résolues automatiquement contre la carte archivée correspondante. Vos tableaux de bord affichent alors de vrais noms ; l”utilisateur ne voit jamais de carte et n”en a jamais besoin.
Quand vous voulez laisser un frame lisible
Le renommage complet plus une carte privée est le bon défaut, mais parfois vous exemptez délibérément quelques membres du renommage — une surface de SDK publique à laquelle les appelants se lient par nom, un contrat de plugin, ou une poignée de points d”entrée de haut niveau que vous voulez lisibles même sans carte. Ces frames arrivent dans une trace lisibles tels quels, et tout ce qui se trouve en dessous reste obscurci. Nebula vous laisse cadrer le renommage avec des règles d”inclusion/exclusion exactement pour cela : protégez les parties internes de façon agressive, conservez les noms délibérément publics, et émettez une carte qui couvre le reste.
Le fil conducteur : une stack trace obscurcie n”est pas une stack trace perdue. Le plantage est réel et l”information est intacte — elle est écrite dans les noms réellement distribués. Conservez la carte par build, résolvez les traces de votre côté, et vous obtenez toute la protection du renommage sans la taxe de débogage. Désactivez le renommage pour rendre les traces lisibles et vous aurez simplement remis votre carte de symboles gratuitement au lecteur ; conservez la carte à la place, et vous seul pourrez la lire.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.