Skip to content
← Tous les articles
· Delta1 Labs ObfuscationSécurité.NET

Anti-débogage en .NET : comment ça marche et où ça s'arrête

Un débogueur, c'est ainsi qu'un attaquant observe vos chaînes déchiffrées et pose un point d'arrêt sur votre vérification de licence. Voici les moyens managés, natifs et basés sur le temps par lesquels une app .NET peut en détecter un — avec du vrai code — et une explication honnête de pourquoi l'anti-débogage est un ralentisseur, pas un mur.

Si l”obfuscation vise à empêcher quelqu”un de lire votre code, l”anti-débogage vise à l”empêcher de le regarder s”exécuter. Les deux attaques sont différentes. Un ingénieur inverse battu par une méthode aplatie et à chaînes chiffrées n”abandonne pas : il attache un débogueur, pose un point d”arrêt après le déchiffrement des chaînes et lit le texte clair directement en mémoire. Il pose un point d”arrêt sur le if à la fin de votre vérification de licence et inverse le booléen. Un débogueur transforme votre processus en cours d”exécution en boîte transparente. L”anti-débogage rend cette boîte plus difficile à ouvrir.

Ce que fait l’attaquant

L”attaque au débogueur sur une app protégée est en général l”un de deux mouvements : observer ou altérer. Observer, c”est laisser le programme déchiffrer une chaîne, calculer une clé ou construire un verdict de licence, puis lire cette valeur depuis un registre ou une locale au moment où elle existe — sans avoir à comprendre le code obfusqué qui l”a produite. Altérer, c”est poser un point d”arrêt sur une décision et changer le résultat : forcer IsValid à true, sauter le throw, passer au-dessus de la vérification. Les deux exigent un débogueur vivant attaché à votre processus, ce qui est précisément ce que vous pouvez détecter.

Détection managée

Les vérifications les moins chères sont dans la BCL et attrapent Visual Studio, la plupart des débogueurs managés et le flux « Déboguer → Attacher au processus » :

using System.Diagnostics;

static bool ManagedDebuggerPresent()
    => Debugger.IsAttached || Debugger.IsLogging();

Debugger.IsAttached est la vérification évidente. Debugger.IsLogging() renvoie true quand un débogueur écoute la sortie de Debugger.Log, attrapant certains cas où IsAttached a été falsifié. Ce ne sont qu”une propriété chacune — triviales à localiser et à inverser pour un attaquant — alors traitez-les comme le premier fil-piège, jamais comme toute la clôture.

Détection native

Les vérifications managées manquent les débogueurs natifs (WinDbg, x64dbg) et tout ce qui s”attache sous le CLR. Le système d”exploitation le sait, et l”expose :

using System.Runtime.InteropServices;

static class Native
{
    [DllImport("kernel32.dll")]
    static extern bool IsDebuggerPresent();

    [DllImport("kernel32.dll")]
    static extern bool CheckRemoteDebuggerPresent(IntPtr hProcess, ref bool present);

    public static bool DebuggerPresent()
    {
        if (IsDebuggerPresent()) return true;
        bool present = false;
        CheckRemoteDebuggerPresent(Process.GetCurrentProcess().Handle, ref present);
        return present;
    }
}

IsDebuggerPresent lit l”octet BeingDebugged dans le PEB du processus lui-même — rapide, mais facile à patcher. CheckRemoteDebuggerPresent interroge le noyau au sujet du processus courant, ce qui attrape les débogueurs qui ont effacé le flag du PEB mais sont toujours attachés. Exécuter les deux ferme des trous que chacune seule laisse ouverts.

Détection par le temps

Les deux approches ci-dessus testent un flag. Un débogueur qui avance pas à pas ou piloté par points d”arrêt laisse un autre type de preuve : le temps. Du code qui devrait prendre des microsecondes prend des secondes quand un humain l”exécute pas à pas. Chronométrez un bloc serré, sans effet de bord, et réagissez s”il s”est exécuté de façon invraisemblablement lente :

static bool SteppingDetected()
{
    var sw = Stopwatch.StartNew();
    // A short deterministic workload with no I/O and no awaits.
    long acc = 0;
    for (int i = 0; i < 1000; i++) acc += i * 31 + (acc & 7);
    sw.Stop();
    // Microseconds normally; a stepping debugger inflates this by orders of magnitude.
    return sw.Elapsed.TotalMilliseconds > 50 && acc != long.MinValue;
}

Le temps est bruyant — une machine chargée ou une pause GC peut le déclencher — donc utilisez un seuil généreux et ne rendez jamais fatale une seule mesure de temps. Sa force est qu”il détecte l”acte d”avancer pas à pas, ce que les vérifications de flags ne peuvent pas.

Managed (IsAttached)Native (kernel32)Timing (stepping)One verdictFail closed, quietly

Réagir sans vous trahir

La façon de réagir compte autant que la façon de détecter. L”instinct est de lancer "Débogueur détecté !" — ne le faites pas. Cela donne à l”attaquant une chaîne à rechercher et une trace de pile qui pointe droit sur votre vérification ; il pose un point d”arrêt sur le throw et remonte jusqu”à la détection, puis la patche. De meilleures réponses sont silencieuses et différées :

  • Fondez le résultat dans l”état normal — dégradez vers l”édition sans licence, renvoyez des données subtilement fausses, ou laissez une fonctionnalité ultérieure sans rapport échouer — plutôt que de réagir sur le lieu de la détection.
  • Retardez la réaction pour que le symptôme soit loin de la vérification, en code et en temps.
  • N”utilisez jamais un message qui nomme le débogage ; ne centralisez jamais toutes les vérifications dans une unique méthode AntiDebug qu”un attaquant peut neutraliser d”un coup.

La limite honnête

Chaque technique ici peut être battue isolément. IsDebuggerPresent peut être hookée pour toujours renvoyer false. Debugger.IsAttached est un callvirt IL qu”un attaquant inverse avec un éditeur hexadécimal. Les vérifications de temps peuvent être remplacées pour renvoyer une petite valeur fixe. À elle seule, l”anti-débogage arrête l”attaquant occasionnel avec un débogueur ouvert et pas grand-chose de plus.

Sa vraie valeur est cumulative. Quand la détection est elle-même aplatie dans son flux de contrôle et ses chaînes chiffrées, trouver la vérification est du travail. Quand plusieurs vérifications indépendantes sont dispersées et leurs réactions différées, en neutraliser une n”aide pas. Quand l”anti-altération vérifie l”intégrité de la méthode, patcher une vérification se détecte tout seul. L”anti-débogage n”est pas la serrure ; c”est l”une des plusieurs choses à crocheter en même temps, et c”est de là que vient réellement la protection : Nebula.NET applique cela en couches plutôt qu”en un seul interrupteur. Utilisée ainsi, la boîte transparente devient bien plus difficile à percer du regard.

Essayez Nebula.NET

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