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
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é.