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.

Découvrir IronLock → Manuel complet

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.
← Retour au blog Article suivant →