SOMMAIRE
0 pourquoi une couche d'autorisation
1 le répertoire de manifests et son acl
2 le schéma de manifest
3 le cli wedr-plugin
4 sha-256 via bcrypt cng
5 doctor — détection de dérive
6 hot reload et auto-launch
──[ 0. Pourquoi une Couche D'Autorisation ]──
L'ACL du pipe de la Partie 7 restreint qui peut se connecter au niveau OS —
seuls les processus tournant comme le même utilisateur que l'agent ou comme
`SYSTEM`. Ça écarte l'attaquant occasionnel mais ce n'est pas assez en soi. Un
processus privilégié est suffisamment privilégié pour être un vecteur d'attaque
: un malware qui tourne en `SYSTEM`, un installeur qui dépose un binaire
hostile, un agent tiers buggué. Le serveur de plugins a besoin d'une seconde
porte qui liste, par identité, quels programmes sont autorisés à pousser de la
télémétrie au travers.
Cette porte, c'est le **répertoire de manifests**. Un fichier JSON par plugin
autorisé, nommé d'après son UUID. L'agent lit le répertoire au démarrage, le
surveille pour les changements, et rejette tout `Hello` dont le `plugin_id` n'a
pas de manifest correspondant. `wedr-plugin` est l'outil côté administrateur qui
écrit dans ce répertoire.
──[ 1. Le Répertoire de Manifests et Son ACL ]──
%ProgramData%\WazabiEDR\plugins\
├── 8f3c1d8e-5a8b-4ad0-94d2-cab9b1d0e2a0.json
├── 0a1b2c3d-aaaa-bbbb-cccc-dddddddddddd.json
└── …
Le chemin est **codé en dur** dans l'agent. Le rendre configurable par un
opérateur non-privilégié défèrait l'ancre de confiance — n'importe qui capable
de rediriger l'agent vers un autre répertoire pourrait écrire son propre
manifest "autorisé" dans ce répertoire.
`%ProgramData%` s'expand vers `C:ProgramData` sur une installation par défaut.
Le sous-répertoire `WazabiEDRplugins` est créé à l'installation avec une ACL qui
n'accorde l'accès en écriture qu'au groupe Administrators. Les autres principaux
peuvent lire le répertoire (et lire le contenu individuel des manifests) mais ne
peuvent pas ajouter, modifier, ou retirer des fichiers. Cette unique ACL, c'est
la frontière de sécurité ; la vérification du `Hello` par l'agent est un
exécuteur de ce que l'ACL a déjà filtré.
──[ 2. Le Schéma de Manifest ]──
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginManifest {
pub plugin_id: String, // UUID v4 (aussi nom de fichier)
pub name: String, // libellé humain
pub vendor: String,
pub expected_path: String, // chemin absolu sur cet hôte
#[serde(default)]
pub expected_sha256: Option<String>, // 64 caractères hex
#[serde(default)]
pub expected_signer: Option<String>, // DN du sujet Authenticode
#[serde(default)]
pub revoked: bool,
#[serde(default)]
pub enrolled_at: Option<String>,
#[serde(default)]
pub auto_launch: bool,
}
Cette struct est **byte-identique** à celle dans
`WazabiEDR_Agent::plugin::manifest::PluginManifest`. Les deux fichiers portent
des commentaires qui pointent l'un vers l'autre ; un test d'intégration
round-trippe un manifest à travers les deux représentations.
La duplication est volontaire. Hisser le type dans une crate partagée forcerait
l'agent à prendre une dépendance sur cette crate, et le CLI prend la même
dépendance — mais le CLI est destiné à rester petit et minimaliste (pas de
features `windows-sys` pour les événements kernel, pas de types de format
d'événements, pas de crates transitives côté driver). Une duplication de type
avec un test de round-trip est moins chère que de traîner la surface de
dépendances de l'agent dans le CLI opérateur.
Trois champs composent le check d'identité :
`expected_path` est matché contre le chemin d'image du processus qui se connecte
au handshake. L'agent obtient le PID du processus via
`GetNamedPipeClientProcessId` et résout le chemin via
`QueryFullProcessImageNameW`.
`expected_sha256` est le SHA-256 du binaire tel qu'il était à l'enrôlement.
L'agent recalcule le hash de l'image *en cours d'exécution* au handshake (le
même binaire sur disque a pu être remplacé ; ce qui compte, c'est ce qui est
chargé dans le processus) et compare. Un mismatch donne `Reject{reason:
"sha256_mismatch"}`.
`expected_signer` est le DN (distinguished name) optionnel du sujet Authenticode
(Authenticode : le schéma de code-signing de Microsoft ; le *subject DN* est
l'identité au style LDAP du sujet du certificat de signature, par exemple
`CN=Acme Corp, O=Acme, C=US`). S'il est présent, l'agent vérifie la signature
Authenticode du binaire en cours contre ce sujet. S'il est absent, l'agent
s'appuie sur le SHA-256 seul (le mode `--allow-unsigned` à l'enrôlement).
──[ 3. Le CLI wedr-plugin ]──
Le CLI a une surface réduite : enroll, list, show, doctor, revoke, unrevoke,
toggle auto-launch, remove. Le workflow quotidien, c'est deux commandes :
wedr-plugin enroll C:\Path\To\acme-plugin.exe \
--name "Acme Telemetry" \
--vendor "Acme Corp" \
--auto-launch
wedr-plugin doctor
`enroll` génère un UUID (via `BCryptGenRandom`), hash le binaire, écrit le
manifest. L'UUID est imprimé sur stdout ; l'opérateur le pipe dans
l'environnement du plugin comme `WEDR_PLUGIN_ID` (Partie 8). Le flag
`--auto-launch` est l'opt-in au superviseur couvert en Section 6.
`update` re-hash après un rebuild — sans ça, un manifest avec `expected_sha256`
épinglé rejetterait le nouveau build :
wedr-plugin update <PLUGIN_ID>
wedr-plugin update <PLUGIN_ID> C:\New\Path\acme-plugin.exe
Deux habitudes opérationnelles méritent d'être exposées :
`wedr-plugin doctor` après chaque changement qui touche un manifest. La commande
doctor (Section 5) parcourt chaque manifest du répertoire et rapporte la dérive
entre le manifest et le binaire sur disque. Un opérateur qui a rebuildé un
plugin et oublié de faire `update` l'apprend de doctor en quelques secondes.
`wedr-plugin revoke` plutôt que `wedr-plugin remove` pour les incidents. Revoke
bascule le champ `revoked` mais garde le manifest ; l'agent rejette les
handshakes ultérieurs pour ce plugin (hot reload, Section 6) sans perdre la
trace d'audit. Remove supprime le fichier en entier. Revoke est réversible
(`wedr-plugin unrevoke`) ; remove ne l'est pas.
Le CLI parse les arguments à la main plutôt que d'utiliser `clap` ou
`structopt`. Le raisonnement est le même qu'ailleurs dans le projet : garder le
binaire petit, garder le graphe de dépendances peu profond, accepter que le
parsing de flags fait main soit cinquante lignes et un petit coût ponctuel.
──[ 4. SHA-256 via BCrypt CNG ]──
Le CLI calcule SHA-256 sur le binaire pour remplir `expected_sha256`. Il ne
dépend pas de la crate `sha2`. À la place, il appelle dans la Cryptography Next
Generation de Windows (*CNG, Cryptography Next Generation* : l'API de
cryptographie moderne de Windows, exposée principalement à travers la
bibliothèque `bcrypt.dll` ; le prédécesseur, c'était CryptoAPI / CAPI dans
`advapi32.dll`) :
BCryptOpenAlgorithmProvider(&mut alg, BCRYPT_SHA256_ALGORITHM, ptr::null(), 0);
BCryptCreateHash(alg, &mut hash, ptr::null_mut(), 0, ptr::null(), 0, 0);
let mut buf = [0u8; 64 * 1024];
loop {
ReadFile(h, buf.as_mut_ptr(), buf.len() as u32, &mut read, ptr::null_mut());
if read == 0 { break; }
BCryptHashData(hash, buf.as_ptr(), read, 0);
}
BCryptFinishHash(hash, digest.as_mut_ptr(), 32, 0);
Deux raisons d'éviter `sha2` :
L'agent vérifie les hash au handshake en utilisant les mêmes primitives CNG.
Utiliser une implémentation SHA-256 Rust côté CLI et CNG côté agent créerait
deux implémentations distinctes qui doivent rester équivalentes en sortie pour
toujours. Partager CNG des deux côtés retire cette obligation — ils appellent
dans le même code fourni par l'OS.
Le binaire du CLI est petit (~200 Kio strippé). Tirer `sha2` et ses bits
transitifs multiplierait ça par à peu près cinq. Ce n'est pas catastrophique,
mais ça déplace le CLI dans une direction qu'on a explicitement écartée.
Le même raisonnement s'étend à la génération d'UUID : `BCryptGenRandom` au lieu
de la crate `uuid`. Le `uuid.rs` local fait à peu près un écran de code.
──[ 5. doctor — Détection de Dérive ]──
$ wedr-plugin doctor
✓ Acme Telemetry 8f3c1d8e-…
✗ Beta Logger 0a1b2c3d-… binaire absent à expected_path
✗ Gamma Sentinel 5c5c5c5c-… sha256 mismatch
attendu : 9ab1…
actuel : f4b9…
✓ Defender Bridge 7e7e7e7e-…
`doctor` parcourt chaque manifest du répertoire et vérifie trois propriétés :
1. binaire présent à expected_path
2. SHA-256 du binaire matche expected_sha256
3. le manifest valide contre le schéma
Le code de sortie est non nul si un plugin échoue à un check, donc la commande
compose dans les scripts CI et monitoring (`wedr-plugin doctor || alert`). La
table rendue, c'est pour l'opérateur ; les scripts lisent le code de sortie.
──[ 6. Hot Reload et Auto-Launch ]──
L'agent n'a pas besoin d'être redémarré après `enroll`, `revoke`, `update`, ou
`remove`. Il rescanne `%ProgramData%WazabiEDRplugins` toutes les 5 secondes et
met à jour sa map en mémoire des plugins autorisés. Une révocation prend effet à
la prochaine tentative de handshake depuis ce plugin.
L'intervalle de 5 secondes est un trade volontaire. Un intervalle de 1 seconde
brûlerait des syscalls pour aucun gain opérationnel ; un intervalle de 30
secondes laisserait des révocations trop longues après une réponse à incident.
Cinq secondes, c'est le compromis.
`auto_launch` est l'opt-in au superviseur. Quand c'est posé, le superviseur de
l'agent (brièvement mentionné en Partie 5 comme `spawn_supervisor`) spawn le
processus du plugin au démarrage de l'agent et le redémarre sur crash avec un
backoff exponentiel (cap 60 secondes). Le superviseur lit `auto_launch` *au
démarrage de l'agent uniquement* — basculer ce flag via `wedr-plugin
auto-launch` ne spawn pas un nouveau processus plugin tant que l'agent ne
redémarre pas. Le help text du CLI le dit, parce que cette contrainte est facile
à oublier et produit de la confusion "pourquoi mon plugin ne démarre pas".
Ça referme l'histoire d'autorisation des plugins. Prochain post : un vrai plugin
construit par-dessus tout ça.