Skip to content
← Tous les articles
· Delta1 Labs KeyrightLicencesSécurité

Épingler votre clé publique de vérification : l'attaquant qui remplace simplement votre clé par la sienne

Votre application vérifie une licence parce qu'une signature concorde avec une clé publique embarquée dans l'assembly. Mais cette clé, ce sont des données : une constante base64 que tout attaquant capable d'éditer votre DLL peut trouver et écraser par une clé qu'il contrôle, si bien que ses licences auto-signées se vérifient désormais et que la vôtre est oubliée. C'est la substitution d'ancre de confiance, et c'est l'attaque au plus fort effet de levier contre les licences offline-first. Cet article montre l'attaque au niveau de l'IL — le ldstr qui charge la clé, l'unique plage d'octets qu'un patch touche — puis en relève le coût : prendre l'empreinte de la clé contre une valeur entrelacée dans le flot de contrôle au lieu de la laisser seule, la vérifier à plusieurs endroits, porter plusieurs clés avec AdditionalPublicKeysBase64 pour qu'un seul remplacement ne suffise pas, et associer le contrôle côté client au strong-naming, à l'anti-tamper et au report-home afin qu'un build altéré soit à la fois plus difficile à produire et visible lorsqu'il circule.

Une licence Keyright est une affirmation — « ce client possède Pro, 5 sièges, jusqu’à mars prochain » — et votre application y croit pour exactement une raison : une signature dessus se vérifie contre une clé publique que l’application porte. Ce design est tout l’intérêt des licences offline-first. Il n’y a pas de serveur dans le chemin de validation à joindre, rien qui puisse être en panne, rien à limiter en débit ; l’application détient la moitié publique de votre clé de signature, contrôle la signature localement et fait confiance à ce qu’elle lit.

Ce qui soulève une question gênante. La clé publique n’est pas secrète : elle est distribuée, en clair, dans un assembly que le client a sur disque. Qu’est-ce qui empêche quelqu’un de la remplacer ?

Rien, par défaut. Et c’est l’attaque au plus fort effet de levier contre la vérification côté client, car elle n’exige de casser aucune cryptographie. Cet article porte sur pourquoi l’attaque fonctionne, à quoi elle ressemble au niveau de l’IL, et comment la rendre assez coûteuse pour qu’elle cesse d’en valoir la peine.

L’attaque : substituer l’ancre de confiance

La vérification de signatures est asymétrique. Votre côté émetteur détient une clé privée et signe la charge canonique de la licence avec elle ; votre application détient la clé publique correspondante et vérifie. Forger une signature que votre clé publique accepte signifie récupérer la clé privée à partir de la publique, ce qui pour Ed25519 ou RSA-2048 n’est pas le travail d’un après-midi — ce n’est le travail de personne.

Un attaquant compétent n’attaque donc pas la signature. Il attaque l’ancre. La clé publique vit dans votre assembly sous forme de constante — dans une intégration Keyright, typiquement une chaîne base64 que vous avez collée depuis le portail :

public static class LicensingConfig
{
    // The PUBLIC half of your product signing key. Safe to ship — but not safe to trust blindly.
    public const string PublicKeyBase64 =
        "MCowBQYDK2VwAyEA9mF3rQ2pxK8vN1cWbYh6Zt0uQ4sJ7eD3rL5nX8kH2A=";
}

L’attaquant génère sa propre paire de clés. Il écrase PublicKeyBase64 par le base64 de sa clé publique. Puis il signe une licence disant ce qu’il veut — Pro, sièges illimités, sans expiration — avec sa clé privée. Votre code de vérification reste inchangé octet pour octet. Il s’exécute, contrôle la signature contre la clé qu’il trouve embarquée, et passe, car la clé qu’il trouve embarquée est désormais la sienne.

PrévuServicesign(claims, PRIV)clé de licenceclaims signésApp : verify(sig, PUB embarquée)clé = la vôtre → claim de confiancetier · sièges · expiration tels que signésAttaque par substitutionAttaquantgenKeyPair()sign(forged, PRIV')licence forgéePro · ∞ siègessans expirationApp : verify(sig, PUB embarquée)PUB écrasée → PUB' (attaquant)la signature concorde avec PUB' → ACCEPTÉEcode de vérification inchangé

Rien n’a été déchiffré, rien n’a été rétro-ingénié. L’attaquant a déplacé la racine de confiance et signé contre la nouvelle racine. Tout ce que votre application fait ensuite — activer des fonctions, respecter les plafonds de sièges, faire appliquer l’expiration — est désormais piloté par une licence qu’il a écrite.

Ce que le patch touche réellement

Si c’est bon marché, c’est que la clé réside dans l’assembly comme un objet autonome et reconnaissable. Compilez le const ci-dessus et la référence à celle-ci est un unique ldstr qui charge la chaîne depuis le heap #US (chaînes utilisateur) :

// IL for: var pub = LicensingConfig.PublicKeyBase64;
ldstr        "MCowBQYDK2VwAyEA9mF3rQ2pxK8vN1cWbYh6Zt0uQ4sJ7eD3rL5nX8kH2A="
stloc.0

Trouvez cette chaîne dans le heap — un blob base64 qui décode en un SubjectPublicKeyInfo DER est follement distinctif ; MCowBQYDK2Vw est le préfixe fixe de toute clé publique Ed25519 dans cet encodage — et vous savez exactement quels octets écraser. L’attaquant n’a même pas besoin de votre outillage : un éditeur hexadécimal et une chaîne de remplacement de même longueur suffisent, et si la sienne est plus courte il complète l’entrée du heap. Une plage d’octets contiguë, aucune logique à comprendre, et l’édition survit intacte dans toute version future de votre produit car la clé change rarement.

C’est cette propriété qu’il faut détruire. Non pas « la clé peut-elle être lue » — elle est publique, la lire est bénin — mais « la clé peut-elle être remplacée sans que le reste du code s’en aperçoive ».

Durcissement, étape 1 : que le code contrôle la clé, pas seulement l’utilise

Le premier geste est de cesser de traiter comme faisant autorité les octets embarqués, quels qu’ils soient. Votre application sait quelle clé elle devrait avoir. Faites-lui donc confirmer que la clé qu’elle s’apprête à faire confiance correspond à une empreinte attendue avant de vérifier quoi que ce soit avec :

static class KeyGuard
{
    // SHA-256 of the DER-encoded public key you shipped with. Not a secret; an integrity anchor.
    static readonly byte[] ExpectedFingerprint = Convert.FromHexString(
        "7b2a1f9c4e0d8a63b5f1c24e6a8d0b3f9e5c7a12d4b60e8f1a3c5d7b9e0f2a4c");

    public static ReadOnlySpan<byte> Trusted(string embeddedBase64)
    {
        byte[] der = Convert.FromBase64String(embeddedBase64);
        byte[] actual = SHA256.HashData(der);
        if (!CryptographicOperations.FixedTimeEquals(actual, ExpectedFingerprint))
            throw new SecurityException("Licensing trust anchor failed integrity check.");
        return der;
    }
}

À lui seul, cela ne fait que déplacer le problème : l’attaquant écrase désormais PublicKeyBase64 et ExpectedFingerprint, deux éditions au lieu d’une. Mais cela change la forme de l’attaque de manière utile. Une empreinte est un hash de longueur fixe, pas une structure de clé reconnaissable, donc son rôle est moins évident ; et comme le contrôle est du code, vous pouvez le placer là où l’attaquant doit le trouver, l’exécuter depuis plusieurs sites d’appel et — la vraie victoire — plier la valeur attendue dans de la logique qui fait autre chose, de sorte qu’elle ne puisse être annulée sans les casser.

Un contrôle d’intégrité naïf est isolé et facile à découper. Un contrôle durable est entrelacé. Supposez que les octets de l’empreinte servent aussi à dériver un décalage ou un masque dont dépend quelque chose de sans rapport — un indice de recherche, une longueur validée, un sélecteur de branche. Désormais un attaquant qui change l’empreinte pour la faire concorder avec sa clé corrompt silencieusement cet autre calcul, et un attaquant qui la laisse telle quelle échoue au contrôle de la clé. Aucune édition ne satisfait les deux. C’est exactement le genre de transformation qu’un obfuscateur automatise ; le chiffrement de constantes et les passes de flot de contrôle de Nebula existent pour transformer une constante comparable isolée en une valeur reconstruite à l’exécution et consommée à plusieurs endroits, de sorte qu’il n’y ait pas un unique ldc/ldstr à trouver ni une unique branche à découper.

Durcissement, étape 2 : portez plus d’une ancre

Une seule clé est un unique point de substitution. Les SDK Keyright acceptent une signature qui se valide contre votre clé embarquée principale ou contre toute clé listée dans AdditionalPublicKeysBase64 :

var options = new LicenseVerifierOptions
{
    PublicKeyBase64 = LicensingConfig.PublicKeyBase64,
    AdditionalPublicKeysBase64 =
    {
        // a second, independently generated anchor — rotation today, defense in depth always
        "MCowBQYDK2VwAyEAq7F2dN8kP0wZ5bYh3Rt9uL1cXe4sJ6D2rN5mV8kH0B=",
    },
};
var verifier = new LicenseVerifier(options);

Cela existe pour la rotation de clés — distribuez la clé suivante comme ancre supplémentaire, basculez-y les leases, retirez l’ancienne — mais sert aussi de défense contre la substitution. Il n’y a plus un unique blob à remplacer. Un attaquant doit localiser et écraser toutes les clés de confiance, et s’il en manque une, vos licences légitimement signées se vérifient encore contre la survivante tandis que sa licence forgée ne se valide contre aucune des clés qu’il n’a pas plantées. Associez cela au contrôle d’empreinte — protégez chaque ancre contre son propre hash attendu — et une unique édition chirurgicale devient plusieurs éditions dont chacune doit être exactement correcte.

Durcissement, étape 3 : que l’édition ne soit pas silencieuse

Tout ce qui précède relève le nombre et la fragilité des éditions. La dernière couche rend une édition réussie visible et le build plus difficile à produire tout court.

Strong-namez l’assembly. Un strong-name est un hash du contenu de l’assembly signé par votre clé d’identité. Changez un octet de la clé publique embarquée et le hash du strong-name ne concorde plus — le chargeur voit une identité d’assembly différente, et toute référence dépendante du strong-name casse. Un attaquant peut retirer le strong-name et re-signer avec sa propre clé, mais c’est plus de travail, cela change votre identité d’éditeur, et cela donne à tout contrôle d’intégrité en aval quelque chose à remarquer.

Vérifiez vos propres corps de méthode. Le strong-naming est imposé par l’hôte, que l’attaquant contrôle, donc la version durable est de contrôler l’intégrité depuis l’intérieur de votre propre code. C’est ce que fait l’anti-tamper : Nebula peut hasher les corps de méthode protégés au moment du build et les vérifier à l’exécution, de sorte qu’un assembly dont l’IL — y compris le chemin de vérification et le guard de la clé — a été édité refuse d’exécuter son code protégé. L’attaquant ne peut désormais plus patcher le contrôle de la clé ni les méthodes autour sans déclencher un contrôle qui fait lui-même partie de l’ensemble protégé.

Faites report-home quand le contrôle échoue. Un contrôle d’altération qui se contente de lever une exception dit à l’attaquant qu’il a été pris et ne vous dit rien. Le tamper report-home de Nebula envoie un signal à un endpoint de votre choix quand un contrôle d’intégrité échoue sur le terrain, de sorte qu’un build dont quelqu’un a remplacé la clé — s’il s’exécute — remonte comme un événement que vous pouvez voir, corréler et traiter, au lieu d’un crack silencieux circulant pendant des mois.

Relever le coût de la substitutionClé base64 nueune plage d'octets, survit à chaque version+ contrôle d'empreinte, entrelacé dans la logiquel'édition doit concorder avec un hash utilisé ailleurs, ou le casse+ plusieurs ancres (AdditionalPublicKeysBase64)chaque clé doit être trouvée et écrasée correctement+ strong-name · anti-tamper · report-homebuild difficile à produire ; l'altération est visible sur le terrain

Le plafond honnête

Rien de tout cela ne rend la substitution impossible. Le code qui décide si une licence est valide s’exécute sur une machine que l’attaquant possède, et tout ce qui s’exécute sur sa machine peut, avec assez d’efforts, être amené à faire autre chose. L’objectif n’a jamais été une preuve d’impossibilité — c’était l’économie. Une clé base64 nue est une édition de trente secondes qu’une personne fait une fois et partage pour toujours. Une clé à empreinte, entrelacée et multi-ancres dans un build strong-named, anti-tampered et report-home est un projet de recherche à refaire chaque fois que vous distribuez, et qui vous signale quand il a été tenté. Même attaquant ; incitation complètement différente.

C’est la bonne façon de lire tout contrôle côté client, celui-ci compris. Vous ne construisez pas un mur. Vous déplacez le coût de le franchir au-dessus de la valeur de ce qui se trouve de l’autre côté — et, avec report-home, vous vous assurez que si quelqu’un paie ce coût, vous êtes le premier à le savoir plutôt que le dernier.

Essayez Nebula.NET

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