Skip to content
← Tous les articles
· Delta1 Labs Obfuscation.NETSécurité

L'obfuscation au-delà du renommage : pourquoi un .NET renommé se lit encore comme du code source

Le renommage d'identifiants est la couche d'obfuscation la plus faible : un décompilateur sur du code seulement renommé montre encore votre logique, vos chaînes et vos constantes telles quelles. Voici ce que le renommage cache et ne cache pas, et les couches — chiffrement des chaînes, masquage des constantes, aplatissement du flux de contrôle et virtualisation — qui rendent vraiment le .NET difficile à lire.

Ouvrez presque n”importe quelle app .NET dans un décompilateur et la première chose qui frappe est sa lisibilité. Les décompilateurs reconstruisent un C# proche de l”original, car le compilateur préserve dans l”IL bien plus de structure que la plupart des développeurs ne l”imaginent. La réponse habituelle est « obfusquez-le », et l”obfuscation habituelle est le renommage : transformer ValidateLicense en a et expiryDate en b. Cela aide un peu. Cela trompe aussi beaucoup d”équipes qui se croient protégées, parce qu”elles ne regardent jamais le résultat à travers un décompilateur. Quand vous le faites, l”illusion tombe.

Ce que le renommage laisse derrière lui

Prenons une porte de licence triviale :

public bool IsActivated(string key)
{
    if (string.IsNullOrEmpty(key)) return false;
    var parts = key.Split('-');
    if (parts.Length != 4 || parts[0] != "LIC") return false;
    long expiry = long.Parse(parts[3]);
    return expiry > DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}

Après une obfuscation par renommage seul, un décompilateur donne à peu près ceci :

public bool a(string A_0)
{
    if (string.IsNullOrEmpty(A_0)) return false;
    string[] b = A_0.Split('-');
    if (b.Length != 4 || b[0] != "LIC") return false;
    long c = long.Parse(b[3]);
    return c > DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}

Les noms ont disparu. Rien d”autre. Le flux de contrôle est identique, l”appel à DateTimeOffset.UtcNow.ToUnixTimeSeconds() est en clair (les noms du framework ne peuvent pas être renommés), la structure d”une clé de licence est détaillée, et la chaîne magique "LIC" est là, indiquant à l”attaquant exactement quoi falsifier. Un lecteur n”a pas besoin de vos noms de variables pour comprendre cette méthode ; il lui faut trente secondes. Le renommage a fait passer le coût de lecture de « trivial » à « un peu moins trivial ».

C”est le plafond du renommage. Il cache des étiquettes. Votre logique, vos littéraux et vos constantes ne sont pas des étiquettes.

Couche 1 — chiffrement des chaînes

Les littéraux de chaîne sont ce qu”un lecteur récolte de plus précieux, car ils s”autodocumentent : messages d”erreur, chemins de registre, endpoints d”API, le préfixe "LIC" ci-dessus. Le chiffrement des chaînes les retire des métadonnées et remplace chaque littéral par un appel qui le déchiffre à l”exécution :

if (b.Length != 4 || b[0] != Strings.Get(0x4a1)) return false;

Désormais un décompilateur affiche Strings.Get(0x4a1) au lieu de "LIC". La valeur existe toujours en mémoire quand le programme s”exécute, donc ce n”est pas de la cryptographie contre un débogueur en direct — mais cela défait l”attaque extrêmement courante qui consiste à lire le binaire et à grep ses chaînes. La liste des littéraux qui cartographiait tout votre programme a disparu.

Couche 2 — masquage des constantes

Le cadeau gratuit suivant au lecteur, ce sont les constantes numériques : limites de sièges, délais, seuils de fonctionnalités, le 4 et le 3 qui décrivent votre format de clé. Le masquage des constantes remplace les nombres littéraux par de petites expressions évaluées à l”exécution, de sorte que parts.Length != 4 devienne quelque chose que le décompilateur rend comme de l”arithmétique sur des valeurs opaques plutôt qu”un 4 imprimé. Mineur individuellement ; collectivement, cela supprime la deuxième façon la plus facile de comprendre ce que vérifie une méthode.

Couche 3 — aplatissement du flux de contrôle

Le renommage, le chiffrement des chaînes et le masquage des constantes laissent intacte la forme de la méthode : le décompilateur imprime toujours des blocs if/else/while propres. L”aplatissement du flux de contrôle casse cela. Il réécrit la méthode comme une seule boucle autour d”un switch de machine à états, où chaque bloc de base d”origine devient un cas et une variable d”aiguillage décide de ce qui s”exécute ensuite :

int state = 0;
while (true)
{
    switch (state)
    {
        case 0: /* original block A */ state = 7; break;
        case 7: /* original block B */ state = 2; break;
        // ... blocks reordered, linked only by the dispatcher
        case 2: return result;
    }
}

Le décompilateur ne peut plus reconstruire la structure if/while d”origine, car cette structure a été dissoute en données. Un humain peut encore la tracer, mais « tracer une machine à états aplatie avec des chaînes chiffrées et des constantes masquées » est un autre après-midi que « lire la méthode renommée ».

Renaming+ String enc.+ Const. mask+ CF flatten+ Virtualize

Couche 4 — virtualisation, pour le code qui compte

Tout ce qui précède se compile encore en IL, et l”IL a d”excellents décompilateurs. Pour la poignée de méthodes qui valent vraiment la peine d”être protégées — la vérification de licence, un algorithme de tarification, un secret industriel central — la virtualisation de méthodes va plus loin : elle remplace l”IL de la méthode par un bytecode propre et embarque un petit interpréteur qui l”exécute. Il n”existe pas de décompilateur pour un bytecode qui n”existait pas avant que votre build ne le produise. Un attaquant doit d”abord rétro-concevoir la machine virtuelle, puis le programme qui tourne dessus. C”est coûteux, ce qui est précisément le but.

La virtualisation est la transformation la plus lourde, vous l”appliquez donc de façon ciblée — aux rares méthodes où le coût de lecture devrait se mesurer en jours, pas en minutes — et laissez le reste de l”assembly sur les couches plus légères.

À retenir en pratique

Le renommage est une bonne première couche et une mauvaise dernière couche. Traitez la protection comme une pile : renommez largement, chiffrez les chaînes et masquez les constantes sur tout ce qui est sensible, aplatissez le flux de contrôle de votre code d”application de règles et de PI, et virtualisez les rares méthodes dont vous devez le plus préserver la logique. Le test pour savoir si cela a marché est simple et le seul qui compte : ouvrez votre build protégé dans un décompilateur et lisez-le. Si votre vérification de licence raconte encore sa propre histoire, vous vous êtes arrêté au renommage.

Essayez Nebula.NET

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