#!/usr/bin/env python3 """Agent for analyzing Windows LNK shortcut files for forensic artifacts.""" import os import json import csv import argparse from datetime import datetime import LnkParse3 def parse_lnk_file(filepath): """Parse a single LNK file and extract forensic artifacts.""" with open(filepath, "rb") as f: lnk = LnkParse3.lnk_file(f) info = lnk.get_json() parsed = { "lnk_file": os.path.basename(filepath), "target_path": "", "working_dir": "", "arguments": "", "target_created": "", "target_modified": "", "target_accessed": "", "file_size": "", "drive_type": "", "volume_serial": "", "volume_label": "", "machine_id": "", "mac_address": "", } header = info.get("header", {}) parsed["target_created"] = str(header.get("creation_time", "")) parsed["target_modified"] = str(header.get("modified_time", "")) parsed["target_accessed"] = str(header.get("accessed_time", "")) parsed["file_size"] = str(header.get("file_size", "")) link_info = info.get("link_info", {}) if link_info: local_path = link_info.get("local_base_path", "") net_link = link_info.get("common_network_relative_link", {}) network_path = net_link.get("net_name", "") if net_link else "" parsed["target_path"] = local_path or network_path vol_info = link_info.get("volume_id", {}) if vol_info: parsed["drive_type"] = str(vol_info.get("drive_type", "")) parsed["volume_serial"] = str(vol_info.get("drive_serial_number", "")) parsed["volume_label"] = str(vol_info.get("volume_label", "")) string_data = info.get("string_data", {}) parsed["working_dir"] = str(string_data.get("working_dir", "")) parsed["arguments"] = str(string_data.get("command_line_arguments", "")) extra = info.get("extra", {}) tracker = extra.get("DISTRIBUTED_LINK_TRACKER_BLOCK", {}) if tracker: parsed["machine_id"] = str(tracker.get("machine_id", "")) parsed["mac_address"] = str(tracker.get("mac_address", "")) return parsed def parse_lnk_directory(directory): """Parse all LNK files in a directory.""" results = [] for filename in sorted(os.listdir(directory)): if not filename.lower().endswith(".lnk"): continue filepath = os.path.join(directory, filename) try: parsed = parse_lnk_file(filepath) results.append(parsed) except Exception as e: print(f" Error parsing {filename}: {e}") return results def filter_removable_media(results): """Filter LNK files that point to removable media.""" return [r for r in results if "removable" in r.get("drive_type", "").lower()] def filter_network_shares(results): """Filter LNK files pointing to network shares.""" return [ r for r in results if "network" in r.get("drive_type", "").lower() or r.get("target_path", "").startswith("\\\\") ] def detect_suspicious_startup(startup_dir): """Analyze Startup folder LNK files for potential persistence.""" suspicious = [] for filename in os.listdir(startup_dir): if not filename.lower().endswith(".lnk"): continue filepath = os.path.join(startup_dir, filename) try: parsed = parse_lnk_file(filepath) target = parsed["target_path"].lower() args = parsed["arguments"].lower() if any(s in target for s in ["temp", "appdata", "programdata", "public"]): parsed["risk"] = "HIGH" suspicious.append(parsed) elif any(s in args for s in ["-enc", "powershell", "cmd /c", "wscript"]): parsed["risk"] = "HIGH" suspicious.append(parsed) except Exception: pass return suspicious def export_csv(results, output_path): """Export parsed LNK results to CSV.""" if not results: return with open(output_path, "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=results[0].keys()) writer.writeheader() writer.writerows(results) def extract_unique_machines(results): """Extract unique machine IDs and MAC addresses from LNK files.""" machines = {} for r in results: mid = r.get("machine_id", "") mac = r.get("mac_address", "") if mid: machines[mid] = mac return machines def main(): parser = argparse.ArgumentParser(description="Windows LNK File Forensic Analysis Agent") parser.add_argument("--lnk-dir", required=True, help="Directory containing LNK files") parser.add_argument("--startup-dir", help="Startup folder to check for persistence") parser.add_argument("--output-dir", default="./lnk_analysis") parser.add_argument("--action", choices=[ "parse_all", "removable", "network", "startup", "machines", "full_analysis" ], default="full_analysis") args = parser.parse_args() os.makedirs(args.output_dir, exist_ok=True) all_results = parse_lnk_directory(args.lnk_dir) print(f"[+] Parsed {len(all_results)} LNK files") if args.action in ("parse_all", "full_analysis"): csv_path = os.path.join(args.output_dir, "lnk_analysis.csv") export_csv(all_results, csv_path) print(f"[+] Exported to {csv_path}") if args.action in ("removable", "full_analysis"): removable = filter_removable_media(all_results) print(f"[+] Removable media files: {len(removable)}") for r in removable: print(f" {r['target_modified']} | {r['target_path']} | Vol: {r['volume_serial']}") if args.action in ("network", "full_analysis"): network = filter_network_shares(all_results) print(f"[+] Network share files: {len(network)}") if args.action in ("startup", "full_analysis") and args.startup_dir: suspicious = detect_suspicious_startup(args.startup_dir) print(f"[+] Suspicious startup LNK: {len(suspicious)}") for s in suspicious: print(f" [{s.get('risk')}] {s['lnk_file']} -> {s['target_path']}") if args.action in ("machines", "full_analysis"): machines = extract_unique_machines(all_results) print(f"[+] Unique machines: {len(machines)}") for mid, mac in machines.items(): print(f" Machine: {mid} | MAC: {mac}") print(json.dumps({"total_lnk": len(all_results), "generated_at": datetime.utcnow().isoformat()}, indent=2)) if __name__ == "__main__": main()