Jump List Parser
Zurück zum Blog

AutomaticDestinations in Python parsen, auf die harte Tour

2026-06-154 Min. Lesezeit

JLECmd parst diese Dateien bereits korrekt, der ehrliche Grund, einen eigenen zu schreiben, ist also die Integration: Sie wollen, dass Jump-List-Ereignisse in eine Pipeline, ein eigenes Schema, ein Triage-Werkzeug oder ein Notebook fließen, ohne ein Windows-Binary aufzurufen und CSV abzukratzen. Das ist ein guter Grund. "Weil ich dem bestehenden Werkzeug nicht traue" ist keiner, und Sie sollten validieren, was Sie bauen, gegen JLECmd, bevor Sie sich darauf verlassen.

Hier ist der Weg durch die Datei, in Python, mit den Stellen, an denen Leute tatsächlich stolpern, hervorgehoben. Das Feldlayout auf Byte-Ebene lebt im Beitrag zum automaticDestinations-Dateiformat; hier geht es darum, im Code dranzukommen.

Der Container ist OLE, beginnen Sie also dort

Eine .automaticDestinations-ms-Datei ist eine OLE-Verbunddatei (das alte Structured-Storage- / CFBF-Format). Diese eine Tatsache entscheidet über Ihre erste Abhängigkeit. Verwenden Sie olefile:

import olefile

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

# Streams: Die nummerierten Streams enthalten je ein LNK, plus einen "DestList"-Stream.
for entry in ole.listdir():
    print(entry)        # z. B. ['1'], ['2'], ['DestList']

Die nummerierten Streams (1, 2, 3, ...) enthalten jeweils eine einzelne Windows-Verknüpfung, dieselbe Struktur auf der Festplatte wie eine eigenständige .lnk. DestList ist der Index, der sie verbindet und die Metadaten trägt, wegen derer Sie eigentlich kommen: den Hostnamen, den Zugriffszähler, die Zeit des letzten Zugriffs, den Anheft-Status und auf welchen nummerierten Stream jeder Eintrag verweist.

Erste Falle, vor jedem Parsen: Auf einem aktiven System ist die Datei durch explorer.exe gesperrt. OleFileIO schlägt fehl oder liest eine unvollständige Datei. Arbeiten Sie von einem forensischen Image, einer Volume Shadow Copy oder einem robocopy /b eines gemounteten Snapshots. Richten Sie das niemals auf das aktive Profil und vertrauen Sie dem Ergebnis.

Die LNK-Streams dekodieren

Schreiben Sie den LNK-Parser nicht von Hand, es sei denn, Sie genießen Schmerz. pylnk3 liest die Verknüpfungsstruktur einschließlich des Zielpfads und des 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)             # Zielpfad, wenn vorhanden

Seien Sie hier defensiv. Manche Streams tragen Verknüpfungen ohne LinkInfo-Block (rein netzwerkbasierte Ziele oder Elemente, die rein über eine Shell-Item-ID-Liste adressiert sind), lnk.path kann also leer sein, obwohl der Eintrag vollkommen gültig ist. Fangen Sie den leeren Fall ab und weichen Sie auf den relativen Pfad oder die Shell-Item-ID-Liste aus, statt den Eintrag zu verwerfen.

Die DestList durchlaufen

DestList ist ein Header, gefolgt von Einträgen fester Form, die jeweils mit einem UTF-16-Pfad variabler Länge enden. Die Felder, die Sie pro Eintrag wollen, sind die Eintrags-ID, der NetBIOS-Hostname, der FILETIME des letzten Zugriffs, der Anheft-Status und die Ziel-Stream-Nummer. Skizze:

import struct

def parse_destlist(raw):
    # Header: Version + Zähler. Die Version steuert das Eintragslayout.
    version, n_entries = struct.unpack_from("<II", raw, 0)
    offset = 32  # Header-Größe; gegen die Format-Referenz bestätigen
    entries = []
    for _ in range(n_entries):
        # Die folgenden Feld-Offsets sind illustrativ. Lesen Sie den
        # Format-Beitrag und bestätigen Sie sie gegen Ihr Sample, bevor Sie ihnen trauen.
        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

Ich habe die Offsets bewusst als illustrativ gekennzeichnet, denn das ist der Teil, der beißt. Kopieren Sie keine Feldtabelle aus einem Blog und bringen Sie sie nicht in Produktion. Öffnen Sie Ihr eigenes Sample, bestätigen Sie die Offsets, und vertrauen Sie erst dann der Schleife.

Die zwei Dinge, die wirklich brechen

Die FILETIME-Umrechnung. Es sind 100-Nanosekunden-Intervalle seit dem 1601-01-01 UTC. Jede falsche Zeitleiste, die ich nachgeprüft habe, hatte das leicht verfehlt.

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)

Schützen Sie den Null-Fall. Ein FILETIME von 0 bedeutet "nicht gesetzt", nicht 1601, und durch die Rechnung gejagt erzeugt er einen echt aussehenden Zeitstempel aus dem siebzehnten Jahrhundert, der eine Zeitleiste still vergiftet.

Die DestList-Versionsabweichung. Windows 7 schrieb Version 1. Windows 10 und 11 schreiben häufig Version 3 oder 4. Die Eintragsgröße und mehrere Feld-Offsets haben sich dazwischen verschoben. Eine Schleife, die für eine Version fest verdrahtet ist, liest spätere Dateien falsch ausgerichtet, und das Symptom ist subtil: Hostnamen kommen als Kauderwelsch heraus, Pfade wirken abgeschnitten, der Byte-Cursor driftet mit jedem Eintrag ein wenig weiter ab. Wenn Ihre Ausgabe sich über eine Datei hinweg fortschreitend verschlechtert, haben Sie eine Versionsabweichung, keine beschädigte Datei. Verzweigen Sie nach der Header-Version und führen Sie Offset-Tabellen pro Version.

Was olefile nicht für Sie tut

customDestinations-ms ist ein anderes Format. Es ist kein OLE. Es ist eine Folge von LNK-Strukturen mit eigenem Rahmen, daher wirft OleFileIO darauf eine Ausnahme, und Sie parsen es von Hand oder mit einem dedizierten Reader. Der Beitrag zum customDestinations-Dateiformat behandelt diese Struktur. Die beiden zu verwechseln ist der häufigste Grund, warum ein "Jump-List-Parser" bei manchen Dateien funktioniert und bei anderen eine Ausnahme wirft.

Wenn Sie nur eine Datei ansehen wollen, ohne all das zu schreiben, führt der Parser auf der Startseite dieselbe Dekodierung im Browser aus, clientseitig, nichts wird hochgeladen. Das Skript ist für den Fall, dass Sie es in einer Pipeline brauchen.