Skip to content

Leçon 7.2 · Comportements malveillants· 45 min

Mécanismes de persistance

Reconnaître la persistance dans un échantillon, la détecter dans Sysmon et les journaux Windows, et supprimer chaque point d’ancrage dans le bon ordre.

Objectifs

  • Expliquer pourquoi la persistance est le principal levier de remédiation et comment elle s’inscrit dans la tactique Persistence d’ATT&CK
  • Associer chaque famille de persistance à ses indices statiques dans un échantillon, à la télémétrie qui l’enregistre et à son identifiant ATT&CK
  • Confirmer la persistance en laboratoire par comparaison d’instantanés et d’Autoruns, et exprimer les détections sous forme de logique de type Sigma
  • Mener la remédiation dans le bon ordre : contenir, collecter, supprimer chaque point de persistance, vérifier et surveiller la réinfection

Le registre, les services et les tâches planifiées vous a montré où Windows conserve les instructions qu’il suit au démarrage et à l’ouverture de session, et comment les lire en direct ou depuis une image disque. Cette leçon aborde le même terrain depuis l’autre côté d’un incident : comment repérer la persistance en analysant un échantillon, comment écrire des détections qui se déclenchent lorsqu’elle est installée, et comment la supprimer pour que l’intrus ne revienne pas simplement au redémarrage suivant.

La persistance correspond à la tactique ATT&CK TA0003 : toutes les techniques qu’un attaquant emploie pour conserver son accès malgré les redémarrages, les déconnexions et les changements d’identifiants. La plupart des malwares (logiciels malveillants) l’installent une seule fois, quelques secondes après leur première exécution, ce qui en fait l’une des actions les plus sûrement observables d’un échantillon.

Pourquoi la persistance est votre levier de remédiation

Une grande partie de ce que fait un échantillon disparaît quand le processus se termine : le beacon s’arrête, la mémoire est libérée, les fichiers temporaires sont supprimés. La persistance, elle, reste sur le disque et doit être supprimée. C’est aussi le moment où l’attaquant doit écrire quelque chose à un emplacement que Windows lit de lui-même, et ces emplacements sont limités, documentés et généralement journalisés.

La persistance joue donc trois rôles dans votre travail :

  • En analyse, c’est une question à laquelle tout rapport doit répondre : comment ceci survit-il à un redémarrage ? Un échantillon sans persistance est soit un one-shot (un stealer qui exfiltre puis se termine), soit une étape lancée par autre chose (un loader, une macro, un service sous lequel il a été installé), soit quelque chose qui ne vit qu’en mémoire jusqu’au redémarrage de l’hôte. Chaque réponse change la réponse à incident.
  • En détection, la persistance écrit dans un petit ensemble de clés, de dossiers et de magasins ; une poignée de bonnes règles couvre donc une large part des malwares courants.
  • En réponse à incident, la liste des points de persistance est le plan de nettoyage. Oubliez-en un et l’implant revient. Supprimez-les dans le mauvais ordre et un watchdog réinstalle ce que vous venez d’effacer.

Une entrée de persistance comporte deux parties : le déclencheur (la valeur Run, la tâche, le service, la liaison WMI) et la charge utile vers laquelle il pointe (un EXE, une DLL, un script ou une ligne de commande encodée). Votre rapport, votre détection et votre nettoyage doivent couvrir les deux.

Une carte par famille

Le catalogue des techniques consacre une page à chaque mécanisme, avec son fonctionnement interne et ses variantes. Le tableau ci-dessous est le résumé de l’analyste : ce qu’il faut chercher dans un échantillon avant de l’exécuter, ce qui enregistre la modification sur un hôte, et quel identifiant ATT&CK citer. Les emplacements de démarrage automatique et les champs à lire ont été traités dans le module précédent ; l’accent est mis ici sur les traces que laisse chaque famille.

FamilleIndice statique dans un échantillonTélémétrie qui l’enregistreATT&CK
Clés Run / RunOnceImports RegCreateKeyExW / RegSetValueExW ; la chaîne Software\Microsoft\Windows\CurrentVersion\Run ; reg add ... /v dans une commandeSysmon 12/13 ; heure de dernière écriture de la cléT1547.001
Dossier de démarrageSHGetKnownFolderPath / SHGetFolderPathW (identifiants du dossier Startup), IShellLinkW pour créer un .lnk, chaînes Start Menu\Programs\StartupSysmon 11 dans les chemins StartupT1547.001
Services WindowsOpenSCManagerW, CreateServiceW, ChangeServiceConfigW ; chaînes sc create, New-Service ; un export ServiceMain dans une DLLSystem 7045, Security 4697, Sysmon 12/13 sous ServicesT1543.003
Tâches planifiéesLignes de commande schtasks /create ; taskschd.dll ou CoCreateInstance avec la classe Schedule.Service ; XML de tâche embarqué (<Triggers>, <Exec>)Security 4698, TaskScheduler Operational 106, Sysmon 1 pour schtasks.exe, Sysmon 11 dans System32\TasksT1053.005
Abonnements aux événements WMIChaînes __EventFilter, CommandLineEventConsumer, ActiveScriptEventConsumer, __FilterToConsumerBinding, root\subscription ; mofcomp ou Set-WmiInstance / New-CimInstance dans des scriptsSysmon 19/20/21, WMI-Activity Operational 5861T1546.003
Valeurs WinlogonChaînes Winlogon, Userinit, Shell à côté d’imports d’écriture dans le registreSysmon 13 sur la clé WinlogonT1547.004
Détournement de l’ordre de recherche des DLLUne DLL nommée comme une bibliothèque système (version.dll, dbghelp.dll) qui transfère ses exports à la vraie ; un dropper qui écrit côte à côte un EXE légitime signé et une DLLSysmon 11 (DLL écrite à côté d’un EXE), Sysmon 7 (image non signée chargée depuis un chemin non système)T1574.001
Détournement COMChaînes de CLSID et Software\Classes\CLSID\{...}\InprocServer32 sous HKCUSysmon 12/13 sous HKU\<SID>_Classes\CLSID ou HKCU\Software\Classes\CLSIDT1546.015
Linux : cron, systemd, profils de shellcrontab -, /etc/cron.d/, /var/spool/cron ; texte d’unité [Service] / ExecStart=, systemctl enable ; .bashrc, /etc/profile.d/Surveillance de fichiers auditd sur ces chemins, entrées du journal pour les nouvelles unités, contrôle d’intégrité des fichiersT1053.003, T1543.002, T1546.004

Plusieurs mécanismes moins courants suivent le même schéma et ont leur propre page : Image File Execution Options (T1546.012), AppInit DLLs (T1546.010) et tâches BITS (T1197). Les scripts d’ouverture de session définis via la valeur UserInitMprLogonScript (T1037.001) sont une autre variante discrète qu’il vaut la peine de connaître.

Bien lire les indices statiques

Un indice est une piste, pas un verdict. RegSetValueExW est importée par presque tous les programmes Windows : l’import seul ne vous apprend donc rien ; l’import plus un chemin de clé Run dans les chaînes décodées constitue une piste solide. Trois habitudes gardent la lecture statique honnête :

  • Décodez avant de chercher. Les chemins de persistance sont précisément les chaînes que les auteurs de malwares chiffrent ou construisent sur la pile. Un échantillon qui importe RegSetValueExW sans chemin de registre visible est une raison de lancer le déchiffrement de chaînes vu dans Scripter le déchiffrement des chaînes, pas une raison d’écarter les clés Run.
  • Cherchez les imports résolus. Un échantillon qui résout ses API par hash cache CreateServiceW à l’IAT. Les outils de détection de capacités et un rapide coup d’œil au résolveur les retrouvent souvent, comme expliqué dans Lire les capacités à partir des imports.
  • Suivez les lignes de commande. Une grande partie de la persistance est déléguée à des outils intégrés : schtasks, sc, reg, powershell, wmic. L’indice est alors une chaîne transmise à CreateProcessW ou ShellExecuteW, et la télémétrie un événement Sysmon 1 portant cette ligne de commande.

Consignez quel mécanisme, quel nom, quel chemin de charge utile et quel niveau de privilège. Une valeur Run par utilisateur ne nécessite aucun droit d’administrateur ; un service, une tâche à l’échelle de la machine ou un abonnement WMI, si, ce qui indique aux intervenants jusqu’où l’intrus est allé.

Confirmer la persistance en laboratoire

Les indices statiques disent ce qu’un échantillon peut installer. La confirmation exige une exécution selon la routine de Surveillance comportementale : instantané sain, baseline, exécution, comparaison. Pour la persistance, trois comparaisons font l’essentiel du travail :

  1. Autoruns avant et après. Enregistrez un fichier .arn ou un CSV autorunsc sur l’instantané sain, exécutez l’échantillon, collectez de nouveau et comparez. Chaque nouvelle entrée est une candidate, et Autoruns rattache chacune à un fichier et à un état de signature. Incluez les onglets Scheduled Tasks, Services et WMI ; on les oublie facilement quand l’onglet Logon montre déjà un résultat.
  2. Instantané du registre et des fichiers. Regshot ou une comparaison similaire détecte des modifications qu’Autoruns ne considère pas comme du démarrage automatique, comme une surcharge COM InprocServer32 pour une classe rarement utilisée, ou une valeur de configuration que l’implant lit au démarrage.
  3. Sysmon pour l’attribution. Les comparaisons vous disent ce qui a changé ; Sysmon 1, 11, 12/13 et 19–21 vous disent quel processus a effectué chaque modification et dans quel ordre. Cet ordre compte plus tard : si une tâche planifiée recrée une valeur Run toutes les trente minutes, supprimer d’abord la valeur Run est un effort perdu.

Ensuite, redémarrez l’instantané et rouvrez une session. Une persistance installée mais qui ne se déclenche pas (mauvais chemin, déclencheur expiré, tâche qui a besoin du réseau) reste un constat, mais différent de celui d’un implant fonctionnel. De nombreux échantillons n’installent en outre leur persistance qu’après un premier contact réussi avec leur serveur : une exécution silencieuse dans un laboratoire déconnecté ne prouve donc pas que l’échantillon n’en a pas ; les indices statiques et un réseau simulé, comme dans Simuler et capturer le trafic réseau, aident à combler cette lacune.

Astuce : Conservez la sortie d’Autoruns après exécution comme fichier de preuve, pas seulement comme capture d’écran. Le CSV contient les noms de valeurs, lignes de commande et hashs exacts dont vous aurez besoin pour les détections et pour la liste de contrôle du nettoyage.

Écrire des détections

Les détections de persistance fonctionnent le mieux sous la forme d’un petit ensemble de règles qui posent chacune une question sur un type d’événement. Quatre motifs couvrent une large part de ce que font les malwares courants. Exprimés en logique de type Sigma, en prose :

  • Écriture d’une clé Run vers un chemin accessible en écriture par l’utilisateur. Sélectionnez les événements Sysmon 13 dont le TargetObject contient \CurrentVersion\Run\ ou \CurrentVersion\RunOnce\ (y compris les variantes WOW6432Node et Policies\Explorer\Run), et dont le champ Details contient \AppData\, \ProgramData\, \Users\Public\ ou un interpréteur comme powershell.exe ou mshta.exe. Filtrez les écrivains connus par chemin d’image et nom de valeur. Souvenez-vous que Sysmon écrit HKCU sous la forme HKU\<SID> : faites correspondre le sous-chemin, pas la racine.
  • Création de tâche par un parent inhabituel. Sélectionnez les événements Sysmon 1 pour schtasks.exe avec /create dans la ligne de commande, et alertez quand le parent ne fait pas partie de vos outils de déploiement ou quand /tr pointe vers un chemin accessible en écriture par l’utilisateur. Le fichier XML de la tâche sous System32\Tasks est écrit par le service Planificateur de tâches lui-même, si bien que Sysmon 11 montre toujours svchost.exe comme écrivain ; utilisez-le comme contexte, pas pour la logique sur le parent. Security 4698 couvre les tâches enregistrées via l’interface COM, qui ne lancent jamais schtasks.exe.
  • Installation de service avec un binaire étrange. Sélectionnez les événements System 7045 (ou Security 4697) dont le chemin d’image se trouve dans un répertoire accessible en écriture par l’utilisateur, utilise cmd.exe /c ou powershell, ou dont le nom de service semble aléatoire. Les services installés par des outils d’exécution à distance méritent leur propre règle.
  • Tout abonnement WMI permanent. Les événements Sysmon 19, 20 et 21 sont rares sur un poste de travail normal. Alertez sur tous, augmentez la sévérité pour ActiveScriptEventConsumer ou un consommateur en ligne de commande qui lance un interpréteur, et mettez sur liste d’autorisation les quelques produits de gestion que vous avez examinés.

SigmaHQ dispose déjà de règles matures pour la plupart de ces motifs ; leurs sections de filtre consignent les faux positifs rencontrés par d’autres équipes.

Astuce : Les configurations Sysmon communautaires incluent déjà les clés et dossiers de persistance courants, mais vérifiez que la vôtre couvre les moins courants du tableau ci-dessus (COM InprocServer32 sous HKCU, Winlogon, IFEO). Une règle portant sur un événement que votre capteur ne collecte jamais ne se déclenchera jamais, et rien ne vous le signalera.

Ajuster sans devenir aveugle

Toute règle de persistance rencontre des logiciels légitimes. Les installeurs par utilisateur (outils de mise à jour de navigateurs, clients de messagerie, outils de synchronisation) vivent sous AppData par conception, et les agents de gestion informatique enregistrent des abonnements WMI. Ajustez avec des exceptions étroites et revues : le chemin complet de l’écrivain et le nom de la valeur, idéalement joints à la signature de l’écrivain issue de son événement Sysmon 1 ou 7. Une exception sur le seul nom de valeur est une invitation : un attaquant qui nomme sa valeur Run OneDrive passe tout droit.

Remédiation pendant la réponse à incident

Une fois que l’analyse et la chasse ont produit une liste de points de persistance, l’ordre du nettoyage détermine s’il tient.

  1. Contenir. Isolez l’hôte du réseau via votre EDR ou un port de switch, mais laissez-le allumé. Un redémarrage déclenche tous les mécanismes de persistance, détruit les preuves en mémoire et peut activer des charges utiles qui ne s’exécutent qu’au démarrage.
  2. Collecter d’abord les preuves. Avant de modifier quoi que ce soit : une image mémoire, un CSV Autoruns avec les hashs, les ruches du registre avec leurs journaux de transactions, le dossier System32\Tasks, le dépôt WMI (wbem\Repository), les journaux d’événements et chaque fichier de charge utile vers lequel pointent les entrées. Dès que vous supprimez une valeur Run, l’heure de dernière écriture de sa clé change et la preuve d’origine disparaît.
  3. Délimiter avant de supprimer. Transformez les constats en chasses sur l’ensemble du parc : noms de valeurs, noms de tâches, noms de services, hashs et chemins des charges utiles. Si dix hôtes portent la même tâche, en nettoyer un signale à l’attaquant que vous l’avez repéré et en laisse neuf.
  4. Supprimer chaque point de persistance dans une seule fenêtre planifiée. Arrêtez ou suspendez d’abord les processus de l’implant, puis supprimez ensemble déclencheurs et charges utiles, sur tous les hôtes touchés à la fois. Les échantillons installent souvent deux ou trois mécanismes pour que chacun puisse restaurer les autres ; une tâche planifiée qui réécrit une valeur Run est courante. Supprimez aussi ce que l’attaquant a utilisé pour entrer, et réinitialisez les identifiants qu’il détenait ; nettoyer la persistance n’est pas la même chose qu’évincer l’intrus.
  5. Vérifier avec une nouvelle baseline. Collectez de nouveau Autoruns et comparez avec votre image de référence ou un hôte sain de même build. Relancez les chasses. Redémarrez et vérifiez encore, car certains mécanismes ne réapparaissent qu’après un démarrage.
  6. Surveiller la réinfection. Gardez les indicateurs spécifiques en alerte prioritaire pendant des semaines, pas des jours. Une entrée de persistance qui revient signifie que vous avez manqué un mécanisme, un hôte ou le vecteur d’accès initial.

Quand l’implant s’est exécuté avec des droits d’administrateur ou SYSTEM, a installé un pilote, ou que vous ne pouvez pas être sûr d’avoir trouvé tous les mécanismes, reconstruire l’hôte à partir d’une image saine coûte généralement moins cher que de prouver qu’un nettoyage est complet.

Rapporter la persistance

Le rapport de triage place la persistance parmi les capacités et les indicateurs d’hôte. Pour la réponse à incident, donnez à chaque point de persistance sa propre ligne afin qu’elle serve de liste de contrôle :

ChampExemple
Mécanisme et identifiant ATT&CKTâche planifiée, T1053.005
Emplacement et nom\DemoUpdateCheck dans System32\Tasks et TaskCache
Déclencheur et charge utileToutes les 30 minutes ; %APPDATA%\DemoCorp\demoupd.exe (SHA-256)
Privilège et portéeUtilisateur courant ; un hôte confirmé, chasse sur le parc en cours
Créé par et quandDemoInvoice.exe, 10:14:10 UTC, Sysmon 1 et 11
DétectionNom de la règle ou requête qui se déclenche dessus
Suppression et vérificationschtasks /delete, suppression de la charge utile ; comparaison Autoruns après redémarrage

Indiquez quels mécanismes ont été observés lors d’une exécution et lesquels ont été déduits du code : « peut installer un service lorsqu’il est exécuté en tant qu’administrateur (code uniquement) » n’est pas le même constat que « a installé le service X ».

Lab : détecter la persistance dans des événements de type Sysmon

Dans ce lab, vous générez un petit fichier d’événements synthétiques de type Sysmon pour un poste de travail, incluant une intrusion simulée qui définit une valeur Run et enregistre une tâche planifiée, puis vous écrivez un script Python qui lui applique quatre règles de détection. Rien dans ce lab ne crée de persistance où que ce soit : les événements sont des lignes JSON, et tous les noms et chemins sont inventés.

  1. Préparez un dossier de travail et un environnement virtuel (bibliothèque standard uniquement) :

    bash
    mkdir m7p && cd m7p
    python3 -m venv .venv
    .venv/bin/python --version
    text
    Python 3.14.7
  2. Générez les événements. Les champs reprennent les noms de Sysmon (Image, ParentImage, TargetObject, Details, TargetFilename, et les champs WMI des événements 19–21). L’activité normale comprend un installeur qui ajoute une valeur Run à l’échelle de la machine et une tâche, OneDrive qui s’enregistre, et l’abonnement WMI d’un agent d’inventaire informatique :

    python
    # make_events.py - writes events.jsonl: synthetic Sysmon-style events for one workstation.
    # Nothing here touches the registry, the Task Scheduler or WMI; it only writes JSON.
    import json
    
    HOST = "WS-DEMO-07"
    SID = "S-1-5-21-1111111111-2222222222-3333333333-1001"
    HKU_RUN = rf"HKU\{SID}\Software\Microsoft\Windows\CurrentVersion\Run"
    HKLM_RUN = r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
    APPDATA = r"C:\Users\demo\AppData\Roaming"
    LOCAL = r"C:\Users\demo\AppData\Local"
    DROPPER = r"C:\Users\demo\Downloads\DemoInvoice.exe"
    IMPLANT = APPDATA + r"\DemoCorp\demoupd.exe"
    
    def ev(t, eid, **f):
        return {"UtcTime": f"2026-09-29 {t}", "EventID": eid, "Computer": HOST, **f}
    
    events = [
        # --- normal workstation activity ---
        ev("08:58:02.114", 1, Image=r"C:\Program Files\DemoBrowser\browser.exe",
           ParentImage=r"C:\Windows\explorer.exe", CommandLine=r'"C:\Program Files\DemoBrowser\browser.exe"'),
        ev("08:58:40.301", 13, EventType="SetValue", Image=r"C:\Windows\explorer.exe",
           TargetObject=rf"HKU\{SID}\Software\Microsoft\Windows\CurrentVersion\Explorer\RecentDocs\MRUListEx",
           Details="Binary Data"),
        ev("09:01:15.870", 1, Image=r"C:\Windows\System32\msiexec.exe",
           ParentImage=r"C:\Windows\System32\services.exe", CommandLine=r"C:\Windows\system32\msiexec.exe /V"),
        ev("09:01:19.442", 13, EventType="SetValue", Image=r"C:\Windows\System32\msiexec.exe",
           TargetObject=HKLM_RUN + r"\DemoVendorTray",
           Details=r'"C:\Program Files\DemoVendor\tray.exe" /minimized'),
        ev("09:01:20.013", 1, Image=r"C:\Windows\System32\schtasks.exe",
           ParentImage=r"C:\Windows\System32\msiexec.exe",
           CommandLine=r'schtasks.exe /create /tn "DemoVendor\Update" /tr "\"C:\Program Files\DemoVendor\updater.exe\"" /sc daily /st 03:00 /ru SYSTEM /f'),
        ev("09:01:20.391", 11, Image=r"C:\Windows\system32\svchost.exe",
           TargetFilename=r"C:\Windows\System32\Tasks\DemoVendor\Update"),
        ev("09:05:33.508", 13, EventType="SetValue", Image=LOCAL + r"\Microsoft\OneDrive\OneDrive.exe",
           TargetObject=HKU_RUN + r"\OneDrive",
           Details=rf'"{LOCAL}\Microsoft\OneDrive\OneDrive.exe" /background'),
        # A management agent's WMI subscription (fictional product, installed by IT)
        ev("09:12:00.250", 19, EventType="WmiFilterEvent", Operation="Created", User=r"NT AUTHORITY\SYSTEM",
           EventNamespace='"root\\cimv2"', Name='"DemoInventoryFilter"',
           Query='"SELECT * FROM __InstanceModificationEvent WITHIN 3600 WHERE TargetInstance ISA \'Win32_LocalTime\' AND TargetInstance.Hour = 2"'),
        ev("09:12:00.262", 20, EventType="WmiConsumerEvent", Operation="Created", User=r"NT AUTHORITY\SYSTEM",
           Name='"DemoInventoryConsumer"', Type="Command Line",
           Destination=r'"\"C:\Program Files\DemoInventory\collect.exe\" --quiet"'),
        ev("09:12:00.271", 21, EventType="WmiBindingEvent", Operation="Created", User=r"NT AUTHORITY\SYSTEM",
           Consumer='"CommandLineEventConsumer.Name=\\"DemoInventoryConsumer\\""',
           Filter='"__EventFilter.Name=\\"DemoInventoryFilter\\""'),
        # --- simulated intrusion (all names fictional) ---
        ev("10:14:07.906", 1, Image=DROPPER, ParentImage=r"C:\Windows\explorer.exe",
           CommandLine=f'"{DROPPER}"'),
        ev("10:14:09.118", 12, EventType="CreateKey", Image=DROPPER,
           TargetObject=rf"HKU\{SID}\Software\DemoCorp"),
        ev("10:14:09.552", 11, Image=DROPPER, TargetFilename=IMPLANT),
        ev("10:14:09.804", 13, EventType="SetValue", Image=DROPPER,
           TargetObject=HKU_RUN + r"\DemoUpdater", Details=IMPLANT),
        ev("10:14:10.230", 1, Image=r"C:\Windows\System32\schtasks.exe", ParentImage=DROPPER,
           CommandLine=f'schtasks.exe /create /tn "DemoUpdateCheck" /tr "{IMPLANT}" /sc minute /mo 30 /f'),
        ev("10:14:10.611", 11, Image=r"C:\Windows\system32\svchost.exe",
           TargetFilename=r"C:\Windows\System32\Tasks\DemoUpdateCheck"),
    ]
    
    with open("events.jsonl", "w") as f:
        for e in events:
            f.write(json.dumps(e) + "\n")
    print(f"wrote {len(events)} events")
    bash
    .venv/bin/python make_events.py
    sed -n 14p events.jsonl
    text
    wrote 16 events
    {"UtcTime": "2026-09-29 10:14:09.804", "EventID": 13, "Computer": "WS-DEMO-07", "EventType": "SetValue", "Image": "C:\\Users\\demo\\Downloads\\DemoInvoice.exe", "TargetObject": "HKU\\S-1-5-21-1111111111-2222222222-3333333333-1001\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\DemoUpdater", "Details": "C:\\Users\\demo\\AppData\\Roaming\\DemoCorp\\demoupd.exe"}

    Le chemin de registre commence par HKU\<SID>, et non HKCU : c’est ainsi que Sysmon enregistre les clés par utilisateur.

  3. Écrivez le script de détection. Chaque règle examine un événement et ne renvoie rien (ce n’est pas son affaire), un ignore avec une raison (une candidate qui a échoué à une condition ou correspondu à une exception), ou une alerte avec son identifiant ATT&CK. La règle sur les tâches ajoute comme contexte le fichier XML de la tâche écrit par le service Planificateur de tâches dans les cinq secondes :

    python
    # detect_persistence.py - apply persistence detection rules to Sysmon-style JSON lines.
    # Usage: detect_persistence.py events.jsonl [--explain] [--no-tuning]
    import json, re, sys
    from datetime import datetime
    
    USER_WRITABLE = re.compile(r"\\users\\[^\\]+\\(appdata|downloads|desktop|documents)\\|\\programdata\\|\\windows\\temp\\|\\users\\public\\", re.I)
    INTERPRETERS = re.compile(r"(^|[\\\s\"])(powershell|pwsh|cmd|wscript|cscript|mshta|rundll32|regsvr32)\.exe", re.I)
    RUN_KEY = re.compile(r"\\(Software\\(WOW6432Node\\)?Microsoft\\Windows\\CurrentVersion\\(Run|RunOnce)"
                         r"|Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer\\Run)\\", re.I)
    STARTUP_DIR = re.compile(r"\\Start Menu\\Programs\\Startup\\", re.I)
    TASKS_DIR = re.compile(r"^C:\\Windows\\System32\\Tasks\\", re.I)
    EXPECTED_TASK_PARENTS = {"msiexec.exe", "svchost.exe", "services.exe", "ccmexec.exe"}
    
    # Tuning: known-good (writer image suffix, value name) pairs for Run-key writes.
    ALLOWLIST_RUN = [(r"\microsoft\onedrive\onedrive.exe", "onedrive")]
    
    ATTACK = {"RUN": "T1547.001", "STARTUP": "T1547.001", "TASK": "T1053.005", "WMI": "T1546.003"}
    
    def base(p):
        return p.lower().rsplit("\\", 1)[-1]
    
    def ts(e):
        return datetime.strptime(e["UtcTime"], "%Y-%m-%d %H:%M:%S.%f")
    
    def rule_run(e, tuned):
        if e["EventID"] != 13 or not RUN_KEY.search(e.get("TargetObject", "")):
            return None
        name = e["TargetObject"].rsplit("\\", 1)[-1]
        data, writer = e.get("Details", ""), e.get("Image", "")
        if not (USER_WRITABLE.search(data) or INTERPRETERS.search(data)):
            return ("ignore", f"Run value '{name}': data not in a user-writable path or interpreter")
        if tuned and any(writer.lower().endswith(w) and name.lower() == n for w, n in ALLOWLIST_RUN):
            return ("ignore", f"Run value '{name}': allowlisted writer {base(writer)}")
        sev = "HIGH" if USER_WRITABLE.search(writer) else "MEDIUM"
        return ("alert", "RUN", sev, f"Run value '{name}' -> {data}", f"written by {writer}")
    
    def rule_startup(e, tuned):
        if e["EventID"] == 11 and STARTUP_DIR.search(e.get("TargetFilename", "")):
            return ("alert", "STARTUP", "MEDIUM", f"file in Startup folder: {e['TargetFilename']}",
                    f"written by {e.get('Image')}")
        return None
    
    def rule_task(e, tuned, events):
        if e["EventID"] != 1 or base(e.get("Image", "")) != "schtasks.exe" or "/create" not in e["CommandLine"].lower():
            return None
        parent, cmd = e.get("ParentImage", ""), e["CommandLine"]
        reasons = []
        if base(parent) not in EXPECTED_TASK_PARENTS:
            reasons.append(f"unusual parent {base(parent)}")
        if USER_WRITABLE.search(cmd):
            reasons.append("action in user-writable path")
        m = re.search(r'/tn\s+"?([^"]+?)"?\s+/', cmd, re.I)
        task = m.group(1) if m else "?"
        if not reasons:
            return ("ignore", f"task '{task}': created by {base(parent)}, action outside user-writable paths")
        # correlate: the Task Scheduler service writes the XML definition shortly afterwards
        xml = [x for x in events if x["EventID"] == 11 and x["Computer"] == e["Computer"]
               and TASKS_DIR.search(x.get("TargetFilename", ""))
               and 0 <= (ts(x) - ts(e)).total_seconds() <= 5]
        ctx = [f"parent {parent}", f"cmd: {cmd}"] + [f"task file {x['TargetFilename']} (by {base(x['Image'])})" for x in xml]
        return ("alert", "TASK", "HIGH" if len(reasons) > 1 else "MEDIUM",
                f"schtasks /create '{task}': " + "; ".join(reasons), *ctx)
    
    def rule_wmi(e, tuned):
        if e["EventID"] not in (19, 20, 21):
            return None
        what = {19: f"filter {e.get('Name')} query={e.get('Query')}",
                20: f"consumer {e.get('Name')} type={e.get('Type')} dest={e.get('Destination')}",
                21: f"binding {e.get('Consumer')} <- {e.get('Filter')}"}[e["EventID"]]
        dest = e.get("Destination", "")
        sev = "HIGH" if (USER_WRITABLE.search(dest) or INTERPRETERS.search(dest) or e.get("Type") == "Script") else "MEDIUM"
        return ("alert", "WMI", sev, f"WMI {e.get('Operation', '').lower()} {what}", f"user {e.get('User')}")
    
    def main(path, explain, tuned):
        events = [json.loads(l) for l in open(path, encoding="utf-8-sig") if l.strip()]
        alerts = ignored = 0
        for e in events:
            for r in (rule_run(e, tuned), rule_startup(e, tuned), rule_task(e, tuned, events), rule_wmi(e, tuned)):
                if r is None:
                    continue
                if r[0] == "ignore":
                    ignored += 1
                    if explain:
                        print(f"[ignored] {e['UtcTime']} EID {e['EventID']:<2} {r[1]}")
                    continue
                _, rid, sev, msg, *ctx = r
                alerts += 1
                print(f"[{sev:<6}] {e['UtcTime']} {e['Computer']} EID {e['EventID']:<2} {ATTACK[rid]} {msg}")
                for c in ctx:
                    print(f"           {c}")
        print(f"-- {len(events)} events, {alerts} alerts, {ignored} candidates ignored (tuning {'on' if tuned else 'off'})")
    
    if __name__ == "__main__":
        args = sys.argv[1:]
        main(args[0], "--explain" in args, "--no-tuning" not in args)
  4. Exécutez-le avec --explain, qui affiche aussi les candidates que les règles ont examinées puis écartées :

    bash
    .venv/bin/python detect_persistence.py events.jsonl --explain
    text
    [ignored] 2026-09-29 09:01:19.442 EID 13 Run value 'DemoVendorTray': data not in a user-writable path or interpreter
    [ignored] 2026-09-29 09:01:20.013 EID 1  task 'DemoVendor\Update': created by msiexec.exe, action outside user-writable paths
    [ignored] 2026-09-29 09:05:33.508 EID 13 Run value 'OneDrive': allowlisted writer onedrive.exe
    [MEDIUM] 2026-09-29 09:12:00.250 WS-DEMO-07 EID 19 T1546.003 WMI created filter "DemoInventoryFilter" query="SELECT * FROM __InstanceModificationEvent WITHIN 3600 WHERE TargetInstance ISA 'Win32_LocalTime' AND TargetInstance.Hour = 2"
               user NT AUTHORITY\SYSTEM
    [MEDIUM] 2026-09-29 09:12:00.262 WS-DEMO-07 EID 20 T1546.003 WMI created consumer "DemoInventoryConsumer" type=Command Line dest="\"C:\Program Files\DemoInventory\collect.exe\" --quiet"
               user NT AUTHORITY\SYSTEM
    [MEDIUM] 2026-09-29 09:12:00.271 WS-DEMO-07 EID 21 T1546.003 WMI created binding "CommandLineEventConsumer.Name=\"DemoInventoryConsumer\"" <- "__EventFilter.Name=\"DemoInventoryFilter\""
               user NT AUTHORITY\SYSTEM
    [HIGH  ] 2026-09-29 10:14:09.804 WS-DEMO-07 EID 13 T1547.001 Run value 'DemoUpdater' -> C:\Users\demo\AppData\Roaming\DemoCorp\demoupd.exe
               written by C:\Users\demo\Downloads\DemoInvoice.exe
    [HIGH  ] 2026-09-29 10:14:10.230 WS-DEMO-07 EID 1  T1053.005 schtasks /create 'DemoUpdateCheck': unusual parent demoinvoice.exe; action in user-writable path
               parent C:\Users\demo\Downloads\DemoInvoice.exe
               cmd: schtasks.exe /create /tn "DemoUpdateCheck" /tr "C:\Users\demo\AppData\Roaming\DemoCorp\demoupd.exe" /sc minute /mo 30 /f
               task file C:\Windows\System32\Tasks\DemoUpdateCheck (by svchost.exe)
    -- 16 events, 5 alerts, 3 candidates ignored (tuning on)

    Les deux alertes HIGH correspondent à l’intrusion : une valeur Run écrite par un programme situé dans Downloads, pointant vers un nouvel EXE sous Roaming, et une tâche créée une seconde plus tard par le même programme pour la même charge utile. La valeur Run DemoVendorTray de l’installeur est correctement ignorée : elle a été écrite par msiexec.exe et pointe dans Program Files. La tâche de l’installeur est ignorée pour les mêmes raisons. L’abonnement WMI de l’agent d’inventaire lève trois alertes MEDIUM, comme prévu pour un type d’événement rare : un analyste l’examine une fois et consigne une décision.

  5. Voyez ce que fait l’ajustement. Relancez avec la liste d’autorisation désactivée :

    bash
    .venv/bin/python detect_persistence.py events.jsonl --no-tuning | grep -A1 OneDrive
    .venv/bin/python detect_persistence.py events.jsonl --no-tuning | tail -1
    text
    [HIGH  ] 2026-09-29 09:05:33.508 WS-DEMO-07 EID 13 T1547.001 Run value 'OneDrive' -> "C:\Users\demo\AppData\Local\Microsoft\OneDrive\OneDrive.exe" /background
               written by C:\Users\demo\AppData\Local\Microsoft\OneDrive\OneDrive.exe
    [MEDIUM] 2026-09-29 09:12:00.250 WS-DEMO-07 EID 19 T1546.003 WMI created filter "DemoInventoryFilter" query="SELECT * FROM __InstanceModificationEvent WITHIN 3600 WHERE TargetInstance ISA 'Win32_LocalTime' AND TargetInstance.Hour = 2"
    -- 16 events, 6 alerts, 2 candidates ignored (tuning off)

    Remarque sur l’ajustement : sans l’exception, la propre valeur Run de OneDrive se déclenche en HIGH, car la charge utile comme l’écrivain vivent sous AppData. Toute application par utilisateur fait de même ; en production, la règle noierait donc les vraies alertes. L’exception de ALLOWLIST_RUN fixe à la fois le chemin de l’écrivain et le nom de la valeur, mais ce n’est toujours qu’un chemin : un fichier nommé OneDrive.exe déposé au même endroit passerait. Une version plus robuste joint chaque événement Sysmon 13 à l’événement Sysmon 1 de l’écrivain via ProcessGuid et vérifie le signataire.

  6. Essayez sur de la télémétrie réelle. Dans une VM d’analyse où Sysmon est installé, exportez les événements récents au même format de lignes JSON et exécutez le script dessus. Ce PowerShell aplatit l’EventData de chaque événement :

    powershell
    Get-WinEvent -LogName 'Microsoft-Windows-Sysmon/Operational' -MaxEvents 5000 |
        Where-Object Id -in 1, 11, 12, 13, 19, 20, 21 | ForEach-Object {
            $h = [ordered]@{ EventID = $_.Id; Computer = $_.MachineName }
            ([xml]$_.ToXml()).Event.EventData.Data | ForEach-Object { $h[$_.Name] = $_.'#text' }
            $h | ConvertTo-Json -Compress
        } | Set-Content -Encoding utf8 C:\analysis\events.jsonl

    Le script lit l’UTF-8 avec ou sans marque d’ordre des octets. Installez une application par utilisateur et vérifiez quelle règle se déclenche et pourquoi.

Questions : La règle Run cherche \AppData\ dans les données de la valeur ; que se passe-t-il si un échantillon stocke %APPDATA%\DemoCorp\demoupd.exe sous forme de REG_EXPAND_SZ, et comment corrigeriez-vous le motif ? Pourquoi la règle sur les tâches ne peut-elle pas utiliser le parent de l’événement Sysmon 11 pour le fichier XML, et quel événement Windows couvre les tâches enregistrées via COM sans schtasks.exe ? Quelle règle supplémentaire unique aurait détecté le dropper écrivant demoupd.exe avant que la moindre persistance n’existe ? Lors du nettoyage de WS-DEMO-07, dans quel ordre supprimeriez-vous la tâche, la valeur Run et la charge utile, et que collecteriez-vous avant de toucher à l’une d’elles ?

À retenir

  • La persistance est ce qu’une intrusion laisse derrière elle sur le disque et ce que le nettoyage doit supprimer ; toute analyse doit indiquer comment un échantillon survit à un redémarrage, ou pourquoi il n’y survit pas.
  • Chaque famille de persistance a des indices statiques (API, chaînes, lignes de commande), une télémétrie qui l’enregistre (Sysmon 1/11/12/13/19–21, System 7045, Security 4697/4698) et un identifiant ATT&CK ; décodez les chaînes et suivez les lignes de commande déléguées avant de conclure qu’un échantillon n’en a pas.
  • Confirmez en laboratoire avec Autoruns et des comparaisons d’instantanés, utilisez Sysmon pour attribuer chaque modification à un processus, et redémarrez pour voir quels mécanismes se déclenchent réellement.
  • Les bonnes détections de persistance posent chacune une question étroite ; ajustez-les avec des exceptions revues qui fixent l’écrivain et le nom, pas le nom seul.
  • En réponse à incident : contenez, collectez les preuves, délimitez l’étendue sur le parc, supprimez chaque mécanisme et sa charge utile dans une seule fenêtre, vérifiez par rapport à une baseline saine et continuez à surveiller la réinfection.
  • Rapportez chaque point de persistance comme une ligne de liste de contrôle : mécanisme, emplacement, charge utile, privilège, origine, détection, suppression et vérification.