Introduction
Python est un langage interprété. Par défaut, distribuer un script Python revient à distribuer son code source — ou au mieux son bytecode .pyc, trivial à décompiler avec uncompyle6 ou pycdc.
Si votre logiciel contient de la propriété intellectuelle, un algorithme commercial ou un système de licensing, vous devez protéger ce code. Ce guide détaille les techniques efficaces, de l'obfuscation basique au chiffrement authentifié.
Aucune protection n'est infaillible contre un attaquant motivé avec les ressources suffisantes. L'objectif est de rendre le coût du reverse engineering supérieur à la valeur du code.
Modèle de menace
Avant de choisir une technique, définissez votre adversaire :
- Script kiddie — utilise des outils automatiques. Obfuscation basique suffit.
- Développeur curieux — capable de décompiler et lire du bytecode. Chiffrement requis.
- Concurrent professionnel — reverse engineering systématique. Anti-debug + liaison machine nécessaires.
- Chercheur en sécurité — analyse dynamique avancée. Seul IronLock-niveau peut ralentir.
1. Obfuscation du bytecode
L'obfuscation au niveau AST (Abstract Syntax Tree) renomme les variables, inline les constantes et réordonne le code avant compilation :
import ast, astunparse
# Transformateur AST qui renomme toutes les variables
class ObfuscateNames(ast.NodeTransformer):
def __init__(self):
self.mapping = {}
self.counter = 0
def _obf(self, name):
if name not in self.mapping:
self.mapping[name] = f"_x{self.counter:04x}"
self.counter += 1
return self.mapping[name]
def visit_Name(self, node):
if node.id not in ('True', 'False', 'None'):
node.id = self._obf(node.id)
return node
source = open('app.py').read()
tree = ast.parse(source)
tree = ObfuscateNames().visit(tree)
obf = astunparse.unparse(tree)
compile(ast.parse(obf), 'app', 'exec')
Limites de l'obfuscation seule : un décompilateur Python restaure le bytecode en code lisible. La protection s'arrête là.
2. Chiffrement AES-256-GCM
Le chiffrement authentifié protège le bytecode compilé. Le code source n'est jamais sur disque en clair — seulement le chargeur.
2.1 Le chargeur chiffré
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
import os, sys, types
def derive_key(machine_id: bytes) -> bytes:
kdf = Scrypt(salt=b'TRUvector_salt_v2',
length=32, n=2**14, r=8, p=1)
return kdf.derive(machine_id)
def load_protected(encrypted_path: str):
machine_id = get_machine_fingerprint() # 8 sources
key = derive_key(machine_id)
data = open(encrypted_path, 'rb').read()
nonce, ciphertext = data[:12], data[12:]
try:
plaintext = AESGCM(key).decrypt(nonce, ciphertext, None)
except Exception:
sys.exit("Licence invalide ou machine non autorisée")
# Exécuter le bytecode déchiffré en mémoire
code = compile(plaintext, '<protected>', 'exec')
module = types.ModuleType('protected_app')
exec(code, module.__dict__)
return module
3. Anti-debug
Avant de déchiffrer le payload, vérifiez que le programme n'est pas en cours d'analyse :
import ctypes, time, os, platform
def check_debugger() -> bool:
# Windows : IsDebuggerPresent
if platform.system() == 'Windows':
if ctypes.windll.kernel32.IsDebuggerPresent():
return True
# Linux : /proc/self/status TracerPid
try:
status = open('/proc/self/status').read()
tracer = [l for l in status.splitlines()
if l.startswith('TracerPid')]
if tracer and int(tracer[0].split()[1]) != 0:
return True
except: pass
# Timing attack : un debugger ralentit l'exécution
t0 = time.perf_counter()
_ = [x*x for x in range(100000)]
elapsed = time.perf_counter() - t0
if elapsed > 0.5: # > 500ms suspect
return True
return False
if check_debugger():
sys.exit(0) # Sortie silencieuse, pas d'erreur
4. Solution clé en main : IronLock
Toutes ces techniques sont implémentées et testées dans IronLock v2.0. En une commande :
# Chiffrer et lier app.py à la machine courante
ironlock encrypt app.py --output dist/
# Générer une licence pour une machine cliente
ironlock license --fingerprint CLIENT_HASH --expires 2027-01-01
# Vérifier sur la machine cliente
ironlock verify licence.ilk
IronLock gère le fingerprint hardware (8 sources), AES-256-GCM, ECDSA P-384, Argon2id, anti-debug et VM detector — sans que vous ayez à implémenter ou tester chacun séparément.
Conclusion
La protection d'un programme Python est un compromis entre sécurité, complexité d'implémentation et impact sur les performances. Résumé des approches :
- Obfuscation seule — niveau de protection : faible. Effort : faible. Suffisant pour décourager les curieux.
- Chiffrement AES + loader — niveau : moyen. Résiste aux outils de décompilation automatiques.
- Chiffrement + Anti-debug + Liaison machine — niveau : élevé. Coût de reverse engineering significatif.
- IronLock v2.0 — niveau : élevé avec fingerprint hardware. Recommandé pour usage commercial.