Jump List Parser
Retour au blog

Analyser AutomaticDestinations en Python, à la dure

2026-06-154 min de lecture

JLECmd analyse déjà ces fichiers correctement, donc la raison honnête d'écrire le vôtre est l'intégration : vous voulez que les événements Jump List circulent dans un pipeline, un schéma personnalisé, un outil de triage, ou un notebook, sans appeler un binaire Windows et gratter du CSV. C'est une bonne raison. "Parce que je ne fais pas confiance à l'outil existant" n'en est pas une, et vous devriez valider ce que vous construisez contre JLECmd avant de vous y fier.

Voici le chemin à travers le fichier, en Python, avec les parties qui font réellement trébucher les gens mises en évidence. La disposition des champs au niveau de l'octet vit dans l'article sur le format de fichier automaticDestinations ; ici on parle d'y accéder en code.

Le conteneur est OLE, donc commencez par là

Un fichier .automaticDestinations-ms est un fichier composé OLE (l'ancien format de stockage structuré / CFBF). Ce seul fait décide de votre première dépendance. Utilisez olefile :

import olefile

path = "1b4dd67f29cb1962.automaticDestinations-ms"
ole = olefile.OleFileIO(path)

# Flux : les flux numérotés contiennent un LNK chacun, plus un flux "DestList".
for entry in ole.listdir():
    print(entry)        # ex. ['1'], ['2'], ['DestList']

Les flux numérotés (1, 2, 3, ...) contiennent chacun un seul raccourci Windows, la même structure sur disque qu'un .lnk autonome. DestList est l'index qui les relie et porte les métadonnées que vous venez vraiment chercher : le nom d'hôte, le compteur d'accès, la date de dernier accès, l'état d'épinglage, et à quel flux numéroté chaque entrée correspond.

Premier piège, avant toute analyse : sur un système actif, le fichier est verrouillé par explorer.exe. OleFileIO échouera ou lira un fichier partiel. Travaillez depuis une image forensique, un Volume Shadow Copy, ou un robocopy /b d'un instantané monté. Ne pointez jamais ceci vers le profil actif en faisant confiance au résultat.

Décoder les flux LNK

Ne codez pas le parseur LNK à la main sauf si vous aimez souffrir. pylnk3 lit la structure du raccourci, y compris le chemin cible et le TrackerDataBlock :

import io, pylnk3

def read_lnk_stream(ole, stream_name):
    raw = ole.openstream(stream_name).read()
    return pylnk3.parse(io.BytesIO(raw))

lnk = read_lnk_stream(ole, "1")
print(lnk.path)             # chemin cible, quand présent

Soyez défensif ici. Certains flux portent des raccourcis sans bloc LinkInfo (cibles uniquement réseau, ou éléments adressés purement par liste d'ID d'éléments shell), donc lnk.path peut être vide même si l'entrée est parfaitement valide. Attrapez le cas vide et repliez-vous sur le chemin relatif ou la liste d'ID d'éléments shell plutôt que d'abandonner l'entrée.

Parcourir le DestList

DestList est un en-tête suivi d'entrées de forme fixe, chacune se terminant par un chemin UTF-16 de longueur variable. Les champs que vous voulez par entrée sont l'ID d'entrée, le nom d'hôte NetBIOS, le FILETIME de dernier accès, l'état d'épinglage, et le numéro de flux cible. Esquisse :

import struct

def parse_destlist(raw):
    # En-tête : version + compteurs. La version pilote la disposition des entrées.
    version, n_entries = struct.unpack_from("<II", raw, 0)
    offset = 32  # taille d'en-tête ; à confirmer contre la référence de format
    entries = []
    for _ in range(n_entries):
        # Les offsets de champs ci-dessous sont illustratifs. Lisez l'article
        # de format et confirmez contre votre échantillon avant de les croire.
        hostname = raw[offset+72:offset+88].split(b"\x00")[0].decode("ascii", "replace")
        entry_id, = struct.unpack_from("<I", raw, offset+88)
        filetime, = struct.unpack_from("<Q", raw, offset+100)
        path_chars, = struct.unpack_from("<H", raw, offset+128)
        path = raw[offset+130:offset+130+path_chars*2].decode("utf-16-le")
        entries.append({
            "entry_id": entry_id,
            "hostname": hostname,
            "filetime": filetime,
            "path": path,
        })
        offset += 130 + path_chars*2
    return entries

J'ai délibérément marqué les offsets comme illustratifs, parce que c'est la partie qui mord. Ne copiez pas une table de champs depuis un blog et ne la mettez pas en production. Ouvrez votre propre échantillon, confirmez les offsets, et seulement alors faites confiance à la boucle.

Les deux choses qui cassent réellement

La conversion du FILETIME. Ce sont des intervalles de 100 nanosecondes depuis le 1601-01-01 UTC. Chaque chronologie fausse que j'ai relue avait légèrement raté cela.

from datetime import datetime, timezone

def filetime_to_dt(ft):
    if ft == 0:
        return None
    return datetime.fromtimestamp(ft / 10_000_000 - 11_644_473_600, tz=timezone.utc)

Protégez le cas zéro. Un FILETIME de 0 signifie "non défini", pas 1601, et le passer dans le calcul produit un horodatage d'allure réelle du dix-septième siècle qui empoisonnera discrètement une chronologie.

La divergence de version du DestList. Windows 7 écrivait la version 1. Windows 10 et 11 écrivent couramment la version 3 ou 4. La taille des entrées et plusieurs offsets de champs ont changé entre eux. Une boucle codée en dur pour une version lit les fichiers ultérieurs désalignés, et le symptôme est subtil : les noms d'hôtes sortent en charabia, les chemins semblent tronqués, le curseur d'octets dérive un peu plus à chaque entrée. Si votre sortie se dégrade progressivement le long d'un fichier, vous avez une incohérence de version, pas un fichier corrompu. Branchez sur la version d'en-tête et gardez des tables d'offsets par version.

Ce que olefile ne fera pas pour vous

customDestinations-ms est un format différent. Ce n'est pas de l'OLE. C'est une séquence de structures LNK avec son propre cadrage, donc OleFileIO lève une exception dessus et vous l'analysez à la main ou avec un lecteur dédié. L'article sur le format de fichier customDestinations couvre cette structure. Confondre les deux est la raison numéro un pour laquelle un "parseur de Jump List" fonctionne sur certains fichiers et lève une exception sur d'autres.

Quand vous avez juste besoin de regarder un fichier sans écrire tout ceci, le parseur sur la page d'accueil exécute le même décodage dans le navigateur, côté client, rien d'envoyé. Le script est pour quand vous en avez besoin dans un pipeline.