Pourquoi l'anti-debug ?

Un debugger permet à un attaquant de stopper l'exécution d'un programme, inspecter sa mémoire, modifier ses variables et contourner les vérifications de licence en temps réel. Un programme qui s'exécute normalement sous debugger est un programme dont on peut extraire les secrets.

L'anti-debug ne vise pas à rendre le reverse engineering impossible — c'est un objectif inatteignable. L'objectif est de rendre l'analyse dynamique coûteuse en temps : chaque technique qu'un attaquant doit contourner lui prend des heures.

Un bon anti-debug force l'attaquant à travailler hors ligne, sur une copie statique du binaire, sans pouvoir observer l'exécution. C'est là que le chiffrement du payload entre en jeu.

Vue d'ensemble des 12 techniques

TECHNIQUE 01
IsDebuggerPresent
API Win32 qui consulte le flag DebuggerPresent dans le PEB. Simple, triviale à patcher, mais toujours utile comme première couche.
Windows
TECHNIQUE 02
CheckRemoteDebuggerPresent
Vérifie si un debugger est attaché depuis un autre processus. Détecte les debuggers kernel-mode que IsDebuggerPresent rate.
Windows
TECHNIQUE 03
NtQueryInformationProcess
API NT non documentée. DebugPort ≠ 0 indique un debugger. Plus difficile à patcher que IsDebuggerPresent.
Windows
TECHNIQUE 04
ptrace (Linux)
Un processus ne peut être tracé que par un seul autre. S'auto-tracer (ptrace PTRACE_TRACEME) empêche un debugger de s'attacher.
Linux
TECHNIQUE 05
TracerPid (/proc/self/status)
Lecture du pseudo-fichier : TracerPid non nul = processus tracé. Fonctionne sans droits particuliers, cross-distrib.
Linux
TECHNIQUE 06
Timing attack (RDTSC)
Un debugger ralentit l'exécution. Mesurer le temps d'une boucle CPU-intensive : si > seuil, debugger probable.
WindowsLinux
TECHNIQUE 07
Parent PID check
Le parent d'un processus lancé normalement est explorer.exe ou bash. Si le parent est un IDE ou debugger connu, alert.
WindowsLinux
TECHNIQUE 08
Exception handler trick
Générer une exception : si un debugger est présent, il l'intercepte avant le handler. Comportement différent détectable.
Windows
TECHNIQUE 09
Heap flags (PEB)
Le PEB.NtGlobalFlag vaut 0x70 sous debugger (ForceFlags activés). Lecture via ctypes sans appel API visible.
Windows
TECHNIQUE 10
CloseHandle invalid
Appeler CloseHandle avec un handle invalide lève une exception uniquement sous debugger. Heuristique simple et efficace.
Windows
TECHNIQUE 11
Processus suspects (liste noire)
Scanner les processus actifs : x64dbg, ollydbg, windbg, ida, ghidra, frida-server, gdb, lldb. Présence = alerte.
WindowsLinux
TECHNIQUE 12
Code checksum
Calculer le hash SHA-256 de ses propres sections de code. Un breakpoint posé par un debugger modifie un octet (0xCC) → hash différent.
WindowsLinux

Implémentation Windows

import ctypes, ctypes.wintypes, time, os, sys

kernel32 = ctypes.windll.kernel32
ntdll    = ctypes.windll.ntdll

def check_isdebuggerpresent() -> bool:
    return bool(kernel32.IsDebuggerPresent())

def check_remote_debugger() -> bool:
    is_debugged = ctypes.wintypes.BOOL(False)
    kernel32.CheckRemoteDebuggerPresent(
        kernel32.GetCurrentProcess(),
        ctypes.byref(is_debugged)
    )
    return bool(is_debugged)

def check_nt_query() -> bool:
    # ProcessDebugPort = 7
    debug_port = ctypes.c_ulong(0)
    status = ntdll.NtQueryInformationProcess(
        kernel32.GetCurrentProcess(),
        7,  # ProcessDebugPort
        ctypes.byref(debug_port),
        ctypes.sizeof(debug_port),
        None
    )
    return debug_port.value != 0

def check_timing(threshold_ms: float = 500) -> bool:
    t0 = time.perf_counter()
    _ = sum(x*x for x in range(200_000))
    elapsed = (time.perf_counter() - t0) * 1000
    return elapsed > threshold_ms

DEBUGGER_PROCESSES = {
    'x32dbg', 'x64dbg', 'ollydbg', 'windbg',
    'ida', 'ida64', 'ghidra', 'frida-server',
    'processhacker', 'pestudio', 'dnspy'
}

def check_suspicious_processes() -> bool:
    import subprocess
    r = subprocess.run(['tasklist', '/fo', 'csv', '/nh'],
                       capture_output=True, text=True, timeout=5)
    running = {l.split(',')[0].strip('"').lower() for l in r.stdout.splitlines()}
    return bool(running & DEBUGGER_PROCESSES)

Implémentation Linux

import ctypes, ctypes.util, time, os

def check_tracerpid() -> bool:
    try:
        status = open('/proc/self/status').read()
        for line in status.splitlines():
            if line.startswith('TracerPid'):
                return int(line.split()[1]) != 0
    except: pass
    return False

def check_ptrace_self() -> bool:
    # PTRACE_TRACEME = 0 — si échec, déjà tracé
    libc = ctypes.CDLL(ctypes.util.find_library('c'))
    result = libc.ptrace(0, 0, 0, 0)
    if result == -1:
        return True  # ptrace refusé = déjà tracé
    # Détacher pour ne pas bloquer un debugger légitime en dev
    libc.ptrace(17, 0, 0, 0)  # PTRACE_DETACH
    return False

Agrégation et décision

IronLock agrège toutes les vérifications avec un système de score. Une seule détection positive peut être un faux positif (machine lente pour le timing, antivirus qui inspecte les processus). Le seuil de déclenchement est configurable :

CHECKS = [
    (check_isdebuggerpresent,  3),  # poids fort
    (check_remote_debugger,    3),
    (check_nt_query,           3),
    (check_tracerpid,          3),
    (check_timing,             1),  # poids faible (faux positifs possibles)
    (check_suspicious_processes,2),
]
THRESHOLD = 3  # déclenche si score ≥ 3

def is_debugger_present() -> bool:
    score = 0
    for fn, weight in CHECKS:
        try:
            if fn(): score += weight
        except: pass
    return score >= THRESHOLD

if is_debugger_present():
    sys.exit(0)  # Sortie silencieuse — pas de message d'erreur

Stratégies de sortie : crash vs silencieux

Quand un debugger est détecté, trois comportements possibles :

  • Sortie silencieuse (sys.exit(0)) — le programme se ferme normalement. L'attaquant ne sait pas pourquoi. Recommandé.
  • Crash volontaire — lever une exception non catchée, corrompre délibérément la mémoire. Plus bruyant, peut déclencher des crash reporters.
  • Mode leurre — continuer à s'exécuter mais avec des données falsifiées. L'attaquant croit avoir contourné la protection mais extrait de fausses clés. Technique avancée.

Implémentation dans IronLock v2

IronLock v2 intègre 12 checks anti-debug activés avant tout déchiffrement du payload. La configuration se fait dans le manifest de packaging :

# Activer les checks anti-debug lors du packaging
ironlock encrypt app.py \
  --antidebug-level high \     # low / medium / high
  --antidebug-score 3 \        # seuil de déclenchement
  --antidebug-action silent    # silent / crash / decoy

Conclusion

L'anti-debug efficace repose sur la combinaison et le score — aucune technique seule n'est fiable. Les techniques Windows API (IsDebuggerPresent, NtQueryInformationProcess) doivent être complétées par une timing attack et un scan des processus. Sur Linux, TracerPid + ptrace self-trace constituent une base solide. IronLock v2 automatise cette combinaison avec un score configurable selon le niveau de risque accepté.

🔐
PRODUIT LIÉ
IronLock v2.0 — Anti-Debug × 12 checks
← Article précédent Article suivant →