wskmon.sys : un backdoor kernel signé WHQL et son kit de trois outils
Catégorie : Backdoor
Analyste : Gordon PEIRS
Famille : pas de nom officiel. Je l'identifie par son nom de fichier et ses chaînes internes (WskMon).
Type : analyse statique uniquement. Aucun échantillon n'est exécuté, aucun driver n'est chargé.
Ce document regroupe toute l'analyse technique de
wskmon.syset des deux autres outils signés avec le même certificat (devhost.sys,844ljfpvz.sys). Le PoC de déclenchement est un livrable à part, pas de la prose d'analyse.
Résumé
wskmon.sys est un driver kernel Windows x64 signé WHQL qui détourne le Windows Filtering Platform (WFP) pour installer un backdoor réseau purement passif. Il n'a aucune infrastructure C2 sortante : il attend qu'on lui envoie le bon paquet sur une connexion existante, authentifié en HMAC-SHA256 avec une clé XOR qu'on extrait en clair du binaire. Il existe déjà un writeup public (Nextron Systems). J'ai mené cette analyse sans m'y fier, et elle le corrige sur un point : la résolution de la primitive de création de thread ne s'arrête pas à RtlCreateUserThread. Un mécanisme de repli reconstruit à la main l'adresse de la SSDT à partir du MSR IA32_LSTAR, une technique qu'on voit d'habitude dans des rootkits, pas dans du code amateur. Mais le plus important est que ce fichier n'est pas seul. Deux autres outils sont signés avec exactement le même certificat : devhost.sys (une primitive de lecture/écriture arbitraire en mémoire physique) et 844ljfpvz.sys (fonction encore indéterminée). On a donc affaire à un kit de trois outils reliés par la même infrastructure de signature, pas à un backdoor isolé.
1. Contexte
L'échantillon a été repéré sur MalwareBazaar (tag rootkit), signalé le 27/06/2026 et compilé le 04/04/2026. Ce n'est pas un malware user-mode comme les droppers habituels de ce dossier. C'est un driver kernel Windows x64 signé WHQL (Windows Hardware Compatibility Publisher) qui abuse du Windows Filtering Platform (WFP) pour installer un backdoor réseau furtif, invisible pour presque tous les antivirus au moment de l'analyse.
Un writeup public existe déjà sur ce sample : Nextron Systems, "Anatomy of a WHQL-signed Windows Filtering Platform (WFP) kernel-resident network backdoor" (26/06/2026), avec un PoC client Python. J'ai travaillé indépendamment, sans reprendre une affirmation de cet article avant de l'avoir vérifiée dans le désassemblage. Une seule s'est révélée incomplète : le nom de la fonction RtlCreateUserThread, qui laisse de côté le vrai mécanisme de repli (§8). Les sections qui suivent vont nettement plus loin que ce qui est documenté publiquement (récapitulatif au §18).
2. Identité de l'échantillon
| Champ | Valeur |
|---|---|
| SHA256 | 495c7e5513fa7766c236e76d8520139139fc4ad7203ddcb2ccdae17bdb691979 |
| Nom de fichier | wskmon.sys |
| Taille | 32 344 octets |
| Type | PE32+ driver kernel (x86-64) |
| Imphash | 5c6478fa51bfe6402aa0892dc333f989 |
| Compilation | 2026-04-04 13:05:19 UTC |
| Première observation MalwareBazaar | 2026-06-27 |
| Détection antivirus au moment de l'analyse | Quasi nulle, Kaspersky "Clean", YOROI "Legit File", FileScan-IO "NO_THREAT", ReversingLabs 1/23 scanners |
| Échantillons apparentés | Aucun par imphash (recherche infructueuse, normal, ce sont des builds différents), mais deux par certificat, voir §4 |
3. La signature : le vrai problème
La signature Authenticode (répertoire Security du PE, extraite du binaire et vérifiée avec deux bibliothèques indépendantes, openssl et cryptography) ne contient que deux certificats X.509, tous les deux à Microsoft :
| Champ | Valeur |
|---|---|
| Sujet (certificat de signature) | Microsoft Windows Hardware Compatibility Publisher |
| Émetteur | Microsoft Windows Third Party Component CA 2012 |
| Validité | 2025-11-13 → 2026-11-10 (valide au moment de la rédaction, vérifié en direct contre la CRL Microsoft, aucune révocation, dernière mise à jour de la liste le 2026-05-07) |
| Numéro de série | 330000013c4a61fb3578d2b6dd00000000013c |
| Empreinte SHA256 | 2e8072ded075c6b6c0df8c364e9d12319577114991f2455ebc4b364b367f7dba |
Une précision sur ce nom. Nextron donne « 深圳市奥联信息安全技术有限公司 » (Shenzhen Aolian Information Security Technology Co., Ltd) comme « Subject » du certificat. Ce n'est pas le champ Subject d'un certificat X.509 : aucun des deux certificats embarqués ne porte ce nom. La chaîne est en clair (UTF-16BE, offset 0x6543 du fichier) dans le répertoire de sécurité, à l'endroit exact où Authenticode range l'attribut signé SpcSpOpusInfo. C'est le champ « nom de programme » que le soumetteur remplit lui-même et que Windows affiche dans la boîte de dialogue « Détails de la signature numérique ». La donnée est donc réelle, signée et liée à la soumission, mais ce n'est pas le certificat. Ça change la formulation d'un signalement : Aolian a fourni cette information lors de la soumission, ce qui n'en fait pas forcément le titulaire du certificat de signature final (qui reste Microsoft, par construction du programme d'attestation).
Aolian est une vraie entreprise chinoise de cryptographie, bien établie (fondée en 2009, unité désignée par l'Administration d'État de la Cryptographie chinoise, algorithme SM9 adopté comme standard ISO/IEEE/3GPP/IETF). Le scénario le plus probable n'est pas qu'Aolian ait écrit ce driver. C'est plutôt que son compte Microsoft Partner Center (Hardware Dev Center), ou l'accès utilisé pour soumettre cette signature, a été compromis ou détourné. Mandiant et SentinelOne ont documenté ce schéma fin 2022 (campagne POORTRY/STONESTOP, groupe UNC3944) : plusieurs comptes de vendeurs légitimes avaient servi, volontairement ou non, à faire signer des drivers malveillants par le canal d'attestation signing. C'est la voie de signature allégée du programme WHCP, qui ne demande qu'un certificat EV et une soumission, sans test matériel complet (HLK).
À noter : ce sample est compilé en avril 2026, après que Microsoft a annoncé (avril 2026) la fin de la confiance accordée aux anciens drivers cross-signés et le renforcement du programme WHCP. Il a donc obtenu une signature légitime après ce durcissement. La nouvelle politique n'a pas empêché cet abus.
4. Le même certificat, réutilisé sur trois fichiers : ce n'est pas un abus isolé
Une recherche MalwareBazaar par numéro de série du certificat (330000013c4a61fb3578d2b6dd00000000013c), et non par imphash, fait remonter deux autres fichiers signés avec exactement le même certificat, que le writeup public ne mentionne pas :
| Fichier | SHA256 | Imphash | Compilation | Première observation | Rapporteur |
|---|---|---|---|---|---|
devhost.sys |
ee8844ffd3879190fb389b0f613cb2dcdcd83375cf0a6994170a648c5ca8c479 |
fb7c5ff17455f725aa59f998a0f324ca |
2026-01-17 | 2026-04-22 | smica83 |
844ljfpvz.sys |
1d9224a72e64bb2aad289edc81ea0720c764511c3e2b5beb5d0d5ce82a719abd |
7282cc9e51cb73de8f89c7740b0e69fb |
2026-04-15 | 2026-06-19 | smica83 |
wskmon.sys (cet échantillon) |
495c7e5513fa7766c236e76d8520139139fc4ad7203ddcb2ccdae17bdb691979 |
5c6478fa51bfe6402aa0892dc333f989 |
2026-04-04 | 2026-06-27 | GDHJDSYDH1 |
Trois imphashes différents, donc trois builds distincts et pas la même charge utile recompilée.
Deux fenêtres temporelles distinctes, à ne pas confondre :
- Par date de compilation (janvier → avril 2026) : devhost.sys (17/01) est le plus ancien, wskmon.sys (04/04) au milieu, 844ljfpvz.sys (15/04) le plus récent, un peu moins de trois mois d'écart entre le premier et le dernier build (17/01 → 15/04).
- Par date de première observation publique (avril → juin 2026) : devhost.sys (22/04) en premier, 844ljfpvz.sys (19/06) ensuite, wskmon.sys (27/06) en dernier, soit devhost.sys → 844ljfpvz.sys → wskmon.sys dans cet ordre-là, différent de l'ordre de compilation. Un peu plus de deux mois d'écart (22/04 → 27/06).
Le certificat a donc servi sur une fenêtre de compilation d'environ trois mois, et il a été observé publiquement pendant environ deux mois, par deux rapporteurs indépendants sans lien apparent. Les noms de fichiers varient d'un build à l'autre : devhost.sys imite la convention *host.sys de Windows, 844ljfpvz.sys est un nom généré, wskmon.sys un nom plausible. Ces déguisements successifs font penser à un accès de signature détenu et réutilisé dans la durée, pas à un usage ponctuel. L'analyse détaillée des deux autres échantillons est aux §12-13.
5. Enregistrement WFP : DriverEntry
DriverEntry (entry0, 0x140001030) :
ExInitializeRundownProtectionsur une structure globale (0x1400060a0), synchronisation pour un arrêt propre.IoCreateDevicecrée un objet device nommé :\Device\WskMon(typeFILE_DEVICE_UNKNOWN). Le pointeur est conservé dans une globale (0x140006080), réutilisée plus tard comme device propriétaire des work items différés.- Remplissage de la table de dispatch du
DRIVER_OBJECT:DriverUnloadreçoit une routine de fermeture propre (§10) ; les 28 entréesMajorFunction[](CREATE, CLOSE, READ, WRITE, DEVICE_CONTROL, etc.) pointent toutes vers le même stub générique (0x140001000) qui ne fait qu'appelerIofCompleteRequestavec succès, sans rien traiter. Aucune IOCTL locale n'est implémentée, le device object n'est qu'un ancrage technique, pas un canal de contrôle. - Résolution différée des primitives de création de thread (§8).
- Enregistrement WFP complet, en transaction :
FwpmEngineOpen0→FwpmTransactionBegin0→FwpmSubLayerAdd0("WskMon Sub") →FwpsCalloutRegister1+FwpmCalloutAdd0("WskMon Callout") →FwpmFilterAdd0("WskMon Filter") →FwpmTransactionCommit0. Sur échec à n'importe quelle étape :FwpmTransactionAbort0et nettoyage en cascade.
Couche réseau ciblée : le GUID chargé pour le callout (0x140005420) correspond exactement, octet pour octet, au GUID Microsoft documenté FWPM_LAYER_STREAM_V4, DEFINE_GUID(FWPM_LAYER_STREAM_V4, 0x3b89653c, 0xc170, 0x49e4, 0xb1, 0xcd, 0xe0, 0xee, 0xee, 0xe1, 0x9a, 0x3e), vérifié contre le header source fwpmu.h, et non pas une simple ressemblance de préfixe. Confirmé indépendamment par l'usage de FwpsCopyStreamDataToBuffer0, une API qui n'existe que sur les couches "stream" (flux TCP déjà réassemblé, pas de l'inspection paquet par paquet). Le driver s'accroche donc au flux TCP IPv4 applicatif.
6. La fonction de classification : double déclencheur réseau
Pour trouver la fonction de classification (0x1400013a0), je suis remonté depuis FwpsCalloutRegister1, qui n'a qu'un appelant dans tout le binaire, jusqu'à la structure FWPS_CALLOUT1. Cette fonction inspecte chaque flux TCP qui passe par la couche Stream V4 :
cmp al, 0x7f ; test direct en tête de flux
cmp byte, 0x4e ; 'N'
cmp byte, 0x54 ; 'T'
cmp byte, 0x46 ; 'F'
→ octets magiques 7F 4E 54 46 en tête de flux (mode brut).
Si le flux commence par "POST" (50 4F 53 54), le driver copie les 1024 premiers octets du corps (FwpsCopyStreamDataToBuffer0, r8d = 0x400) et y cherche le même motif magique. Les commandes se cachent ainsi dans du trafic HTTP d'apparence légitime, hors de portée d'une inspection réseau classique.
Une fois le motif trouvé, la structure de commande est lue directement dans le flux :
| Offset | Champ | Détail |
|---|---|---|
| +4 | Type de commande | 1 octet, valeurs valides 1 à 3 (vérifié : lea eax,[rdx-1]; cmp al,2; ja fail) |
| +0x25 | Longueur du payload | 4 octets big-endian, construits par trois shl 8 / or successifs |
| +0x29 | Payload chiffré | XOR rotatif |
7. Authentification et chiffrement
HMAC-SHA256, d'après la séquence d'appels CNG : BCryptCreateHash → BCryptHashData ×2 → BCryptFinishHash → BCryptDestroyHash (fonction 0x140001bf0, appelée aussi une fois au chargement du driver). Si elle renvoie false, le paquet est jeté en silence : pas de trace, pas de réponse.
La clé XOR est en clair dans une table de 32 octets à 0x140005470 (indexée par and eax, 0x1f) :
d2 47 8a 1e f3 6b c0 95 54 29 e7 3d 81 af 66 0c
b8 72 1f e4 43 9d 5e 28 a6 0d 73 c9 3b 84 f1 52
8. Résolution dynamique des primitives de thread : le double repli
La table d'imports statique ne contient aucune fonction de création de thread. Au chargement, entry0 appelle fcn.140001d50, qui s'y prend en deux temps :
- Voie normale :
MmGetSystemRoutineAddressrésoutRtlCreateUserThreadpar son nom (chaîne UTF-16LE"RtlCreateUserThread"en.rdata), résultat mis en cache dans une globale (0x1400060c8). Si trouvé → terminé. - Voie de repli, si la résolution normale échoue : lecture du MSR
IA32_LSTAR(rdmsr,ecx = 0xc0000082), le registre modèle-spécifique contenant l'adresse du point d'entrée des syscalls (KiSystemCall64). Scan des 1024 premiers octets à partir de cette adresse à la recherche du motif d'opcode4C 8D 15(lea r10, [rip+...]), calcul manuel de la cible RIP-relative pour obtenir la base de la SSDT (System Service Descriptor Table), mise en cache dans0x1400060b8.
Une seconde fonction (0x140002770), appelée juste avant chaque tentative d'injection, transforme cette base de SSDT en pointeur utilisable :
- Parcourt la table d'export de
ntdll.dllpour localiser le stub utilisateurZwCreateThreadEx. - Vérifie que ce stub commence par le prologue syscall x64 standard (
4C 8B D1 B8=mov r10,rcx; mov eax,imm32). - Extrait le numéro de syscall codé en dur dans ce stub.
- Indexe la SSDT avec ce numéro :
entry = table[num],sar entry, 4(les 4 bits de poids faible codent le nombre d'arguments),+ table_base→ adresse noyau réelle de l'implémentation deZwCreateThreadEx. Mis en cache dans0x1400060c0.
C'est une reconstruction manuelle, et correcte, de la table de service de Windows. Elle n'utilise aucune API documentée, seulement un registre modèle-spécifique Intel et la structure binaire connue des stubs syscall. On trouve historiquement cette technique dans des rootkits et des outils de contournement d'EDR, pas dans du code amateur.
La fonction 0x1400025f0, commune aux commandes 1 et 3, ouvre le process cible (ZwOpenProcess, PROCESS_ALL_ACCESS). Elle appelle ensuite le pointeur RtlCreateUserThread s'il a été résolu, et sinon le pointeur ZwCreateThreadEx tiré de la SSDT.
9. Les trois commandes
Cible commune (commandes 1 et 3) : le driver énumère tous les process avec ZwQuerySystemInformation et compare chaque nom à la chaîne "svchost.exe". Il vérifie ensuite le compte propriétaire avec PsReferencePrimaryToken + SeQueryInformationToken(TokenUser) (classe 1, visible dans le désassemblage). La comparaison porte sur la sous-autorité du SID renvoyé : 0x12 = 18 en décimal, soit la sous-autorité du SID bien connu S-1-5-18 (NT AUTHORITY\SYSTEM). Il ne prend donc pas la première instance de svchost.exe venue, il s'assure qu'elle tourne sous le compte SYSTEM avant de la viser.
| Cmd | Fonction | Mécanisme |
|---|---|---|
0x01 |
0x140001e10 |
Parcourt le PEB du process cible pour localiser kernel32.dll, résout WinExec dans sa table d'export, déclenche l'exécution via le mécanisme de la §8 |
0x02 |
0x1400027f0 |
Convertit le chemin UTF8→Unicode (RtlUTF8ToUnicodeN), préfixe \??\ construit octet par octet, ZwCreateFile avec DesiredAccess=GENERIC_WRITE\|SYNCHRONIZE, ShareAccess=0 (exclusif), CreateDisposition=5 (FILE_OVERWRITE_IF), CreateOptions=FILE_SYNCHRONOUS_IO_NONALERT, handle ouvert avec OBJ_KERNEL_HANDLE (invisible en énumération user-mode) |
0x03 |
0x140002040 |
ZwAllocateVirtualMemory dans le process cible, copie du shellcode reçu, déclenche l'exécution via le même mécanisme que la commande 1 |
10. Arrêt propre : DriverUnload
0x1400011c0 lève un flag global (0x1400060a8, celui que la fonction de classification consulte pour savoir si elle doit ignorer les paquets), attend la fin des traitements en cours (ExWaitForRundownProtectionRelease), désenregistre proprement le callout et la sous-couche WFP, puis supprime le device object. La synchronisation kernel est correcte, ce n'est pas du code bâclé.
11. Ce qui n'existe pas dans ce binaire
- Aucune infrastructure C2 sortante. Toutes les URL présentes sont les métadonnées légitimes de la chaîne de certificats Microsoft (CRL, OCSP). Le backdoor est purement passif : il n'appelle personne, il attend qu'on lui envoie le bon paquet sur une connexion existante.
- Aucune persistance. Pas un seul import lié au registre (
ZwCreateKey,ZwSetValueKey, etc.). Le chargement initial du driver dépend d'un composant externe, absent de ce fichier. Le candidat le plus probable,devhost.sys, est analysé au §12. - Aucun chemin PDB. Il y a bien une entrée
IMAGE_DEBUG_TYPE_POGO, mais pas deCodeView, donc pas de fuite du genrecampus.py(à comparer avec l'enquête Efimer).
12. devhost.sys : primitive de lecture/écriture mémoire physique arbitraire
Trouvé au §4 en cherchant le certificat partagé.
| Champ | Valeur |
|---|---|
| SHA256 | ee8844ffd3879190fb389b0f613cb2dcdcd83375cf0a6994170a648c5ca8c479 |
| Taille | 22 104 octets |
| Compilation | 2026-01-17 (le plus ancien des trois, antérieur à wskmon.sys de plus de deux mois) |
| Première observation MalwareBazaar | 2026-04-22 |
Imports : KeRevertToUserAffinityThread, KeSetSystemAffinityThread, KeQueryActiveProcessors, MmProbeAndLockPages, MmUnlockPages, IoAllocateMdl, IoFreeMdl, plus deux symboles de données kernel importés directement : MmSystemRangeStart et MmUserProbeAddress (les bornes de séparation espace utilisateur/kernel), et NtBuildNumber.
À la différence de wskmon.sys, ce driver crée un objet device (\Device\devhost) et son lien symbolique DOS (\??\devhost). Il est donc accessible depuis le mode utilisateur par \\.\devhost, avec un vrai IOCTL, alors que le device de wskmon.sys se contentait de renvoyer un succès sans rien traiter.
Ce que fait vraiment le driver :
-
À la fin de
DriverEntry, lecture directe des registres de contrôle CPU :asm mov rax, cr3 ; base de la table de pages (PML4) du process courant mov qword [rcx + 0x10], rax mov rax, cr4 ; bits PAE/SMEP/SMAP...Ces valeurs sont stockées dans une structure de contexte, exploitable par l'appelant via l'IOCTL. -
Le handler d'E/S (
fcn.140001640, enregistré comme callback sur le device) vérifie l'IRQL courant avecmov rax, cr8avant de traiter la requête. C'est ce que fait un vrai pilote qui respecte les contraintes du kernel, pas du code écrit à la va-vite. -
Le cœur du moteur (
fcn.1400012a0) est un copieur mémoire générique par blocs de 4 Ko (0x1000) : pour chaque page,IoAllocateMdl→MmProbeAndLockPages→ obtention d'une adresse virtuelle système (via le champ MDLMappedSystemVaou, à défaut, un pointeur résolu dynamiquement à0x140004128) → copie effective (fcn.1400011e0) →MmUnlockPages→IoFreeMdl. C'est la technique MDL classique pour mapper temporairement n'importe quelle page physique en espace kernel, lire/écrire, puis démapper.
Interprétation : CR3 exposé, primitive de lecture/écriture physique arbitraire, interface accessible depuis le mode utilisateur. Mis ensemble, c'est tout ce qu'il faut pour mapper à la main un autre driver en mémoire kernel sans passer par le chargeur de Windows, donc sans la vérification de signature standard. C'est le « manual mapping », proche de ce que font des outils publics comme kdmapper. Ce driver est un candidat sérieux pour le mécanisme de chargement externe que je ne trouvais pas dans wskmon.sys seul (§11). Je n'ai toutefois aucune preuve directe d'un lien entre les deux, en dehors du certificat partagé.
Un détail d'implémentation : ce driver est construit sur le Windows Driver Framework (KMDF), et non en WDM brut comme wskmon.sys. On le voit aux appels indirects par des pointeurs de fonction résolus dynamiquement (0x140004080, 0x140004088, 0x140004090, jamais dans la table d'imports statique), ce qui correspond au mécanisme de liaison WdfVersionBind et aux API WdfRequestRetrieve*Buffer. Ça explique la fonction d'initialisation de 1177 octets en tête de DriverEntry : c'est du boilerplate de configuration de structures WDF, pas de la logique métier. J'ai cherché le code IOCTL partout, sans rien trouver. Un scan de toutes les sections du fichier, à la recherche de constantes DWORD dans la plage 0x220000 à 0x22FFFF (celle qui correspond à DeviceType = 0x22 / FILE_DEVICE_UNKNOWN, le type réellement passé à IoCreateDevice), ne renvoie aucun résultat. Les deux fonctions qui traitent la requête (fcn.140001640, fcn.1400014a0) ne font d'ailleurs aucune comparaison sur un tel code : elles ne branchent que sur des tailles de buffer et sur la validité d'adresses via MmUserProbeAddress. La conclusion la plus probable : il n'y a sans doute pas de tri par code IOCTL, et n'importe quel DeviceIoControl vers \\.\devhost déclenche la même primitive de copie, pourvu que les tailles de buffer passent la validation. C'est encore plus permissif qu'un driver à plusieurs IOCTL : un seul comportement, sans aucune porte de sélection.
13. 844ljfpvz.sys : outil minimal, fonction non déterminée
| Champ | Valeur |
|---|---|
| SHA256 | 1d9224a72e64bb2aad289edc81ea0720c764511c3e2b5beb5d0d5ce82a719abd |
| Taille | 14 936 octets |
| Compilation | 2026-04-15 (le plus récent des trois par date de compilation, voir §4) |
| Première observation MalwareBazaar | 2026-06-19 |
C'est un driver minuscule : quatre fonctions, aucun device créé, aucun lien symbolique. Il n'a que deux imports, MmUserProbeAddress et ZwSetSystemInformation, une API privilégiée qui peut modifier des paramètres système globaux.
Sa seule vraie action, dans DriverEntry :
ZwSetSystemInformation(75, &struct{0x53425342 /* "BSBS" */, ...}, 24)
La classe d'information système est 75 (0x4B), et la structure passée contient un magic "BSBS" codé en dur. Cette classe n'est pas documentée publiquement de façon fiable. Faute de source solide, je ne propose pas d'interprétation de ce qu'elle fait. Tout ce qu'on peut dire, c'est que ce driver appelle ZwSetSystemInformation avec ces paramètres. Aller plus loin serait une supposition, exactement le genre de raccourci que j'essaie d'éviter dans cette série.
Cette fois, contrairement à wskmon.sys, il y a bien une fuite PDB. Le répertoire debug contient une entrée CodeView (type 2), absente des deux autres fichiers :
F:\0316 桌面\QDDDD\驱动最新加绘制-改PTE方案\x64\Release\NewDriverMMM.pdb
Traduction approximative : 驱动 (driver), 最新 (dernier, nouveau), 加绘制 (+ rendu/dessin), -改PTE方案 (schéma PTE modifié). 绘制 (le rendu à l'écran) et la mention explicite de manipulation de PTE (Page Table Entry) sont le vocabulaire typique du milieu des cheats de jeux vidéo en Chine (overlay ESP, contournement d'anti-cheat par manipulation de la table de pages). Ce n'est pas du tout le registre d'un outillage d'espionnage ou de cybercriminalité financière. Le projet s'appelle NewDriverMMM, le dossier de travail QDDDD.
14. Empreintes de compilation : un ou plusieurs auteurs ?
J'ai comparé les Rich headers des trois fichiers, c'est-à-dire l'empreinte de leur toolchain MSVC :
| Fichier | Build compilateur (primaire) | Build linker (secondaire) | PDB |
|---|---|---|---|
wskmon.sys |
33145 | 35725 | Absent |
devhost.sys |
33145 | 35214 | Absent |
844ljfpvz.sys |
27412 | 30159 | Présent, chinois, thème cheat-gaming |
wskmon.sys et devhost.sys ont le même build de compilateur principal ; seul le composant linker diffère un peu. Ça correspond à une même machine ou un même développeur sur les ~2,5 mois qui séparent leurs compilations (une mise à jour de Visual Studio entre les deux suffirait à expliquer l'écart). La discipline OPSEC est la même sur les deux : aucun chemin ne fuit.
844ljfpvz.sys sort d'un toolchain complètement différent, sans aucun recouvrement avec les deux autres, et il laisse fuiter un chemin de développement complet. C'est une rupture nette avec la discipline OPSEC des deux premiers.
Interprétation : un certificat partagé ne prouve pas un acteur unique. C'est exactement le schéma décrit par Mandiant en 2022 (POORTRY/STONESTOP) : un accès de signature compromis, réutilisé par plusieurs parties sans lien entre elles. Ici, les indices pointent vers deux origines distinctes. wskmon.sys + devhost.sys forment sans doute un même kit, écrit par un seul auteur ou une seule équipe (même toolchain, même rigueur OPSEC). 844ljfpvz.sys semble venir d'un autre développeur, probablement du milieu chinois du cheat de jeux vidéo, qui a eu accès au même pipeline de signature compromis pour un projet sans rapport apparent avec les deux premiers.
15. Conclusion : un kit d'outils, pas un backdoor isolé
Le certificat n'a pas signé un backdoor isolé. Il a signé trois outils de nature différente, sur environ trois mois de compilation (janvier à avril 2026, §4) et deux mois d'observation publique (avril à juin) :
| Rôle probable | Fichier | Compilation |
|---|---|---|
| Primitive R/W mémoire physique (chargeur possible) | devhost.sys |
2026-01-17 |
| Backdoor réseau WFP (documenté en détail, §5-11) | wskmon.sys |
2026-04-04 |
Fonction non déterminée (ZwSetSystemInformation) |
844ljfpvz.sys |
2026-04-15 |
On ne regarde donc plus la signature d'un fichier malveillant isolé. Tout cela correspond à un kit d'outils complet (chargeur, backdoor, composant additionnel) développé et signé avec le même accès pendant plusieurs mois, très probablement par au moins deux développeurs (§14).
La chaîne reconstituée :
Certificat WHQL valide (Aolian, probablement compromis)
→ devhost.sys (candidat chargeur) : CR3 exposé + R/W mémoire physique via MDL,
accessible en mode utilisateur (\\.\devhost), compilé 3 mois avant wskmon.sys
→ wskmon.sys chargé en kernel (lien direct non prouvé, mais capacité compatible)
→ \Device\WskMon créé (aucune IOCTL réelle, simple ancrage technique)
→ Callout WFP enregistré sur FWPM_LAYER_STREAM_V4
→ Écoute passive : octets magiques 7F 4E 54 46 (TCP brut ou corps POST HTTP)
→ HMAC-SHA256 vérifié → XOR déchiffré (clé 32 octets)
→ Cmd 1/3 : svchost.exe SYSTEM ciblé (SID S-1-5-18 vérifié)
→ RtlCreateUserThread (résolution dynamique)
→ repli : lecture MSR IA32_LSTAR → scan syscall → SSDT manuelle
→ Cmd 2 : écriture fichier arbitraire (handle kernel invisible)
→ 844ljfpvz.sys : composant tiers, fonction non déterminée (ZwSetSystemInformation)
16. IOCs consolidés
| Type | Valeur |
|---|---|
SHA256 (wskmon.sys) |
495c7e5513fa7766c236e76d8520139139fc4ad7203ddcb2ccdae17bdb691979 |
Imphash (wskmon.sys) |
5c6478fa51bfe6402aa0892dc333f989 |
SHA256 (devhost.sys) |
ee8844ffd3879190fb389b0f613cb2dcdcd83375cf0a6994170a648c5ca8c479 |
SHA256 (844ljfpvz.sys) |
1d9224a72e64bb2aad289edc81ea0720c764511c3e2b5beb5d0d5ce82a719abd |
| Nom de device | \Device\WskMon, \Device\devhost |
| Octets magiques réseau | 7F 4E 54 46 |
| Clé XOR (32 octets) | d2478a1ef36bc0955429e73d81af660cb8721fe4439d5e28a60d73c93b84f152[^xorkey] |
| Cible d'injection | svchost.exe (instance privilégiée uniquement) |
| Certificat (thumbprint) | 2e8072ded075c6b6c0df8c364e9d12319577114991f2455ebc4b364b367f7dba |
| Certificat (numéro de série) | 330000013c4a61fb3578d2b6dd00000000013c |
PDB (844ljfpvz.sys uniquement) |
F:\0316 桌面\QDDDD\驱动最新加绘制-改PTE方案\x64\Release\NewDriverMMM.pdb |
[^xorkey]: Regroupée en deux lignes de 16 octets au §7 pour la lecture, donnée ici en une seule chaîne continue de 32 octets pour pouvoir copier-coller l'IOC tel quel.
17. Détection
- La présence du device
\Device\WskMonsur une machine (et de\Device\devhostsidevhost.sysest là aussi). On le voit avec WinObj ou en énumérant\Device\. - Du trafic réseau contenant la séquence
7F 4E 54 46, en tête de flux TCP ou dans les 1024 premiers octets d'un corpsPOST. - Le certificat lui-même (empreinte ci-dessus), tant qu'il n'est pas révoqué.
18. Ce que cette analyse ajoute au writeup public existant
| Élément | Nextron (public) | Cette analyse |
|---|---|---|
| Octets magiques, HMAC, XOR | Oui | Confirmé indépendamment, clé XOR extraite en clair |
| Mécanisme d'exécution (cmd 1/3) | "RtlCreateUserThread dans svchost.exe" | Confirmé + double repli SSDT/MSR entièrement documenté, jamais mentionné |
| Persistance | Non documentée | Confirmée absente de ce binaire (aucun import registre) |
Objet device \Device\WskMon |
Non mentionné | Identifié, IOC exploitable |
| Couche réseau WFP exacte | Non précisée | Stream V4, confirmée par GUID + API |
CreateDisposition exact |
Non précisé | FILE_OVERWRITE_IF (5), confirmé |
| Contexte de signature | Non creusé | Lien avec le précédent POORTRY/STONESTOP (2022), hypothèse de compte compromis plutôt que complice |
devhost.sys / 844ljfpvz.sys |
Non mentionnés | Trouvés par recherche du certificat partagé, analysés en détail (§12-14), candidat chargeur identifié |
19. Documents associés et divulgation
poc_trigger.py: un générateur de paquet de déclenchement (magic bytes + HMAC-SHA256 + XOR), reconstruit entièrement à partir du désassemblage. Il sert de preuve d'exploitabilité pour le signalement au Microsoft Security Response Center. Par défaut il n'envoie rien : il faut lui donner une cible explicite et confirmer à la main.
Note sur la divulgation. J'ai fait un signalement au Microsoft Security Response Center avant de publier cette analyse, selon les pratiques de divulgation coordonnée. La société dont le nom apparaît dans la signature (Aolian) est très probablement une victime dans cette histoire (§3), pas une complice.