windows.netscan vide sur Windows 11 24H2 : l'objet TCP est 8 octets plus loin, et sa disposition change à chaque build
Catégorie : Volatility
Résumé
Sur une image mémoire de Windows 11 24H2, 25H2 ou 26H1, windows.netscan ne renvoie rien. Il trouve bien les allocations de pool TcpL, TcpE et UdpA, mais aucun objet ne passe la validation. Sur 21H2 à 23H2 il renvoie quelque chose, mais avec des ports à 0, pas de propriétaire ou pas de date. Deux causes. Depuis le build 26100, les objets _TCP_LISTENER, _TCP_ENDPOINT et _UDP_ENDPOINT commencent 8 octets après le corps de l'allocation. Et la disposition de _TCP_ENDPOINT change à chaque build, y compris entre deux révisions de 26100. J'ai retrouvé les offsets sur de vraies connexions, construit quatre tables de symboles, et validé sur six builds. Ce travail est local : branche netscan-win11-24h2, pas encore de PR.
État : branche locale, PR à venir vers
volatilityfoundation/volatility3develop.
Environnement
Les six VM Windows 11 du lab (21H2 22000.318, 22H2 22621.382, 23H2 22621.2428, 24H2 26100.1742, 25H2 26100.6584, 26H1 28000.1340). Volatility 3 develop amont, 77ff0b19 (v2.28.3). Capture : restauration du snapshot, démarrage, injection DRAKVUF d'une activité (notepad, mspaint, un listener TCP en loopback, un SYN vers la passerelle, schtasks, net user, reg query, klist), puis xl pause et vmi-dump-memory.
1. D'où ça vient : une matrice de 104 plugins
Après la PR sur l'hibernation, j'ai passé la RAM vive des six VM dans tous les plugins windows.* de Volatility, 103 à 104 par build, deux rounds, timeout 1 500 s par plugin. Bilan : 62 plugins passent partout, 23 ont le même résultat non concluant partout (il leur faut des arguments, des règles YARA, un crash dump, ou ils sont vides par nature), et une liste varie selon le build.
Le tri de cette liste a pris plus de temps que la matrice elle-même, parce que la plupart ne sont pas des bugs Volatility :
- résidence mémoire : hashdump sur 25H2 répond « Hbootkey is not valid », trompeur, en fait SAM\Domains\Account n'est pas chargé ; skeleton_key_check sur 24H2 ne voit que 2 pages résidentes sur 24 de cryptdll.dll ; scheduled_tasks et userassist sur 21H2, clés non résidentes ;
- données de la VM : amcache vide sur 26H1 ;
- attendu : crashinfo, yarascan, memmap ;
- intermittent entre les deux rounds : hashdump et lsadump sur 23H2, certificates sur 21H2, debugregisters sur 22H2, direct_system_calls.
Restent deux vrais bugs. netscan, traité ici. Et les plugins GUI : desktops et deskscan vides dès 24H2, windows.windows vide dès 23H2, alors que windowstations marche. Là je n'ai rien corrigé : tagDESKTOP et tagWND ont changé, la dernière table faite main est gui-win10-19577, et win32kbase.pdb n'est pas sur le serveur de symboles Microsoft (404 pour 23H2 et 24H2). Ce sera du reverse manuel, après la PR netscan.
2. Le symptôme, build par build
| Build | netscan amont |
|---|---|
| 21H2 | ports lus à 0, propriétaire « - » |
| 22H2, 23H2 | propriétaire « - », CreateTime N/A |
| 24H2, 25H2, 26H1 | rien du tout |
3. Première cause : l'objet est 8 octets plus loin
windows.netstat atteint les mêmes objets par la table de partition, sans dépendre du pool, et marche sur les mêmes images. En comparant les deux sur 3 images et 2 builds, une cinquantaine d'objets chacune, chaque listener et chaque endpoint UDP rapporté par netstat est exactement 8 octets après le candidat scanné par netscan. Sur 26100 et 28000, l'objet commence 8 octets après le corps de l'allocation.
Correctif (commit 28975732, 14 octobre) : pool_object_offset() renvoie 8 quand MinorVersion >= 26100, et l'objet est recréé à offset + 8. Les builds 22000, 22621 et 26100 sont ajoutés à la table des versions. Avec ça, 24H2 et plus renvoient les listeners et l'UDP, mais les connexions TCP restent fausses.
4. Deuxième cause : _TCP_ENDPOINT change à chaque build
Pour trouver les champs sans PDB (celui de tcpip.sys n'est pas utilisable pour ça), j'ai fabriqué des connexions dont je connais tout. Script netscan-endpoints.sh : deux curl vers un listener 172.16.99.1:8080 qui accepte sans répondre, donc deux connexions ESTABLISHED qui restent ouvertes, plus un SYN vers un port fermé. Puis ttcb-re.py dans volshell : scan du couple de ports en big-endian, remontée à l'allocation TTcb, et annotation des champs autour : le pointeur vers l'EPROCESS de curl, un FILETIME plausible, les pointeurs vers INETAF et LOCAL_ADDRESS, et un u32 à 4 pour ESTABLISHED.
Offsets trouvés, relatifs à la base de l'objet (donc pool + 8 sur 26100 et plus) :
| Build | State | LocalPort / RemotePort | Owner | CreateTime | Taille |
|---|---|---|---|---|---|
| 20348 (table existante) | 0x6c |
0x70 / 0x72 |
0x2f0 |
0x308 |
632 |
| 22000 (21H2) | 0x70 |
0x74 / 0x76 |
0x2f8 |
0x308 |
784 |
| 22621 (22H2, 23H2) | 0x6c |
0x70 / 0x72 |
0x358 |
0x368 |
880 |
| 26100.1742 (24H2) | 0x68 |
0x6c / 0x6e |
0x340 |
0x350 |
856 |
| 26100.6584 (25H2) et 28000.1340 (26H1) | 0x68 |
0x6c / 0x6e |
0x348 |
0x358 |
864 |
_TCP_LISTENER, _UDP_ENDPOINT, InetAF, AddrInfo et ListEntry ne bougent pas. Quatre tables JSON : netscan-win10-22000-x64.json, -22621-, -26100- et -26100-1742-, avec la contrainte TTcb étendue à chacune.
Un piège à part : la révision de 26100 n'est pas lisible en mémoire. NtBuildLab ne la contient pas, et la ressource version de tcpip.sys n'est pas résidente. La table par défaut pour 26100 est donc celle des builds « maintenus » (.6584 et 28000), et l'entrée (26100, 1742) n'est prise que si le FileVersion de tcpip est lisible. C'est fragile, je le dis dans le texte de la PR.
5. Validation
Sur les six builds, les deux connexions curl en ESTABLISHED apparaissent avec leur propriétaire dans netstat et dans netscan. Listeners et UDP : diff à zéro contre la référence du round 1. Sur 21H2 à 23H2, la sortie est identique à l'ancien code pour ce qui marchait déjà. Les lignes « diff 116, 131, 89 » sur le round 2 de 24H2, 25H2 et 26H1 sont un artefact : la référence venait du dump du round 1, pas du même dump.
6. État et ce que je n'ai pas fait
- Branche locale
netscan-win11-24h2: un commit (le +8) et un diff non commité avec les quatre JSON (les dispositions par build). Rien poussé, pas de PR. Avant de l'ouvrir : un seul commit, ruff 0.17.1, texte de PR. - Pas de désassemblage de
tcpip.sys: tout vient de la mémoire et de connexions connues. - Une seule révision « d'origine » de 26100 (.1742) et deux « maintenues ». Un autre build peut avoir une autre disposition.
- Les plugins GUI (
tagWND,tagDESKTOP) ne sont pas corrigés.
Sources
- Scripts :
netscan-endpoints.sh,ttcb-re.py,ram-campaign*.sh,ram-summarize.py(en local, fournis sur demande). - Sorties :
SUMMARY-ram.md(matrice des 104 plugins, deux rounds),campaign.log,netstat.outetnetscan-*.outpar build. - Les chiffres de ce billet ont été recontrôlés contre ces sorties.