Skip to content
← Tous les articles
· Delta1 Labs Nebula.NETObfuscation.NETAnalyse approfondie

Quand nameof et [CallerMemberName] font fuiter vos membres renommés

Vous avez renommé chaque type et chaque méthode, puis ouvert le binaire obfusqué et retrouvé vos noms d'origine posés dans des littéraux de chaîne en clair. Le coupable n'est pas un bug de l'obfuscateur — c'est le compilateur. nameof(OrderService) grave le littéral "OrderService" dans l'IL à la compilation, bien avant que le renommage ne s'exécute, et [CallerMemberName] fait de même avec le nom d'origine de la méthode comme valeur de paramètre par défaut. Renommez le membre et le littéral dit toujours l'ancien nom, si bien qu'un décompilateur lit votre table de correspondance gratuitement et que toute réflexion qui attendait le nom d'exécution casse silencieusement. Voici exactement d'où viennent ces littéraux dans l'IL, pourquoi le renommage seul ne peut pas les toucher, et comment le chiffrement de chaînes et la gestion consciente du renommage de Nebula.NET colmatent la fuite.

Vous avez activé le renommage, recompilé, et ouvert le résultat dans un décompilateur pour admirer le mur de a, b et c là où se trouvaient vos types. Puis vous les avez quand même retrouvés — OrderService, CustomerName, ValidateCoupon — posés dans des littéraux de chaîne en clair, offerts gratuitement au seul endroit que le renommage n’a jamais touché. Rien n’est cassé. C’est l’œuvre du compilateur, pas de l’obfuscateur, et comprendre pourquoi fait la différence entre une build protégée et une build qui publie discrètement sa propre table de renommage.

Deux fonctionnalités de C# placent vos noms d’origine dans l’assembly sous forme de données avant même que le renommage ne s’exécute : nameof, qui grave un nom dans un littéral à la compilation, et [CallerMemberName], qui fige le nom du membre appelant dans une valeur de paramètre par défaut à chaque site d’appel. Le renommage parcourt les références de types et de membres — une chaîne nue n’est ni l’un ni l’autre, donc l’ancien nom reste dans le tas de chaînes. Cet article montre exactement d’où viennent ces littéraux dans l’IL, pourquoi le renommage seul ne peut pas les toucher, quand les réécrire est juste et quand cela casserait quelque chose, et comment Nebula.NET colmate la fuite avec le chiffrement de chaînes et une gestion consciente du renommage.

La source

Une classe qui utilise les deux fonctionnalités comme le fait du code réel — catégories de journalisation, validation d’arguments et INotifyPropertyChanged :

public sealed class OrderService : INotifyPropertyChanged
{
    private string _customerName = "";

    public string CustomerName
    {
        get => _customerName;
        set { _customerName = value; OnPropertyChanged(); }   // [CallerMemberName] fills in "CustomerName"
    }

    public void ValidateCoupon(string code)
    {
        if (code is null)
            throw new ArgumentNullException(nameof(code));     // nameof -> literal "code"
        _logger.BeginScope(nameof(OrderService));              // nameof -> literal "OrderService"
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string? name = null) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

Où se trouvent réellement les noms dans l’IL

nameof(OrderService) ne référence pas le type dans le code émis. Le compilateur le remplace, lors de la compilation, par un simple littéral de chaîne — un unique ldstr :

// ValidateCoupon body — the names are DATA, not references:
ldstr      "code"                      // nameof(code)        -> literal
newobj     instance void [System.Runtime]System.ArgumentNullException::.ctor(string)
...
ldstr      "OrderService"              // nameof(OrderService) -> literal
callvirt   instance class ... Microsoft.Extensions.Logging.ILogger::BeginScope<...>(...)

[CallerMemberName] est la même astuce appliquée à un paramètre. OnPropertyChanged() est appelé sans argument dans la source, mais le compilateur renseigne la valeur par défaut du paramètre optionnel au site d’appel avec le nom de l’appelant sous forme de littéral :

// CustomerName setter — the compiler injected the name at the CALL SITE:
ldarg.0
ldstr      "CustomerName"              // injected by [CallerMemberName] at compile time
call       instance void OrderService::OnPropertyChanged(string)

Les deux noms sont désormais des opérandes ldstr pointant dans le tas #US (chaînes utilisateur) de l’assembly. Ce sont des données brutes sans aucun lien de métadonnées vers le type ou la propriété dont elles proviennent.

Pourquoi le renommage seul ne peut pas les toucher

Le renommage réécrit les références : un TypeRef vers OrderService, un MethodDef pour ValidateCoupon, chaque call et ldfld qui nomme un membre. Il laisse délibérément le contenu des chaînes intact, car une chaîne n’est pas une référence et changer la sémantique des chaînes par défaut casserait tout programme qui en compare, sérialise ou affiche une. Donc quand le renommeur transforme OrderService en b.c, le ldstr "OrderService" n’est pas entraîné avec lui — rien ne relie le littéral au type. Le résultat est un binaire renommé avec un index en clair de ses propres noms d’origine.

compilation : nameof /CallerMemberNametas de chaînes #USldstr "OrderService"le renommeur réécrit les RÉFSOrderService → b.c...mais pas le tastas INCHANGÉ"OrderService" ← fuiteancien nom en clairpasse de chiffrementvaleur cachée au reposdéchiffrée à l'usage

Chiffrer la valeur — la correction de confidentialité

La fuite est avant tout une fuite d’information : les noms d’origine sont lisibles. Le chiffrement de chaînes y met fin. Nebula remplace chaque ldstr par un appel à un accesseur de déchiffrement, de sorte que le nom en clair n’est plus dans le tas et qu’un décompilateur voit un appel à une routine obfusquée, pas l’ancien nom de votre type :

{
  "inputs": ["bin/Release/net8.0/OrderService.dll"],
  "outputDirectory": "bin/Release/net8.0/obf",
  "renameIdentifiers": true,
  "encryptStrings": true
}

Après cette passe, le ldstr "OrderService" devient quelque chose comme ldstr <cipher> / call string <decrypt>(...), et le tas de chaînes n’annonce plus OrderService, CustomerName ni ValidateCoupon. Pour le cas courant — nameof utilisé pour une catégorie de journalisation ou un nom d’argument d’exception — c’est toute la correction : la valeur se déchiffre toujours en la bonne chaîne à l’exécution, et rien ne dépendait de la lisibilité du nom.

Réécrire la valeur — la correction de correction, quand elle s’applique

Le chiffrement cache le littéral ; il ne change pas ce en quoi il se déchiffre. Cela compte dans deux directions, qui tirent en sens opposés :

  • Le littéral pilote une réflexion contre un membre renommé. typeof(T).GetProperty(nameof(SomeProp)) attend le nom d’exécution. Si SomeProp a été renommé, le littéral déchiffré dit toujours SomeProp et la recherche échoue. Ici la bonne correction est de faire suivre le renommage au littéral, ou d’exclure ce membre du renommage pour que le nom reste stable.
  • Le littéral est un contrat externe. Un nom de propriété sérialisée, un [CallerMemberName] alimentant INotifyPropertyChanged, un chemin de liaison de données que le XAML nomme aussi — la chaîne de l’autre côté n’est pas renommée, donc réécrire votre littéral casserait la correspondance. Ici vous ne devez pas réécrire, et souvent vous ne devez pas non plus renommer le membre.

Le comportement par défaut sûr de Nebula est de chiffrer (cacher) plutôt que de réécrire (changer le sens), car l’IL seul ne permet pas toujours de distinguer un usage réflexif d’un usage contractuel. Vous marquez explicitement les cas réflexifs — excluez les membres dont les noms sont recherchés par chaîne, pour que le renommage les laisse stables et que le littéral nameof continue de correspondre :

{
  "inputs": ["bin/Release/net8.0/OrderService.dll"],
  "outputDirectory": "bin/Release/net8.0/obf",
  "renameIdentifiers": true,
  "encryptStrings": true,
  "excludeMembers": ["OrderService.CustomerName"]   // bound by name in XAML + INPC — keep it stable
}

Le cas INotifyPropertyChanged est le piège d’école. OnPropertyChanged() passe "CustomerName" via [CallerMemberName], et la liaison WPF/XAML résout la propriété par ce nom à l’exécution. Chiffrez le littéral et la liaison fonctionne toujours (il se déchiffre en "CustomerName"). Renommez la propriété en b sans l’exclure, et maintenant le littéral dit CustomerName alors que la propriété est b — la liaison cesse silencieusement de se mettre à jour. La correction consiste à exclure les propriétés liées du renommage, exactement comme dans la configuration ci-dessus, pour que le nom dont la liaison a besoin reste le nom que porte la propriété.

Vérifier que la fuite est colmatée

Après la protection, confirmez que les noms ont disparu du tas de chaînes — la vérification directe que nameof/[CallerMemberName] ne les divulguent plus :

# Dump the user-string heap of the protected assembly; your type names must not appear.
ikdasm /metadata=US obf/OrderService.dll | grep -i "OrderService\|CustomerName\|ValidateCoupon" || echo "clean"

Un résultat clean signifie que les littéraux ont été chiffrés ; une correspondance signifie qu’une chaîne a survécu à une passe — généralement une chaîne exclue du chiffrement, ce qui mérite un second regard si elle porte aussi un nom que vous vouliez cacher.

Ce qu’il faut retenir

Le renommage protège les références ; il ne protège pas les noms que le compilateur a déjà transformés en données. nameof et [CallerMemberName] figent vos noms de types et de membres d’origine dans des littéraux de chaîne à la compilation, si bien qu’un binaire renommé peut encore livrer sa propre table à un lecteur. Colmatez la fuite d’information avec le chiffrement de chaînes pour que les noms en clair quittent le tas. Puis traitez les deux cas que le chiffrement ne peut pas résoudre : là où un littéral pilote une réflexion contre un membre renommé, gardez le nom de ce membre stable en l’excluant du renommage ; là où un littéral est un contrat externe comme une liaison INotifyPropertyChanged, excluez le membre pour que le nom attendu de l’autre côté ne bouge pas. Chiffrez pour la confidentialité, excluez pour la correction, et vérifiez que le tas est propre — et votre build renommée cessera de publier les noms que vous avez renommés.

Essayez Nebula.NET

Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.