# 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/volatility3` `develop`.

---

## 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.out` et `netscan-*.out` par build.
- Les chiffres de ce billet ont été recontrôlés contre ces sorties.
