Le format de fichier ruche regf, en pratique
8 min de lecture
La plupart des analystes traitent les ruches comme des blobs opaques qu'un outil transforme en arbre. Cela marche jusqu'à ce que ça ne marche plus. Le jour où vous avez une ruche partiellement corrompue, ou une ruche qui s'ouvre bien dans un outil et perd silencieusement la moitié des clés dans un autre, vous avez besoin de savoir ce qui est réellement dans le fichier. Le format regf n'est pas compliqué. Il est juste impitoyable.
Le bloc de base de 4096 octets
Une ruche regf commence par un seul bloc de 4096 octets. Les quatre
premiers octets sont le magique regf. Vient ensuite une paire de
numéros de séquence (Primary et Secondary), un FILETIME pour la
dernière écriture, les champs version (majeur et mineur ; Windows 10+
est en 1.5 dans la plupart des cas), le type de fichier, le format de
fichier, et le décalage de la cellule de clé racine. Le bloc porte
aussi un nom UTF-16 de 64 caractères (le chemin de la ruche au
moment de sa création), et à la fin un checksum XOR sur les 508
premiers octets.
Deux choses comptent opérationnellement :
La paire de numéros de séquence est comment le noyau suit si la ruche
a été proprement arrêtée. Si Primary ne correspond pas à
Secondary, la ruche n'a pas été flushée proprement, et les journaux
de transactions doivent être rejoués avant que le fichier ne soit
cohérent. Les outils qui sautent le rejeu de journaux vous donneront
une vue partielle des données et ne vous avertiront pas.
Le checksum est la première chose à vérifier sur toute ruche que vous n'avez pas personnellement acquise. Une ruche au checksum de bloc de base cassé a été altérée, tronquée ou corrompue en transit. Aucun n'est une bonne nouvelle.
Blocs HBIN : les conteneurs d'allocation
Après le bloc de base, le fichier est une séquence de blocs hbin,
chacun commençant par le magique de quatre octets hbin suivi de
son décalage (relatif au début des données de la ruche, pas du
fichier) et de sa taille. Les HBIN font 4096 octets ou un multiple.
Ils sont l'équivalent registre des pages mémoire.
À l'intérieur de chaque HBIN, les vraies données vivent dans des cellules. Une cellule commence par une taille 32 bits signée. Si la taille est négative, la cellule est allouée. Si positive, elle est libre. La valeur absolue de la taille inclut le champ taille lui-même. C'est l'endroit le plus courant où les analyseurs maison se trompent : parcourir les cellules en lisant la taille comme non signée, manquer le drapeau d'allocation, et ensuite soit sauter des données vivantes soit marcher au-delà de la fin du HBIN dans l'en-tête du suivant.
Les cellules libres ne sont pas mises à zéro. C'est la base de la récupération de clés supprimées, que je couvrirai dans un autre article.
Les types de cellules qui comptent réellement
À l'intérieur des cellules vous obtenez un petit zoo d'enregistrements typés. Les signatures sont des étiquettes ASCII de deux octets que vous reconnaîtrez immédiatement après avoir regardé quelques ruches dans un éditeur hexadécimal :
nk: un nœud de clé. Nom, pointeur parent, pointeur de liste de sous-clés, pointeur de liste de valeurs, pointeur de descripteur de sécurité, pointeur de classname, FILETIME LastWrite, et divers comptes. C'est la colonne vertébrale de l'arbre.vk: une valeur. Nom, type de données (REG_SZ,REG_DWORD,REG_BINARY, et consorts), longueur de données, et soit données inline (pour les valeurs de 4 octets ou moins) soit un décalage vers une cellule de données.sk: un descripteur de sécurité. Stocké une fois par descripteur unique et partagé entre clés via une liste chaînée de références. La ruche les déduplique.lh,lf,li,ri: types de listes de sous-clés.lhest le défaut moderne et est une liste triée de paires(name_hash, nk_offset).lfest la variante plus ancienne.liest une liste d'index plate.riest un index d'index utilisé quand il y a trop de sous-clés pour tenir dans un seullh. Les vraies ruches mélangent celles-ci selon comment l'arbre de sous-clés a grandi.db: grosses données. Quand la charge utile d'une valeur dépasse 16344 octets, elle est divisée en une chaîne de segments stockés sous un enregistrementdb. Facile à manquer si vous supposez que toutes les données de valeurs sont dans une cellule.
Un analyseur qui gère nk, vk, sk et lh couvre peut-être 95 %
des ruches réelles. Un analyseur qui gère aussi lf, li, ri et
db est celui que vous voulez vraiment.
L'arbre de nœuds de clés
La clé racine est au décalage listé dans le bloc de base. À partir de
là, c'est un parcours d'arbre simple : lire le nk racine, suivre
son pointeur de liste de sous-clés, déréférencer chaque entrée pour
obtenir les nk enfants, récurser. Les valeurs pendent de chaque
nk via le pointeur de liste de valeurs, qui pointe vers une
cellule contenant un tableau de décalages vk.
Deux pièges :
L'encodage du nom sur les enregistrements nk et vk peut être ASCII
ou UTF-16. Un drapeau dans l'en-tête de l'enregistrement dit lequel.
Les outils qui supposent l'un ou l'autre mojibakeront les valeurs
qu'ils analysent depuis des ruches aux noms non-ASCII. Courant dans
les installations Windows localisées.
L'horodatage LastWrite sur un nk est mis à jour quand la clé est
modifiée, ce qui inclut quand ses sous-clés ou valeurs changent. Il
n'est pas mis à jour quand les données d'une valeur changent sans
que la valeur ne soit ajoutée ou retirée. Cette nuance mord les gens
qui essaient d'utiliser LastWrite comme chronologie d'activité à
grain fin.
La table des descripteurs de sécurité
Chaque nk porte un pointeur vers une cellule sk. Les cellules
sk forment une liste doublement chaînée (flink/blink) que le
noyau parcourt pour dédupliquer les descripteurs. Si vous regardez
des ACL registre hors ligne (et vous devriez, pour tout hôte où
l'attaquant peut avoir caché des clés via les permissions), c'est là
que vivent les données. Les descripteurs eux-mêmes sont des
structures SECURITY_DESCRIPTOR standard.
Usage pratique : extraire le descripteur sur
HKLM\SYSTEM\CurrentControlSet\Services\<nom> pour tout service qui
semble suspect. S'il restreint l'accès en lecture à SYSTEM seulement,
c'est du masquage intentionnel. Les vrais services Microsoft ne font
pas cela.
Journaux de transactions et le schéma double-LOG
Chaque ruche a deux fichiers de journal de transactions, .LOG1 et
.LOG2. Avant Windows 8.1, il n'y en avait qu'un. Le schéma à double
journal existe pour que le noyau puisse écrire dans un journal en
gardant l'autre intact, empêchant une perte de courant pendant
l'écriture du journal de tuer les deux copies.
Un journal est une séquence d'entrées de pages sales. Chaque entrée dit "au décalage N dans la ruche primaire, les M prochains octets doivent être ceci". Le rejeu est conceptuellement trivial : appliquer les entrées dans l'ordre. En pratique, vous devez aussi valider les numéros de séquence, gérer le cas où la ruche primaire a été écrite depuis que le journal a été généré, et détecter les écritures déchirées.
Les entrées de pages sales peuvent contenir des données qui n'apparaissent jamais dans la ruche principale. Si le système a planté entre l'écriture du journal et le flush de la ruche, rejouer le journal vous donne des enregistrements que le registre vivant n'a jamais vus.
Si vous ignorez les journaux, vous regardez une ruche périmée. Rejouez toujours.
Pourquoi c'est plus difficile qu'on ne le suppose
Le format lui-même est documenté. Les notes d'ingénierie inverse de Maxim Suhanov sont excellentes. Le problème est le volume de cas limites :
- Certaines valeurs revendiquent une longueur de données qui dépasse la cellule qui les contient, et la spec dit que l'analyseur doit tronquer. Différents outils gèrent cela différemment.
- AmCache est une ruche regf mais Microsoft l'utilise légèrement différemment, avec des particularités de format mineures que certains analyseurs ne gèrent pas.
- Les ruches sauvegardées avec
reg saveversus copiées avecesentutl /vssversus extraites d'une image mémoire ne sont pas identiques au niveau octet, et des différences mineures (octets de fin supplémentaires, valeurs d'en-tête légèrement différentes) peuvent perturber les analyseurs plus stricts.
Les outils matures gèrent cela. Le script Python fraîchement écrit dans le billet de blog de quelqu'un, souvent non.
Outils à connaître
- yarp de Maxim Suhanov. La bibliothèque de plus bas niveau et la plus proche d'une implémentation de référence. Si yarp se plaint, écoutez.
- libregf de Joachim Metz. La bibliothèque C derrière la plupart des outils forensiques. Mature, rapide, conservative.
- RegRipper d'Harlan Carvey. La couche de plugins par-dessus
Parse::Win32Registry. Comment la plupart des analystes consomment réellement les ruches.
Recouper deux d'entre eux sur toute ruche qui compte est une assurance bon marché. Si yarp et RegRipper s'accordent sur le compte de clés et les LastWrite, vous pouvez faire confiance aux données. S'ils sont en désaccord, vous avez une histoire à suivre.
Pour aller plus loin
- Maxim Suhanov, spécification du format de fichier registre Windows : la spec canonique issue d'ingénierie inverse.
- Joachim Metz, documentation libregf : documentation du format écrite du point de vue de l'implémenteur d'analyseur.
- Microsoft, Registry Hives : la référence du fournisseur. Mince sur les détails de format, mais utile pour la sémantique des ruches.
- Le blog Windows Incident Response d'Harlan Carvey : décennies de notes d'analyse registre.
Un fichier ruche n'est pas une boîte noire. Passez un après-midi avec un éditeur hexadécimal et la spec ci-dessus, et la prochaine fois qu'un outil vous donne des résultats qui semblent faux, vous saurez dire si l'outil a tort ou si la ruche a tort.