digital forensics

Analyzing LNK File and Jump List Artifacts

Analyze Windows LNK shortcut files and Jump List artifacts to establish evidence of file access, program execution, and user activity using LECmd, JLECmd, and manual binary parsing of the Shell Link Binary format.

file-accessjlecmdjump-listslecmdlnk-filesprogram-executionrecent-filesshell-link
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

Windows LNK (shortcut) files and Jump Lists are critical forensic artifacts that provide evidence of file access, program execution, and user behavior. LNK files are created automatically when a user opens a file through Windows Explorer or the Open/Save dialog, storing metadata about the target file including its original path, timestamps, volume serial number, NetBIOS name, and MAC address of the host system. Jump Lists, introduced in Windows 7, extend this by maintaining per-application lists of recently and frequently accessed files. These artifacts persist even after the target files are deleted, making them invaluable for establishing that a user accessed specific files at specific times.

When to Use

  • When investigating security incidents that require analyzing lnk file and jump list artifacts
  • When building detection rules or threat hunting queries for this domain
  • When SOC analysts need structured procedures for this analysis type
  • When validating security monitoring coverage for related attack techniques

Prerequisites

  • LECmd (Eric Zimmerman) for LNK file parsing
  • JLECmd (Eric Zimmerman) for Jump List parsing
  • Python 3.8+ with pylnk3 or LnkParse3 libraries
  • Forensic image or triage collection from Windows system
  • Timeline Explorer for CSV analysis

LNK File Locations

Location Description
%USERPROFILE%\AppData\Roaming\Microsoft\Windows\Recent\ Recent files accessed
%USERPROFILE%\Desktop\ User-created shortcuts
%USERPROFILE%\AppData\Roaming\Microsoft\Windows\Start Menu\ Start Menu shortcuts
%USERPROFILE%\AppData\Roaming\Microsoft\Office\Recent\ Office recent documents

LNK File Structure

Shell Link Header (76 bytes)

Offset Size Field
0x00 4 HeaderSize (always 0x0000004C)
0x04 16 LinkCLSID (always 00021401-0000-0000-C000-000000000046)
0x14 4 LinkFlags
0x18 4 FileAttributes
0x1C 8 CreationTime (FILETIME)
0x24 8 AccessTime (FILETIME)
0x2C 8 WriteTime (FILETIME)
0x34 4 FileSize of target
0x38 4 IconIndex
0x3C 4 ShowCommand
0x40 2 HotKey

Key Forensic Fields in LNK Files

  • Target file timestamps: Creation, access, modification times of the referenced file
  • Volume information: Serial number, drive type, volume label
  • Network share information: UNC path, share name
  • Machine identifiers: NetBIOS name, MAC address (from TrackerDataBlock)
  • Distributed Link Tracking: Machine ID and object GUID

Analysis with EZ Tools

LECmd - LNK File Parser

# Parse all LNK files in Recent folder
LECmd.exe -d "C:\Evidence\Users\suspect\AppData\Roaming\Microsoft\Windows\Recent" --csv C:\Output --csvf lnk_analysis.csv
 
# Parse a single LNK file with full details
LECmd.exe -f "C:\Evidence\Users\suspect\Desktop\Confidential.docx.lnk" --json C:\Output
 
# Parse LNK files with additional detail levels
LECmd.exe -d "C:\Evidence\Users\suspect\AppData\Roaming\Microsoft\Windows\Recent" --csv C:\Output --csvf lnk_all.csv --all

JLECmd - Jump List Parser

# Parse Automatic Jump Lists
JLECmd.exe -d "C:\Evidence\Users\suspect\AppData\Roaming\Microsoft\Windows\Recent\AutomaticDestinations" --csv C:\Output --csvf jumplists_auto.csv
 
# Parse Custom Jump Lists
JLECmd.exe -d "C:\Evidence\Users\suspect\AppData\Roaming\Microsoft\Windows\Recent\CustomDestinations" --csv C:\Output --csvf jumplists_custom.csv
 
# Parse all jump lists with detailed output
JLECmd.exe -d "C:\Evidence\Users\suspect\AppData\Roaming\Microsoft\Windows\Recent\AutomaticDestinations" --csv C:\Output --csvf jumplists_auto.csv --ld

Jump List Structure

Automatic Destinations (automaticDestinations-ms)

These are OLE Compound files (Structured Storage) identified by AppID hash in the filename:

AppID Hash Application
5f7b5f1e01b83767 Windows Explorer Pinned/Frequent
1b4dd67f29cb1962 Windows Explorer Recent
9b9cdc69c1c24e2b Notepad
a7bd71699cd38d1c Notepad++
12dc1ea8e34b5a6 Microsoft Paint
7e4dca80246863e3 Control Panel
1cf97c38a5881255 Microsoft Edge
f01b4d95cf55d32a Windows Explorer
9d1f905ce5044aee Microsoft Excel
a4a5324453625195 Microsoft Word
d00655d2aa12ff6d Microsoft PowerPoint
bc03160ee1a59fc1 Outlook

Custom Destinations (customDestinations-ms)

Created when users pin items to application jump lists. These files contain sequential LNK entries.

Python Analysis Script

import struct
import os
from datetime import datetime, timedelta
 
FILETIME_EPOCH = datetime(1601, 1, 1)
 
def filetime_to_datetime(filetime_bytes: bytes) -> datetime:
    """Convert Windows FILETIME (100-ns intervals since 1601) to datetime."""
    ft = struct.unpack("<Q", filetime_bytes)[0]
    if ft == 0:
        return None
    return FILETIME_EPOCH + timedelta(microseconds=ft // 10)
 
def parse_lnk_header(lnk_path: str) -> dict:
    """Parse the Shell Link header from an LNK file."""
    with open(lnk_path, "rb") as f:
        header = f.read(76)
 
    header_size = struct.unpack("<I", header[0:4])[0]
    if header_size != 0x4C:
        return {"error": "Invalid LNK header"}
 
    link_flags = struct.unpack("<I", header[0x14:0x18])[0]
    file_attrs = struct.unpack("<I", header[0x18:0x1C])[0]
 
    result = {
        "header_size": header_size,
        "link_flags": hex(link_flags),
        "file_attributes": hex(file_attrs),
        "creation_time": filetime_to_datetime(header[0x1C:0x24]),
        "access_time": filetime_to_datetime(header[0x24:0x2C]),
        "write_time": filetime_to_datetime(header[0x2C:0x34]),
        "file_size": struct.unpack("<I", header[0x34:0x38])[0],
        "has_target_id_list": bool(link_flags & 0x01),
        "has_link_info": bool(link_flags & 0x02),
        "has_name": bool(link_flags & 0x04),
        "has_relative_path": bool(link_flags & 0x08),
        "has_working_dir": bool(link_flags & 0x10),
        "has_arguments": bool(link_flags & 0x20),
        "has_icon_location": bool(link_flags & 0x40),
    }
    return result

Investigation Use Cases

Evidence of File Access

  1. Parse LNK files from Recent folder to identify accessed documents
  2. Cross-reference with MFT timestamps and USN Journal entries
  3. Note that LNK files persist even after target files are deleted

Removable Media Access

  1. LNK files referencing drive letters E:, F:, G: indicate removable media usage
  2. Volume serial number in LNK identifies the specific device
  3. MAC address in TrackerDataBlock identifies the source machine

Network Share Activity

  1. LNK files with UNC paths (\server\share) indicate network file access
  2. NetBIOS name identifies the remote server
  3. Timestamps establish when access occurred

Differences Between Windows 10 and Windows 11

Recent research (IEEE 2025) shows that Windows 11 produces different LNK and Jump List artifacts:

  • Fewer automatic LNK files generated for certain file types
  • Modified Jump List behavior for modern applications
  • UWP/MSIX applications may not generate traditional Jump Lists
  • Windows 11 Quick Access replaces some Recent functionality

References

Example Output

$ LECmd.exe -d "C:\Evidence\Users\jsmith\AppData\Roaming\Microsoft\Windows\Recent" --csv /analysis/lnk_output
 
LECmd v1.11.0 - LNK File Parser
================================
 
Processing 47 LNK files...
 
--- LNK File: Q4_Report.xlsx.lnk ---
  Source:           C:\Evidence\Users\jsmith\Recent\Q4_Report.xlsx.lnk
  Target Path:      C:\Users\jsmith\Downloads\Q4_Report.xlsm
  Target Created:   2024-01-15 14:33:45 UTC
  Target Modified:  2024-01-15 14:33:45 UTC
  Target Accessed:  2024-01-15 14:35:12 UTC
  File Size:        251,904 bytes
  Drive Type:       Fixed (C:)
  Volume Serial:    A4E7-3F21
  Machine ID:       DESKTOP-J5M1TH
  MAC Address:      48:2A:E3:5C:9B:01
 
--- LNK File: update_client.exe.lnk ---
  Source:           C:\Evidence\Users\jsmith\Recent\update_client.exe.lnk
  Target Path:      C:\ProgramData\Updates\update_client.exe
  Target Created:   2024-01-15 14:34:02 UTC
  Target Modified:  2024-01-15 14:34:02 UTC
  Target Accessed:  2024-01-15 14:36:30 UTC
  File Size:        1,258,496 bytes
  Drive Type:       Fixed (C:)
  Volume Serial:    A4E7-3F21
  Machine ID:       DESKTOP-J5M1TH
  Working Dir:      C:\ProgramData\Updates
  Arguments:        --silent --no-update-check
  Run Window:       Hidden
 
======================================================================
 
$ JLECmd.exe -d "C:\Evidence\Users\jsmith\AppData\Roaming\Microsoft\Windows\Recent\AutomaticDestinations" --csv /analysis/jumplist_output
 
JLECmd v1.5.0 - Jump List Parser
==================================
 
Processing 23 AutomaticDestinations files...
 
--- Application: Microsoft Excel (AppID: 12dc1ea8e34b5a6) ---
  Entries: 15
  Most Recent:
    Entry 0:  C:\Users\jsmith\Downloads\Q4_Report.xlsm         (2024-01-15 14:35:12 UTC)
    Entry 1:  \\FILESERV01\Finance\Budget_2024.xlsx             (2024-01-14 09:22:30 UTC)
    Entry 2:  C:\Users\jsmith\Documents\Expenses\Dec2023.xlsx   (2024-01-10 16:45:00 UTC)
 
--- Application: Windows Explorer (AppID: f01b4d95cf55d32a) ---
  Entries: 28
  Most Recent:
    Entry 0:  C:\ProgramData\Updates\                           (2024-01-15 14:36:25 UTC)
    Entry 1:  E:\Backup\                                        (2024-01-15 15:30:00 UTC)
    Entry 2:  \\FILESERV01\HR\Employees\                        (2024-01-15 16:12:45 UTC)
 
--- Application: cmd.exe (AppID: 9b9cdc69c1c24e2b) ---
  Entries: 5
  Most Recent:
    Entry 0:  C:\Windows\System32\cmd.exe                       (2024-01-15 14:36:00 UTC)
 
Summary:
  Total LNK files processed:    47
  Total Jump List entries:       156
  Suspicious artifacts:          3 (hidden window execution, USB drive access, network shares)
  CSV exported to:               /analysis/lnk_output/ and /analysis/jumplist_output/
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 3

api-reference.md3.2 KB

API Reference: LNK File and Jump List Forensics

LECmd (Eric Zimmerman) - LNK Parser

Syntax

LECmd.exe -f <file.lnk>                  # Single file
LECmd.exe -d <directory> --all            # All files in directory
LECmd.exe -d <dir> --csv <output_dir>     # CSV export
LECmd.exe -d <dir> --json <output_dir>    # JSON export
LECmd.exe -f <file.lnk> -q               # Quiet mode
LECmd.exe -d <dir> -r                     # Only removable drives

Output Fields

Field Description
SourceFile Path to the .lnk file
TargetCreated Target file creation timestamp
TargetModified Target file modification timestamp
TargetAccessed Target file access timestamp
FileSize Target file size
RelativePath Relative path to target
WorkingDirectory Working directory for target
Arguments Command-line arguments
LocalPath Full local path to target
VolumeSerialNumber Volume serial of target drive
DriveType Fixed, Removable, Network
MachineID NetBIOS name from tracker block
MacAddress MAC from distributed tracker

JLECmd (Eric Zimmerman) - Jump List Parser

Syntax

JLECmd.exe -f <jumplist_file>             # Single file
JLECmd.exe -d <directory>                 # All jump lists
JLECmd.exe -d <dir> --csv <output>        # CSV export
JLECmd.exe -d <dir> --fd                  # Full LNK details
JLECmd.exe -d <dir> --dumpTo <dir>        # Extract embedded LNK files

Jump List Locations

%APPDATA%\Microsoft\Windows\Recent\AutomaticDestinations\
%APPDATA%\Microsoft\Windows\Recent\CustomDestinations\

LnkParse3 (Python)

Installation

pip install LnkParse3

Usage

import LnkParse3
 
with open("shortcut.lnk", "rb") as f:
    lnk = LnkParse3.lnk_file(f)
 
info = lnk.get_json()
print(info["data"]["relative_path"])
print(info["header"]["creation_time"])
print(info["link_info"]["local_base_path"])
 
# Extra data blocks
extra = info.get("extra", {})
tracker = extra.get("DISTRIBUTED_LINK_TRACKER_BLOCK", {})
print(tracker.get("machine_id"))
print(tracker.get("mac_address"))

Shell Link Binary Format (MS-SHLLINK)

Header Structure (76 bytes)

Offset Size Field
0 4 HeaderSize (0x0000004C)
4 16 LinkCLSID
20 4 LinkFlags
24 4 FileAttributes
28 8 CreationTime (FILETIME)
36 8 AccessTime (FILETIME)
44 8 WriteTime (FILETIME)
52 4 FileSize
56 4 IconIndex
60 4 ShowCommand

Common App IDs (Jump Lists)

App ID Application
1b4dd67f29cb1962 Windows Explorer
5d696d521de238c3 Google Chrome
ecd21b58c2f65a4f Firefox
1bc392b8e104a00e Remote Desktop (mstsc)
b8ab77100df80ab2 Microsoft Word
cfb56c56fa0f0478 PuTTY
b74736c2bd8cc8a5 WinSCP

Suspicious LNK Indicators

Pattern Concern
PowerShell in arguments Script execution via shortcut
cmd.exe /c in target Command execution chain
UNC path to IP Network-based payload delivery
Base64 encoded arguments Obfuscated commands
mshta/wscript target Living-off-the-land execution
standards.md0.8 KB

Standards - LNK File and Jump List Forensics

Standards

  • MS-SHLLINK: Shell Link Binary File Format (Microsoft Open Specifications)
  • NIST SP 800-86: Guide to Integrating Forensic Techniques
  • SWGDE Best Practices for Computer Forensics

Tools

  • LECmd (Eric Zimmerman): LNK file parser
  • JLECmd (Eric Zimmerman): Jump List parser
  • LnkParse3 (Python): Cross-platform LNK parser
  • Magnet AXIOM: Commercial forensic tool with LNK/Jump List support

Key Artifact Locations

  • Recent files: %APPDATA%\Microsoft\Windows\Recent\
  • AutomaticDestinations: %APPDATA%\Microsoft\Windows\Recent\AutomaticDestinations\
  • CustomDestinations: %APPDATA%\Microsoft\Windows\Recent\CustomDestinations\
  • Office Recent: %APPDATA%\Microsoft\Office\Recent\

MITRE ATT&CK Mappings

  • T1547.009 - Shortcut Modification
  • T1204.002 - User Execution: Malicious File
workflows.md0.9 KB

Workflows - LNK and Jump List Analysis

Workflow 1: User File Access Investigation

Collect LNK files from Recent directory
    |
Parse with LECmd to CSV
    |
Filter by target path for specific files/locations
    |
Extract timestamps, volume serial, NetBIOS name
    |
Correlate with MFT and Event Log timestamps
    |
Document file access timeline

Workflow 2: Jump List Application Activity

Collect AutomaticDestinations and CustomDestinations
    |
Parse with JLECmd to CSV
    |
Map AppID hashes to applications
    |
Extract embedded LNK entries per application
    |
Build per-application file access timeline
    |
Identify removable media and network paths

Workflow 3: Removable Media Usage

Filter LNK files for drive letters (E:, F:, G:)
    |
Extract volume serial numbers
    |
Match with SYSTEM registry USBSTOR entries
    |
Identify specific USB devices accessed
    |
Build user-device-file timeline

Scripts 2

agent.py11.1 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Windows LNK file and Jump List artifact analysis agent.

Parses Windows Shell Link (.lnk) files and Jump List artifacts to extract
file access evidence, program execution history, and user activity timelines.
Uses LnkParse3 for binary parsing and supports LECmd/JLECmd CSV output analysis.
"""

import struct
import os
import sys
import json
import hashlib
import datetime
import re
import glob as glob_mod

try:
    import LnkParse3
    HAS_LNKPARSE = True
except ImportError:
    HAS_LNKPARSE = False


def compute_hash(filepath):
    """Compute SHA-256 hash of file."""
    sha256 = hashlib.sha256()
    with open(filepath, "rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            sha256.update(chunk)
    return sha256.hexdigest()


def parse_lnk_with_lnkparse3(filepath):
    """Parse LNK file using LnkParse3 library."""
    if not HAS_LNKPARSE:
        return {"error": "LnkParse3 not installed. pip install LnkParse3"}
    with open(filepath, "rb") as f:
        lnk = LnkParse3.lnk_file(f)
    info = lnk.get_json()
    result = {
        "target_path": info.get("data", {}).get("relative_path", ""),
        "working_dir": info.get("data", {}).get("working_directory", ""),
        "arguments": info.get("data", {}).get("command_line_arguments", ""),
        "icon_location": info.get("data", {}).get("icon_location", ""),
        "description": info.get("data", {}).get("description", ""),
    }
    header = info.get("header", {})
    result["creation_time"] = header.get("creation_time", "")
    result["access_time"] = header.get("access_time", "")
    result["write_time"] = header.get("write_time", "")
    result["file_size"] = header.get("file_size", 0)
    result["file_flags"] = header.get("file_attributes", "")
    link_info = info.get("link_info", {})
    if link_info:
        result["local_base_path"] = link_info.get("local_base_path", "")
        result["volume_serial"] = link_info.get("volume_serial_number", "")
        result["volume_label"] = link_info.get("volume_label", "")
        result["drive_type"] = link_info.get("drive_type", "")
    extra = info.get("extra", {})
    if extra:
        tracker = extra.get("DISTRIBUTED_LINK_TRACKER_BLOCK", {})
        if tracker:
            result["machine_id"] = tracker.get("machine_id", "")
            result["mac_address"] = tracker.get("mac_address", "")
            result["droid_volume_id"] = tracker.get("droid_volume_identifier", "")
            result["droid_file_id"] = tracker.get("droid_file_identifier", "")
    return result


def parse_lnk_header_raw(filepath):
    """Parse LNK file header manually from raw bytes."""
    with open(filepath, "rb") as f:
        data = f.read()

    if len(data) < 76:
        return {"error": "File too small for LNK header"}

    # Shell Link Header (76 bytes)
    header_size = struct.unpack_from("<I", data, 0)[0]
    if header_size != 0x4C:
        return {"error": f"Invalid header size: {header_size:#x} (expected 0x4C)"}

    # CLSID check: 00021401-0000-0000-C000-000000000046
    clsid = data[4:20]
    expected_clsid = bytes.fromhex("01140200000000c0000000000000046".replace("0", "0"))

    link_flags = struct.unpack_from("<I", data, 20)[0]
    file_attrs = struct.unpack_from("<I", data, 24)[0]

    creation_time = filetime_to_datetime(struct.unpack_from("<Q", data, 28)[0])
    access_time = filetime_to_datetime(struct.unpack_from("<Q", data, 36)[0])
    write_time = filetime_to_datetime(struct.unpack_from("<Q", data, 44)[0])

    file_size = struct.unpack_from("<I", data, 52)[0]
    icon_index = struct.unpack_from("<I", data, 56)[0]
    show_command = struct.unpack_from("<I", data, 60)[0]

    result = {
        "header_size": header_size,
        "link_flags": f"0x{link_flags:08X}",
        "file_attributes": f"0x{file_attrs:08X}",
        "creation_time": creation_time,
        "access_time": access_time,
        "write_time": write_time,
        "target_file_size": file_size,
        "icon_index": icon_index,
        "show_command": {1: "Normal", 3: "Maximized", 7: "Minimized"}.get(show_command, str(show_command)),
        "flags_decoded": decode_link_flags(link_flags),
    }
    return result


def filetime_to_datetime(filetime):
    """Convert Windows FILETIME to ISO string."""
    if filetime == 0:
        return "N/A"
    try:
        epoch = datetime.datetime(1601, 1, 1)
        delta = datetime.timedelta(microseconds=filetime // 10)
        return (epoch + delta).isoformat() + "Z"
    except (OverflowError, OSError):
        return "Invalid"


def decode_link_flags(flags):
    """Decode Shell Link header flags."""
    flag_names = {
        0x00000001: "HasLinkTargetIDList",
        0x00000002: "HasLinkInfo",
        0x00000004: "HasName",
        0x00000008: "HasRelativePath",
        0x00000010: "HasWorkingDir",
        0x00000020: "HasArguments",
        0x00000040: "HasIconLocation",
        0x00000080: "IsUnicode",
        0x00000100: "ForceNoLinkInfo",
        0x00000800: "RunInSeparateProcess",
        0x00001000: "HasDarwinID",
        0x00002000: "RunAsUser",
        0x00004000: "HasExpIcon",
        0x00020000: "HasExpString",
        0x00040000: "RunInSeparateProcess",
        0x00080000: "PreferEnvironmentPath",
        0x00200000: "DisableLinkPathTracking",
        0x00800000: "EnableTargetMetadata",
        0x04000000: "AllowLinkToLink",
    }
    decoded = []
    for bit, name in flag_names.items():
        if flags & bit:
            decoded.append(name)
    return decoded


JUMP_LIST_APP_IDS = {
    "1b4dd67f29cb1962": "Windows Explorer",
    "5d696d521de238c3": "Google Chrome",
    "9b9cdc69c1c24e2b": "Notepad",
    "f01b4d95cf55d32a": "Windows Explorer",
    "a7bd71699cd38d1c": "Notepad++",
    "918e0ecb43d17e23": "Notepad (Win10)",
    "12dc1ea8e34b5a6": "Microsoft Paint",
    "b8ab77100df80ab2": "Microsoft Word 2019",
    "a4a5324453625195": "Microsoft Excel 2019",
    "bc0c37e84e063727": "Microsoft PowerPoint 2019",
    "9839aec31243a928": "Microsoft Outlook 2019",
    "fb3b0dbfee58fac8": "Acrobat Reader DC",
    "ecd21b58c2f65a4f": "Firefox",
    "1bc392b8e104a00e": "Remote Desktop (mstsc)",
    "b91050d8b077a4e8": "WinRAR",
    "290532160612e071": "Windows Media Player",
    "28c8b86deab549a1": "Internet Explorer",
    "7e4dca80246863e3": "Control Panel",
    "e2a593822e01aed3": "Snipping Tool",
    "b74736c2bd8cc8a5": "WinSCP",
    "cfb56c56fa0f0478": "PuTTY",
}


def scan_jump_lists(jump_list_dir):
    """Scan Jump List directory for automatic and custom destinations."""
    results = []
    auto_pattern = os.path.join(jump_list_dir, "*.automaticDestinations-ms")
    custom_pattern = os.path.join(jump_list_dir, "*.customDestinations-ms")

    for jl_file in sorted(glob_mod.glob(auto_pattern) + glob_mod.glob(custom_pattern)):
        basename = os.path.basename(jl_file)
        app_id = basename.split(".")[0]
        jl_type = "automatic" if "automatic" in basename else "custom"
        app_name = JUMP_LIST_APP_IDS.get(app_id, "Unknown Application")
        results.append({
            "file": basename,
            "app_id": app_id,
            "app_name": app_name,
            "type": jl_type,
            "size": os.path.getsize(jl_file),
            "modified": datetime.datetime.fromtimestamp(
                os.path.getmtime(jl_file)).isoformat(),
        })
    return results


def detect_suspicious_lnk(parsed_lnk):
    """Detect suspicious characteristics in LNK files."""
    findings = []
    args = parsed_lnk.get("arguments", "")
    target = parsed_lnk.get("target_path", "") + " " + parsed_lnk.get("local_base_path", "")

    suspicious_patterns = [
        (r"powershell", "PowerShell execution via LNK"),
        (r"cmd\.exe\s*/c", "Command prompt execution via LNK"),
        (r"mshta", "MSHTA execution (HTA payload)"),
        (r"certutil.*-decode", "CertUtil decode (file download)"),
        (r"bitsadmin.*transfer", "BitsAdmin file download"),
        (r"regsvr32.*scrobj", "Regsvr32 COM scriptlet execution"),
        (r"wscript|cscript", "Script host execution"),
        (r"\\\\[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\\", "UNC path to IP address"),
        (r"http[s]?://", "URL in LNK arguments"),
        (r"-enc\s+[A-Za-z0-9+/=]{20,}", "Base64-encoded PowerShell"),
    ]
    combined = f"{target} {args}".lower()
    for pattern, description in suspicious_patterns:
        if re.search(pattern, combined, re.IGNORECASE):
            findings.append({"indicator": description, "pattern": pattern})

    if parsed_lnk.get("drive_type") == "DRIVE_REMOTE":
        findings.append({"indicator": "Target on network drive", "pattern": "DRIVE_REMOTE"})

    return findings


def scan_lnk_directory(directory):
    """Scan directory for LNK files and analyze each."""
    results = []
    for lnk_file in sorted(glob_mod.glob(os.path.join(directory, "*.lnk"))):
        parsed = parse_lnk_with_lnkparse3(lnk_file) if HAS_LNKPARSE else parse_lnk_header_raw(lnk_file)
        suspicious = detect_suspicious_lnk(parsed)
        results.append({
            "file": os.path.basename(lnk_file),
            "sha256": compute_hash(lnk_file),
            "parsed": parsed,
            "suspicious": suspicious,
        })
    return results


if __name__ == "__main__":
    print("=" * 60)
    print("Windows LNK & Jump List Forensics Agent")
    print("Shell Link parsing, Jump List analysis, suspicious detection")
    print("=" * 60)

    target = sys.argv[1] if len(sys.argv) > 1 else None

    if not target or not os.path.exists(target):
        print("\n[DEMO] Usage:")
        print("  python agent.py <file.lnk>         # Analyze single LNK")
        print("  python agent.py <directory>         # Scan directory for LNK/JumpList")
        print(f"\n  LnkParse3 available: {HAS_LNKPARSE}")
        sys.exit(0)

    if os.path.isfile(target) and target.lower().endswith(".lnk"):
        print(f"\n[*] Analyzing: {target}")
        print(f"[*] SHA-256: {compute_hash(target)}")
        if HAS_LNKPARSE:
            parsed = parse_lnk_with_lnkparse3(target)
        else:
            parsed = parse_lnk_header_raw(target)
        print("\n--- LNK Properties ---")
        for k, v in parsed.items():
            print(f"  {k}: {v}")
        suspicious = detect_suspicious_lnk(parsed)
        if suspicious:
            print("\n--- Suspicious Indicators ---")
            for s in suspicious:
                print(f"  [!] {s['indicator']}")
    elif os.path.isdir(target):
        print(f"\n[*] Scanning directory: {target}")
        lnk_results = scan_lnk_directory(target)
        print(f"[*] Found {len(lnk_results)} LNK files")
        for r in lnk_results[:20]:
            print(f"  {r['file']}: {r['parsed'].get('target_path', r['parsed'].get('local_base_path', '?'))}")
            for s in r.get("suspicious", []):
                print(f"    [!] {s['indicator']}")

        jl_dir = os.path.join(target, "AutomaticDestinations")
        if not os.path.isdir(jl_dir):
            jl_dir = target
        jl_results = scan_jump_lists(jl_dir)
        if jl_results:
            print(f"\n--- Jump Lists ({len(jl_results)}) ---")
            for jl in jl_results:
                print(f"  {jl['app_name']:30s} [{jl['type']}] {jl['app_id']}")

    print(f"\n{json.dumps({'lnk_count': len(lnk_results) if os.path.isdir(target) else 1}, indent=2)}")
process.py3.3 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""
LNK File and Jump List Forensic Analyzer

Parses LNK file headers and extracts forensic metadata including
target paths, timestamps, volume information, and machine identifiers.
"""

import struct
import os
import sys
import json
import csv
from datetime import datetime, timedelta
from pathlib import Path


FILETIME_EPOCH = datetime(1601, 1, 1)


def filetime_to_datetime(ft_bytes: bytes):
    """Convert Windows FILETIME to datetime."""
    ft = struct.unpack("<Q", ft_bytes)[0]
    if ft == 0:
        return None
    try:
        return FILETIME_EPOCH + timedelta(microseconds=ft // 10)
    except (OverflowError, OSError):
        return None


def parse_lnk_file(filepath: str) -> dict:
    """Parse a Windows LNK file and extract forensic metadata."""
    with open(filepath, "rb") as f:
        data = f.read()

    if len(data) < 76:
        return {"error": "File too small for LNK header"}

    header_size = struct.unpack("<I", data[0:4])[0]
    if header_size != 0x4C:
        return {"error": "Invalid LNK header signature"}

    link_flags = struct.unpack("<I", data[0x14:0x18])[0]
    file_attrs = struct.unpack("<I", data[0x18:0x1C])[0]

    result = {
        "file": filepath,
        "file_size_lnk": len(data),
        "creation_time": str(filetime_to_datetime(data[0x1C:0x24])),
        "access_time": str(filetime_to_datetime(data[0x24:0x2C])),
        "write_time": str(filetime_to_datetime(data[0x2C:0x34])),
        "target_file_size": struct.unpack("<I", data[0x34:0x38])[0],
        "flags": {
            "has_target_id_list": bool(link_flags & 0x01),
            "has_link_info": bool(link_flags & 0x02),
            "has_name": bool(link_flags & 0x04),
            "has_relative_path": bool(link_flags & 0x08),
            "has_working_dir": bool(link_flags & 0x10),
            "has_arguments": bool(link_flags & 0x20),
            "has_icon_location": bool(link_flags & 0x40),
        },
        "attributes": {
            "readonly": bool(file_attrs & 0x01),
            "hidden": bool(file_attrs & 0x02),
            "system": bool(file_attrs & 0x04),
            "directory": bool(file_attrs & 0x10),
            "archive": bool(file_attrs & 0x20),
        }
    }
    return result


def scan_directory(lnk_dir: str, output_dir: str) -> str:
    """Scan a directory for LNK files and generate analysis report."""
    os.makedirs(output_dir, exist_ok=True)
    results = []

    for root, dirs, files in os.walk(lnk_dir):
        for fname in files:
            if fname.lower().endswith(".lnk"):
                filepath = os.path.join(root, fname)
                parsed = parse_lnk_file(filepath)
                results.append(parsed)

    report_path = os.path.join(output_dir, "lnk_analysis_report.json")
    with open(report_path, "w") as f:
        json.dump({
            "analysis_timestamp": datetime.now().isoformat(),
            "source_directory": lnk_dir,
            "total_lnk_files": len(results),
            "files": results
        }, f, indent=2, default=str)

    print(f"[*] Analyzed {len(results)} LNK files")
    print(f"[*] Report: {report_path}")
    return report_path


def main():
    if len(sys.argv) < 3:
        print("Usage: python process.py <lnk_directory> <output_dir>")
        sys.exit(1)
    scan_directory(sys.argv[1], sys.argv[2])


if __name__ == "__main__":
    main()

Assets 1

template.mdtext/markdown · 0.7 KB
Keep exploring