# 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.sys` et des deux autres outils signés avec le même certificat (`devhost.sys`, `844ljfpvz.sys`). Le [PoC de déclenchement](writeups/wskmon-whql-backdoor/poc_trigger.py) 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"](https://www.nextron-systems.com/2026/06/26/anatomy-of-a-whql-signed-windows-filtering-platform-wfp-kernel-resident-network-backdoor) (26/06/2026), avec un [PoC client Python](https://gist.github.com/pierrehpezier/fe1977d390a45e64d522e657fb8d3640). 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`) :

1. `ExInitializeRundownProtection` sur une structure globale (`0x1400060a0`), synchronisation pour un arrêt propre.
2. `IoCreateDevice` crée un objet device **nommé** : `\Device\WskMon` (type `FILE_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.
3. Remplissage de la table de dispatch du `DRIVER_OBJECT` : `DriverUnload` reçoit une routine de fermeture propre (§10) ; les **28 entrées `MajorFunction[]`** (CREATE, CLOSE, READ, WRITE, DEVICE_CONTROL, etc.) pointent **toutes vers le même stub générique** (`0x140001000`) qui ne fait qu'appeler `IofCompleteRequest` avec 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.
4. Résolution différée des primitives de création de thread (§8).
5. Enregistrement WFP complet, en transaction : `FwpmEngineOpen0` → `FwpmTransactionBegin0` → `FwpmSubLayerAdd0` ("WskMon Sub") → `FwpsCalloutRegister1` + `FwpmCalloutAdd0` ("WskMon Callout") → `FwpmFilterAdd0` ("WskMon Filter") → `FwpmTransactionCommit0`. Sur échec à n'importe quelle étape : `FwpmTransactionAbort0` et 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 :

1. **Voie normale :** `MmGetSystemRoutineAddress` résout `RtlCreateUserThread` par son nom (chaîne UTF-16LE `"RtlCreateUserThread"` en `.rdata`), résultat mis en cache dans une globale (`0x1400060c8`). Si trouvé → terminé.
2. **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'opcode `4C 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 dans `0x1400060b8`.

Une seconde fonction (`0x140002770`), appelée juste avant chaque tentative d'injection, transforme cette base de SSDT en pointeur utilisable :

1. Parcourt la table d'export de `ntdll.dll` pour localiser le stub utilisateur `ZwCreateThreadEx`.
2. Vérifie que ce stub commence par le prologue syscall x64 standard (`4C 8B D1 B8` = `mov r10,rcx; mov eax,imm32`).
3. Extrait le **numéro de syscall** codé en dur dans ce stub.
4. 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 de `ZwCreateThreadEx`. Mis en cache dans `0x1400060c0`.

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 de `CodeView`, donc pas de fuite du genre `campus.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 :**

1. À 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.

2. Le handler d'E/S (`fcn.140001640`, enregistré comme callback sur le device) vérifie l'IRQL courant avec `mov rax, cr8` avant 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.

3. 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 MDL `MappedSystemVa` ou, à 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\WskMon` sur une machine (et de `\Device\devhost` si `devhost.sys` est 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 corps `POST`.
- 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`](writeups/wskmon-whql-backdoor/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.
