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.
| Famille | Indice statique dans un échantillon | Télémétrie qui l’enregistre | ATT&CK |
|---|---|---|---|
| Clés Run / RunOnce | Imports RegCreateKeyExW / RegSetValueExW ; la chaîne Software\Microsoft\Windows\CurrentVersion\Run ; reg add ... /v dans une commande | Sysmon 12/13 ; heure de dernière écriture de la clé | T1547.001 |
| Dossier de démarrage | SHGetKnownFolderPath / SHGetFolderPathW (identifiants du dossier Startup), IShellLinkW pour créer un .lnk, chaînes Start Menu\Programs\Startup | Sysmon 11 dans les chemins Startup | T1547.001 |
| Services Windows | OpenSCManagerW, CreateServiceW, ChangeServiceConfigW ; chaînes sc create, New-Service ; un export ServiceMain dans une DLL | System 7045, Security 4697, Sysmon 12/13 sous Services | T1543.003 |
| Tâches planifiées | Lignes 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\Tasks | T1053.005 |
| Abonnements aux événements WMI | Chaînes __EventFilter, CommandLineEventConsumer, ActiveScriptEventConsumer, __FilterToConsumerBinding, root\subscription ; mofcomp ou Set-WmiInstance / New-CimInstance dans des scripts | Sysmon 19/20/21, WMI-Activity Operational 5861 | T1546.003 |
| Valeurs Winlogon | Chaînes Winlogon, Userinit, Shell à côté d’imports d’écriture dans le registre | Sysmon 13 sur la clé Winlogon | T1547.004 |
| Détournement de l’ordre de recherche des DLL | Une 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 DLL | Sysmon 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 COM | Chaînes de CLSID et Software\Classes\CLSID\{...}\InprocServer32 sous HKCU | Sysmon 12/13 sous HKU\<SID>_Classes\CLSID ou HKCU\Software\Classes\CLSID | T1546.015 |
| Linux : cron, systemd, profils de shell | crontab -, /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 fichiers | T1053.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
RegSetValueExWsans 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 àCreateProcessWouShellExecuteW, 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 :
- Autoruns avant et après. Enregistrez un fichier
.arnou un CSVautorunscsur 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. - 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
InprocServer32pour une classe rarement utilisée, ou une valeur de configuration que l’implant lit au démarrage. - 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
TargetObjectcontient\CurrentVersion\Run\ou\CurrentVersion\RunOnce\(y compris les variantesWOW6432NodeetPolicies\Explorer\Run), et dont le champDetailscontient\AppData\,\ProgramData\,\Users\Public\ou un interpréteur commepowershell.exeoumshta.exe. Filtrez les écrivains connus par chemin d’image et nom de valeur. Souvenez-vous que Sysmon écritHKCUsous la formeHKU\<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.exeavec/createdans la ligne de commande, et alertez quand le parent ne fait pas partie de vos outils de déploiement ou quand/trpointe vers un chemin accessible en écriture par l’utilisateur. Le fichier XML de la tâche sousSystem32\Tasksest écrit par le service Planificateur de tâches lui-même, si bien que Sysmon 11 montre toujourssvchost.execomme é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 jamaisschtasks.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 /coupowershell, 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
ActiveScriptEventConsumerou 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
InprocServer32sousHKCU,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.
- 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.
- 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. - 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.
- 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.
- 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.
- 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 :
| Champ | Exemple |
|---|---|
| Mécanisme et identifiant ATT&CK | Tâche planifiée, T1053.005 |
| Emplacement et nom | \DemoUpdateCheck dans System32\Tasks et TaskCache |
| Déclencheur et charge utile | Toutes les 30 minutes ; %APPDATA%\DemoCorp\demoupd.exe (SHA-256) |
| Privilège et portée | Utilisateur courant ; un hôte confirmé, chasse sur le parc en cours |
| Créé par et quand | DemoInvoice.exe, 10:14:10 UTC, Sysmon 1 et 11 |
| Détection | Nom de la règle ou requête qui se déclenche dessus |
| Suppression et vérification | schtasks /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.
-
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 --versiontext Python 3.14.7 -
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.jsonltext 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 nonHKCU: c’est ainsi que Sysmon enregistre les clés par utilisateur. -
É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) -
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 --explaintext [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 sousRoaming, et une tâche créée une seconde plus tard par le même programme pour la même charge utile. La valeur RunDemoVendorTrayde l’installeur est correctement ignorée : elle a été écrite parmsiexec.exeet pointe dansProgram 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. -
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 -1text [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 deALLOWLIST_RUNfixe à 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.exedéposé au même endroit passerait. Une version plus robuste joint chaque événement Sysmon 13 à l’événement Sysmon 1 de l’écrivain viaProcessGuidet vérifie le signataire. -
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’
EventDatade 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.jsonlLe 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.