peinjector/tool/pylnk3.py

2040 lines
68 KiB
Python
Raw Permalink Normal View History

2024-02-10 12:55:33 +08:00
#!/usr/bin/env python3
# original version written by Tim-Christian Mundt (2011):
# https://sourceforge.net/p/pylnk/code/HEAD/tree/trunk/pylnk.py
# converted to python3 by strayge:
# https://github.com/strayge/pylnk
import argparse
import ntpath
import os
import re
import time
from datetime import datetime
from io import BytesIO, IOBase
from pprint import pformat
from struct import pack, unpack
from typing import Dict, Optional, Tuple, Union
DEFAULT_CHARSET = 'cp936' # default is "cp1251", we changed it
# ---- constants
_SIGNATURE = b'L\x00\x00\x00'
_GUID = b'\x01\x14\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00F'
_LINK_INFO_HEADER_DEFAULT = 0x1C
_LINK_INFO_HEADER_OPTIONAL = 0x24
_LINK_FLAGS = (
'HasLinkTargetIDList',
'HasLinkInfo',
'HasName',
'HasRelativePath',
'HasWorkingDir',
'HasArguments',
'HasIconLocation',
'IsUnicode',
'ForceNoLinkInfo',
# new
'HasExpString',
'RunInSeparateProcess',
'Unused1',
'HasDarwinID',
'RunAsUser',
'HasExpIcon',
'NoPidlAlias',
'Unused2',
'RunWithShimLayer',
'ForceNoLinkTrack',
'EnableTargetMetadata',
'DisableLinkPathTracking',
'DisableKnownFolderTracking',
'DisableKnownFolderAlias',
'AllowLinkToLink',
'UnaliasOnSave',
'PreferEnvironmentPath',
'KeepLocalIDListForUNCTarget',
)
_FILE_ATTRIBUTES_FLAGS = (
'read_only', 'hidden', 'system_file', 'reserved1',
'directory', 'archive', 'reserved2', 'normal',
'temporary', 'sparse_file', 'reparse_point',
'compressed', 'offline', 'not_content_indexed',
'encrypted',
)
_MODIFIER_KEYS = ('SHIFT', 'CONTROL', 'ALT')
WINDOW_NORMAL = "Normal"
WINDOW_MAXIMIZED = "Maximized"
WINDOW_MINIMIZED = "Minimized"
_SHOW_COMMANDS = {1: WINDOW_NORMAL, 3: WINDOW_MAXIMIZED, 7: WINDOW_MINIMIZED}
_SHOW_COMMAND_IDS = dict((v, k) for k, v in _SHOW_COMMANDS.items())
DRIVE_UNKNOWN = "Unknown"
DRIVE_NO_ROOT_DIR = "No root directory"
DRIVE_REMOVABLE = "Removable"
DRIVE_FIXED = "Fixed (Hard disk)"
DRIVE_REMOTE = "Remote (Network drive)"
DRIVE_CDROM = "CD-ROM"
DRIVE_RAMDISK = "Ram disk"
_DRIVE_TYPES = {0: DRIVE_UNKNOWN,
1: DRIVE_NO_ROOT_DIR,
2: DRIVE_REMOVABLE,
3: DRIVE_FIXED,
4: DRIVE_REMOTE,
5: DRIVE_CDROM,
6: DRIVE_RAMDISK}
_DRIVE_TYPE_IDS = dict((v, k) for k, v in _DRIVE_TYPES.items())
_KEYS = {
0x30: '0', 0x31: '1', 0x32: '2', 0x33: '3', 0x34: '4', 0x35: '5', 0x36: '6',
0x37: '7', 0x38: '8', 0x39: '9', 0x41: 'A', 0x42: 'B', 0x43: 'C', 0x44: 'D',
0x45: 'E', 0x46: 'F', 0x47: 'G', 0x48: 'H', 0x49: 'I', 0x4A: 'J', 0x4B: 'K',
0x4C: 'L', 0x4D: 'M', 0x4E: 'N', 0x4F: 'O', 0x50: 'P', 0x51: 'Q', 0x52: 'R',
0x53: 'S', 0x54: 'T', 0x55: 'U', 0x56: 'V', 0x57: 'W', 0x58: 'X', 0x59: 'Y',
0x5A: 'Z', 0x70: 'F1', 0x71: 'F2', 0x72: 'F3', 0x73: 'F4', 0x74: 'F5',
0x75: 'F6', 0x76: 'F7', 0x77: 'F8', 0x78: 'F9', 0x79: 'F10', 0x7A: 'F11',
0x7B: 'F12', 0x7C: 'F13', 0x7D: 'F14', 0x7E: 'F15', 0x7F: 'F16', 0x80: 'F17',
0x81: 'F18', 0x82: 'F19', 0x83: 'F20', 0x84: 'F21', 0x85: 'F22', 0x86: 'F23',
0x87: 'F24', 0x90: 'NUM LOCK', 0x91: 'SCROLL LOCK'
}
_KEY_CODES = dict((v, k) for k, v in _KEYS.items())
ROOT_MY_COMPUTER = 'MY_COMPUTER'
ROOT_MY_DOCUMENTS = 'MY_DOCUMENTS'
ROOT_NETWORK_SHARE = 'NETWORK_SHARE'
ROOT_NETWORK_SERVER = 'NETWORK_SERVER'
ROOT_NETWORK_PLACES = 'NETWORK_PLACES'
ROOT_NETWORK_DOMAIN = 'NETWORK_DOMAIN'
ROOT_INTERNET = 'INTERNET'
RECYCLE_BIN = 'RECYCLE_BIN'
ROOT_CONTROL_PANEL = 'CONTROL_PANEL'
ROOT_USER = 'USERPROFILE'
ROOT_UWP_APPS = 'APPS'
_ROOT_LOCATIONS = {
'{20D04FE0-3AEA-1069-A2D8-08002B30309D}': ROOT_MY_COMPUTER,
'{450D8FBA-AD25-11D0-98A8-0800361B1103}': ROOT_MY_DOCUMENTS,
'{54a754c0-4bf1-11d1-83ee-00a0c90dc849}': ROOT_NETWORK_SHARE,
'{c0542a90-4bf0-11d1-83ee-00a0c90dc849}': ROOT_NETWORK_SERVER,
'{208D2C60-3AEA-1069-A2D7-08002B30309D}': ROOT_NETWORK_PLACES,
'{46e06680-4bf0-11d1-83ee-00a0c90dc849}': ROOT_NETWORK_DOMAIN,
'{871C5380-42A0-1069-A2EA-08002B30309D}': ROOT_INTERNET,
'{645FF040-5081-101B-9F08-00AA002F954E}': RECYCLE_BIN,
'{21EC2020-3AEA-1069-A2DD-08002B30309D}': ROOT_CONTROL_PANEL,
'{59031A47-3F72-44A7-89C5-5595FE6B30EE}': ROOT_USER,
'{4234D49B-0245-4DF3-B780-3893943456E1}': ROOT_UWP_APPS,
}
_ROOT_LOCATION_GUIDS = dict((v, k) for k, v in _ROOT_LOCATIONS.items())
TYPE_FOLDER = 'FOLDER'
TYPE_FILE = 'FILE'
_ENTRY_TYPES = {
0x00: 'KNOWN_FOLDER',
0x31: 'FOLDER',
0x32: 'FILE',
0x35: 'FOLDER (UNICODE)',
0x36: 'FILE (UNICODE)',
0x802E: 'ROOT_KNOWN_FOLDER',
# founded in doc, not tested
0x1f: 'ROOT_FOLDER',
0x61: 'URI',
0x71: 'CONTROL_PANEL',
}
_ENTRY_TYPE_IDS = dict((v, k) for k, v in _ENTRY_TYPES.items())
_DRIVE_PATTERN = re.compile(r'(\w)[:/\\]*$')
# ---- read and write binary data
def read_byte(buf):
return unpack('<B', buf.read(1))[0]
def read_short(buf):
return unpack('<H', buf.read(2))[0]
def read_int(buf):
return unpack('<I', buf.read(4))[0]
def read_double(buf):
return unpack('<Q', buf.read(8))[0]
def read_cunicode(buf):
s = b""
b = buf.read(2)
while b != b'\x00\x00':
s += b
b = buf.read(2)
return s.decode('utf-16-le')
def read_cstring(buf, padding=False):
s = b""
b = buf.read(1)
while b != b'\x00':
s += b
b = buf.read(1)
if padding and not len(s) % 2:
buf.read(1) # make length + terminator even
# TODO: encoding is not clear, unicode-escape has been necessary sometimes
return s.decode(DEFAULT_CHARSET)
def read_sized_string(buf, string=True):
size = read_short(buf)
if string:
return buf.read(size*2).decode('utf-16-le')
else:
return buf.read(size)
def get_bits(value, start, count, length=16):
mask = 0
for i in range(count):
mask = mask | 1 << i
shift = length - start - count
return value >> shift & mask
def read_dos_datetime(buf):
date = read_short(buf)
time = read_short(buf)
year = get_bits(date, 0, 7) + 1980
month = get_bits(date, 7, 4)
day = get_bits(date, 11, 5)
hour = get_bits(time, 0, 5)
minute = get_bits(time, 5, 6)
second = get_bits(time, 11, 5)
# fix zeroes
month = max(month, 1)
day = max(day, 1)
return datetime(year, month, day, hour, minute, second)
def write_byte(val, buf):
buf.write(pack('<B', val))
def write_short(val, buf):
buf.write(pack('<H', val))
def write_int(val, buf):
buf.write(pack('<I', val))
def write_double(val, buf):
buf.write(pack('<Q', val))
def write_cstring(val, buf, padding=False):
# val = val.encode('unicode-escape').replace('\\\\', '\\')
val = val.encode(DEFAULT_CHARSET)
buf.write(val + b'\x00')
if padding and not len(val) % 2:
buf.write(b'\x00')
def write_cunicode(val, buf):
uni = val.encode('utf-16-le')
buf.write(uni + b'\x00\x00')
def write_sized_string(val, buf, string=True):
size = len(val)
write_short(size, buf)
if string:
buf.write(val.encode('utf-16-le'))
else:
buf.write(val.encode())
def put_bits(bits, target, start, count, length=16):
return target | bits << (length - start - count)
def write_dos_datetime(val, buf):
date = time = 0
date = put_bits(val.year-1980, date, 0, 7)
date = put_bits(val.month, date, 7, 4)
date = put_bits(val.day, date, 11, 5)
time = put_bits(val.hour, time, 0, 5)
time = put_bits(val.minute, time, 5, 6)
time = put_bits(val.second, time, 11, 5)
write_short(date, buf)
write_short(time, buf)
# ---- helpers
def convert_time_to_unix(windows_time):
# Windows time is specified as the number of 0.1 nanoseconds since January 1, 1601.
# UNIX time is specified as the number of seconds since January 1, 1970.
# There are 134774 days (or 11644473600 seconds) between these dates.
unix_time = windows_time / 10000000.0 - 11644473600
try:
return datetime.fromtimestamp(unix_time)
except OSError:
return datetime.now()
def convert_time_to_windows(unix_time):
if isinstance(unix_time, datetime):
unix_time = time.mktime(unix_time.timetuple())
return int((unix_time + 11644473600) * 10000000)
class FormatException(Exception):
pass
class MissingInformationException(Exception):
pass
class InvalidKeyException(Exception):
pass
def guid_from_bytes(bytes):
if len(bytes) != 16:
raise FormatException("This is no valid _GUID: %s" % bytes)
ordered = [
bytes[3], bytes[2], bytes[1], bytes[0],
bytes[5], bytes[4], bytes[7], bytes[6],
bytes[8], bytes[9], bytes[10], bytes[11],
bytes[12], bytes[13], bytes[14], bytes[15]
]
return "{%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X}" % tuple([x for x in ordered])
def bytes_from_guid(guid):
nums = [
guid[1:3], guid[3:5], guid[5:7], guid[7:9],
guid[10:12], guid[12:14], guid[15:17], guid[17:19],
guid[20:22], guid[22:24], guid[25:27], guid[27:29],
guid[29:31], guid[31:33], guid[33:35], guid[35:37]
]
ordered_nums = [
nums[3], nums[2], nums[1], nums[0],
nums[5], nums[4], nums[7], nums[6],
nums[8], nums[9], nums[10], nums[11],
nums[12], nums[13], nums[14], nums[15],
]
return bytes([int(x, 16) for x in ordered_nums])
def assert_lnk_signature(f):
f.seek(0)
sig = f.read(4)
guid = f.read(16)
if sig != _SIGNATURE:
raise FormatException("This is not a .lnk file.")
if guid != _GUID:
raise FormatException("Cannot read this kind of .lnk file.")
def is_lnk(f):
if hasattr(f, 'name'):
if f.name.split(os.path.extsep)[-1] == "lnk":
assert_lnk_signature(f)
return True
else:
return False
else:
try:
assert_lnk_signature(f)
return True
except FormatException:
return False
def path_levels(p):
dirname, base = ntpath.split(p)
if base != '':
for level in path_levels(dirname):
yield level
yield p
def is_drive(data):
if type(data) not in (str, str):
return False
p = re.compile("[a-zA-Z]:\\\\?$")
return p.match(data) is not None
# ---- data structures
class Flags(object):
def __init__(self, flag_names: Tuple[str, ...], flags_bytes=0):
self._flag_names = flag_names
self._flags: Dict[str, bool] = dict(
[(name, False) for name in flag_names])
self.set_flags(flags_bytes)
def set_flags(self, flags_bytes):
for pos, flag_name in enumerate(self._flag_names):
self._flags[flag_name] = bool(flags_bytes >> pos & 0x1)
@property
def bytes(self):
bytes = 0
for pos in range(len(self._flag_names)):
bytes = (self._flags[self._flag_names[pos]]
and 1 or 0) << pos | bytes
return bytes
def __getitem__(self, key):
if key in self._flags:
return object.__getattribute__(self, '_flags')[key]
return object.__getattribute__(self, key)
def __setitem__(self, key, value):
if key not in self._flags:
raise KeyError(
"The key '%s' is not defined for those flags." % key)
self._flags[key] = value
def __getattr__(self, key):
if key in self._flags:
return object.__getattribute__(self, '_flags')[key]
return object.__getattribute__(self, key)
def __setattr__(self, key, value):
if '_flags' not in self.__dict__:
object.__setattr__(self, key, value)
elif key in self.__dict__:
object.__setattr__(self, key, value)
else:
self.__setitem__(key, value)
def __str__(self):
return pformat(self._flags, indent=2)
class ModifierKeys(Flags):
def __init__(self, flags_bytes=0):
Flags.__init__(self, _MODIFIER_KEYS, flags_bytes)
def __str__(self):
s = ""
s += self.CONTROL and "CONTROL+" or ""
s += self.SHIFT and "SHIFT+" or ""
s += self.ALT and "ALT+" or ""
return s
# _ROOT_INDEX = {
# 0x00: 'INTERNET_EXPLORER1',
# 0x42: 'LIBRARIES',
# 0x44: 'USERS',
# 0x48: 'MY_DOCUMENTS',
# 0x50: 'MY_COMPUTER',
# 0x58: 'MY_NETWORK_PLACES',
# 0x60: 'RECYCLE_BIN',
# 0x68: 'INTERNET_EXPLORER2',
# 0x70: 'UNKNOWN',
# 0x80: 'MY_GAMES',
# }
class RootEntry(object):
def __init__(self, root):
if root is not None:
# create from text representation
if root in list(_ROOT_LOCATION_GUIDS.keys()):
self.root = root
self.guid = _ROOT_LOCATION_GUIDS[root]
return
# from binary
root_type = root[0]
index = root[1]
guid_bytes = root[2:18]
self.guid = guid_from_bytes(guid_bytes)
self.root = _ROOT_LOCATIONS.get(self.guid, f"UNKNOWN {self.guid}")
# if self.root == "UNKNOWN":
# self.root = _ROOT_INDEX.get(index, "UNKNOWN")
@property
def bytes(self):
guid = self.guid[1:-1].replace('-', '')
chars = [bytes([int(x, 16)]) for x in [guid[i:i+2]
for i in range(0, 32, 2)]]
return (
b'\x1F\x50'
+ chars[3] + chars[2] + chars[1] + chars[0]
+ chars[5] + chars[4] + chars[7] + chars[6]
+ b''.join(chars[8:])
)
def __str__(self):
return "<RootEntry: %s>" % self.root
class DriveEntry(object):
def __init__(self, drive: str):
if len(drive) == 23:
# binary data from parsed lnk
self.drive = drive[1:3]
else:
# text representation
m = _DRIVE_PATTERN.match(drive.strip())
if m:
self.drive = m.groups()[0].upper() + ':'
self.drive = self.drive.encode()
else:
raise FormatException(
"This is not a valid drive: " + str(drive))
@property
def bytes(self):
drive = self.drive
padded_str = drive + b'\\' + b'\x00' * 19
return b'\x2F' + padded_str
# drive = self.drive
# if isinstance(drive, str):
# drive = drive.encode()
# return b'/' + drive + b'\\' + b'\x00' * 19
def __str__(self):
return "<DriveEntry: %s>" % self.drive
class PathSegmentEntry(object):
def __init__(self, bytes=None):
self.type = None
self.file_size = None
self.modified = None
self.short_name = None
self.created = None
self.accessed = None
self.full_name = None
if bytes is None:
return
buf = BytesIO(bytes)
self.type = _ENTRY_TYPES.get(read_short(buf), 'UNKNOWN')
short_name_is_unicode = self.type.endswith('(UNICODE)')
if self.type == 'ROOT_KNOWN_FOLDER':
self.full_name = '::' + guid_from_bytes(buf.read(16))
# then followed Beef0026 structure:
# short size
# short version
# int signature == 0xBEEF0026
# (16 bytes) created timestamp
# (16 bytes) modified timestamp
# (16 bytes) accessed timestamp
return
if self.type == 'KNOWN_FOLDER':
_ = read_short(buf) # extra block size
extra_signature = read_int(buf)
if extra_signature == 0x23FEBBEE:
_ = read_short(buf) # unknown
_ = read_short(buf) # guid len
# that format recognized by explorer
self.full_name = '::' + guid_from_bytes(buf.read(16))
return
self.file_size = read_int(buf)
self.modified = read_dos_datetime(buf)
unknown = read_short(buf) # FileAttributesL
if short_name_is_unicode:
self.short_name = read_cunicode(buf)
else:
self.short_name = read_cstring(buf, padding=True)
extra_size = read_short(buf)
extra_version = read_short(buf)
extra_signature = read_int(buf)
if extra_signature == 0xBEEF0004:
# indicator_1 = read_short(buf) # see below
# only_83 = read_short(buf) < 0x03
# unknown = read_short(buf) # 0x04
# self.is_unicode = read_short(buf) == 0xBeef
self.created = read_dos_datetime(buf) # 4 bytes
self.accessed = read_dos_datetime(buf) # 4 bytes
# offset from start of extra_size
offset_unicode = read_short(buf)
# only_83_2 = offset_unicode >= indicator_1 or offset_unicode < 0x14
if extra_version >= 7:
offset_ansi = read_short(buf)
file_reference = read_double(buf)
unknown2 = read_double(buf)
long_string_size = 0
if extra_version >= 3:
long_string_size = read_short(buf)
if extra_version >= 9:
unknown4 = read_int(buf)
if extra_version >= 8:
unknown5 = read_int(buf)
if extra_version >= 3:
self.full_name = read_cunicode(buf)
if long_string_size > 0:
if extra_version >= 7:
self.localized_name = read_cunicode(buf)
else:
self.localized_name = read_cstring(buf)
version_offset = read_short(buf)
@classmethod
def create_for_path(cls, path):
entry = cls()
entry.type = os.path.isdir(path) and TYPE_FOLDER or TYPE_FILE
try:
st = os.stat(path)
entry.file_size = st.st_size
entry.modified = datetime.fromtimestamp(st.st_mtime)
entry.created = datetime.fromtimestamp(st.st_ctime)
entry.accessed = datetime.fromtimestamp(st.st_atime)
except FileNotFoundError:
now = datetime.now()
entry.file_size = 0
entry.modified = now
entry.created = now
entry.accessed = now
entry.short_name = ntpath.split(path)[1]
entry.full_name = entry.short_name
return entry
def _validate(self):
if self.type is None:
raise MissingInformationException(
"Type is missing, choose either TYPE_FOLDER or TYPE_FILE.")
if self.file_size is None:
if self.type.startswith('FOLDER') or self.type in ['KNOWN_FOLDER', 'ROOT_KNOWN_FOLDER']:
self.file_size = 0
else:
raise MissingInformationException("File size missing")
if self.created is None:
self.created = datetime.now()
if self.modified is None:
self.modified = datetime.now()
if self.accessed is None:
self.accessed = datetime.now()
# if self.modified is None or self.accessed is None or self.created is None:
# raise MissingInformationException("Date information missing")
if self.full_name is None:
raise MissingInformationException("A full name is missing")
if self.short_name is None:
self.short_name = self.full_name
@property
def bytes(self):
if self.full_name is None:
return
self._validate()
out = BytesIO()
entry_type = self.type
if entry_type == 'KNOWN_FOLDER':
write_short(_ENTRY_TYPE_IDS[entry_type], out)
write_short(0x1A, out) # size
write_int(0x23FEBBEE, out) # extra signature
write_short(0x00, out) # extra signature
write_short(0x10, out) # guid size
out.write(bytes_from_guid(self.full_name.strip(':')))
return out.getvalue()
if entry_type == 'ROOT_KNOWN_FOLDER':
write_short(_ENTRY_TYPE_IDS[entry_type], out)
out.write(bytes_from_guid(self.full_name.strip(':')))
write_short(0x26, out) # 0xBEEF0026 structure size
write_short(0x01, out) # version
write_int(0xBEEF0026, out) # extra signature
write_int(0x11, out) # some flag for containing datetime
write_double(0x00, out) # created datetime
write_double(0x00, out) # modified datetime
write_double(0x00, out) # accessed datetime
write_short(0x14, out) # unknown
return out.getvalue()
short_name_len = len(self.short_name) + 1
try:
self.short_name.encode("ascii")
short_name_is_unicode = False
short_name_len += short_name_len % 2 # padding
except (UnicodeEncodeError, UnicodeDecodeError):
short_name_is_unicode = True
short_name_len = short_name_len * 2
self.type += " (UNICODE)"
write_short(_ENTRY_TYPE_IDS[entry_type], out)
write_int(self.file_size, out)
write_dos_datetime(self.modified, out)
write_short(0x10, out)
if short_name_is_unicode:
write_cunicode(self.short_name, out)
else:
write_cstring(self.short_name, out, padding=True)
indicator = 24 + 2 * len(self.short_name)
write_short(indicator, out) # size
write_short(0x03, out) # version
write_short(0x04, out) # signature part1
write_short(0xBeef, out) # signature part2
write_dos_datetime(self.created, out)
write_dos_datetime(self.accessed, out)
offset_unicode = 0x14 # fixed data structure, always the same
write_short(offset_unicode, out)
offset_ansi = 0 # we always write unicode
write_short(offset_ansi, out) # long_string_size
write_cunicode(self.full_name, out)
offset_part2 = 0x0E + short_name_len
write_short(offset_part2, out)
return out.getvalue()
def __str__(self):
return "<PathSegmentEntry: %s>" % self.full_name
class UwpSubBlock:
block_names = {
0x11: 'PackageFamilyName',
# 0x0e: '',
# 0x19: '',
0x15: 'PackageFullName',
0x05: 'Target',
0x0f: 'Location',
0x20: 'RandomGuid',
0x0c: 'Square150x150Logo',
0x02: 'Square44x44Logo',
0x0d: 'Wide310x150Logo',
# 0x04: '',
# 0x05: '',
0x13: 'Square310x310Logo',
# 0x0e: '',
0x0b: 'DisplayName',
0x14: 'Square71x71Logo',
0x64: 'RandomByte',
0x0a: 'DisplayName',
# 0x07: '',
}
block_types = {
'string': [0x11, 0x15, 0x05, 0x0f, 0x0c, 0x02, 0x0d, 0x13, 0x0b, 0x14, 0x0a],
}
def __init__(self, bytes=None, type=None, value=None):
self._data = bytes or b''
self.type = type
self.value = value
self.name = None
if self.type is not None:
self.name = self.block_names.get(self.type, 'UNKNOWN')
if not bytes:
return
buf = BytesIO(bytes)
self.type = read_byte(buf)
self.name = self.block_names.get(self.type, 'UNKNOWN')
self.value = self._data[1:] # skip type
if self.type in self.block_types['string']:
unknown = read_int(buf)
probably_type = read_int(buf)
if probably_type == 0x1f:
string_len = read_int(buf)
self.value = read_cunicode(buf)
def __str__(self):
string = f'UwpSubBlock {self.name} ({hex(self.type)}): {self.value}'
return string.strip()
@property
def bytes(self):
out = BytesIO()
if self.value:
if isinstance(self.value, str):
string_len = len(self.value) + 1
write_byte(self.type, out)
write_int(0, out)
write_int(0x1f, out)
write_int(string_len, out)
write_cunicode(self.value, out)
if string_len % 2 == 1: # padding
write_short(0, out)
elif isinstance(self.value, bytes):
write_byte(self.type, out)
out.write(self.value)
result = out.getvalue()
return result
class UwpMainBlock:
magic = b'\x31\x53\x50\x53'
def __init__(self, bytes=None, guid: Optional[str] = None, blocks=None):
self._data = bytes or b''
self._blocks = blocks or []
self.guid: str = guid
if not bytes:
return
buf = BytesIO(bytes)
magic = buf.read(4)
self.guid = guid_from_bytes(buf.read(16))
# read sub blocks
while True:
sub_block_size = read_int(buf)
if not sub_block_size: # last size is zero
break
sub_block_data = buf.read(
sub_block_size - 4) # includes block_size
self._blocks.append(UwpSubBlock(sub_block_data))
def __str__(self):
string = f'<UwpMainBlock> {self.guid}:\n'
for block in self._blocks:
string += f' {block}\n'
return string.strip()
@property
def bytes(self):
blocks_bytes = [block.bytes for block in self._blocks]
out = BytesIO()
out.write(self.magic)
out.write(bytes_from_guid(self.guid))
for block in blocks_bytes:
write_int(len(block) + 4, out)
out.write(block)
write_int(0, out)
result = out.getvalue()
return result
class UwpSegmentEntry:
magic = b'APPS'
header = b'\x08\x00\x03\x00\x00\x00\x00\x00\x00\x00'
def __init__(self, bytes=None):
self._blocks = []
self._data = bytes
if bytes is None:
return
buf = BytesIO(bytes)
unknown = read_short(buf)
size = read_short(buf)
magic = buf.read(4) # b'APPS'
blocks_size = read_short(buf)
unknown2 = buf.read(10)
# read main blocks
while True:
block_size = read_int(buf)
if not block_size: # last size is zero
break
block_data = buf.read(block_size - 4) # includes block_size
self._blocks.append(UwpMainBlock(block_data))
def __str__(self):
string = '<UwpSegmentEntry>:\n'
for block in self._blocks:
string += f' {block}\n'
return string.strip()
@property
def bytes(self):
blocks_bytes = [block.bytes for block in self._blocks]
# with terminator
blocks_size = sum([len(block) + 4 for block in blocks_bytes]) + 4
size = (
2 # size
+ len(self.magic)
+ 2 # second size
+ len(self.header)
+ blocks_size # blocks with terminator
)
out = BytesIO()
write_short(0, out)
write_short(size, out)
out.write(self.magic)
write_short(blocks_size, out)
out.write(self.header)
for block in blocks_bytes:
write_int(len(block) + 4, out)
out.write(block)
write_int(0, out) # empty block
write_short(0, out) # ??
result = out.getvalue()
return result
@classmethod
def create(cls, package_family_name, target, location=None, logo44x44=None):
segment = cls()
blocks = [
UwpSubBlock(type=0x11, value=package_family_name),
UwpSubBlock(
type=0x0e, value=b'\x00\x00\x00\x00\x13\x00\x00\x00\x02\x00\x00\x00'),
UwpSubBlock(type=0x05, value=target),
]
if location:
# need for relative icon path
blocks.append(UwpSubBlock(type=0x0f, value=location))
main1 = UwpMainBlock(
guid='{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}', blocks=blocks)
segment._blocks.append(main1)
if logo44x44:
main2 = UwpMainBlock(
guid='{86D40B4D-9069-443C-819A-2A54090DCCEC}',
blocks=[UwpSubBlock(type=0x02, value=logo44x44)]
)
segment._blocks.append(main2)
return segment
class LinkTargetIDList(object):
def __init__(self, bytes=None):
self.items = []
if bytes is not None:
buf = BytesIO(bytes)
raw = []
entry_len = read_short(buf)
while entry_len > 0:
# the length includes the size
raw.append(buf.read(entry_len - 2))
entry_len = read_short(buf)
self._interpret(raw)
def _interpret(self, raw):
if not raw:
return
elif raw[0][0] == 0x1F:
self.items.append(RootEntry(raw[0]))
if self.items[0].root == ROOT_MY_COMPUTER:
if len(raw[1]) == 0x17:
self.items.append(DriveEntry(raw[1]))
elif raw[1][0:2] == b'\x2E\x80': # ROOT_KNOWN_FOLDER
self.items.append(PathSegmentEntry(raw[1]))
else:
raise ValueError(
"This seems to be an absolute link which requires a drive as second element.")
items = raw[2:]
elif self.items[0].root == ROOT_NETWORK_PLACES:
raise NotImplementedError(
"Parsing network lnks has not yet been implemented. "
"If you need it just contact me and we'll see..."
)
else:
items = raw[1:]
else:
items = raw
for item in items:
if item[4:8] == b'APPS':
self.items.append(UwpSegmentEntry(item))
else:
self.items.append(PathSegmentEntry(item))
def get_path(self):
segments = []
for item in self.items:
if type(item) == RootEntry:
segments.append('%' + item.root + '%')
elif type(item) == DriveEntry:
segments.append(item.drive.decode())
elif type(item) == PathSegmentEntry:
if item.full_name is not None:
segments.append(item.full_name)
else:
segments.append(item)
return '\\'.join(segments)
def _validate(self):
if not len(self.items):
return
if type(self.items[0]) == RootEntry and self.items[0].root == ROOT_MY_COMPUTER:
if type(self.items[1]) == DriveEntry:
return
if type(self.items[1]) == PathSegmentEntry and self.items[1].full_name.startswith('::'):
return
raise ValueError("A drive is required for absolute lnks")
@property
def bytes(self):
self._validate()
out = BytesIO()
for item in self.items:
bytes = item.bytes
# skip invalid
if bytes is None:
continue
write_short(len(bytes) + 2, out) # len + terminator
out.write(bytes)
out.write(b'\x00\x00')
return out.getvalue()
def __str__(self):
string = '<LinkTargetIDList>:\n'
for item in self.items:
string += f' {item}\n'
return string.strip()
class LinkInfo(object):
def __init__(self, lnk=None):
if lnk is not None:
self.start = lnk.tell()
self.size = read_int(lnk)
self.header_size = read_int(lnk)
link_info_flags = read_int(lnk)
self.local = link_info_flags & 1
self.remote = link_info_flags & 2
self.offs_local_volume_table = read_int(lnk)
self.offs_local_base_path = read_int(lnk)
self.offs_network_volume_table = read_int(lnk)
self.offs_base_name = read_int(lnk)
if self.header_size >= _LINK_INFO_HEADER_OPTIONAL:
# TODO: read the unicode stuff
print("TODO: read the unicode stuff")
self._parse_path_elements(lnk)
else:
self.size = None
self.header_size = _LINK_INFO_HEADER_DEFAULT
self.local = 0
self.remote = 0
self.offs_local_volume_table = 0
self.offs_local_base_path = 0
self.offs_network_volume_table = 0
self.offs_base_name = 0
self.drive_type = None
self.drive_serial = None
self.volume_label = None
self.local_base_path = None
self.network_share_name = None
self.base_name = None
self._path = None
def _parse_path_elements(self, lnk):
if self.remote:
# 20 is the offset of the network share name
lnk.seek(self.start + self.offs_network_volume_table + 20)
self.network_share_name = read_cstring(lnk)
lnk.seek(self.start + self.offs_base_name)
self.base_name = read_cstring(lnk)
if self.local:
lnk.seek(self.start + self.offs_local_volume_table + 4)
self.drive_type = _DRIVE_TYPES.get(read_int(lnk))
self.drive_serial = read_int(lnk)
lnk.read(4) # volume name offset (10h)
self.volume_label = read_cstring(lnk)
lnk.seek(self.start + self.offs_local_base_path)
self.local_base_path = read_cstring(lnk)
# TODO: unicode
self.make_path()
def make_path(self):
if self.remote:
self._path = self.network_share_name + '\\' + self.base_name
if self.local:
self._path = self.local_base_path
def write(self, lnk):
if self.remote is None:
raise MissingInformationException("No location information given.")
self.start = lnk.tell()
self._calculate_sizes_and_offsets()
write_int(self.size, lnk)
write_int(self.header_size, lnk)
write_int((self.local and 1) + (self.remote and 2), lnk)
write_int(self.offs_local_volume_table, lnk)
write_int(self.offs_local_base_path, lnk)
write_int(self.offs_network_volume_table, lnk)
write_int(self.offs_base_name, lnk)
if self.remote:
self._write_network_volume_table(lnk)
write_cstring(self.base_name, lnk, padding=False)
else:
self._write_local_volume_table(lnk)
write_cstring(self.local_base_path, lnk, padding=False)
write_byte(0, lnk)
def _calculate_sizes_and_offsets(self):
# len(self.base_name) + 1 # zero terminated strings
self.size_base_name = 1
self.size = 28 + self.size_base_name
if self.remote:
self.size_network_volume_table = 20 + \
len(self.network_share_name) + len(self.base_name) + 1
self.size += self.size_network_volume_table
self.offs_local_volume_table = 0
self.offs_local_base_path = 0
self.offs_network_volume_table = 28
self.offs_base_name = self.offs_network_volume_table + \
self.size_network_volume_table
else:
self.size_local_volume_table = 16 + len(self.volume_label) + 1
self.size_local_base_path = len(self.local_base_path) + 1
self.size += self.size_local_volume_table + self.size_local_base_path
self.offs_local_volume_table = 28
self.offs_local_base_path = self.offs_local_volume_table + \
self.size_local_volume_table
self.offs_network_volume_table = 0
self.offs_base_name = self.offs_local_base_path + self.size_local_base_path
def _write_network_volume_table(self, buf):
write_int(self.size_network_volume_table, buf)
write_int(2, buf) # ?
write_int(20, buf) # size of Network Volume Table
write_int(0, buf) # ?
write_int(131072, buf) # ?
write_cstring(self.network_share_name, buf)
def _write_local_volume_table(self, buf):
write_int(self.size_local_volume_table, buf)
try:
drive_type = _DRIVE_TYPE_IDS[self.drive_type]
except KeyError:
raise ValueError(
"This is not a valid drive type: %s" % self.drive_type)
write_int(drive_type, buf)
write_int(self.drive_serial, buf)
write_int(16, buf) # volume name offset
write_cstring(self.volume_label, buf)
@property
def path(self):
return self._path
def __str__(self):
s = "File Location Info:"
if self._path is None:
return s + " <not specified>"
if self.remote:
s += "\n (remote)"
s += "\n Network Share: %s" % self.network_share_name
s += "\n Base Name: %s" % self.base_name
else:
s += "\n (local)"
s += "\n Volume Type: %s" % self.drive_type
s += "\n Volume Serial Number: %s" % self.drive_serial
s += "\n Volume Label: %s" % self.volume_label
s += "\n Path: %s" % self.local_base_path
return s
EXTRA_DATA_TYPES = {
0xA0000002: 'ConsoleDataBlock', # size 0x000000CC
0xA0000004: 'ConsoleFEDataBlock', # size 0x0000000C
0xA0000006: 'DarwinDataBlock', # size 0x00000314
0xA0000001: 'EnvironmentVariableDataBlock', # size 0x00000314
0xA0000007: 'IconEnvironmentDataBlock', # size 0x00000314
0xA000000B: 'KnownFolderDataBlock', # size 0x0000001C
0xA0000009: 'PropertyStoreDataBlock', # size >= 0x0000000C
0xA0000008: 'ShimDataBlock', # size >= 0x00000088
0xA0000005: 'SpecialFolderDataBlock', # size 0x00000010
0xA0000003: 'VistaAndAboveIDListDataBlock', # size 0x00000060
0xA000000C: 'VistaIDListDataBlock', # size 0x00000173
}
class ExtraData_Unparsed(object):
def __init__(self, bytes=None, signature=None, data=None):
self._signature = signature
self._size = None
self.data = data
# if data:
# self._size = len(data)
if bytes:
# self._size = len(bytes)
self.data = bytes
# self.read(bytes)
# def read(self, bytes):
# buf = BytesIO(bytes)
# size = len(bytes)
# # self._size = read_int(buf)
# # self._signature = read_int(buf)
# self.data = buf.read(self._size - 8)
def bytes(self):
buf = BytesIO()
write_int(len(self.data)+8, buf)
write_int(self._signature, buf)
buf.write(self.data)
return buf.getvalue()
def __str__(self):
s = 'ExtraDataBlock\n signature %s\n data: %s' % (
hex(self._signature), self.data)
return s
def padding(val, size, byte=b'\x00'):
return val + (size-len(val)) * byte
class ExtraData_IconEnvironmentDataBlock(object):
def __init__(self, bytes=None):
# self._size = None
# self._signature = None
self._signature = 0xA0000007
self.target_ansi = None
self.target_unicode = None
if bytes:
self.read(bytes)
def read(self, bytes):
buf = BytesIO(bytes)
# self._size = read_int(buf)
# self._signature = read_int(buf)
self.target_ansi = buf.read(260).decode('ansi')
self.target_unicode = buf.read(520).decode('utf-16-le')
def bytes(self):
target_ansi = padding(self.target_ansi.encode(), 260)
target_unicode = padding(self.target_unicode.encode('utf-16-le'), 520)
size = 8 + len(target_ansi) + len(target_unicode)
assert self._signature == 0xA0000007
assert size == 0x00000314
buf = BytesIO()
write_int(size, buf)
write_int(self._signature, buf)
buf.write(target_ansi)
buf.write(target_unicode)
return buf.getvalue()
def __str__(self):
target_ansi = self.target_ansi.replace('\x00', '')
target_unicode = self.target_unicode.replace('\x00', '')
s = f'IconEnvironmentDataBlock\n TargetAnsi: {
target_ansi}\n TargetUnicode: {target_unicode}'
return s
def guid_to_str(guid):
ordered = [guid[3], guid[2], guid[1], guid[0], guid[5], guid[4],
guid[7], guid[6], guid[8], guid[9], guid[10], guid[11],
guid[12], guid[13], guid[14], guid[15]]
res = "{%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X}" % tuple([
x for x in ordered])
# print(guid, res)
return res
class TypedPropertyValue(object):
# types: [MS-OLEPS] section 2.15
def __init__(self, bytes=None, type=None, value=None):
self.type = type
self.value = value
if bytes:
self.type = read_short(BytesIO(bytes))
padding = bytes[2:4]
self.value = bytes[4:]
def set_string(self, value):
self.type = 0x1f
buf = BytesIO()
write_int(len(value)+2, buf)
buf.write(value.encode('utf-16-le'))
# terminator (included in size)
buf.write(b'\x00\x00\x00\x00')
# padding (not included in size)
if len(value) % 2:
buf.write(b'\x00\x00')
self.value = buf.getvalue()
@property
def bytes(self):
buf = BytesIO()
write_short(self.type, buf)
write_short(0x0000, buf)
buf.write(self.value)
return buf.getvalue()
def __str__(self):
value = self.value
if self.type == 0x1F:
size = value[:4]
value = value[4:].decode('utf-16-le')
if self.type == 0x15:
value = unpack('<Q', value)[0]
if self.type == 0x13:
value = unpack('<I', value)[0]
if self.type == 0x14:
value = unpack('<q', value)[0]
if self.type == 0x16:
value = unpack('<i', value)[0]
if self.type == 0x17:
value = unpack('<I', value)[0]
if self.type == 0x48:
value = guid_to_str(value)
if self.type == 0x40:
# FILETIME (Packet Version)
stream = BytesIO(value)
low = read_int(stream)
high = read_int(stream)
num = (high << 32) + low
value = convert_time_to_unix(num)
return '%s: %s' % (hex(self.type), value)
class PropertyStore:
def __init__(self, bytes=None, properties=None, format_id=None, is_strings=False):
self.is_strings = is_strings
self.properties = []
self.format_id = format_id
self._is_end = False
if properties:
self.properties = properties
if bytes:
self.read(bytes)
def read(self, bytes_io):
buf = bytes_io
size = read_int(buf)
assert size < len(buf.getvalue())
if size == 0x00000000:
self._is_end = True
return
version = read_int(buf)
assert version == 0x53505331
self.format_id = buf.read(16)
if self.format_id == b'\xD5\xCD\xD5\x05\x2E\x9C\x10\x1B\x93\x97\x08\x00\x2B\x2C\xF9\xAE':
self.is_strings = True
else:
self.is_strings = False
while True:
# assert lnk.tell() < (start + size)
value_size = read_int(buf)
if value_size == 0x00000000:
break
if self.is_strings:
name_size = read_int(buf)
reserved = read_byte(buf)
name = buf.read(name_size).decode('utf-16-le')
value = TypedPropertyValue(buf.read(value_size-9))
self.properties.append((name, value))
else:
value_id = read_int(buf)
reserved = read_byte(buf)
value = TypedPropertyValue(buf.read(value_size-9))
self.properties.append((value_id, value))
@property
def bytes(self):
size = 8 + len(self.format_id)
properties = BytesIO()
for name, value in self.properties:
value_bytes = value.bytes
if self.is_strings:
name_bytes = name.encode('utf-16-le')
value_size = 9 + len(name_bytes) + len(value_bytes)
write_int(value_size, properties)
name_size = len(name_bytes)
write_int(name_size, properties)
properties.write(b'\x00')
properties.write(name_bytes)
else:
value_size = 9 + len(value_bytes)
write_int(value_size, properties)
write_int(name, properties)
properties.write(b'\x00')
properties.write(value_bytes)
size += value_size
write_int(0x00000000, properties)
size += 4
buf = BytesIO()
write_int(size, buf)
write_int(0x53505331, buf)
buf.write(self.format_id)
buf.write(properties.getvalue())
return buf.getvalue()
def __str__(self):
s = ' PropertyStore'
s += '\n FormatID: %s' % guid_to_str(self.format_id)
for name, value in self.properties:
s += '\n %3s = %s' % (name, str(value))
return s.strip()
class ExtraData_PropertyStoreDataBlock(object):
def __init__(self, bytes=None, stores=None):
self._size = None
self._signature = 0xA0000009
self.stores = []
if stores:
self.stores = stores
if bytes:
self.read(bytes)
def read(self, bytes):
buf = BytesIO(bytes)
# self._size = read_int(buf)
# self._signature = read_int(buf)
# [MS-PROPSTORE] section 2.2
while True:
prop_store = PropertyStore(buf)
if prop_store._is_end:
break
self.stores.append(prop_store)
def bytes(self):
stores = b''
for prop_store in self.stores:
stores += prop_store.bytes
size = len(stores) + 8 + 4
assert self._signature == 0xA0000009
assert size >= 0x0000000C
buf = BytesIO()
write_int(size, buf)
write_int(self._signature, buf)
buf.write(stores)
write_int(0x00000000, buf)
return buf.getvalue()
def __str__(self):
s = 'PropertyStoreDataBlock'
for prop_store in self.stores:
s += '\n %s' % str(prop_store)
return s
class ExtraData_EnvironmentVariableDataBlock(object):
def __init__(self, bytes=None):
self._signature = 0xA0000001
self.target_ansi = None
self.target_unicode = None
if bytes:
self.read(bytes)
def read(self, bytes):
buf = BytesIO(bytes)
self.target_ansi = buf.read(260).decode()
self.target_unicode = buf.read(520).decode('utf-16-le')
def bytes(self):
target_ansi = padding(self.target_ansi.encode(), 260)
target_unicode = padding(self.target_unicode.encode('utf-16-le'), 520)
size = 8 + len(target_ansi) + len(target_unicode)
assert self._signature == 0xA0000001
assert size == 0x00000314
buf = BytesIO()
write_int(size, buf)
write_int(self._signature, buf)
buf.write(target_ansi)
buf.write(target_unicode)
return buf.getvalue()
def __str__(self):
target_ansi = self.target_ansi.replace('\x00', '')
target_unicode = self.target_unicode.replace('\x00', '')
s = f'EnvironmentVariableDataBlock\n TargetAnsi: {
target_ansi}\n TargetUnicode: {target_unicode}'
return s
EXTRA_DATA_TYPES_CLASSES = {
'IconEnvironmentDataBlock': ExtraData_IconEnvironmentDataBlock,
'PropertyStoreDataBlock': ExtraData_PropertyStoreDataBlock,
'EnvironmentVariableDataBlock': ExtraData_EnvironmentVariableDataBlock,
}
class ExtraData(object):
# EXTRA_DATA = *EXTRA_DATA_BLOCK TERMINAL_BLOCK
def __init__(self, lnk=None, blocks=None):
self.blocks = []
if blocks:
self.blocks = blocks
if lnk is None:
return
while True:
size = read_int(lnk)
if size < 4: # TerminalBlock
break
signature = read_int(lnk)
bytes = lnk.read(size-8)
# lnk.seek(-8, 1)
block_type = EXTRA_DATA_TYPES[signature]
if block_type in EXTRA_DATA_TYPES_CLASSES:
block_class = EXTRA_DATA_TYPES_CLASSES[block_type]
block = block_class(bytes=bytes)
else:
block_class = ExtraData_Unparsed
block = block_class(bytes=bytes, signature=signature)
self.blocks.append(block)
@property
def bytes(self):
result = b''
for block in self.blocks:
result += block.bytes()
result += b'\x00\x00\x00\x00' # TerminalBlock
return result
def __str__(self):
s = ''
for block in self.blocks:
s += '\n' + str(block)
return s
class Lnk(object):
def __init__(self, f=None):
self.file = None
if type(f) == str or type(f) == str:
self.file = f
try:
f = open(self.file, 'rb')
except IOError:
self.file += ".lnk"
f = open(self.file, 'rb')
# defaults
self.link_flags = Flags(_LINK_FLAGS)
self.file_flags = Flags(_FILE_ATTRIBUTES_FLAGS)
self.creation_time = datetime.now()
self.access_time = datetime.now()
self.modification_time = datetime.now()
self.file_size = 0
self.icon_index = 0
self._show_command = WINDOW_NORMAL
self.hot_key = None
self._link_info = LinkInfo()
self.description = None
self.relative_path = None
self.work_dir = None
self.arguments = None
self.icon = None
self.extra_data = None
if f is not None:
assert_lnk_signature(f)
self._parse_lnk_file(f)
if self.file:
f.close()
def _read_hot_key(self, lnk):
low = read_byte(lnk)
high = read_byte(lnk)
key = _KEYS.get(low, '')
modifier = high and str(ModifierKeys(high)) or ''
return modifier + key
def _write_hot_key(self, hot_key, lnk):
if hot_key is None or not hot_key:
low = high = 0
else:
hot_key = hot_key.split('+')
try:
low = _KEY_CODES[hot_key[-1]]
except KeyError:
raise InvalidKeyException(
"Cannot find key code for %s" % hot_key[1])
modifiers = ModifierKeys()
for modifier in hot_key[:-1]:
modifiers[modifier.upper()] = True
high = modifiers.bytes
write_byte(low, lnk)
write_byte(high, lnk)
def _parse_lnk_file(self, lnk):
# SHELL_LINK_HEADER [LINKTARGET_IDLIST] [LINKINFO] [STRING_DATA] *EXTRA_DATA
# SHELL_LINK_HEADER
lnk.seek(20) # after signature and guid
self.link_flags.set_flags(read_int(lnk))
self.file_flags.set_flags(read_int(lnk))
self.creation_time = convert_time_to_unix(read_double(lnk))
self.access_time = convert_time_to_unix(read_double(lnk))
self.modification_time = convert_time_to_unix(read_double(lnk))
self.file_size = read_int(lnk)
self.icon_index = read_int(lnk)
show_command = read_int(lnk)
self._show_command = _SHOW_COMMANDS[show_command] if show_command in _SHOW_COMMANDS else _SHOW_COMMANDS[1]
self.hot_key = self._read_hot_key(lnk)
lnk.read(10) # reserved (0)
# LINKTARGET_IDLIST (HasLinkTargetIDList)
if self.link_flags.HasLinkTargetIDList:
shell_item_id_list_size = read_short(lnk)
self.shell_item_id_list = LinkTargetIDList(
lnk.read(shell_item_id_list_size))
# LINKINFO (HasLinkInfo)
if self.link_flags.HasLinkInfo and not self.link_flags.ForceNoLinkInfo:
self._link_info = LinkInfo(lnk)
lnk.seek(self._link_info.start + self._link_info.size)
# STRING_DATA = [NAME_STRING] [RELATIVE_PATH] [WORKING_DIR] [COMMAND_LINE_ARGUMENTS] [ICON_LOCATION]
if self.link_flags.HasName:
self.description = read_sized_string(
lnk, self.link_flags.IsUnicode)
if self.link_flags.HasRelativePath:
self.relative_path = read_sized_string(
lnk, self.link_flags.IsUnicode)
if self.link_flags.HasWorkingDir:
self.work_dir = read_sized_string(lnk, self.link_flags.IsUnicode)
if self.link_flags.HasArguments:
self.arguments = read_sized_string(lnk, self.link_flags.IsUnicode)
if self.link_flags.HasIconLocation:
self.icon = read_sized_string(lnk, self.link_flags.IsUnicode)
# *EXTRA_DATA
self.extra_data = ExtraData(lnk)
def save(self, f: Optional[Union[str, IOBase]] = None, force_ext=False):
if f is None:
f = self.file
if f is None:
raise ValueError("File (name) missing for saving the lnk")
is_file = hasattr(f, 'write')
if not is_file:
if not type(f) == str and not type(f) == str:
raise ValueError(
"Need a writeable object or a file name to save to, got %s" % f)
if force_ext:
if not f.lower().endswith('.lnk'):
f += '.lnk'
f = open(f, 'wb')
self.write(f)
# only close the stream if it's our own
if not is_file:
f.close()
def write(self, lnk):
lnk.write(_SIGNATURE)
lnk.write(_GUID)
write_int(self.link_flags.bytes, lnk)
write_int(self.file_flags.bytes, lnk)
write_double(convert_time_to_windows(self.creation_time), lnk)
write_double(convert_time_to_windows(self.access_time), lnk)
write_double(convert_time_to_windows(self.modification_time), lnk)
write_int(self.file_size, lnk)
write_int(self.icon_index, lnk)
write_int(_SHOW_COMMAND_IDS[self._show_command], lnk)
self._write_hot_key(self.hot_key, lnk)
lnk.write(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') # reserved
if self.link_flags.HasLinkTargetIDList:
shell_item_id_list = self.shell_item_id_list.bytes
write_short(len(shell_item_id_list), lnk)
lnk.write(shell_item_id_list)
if self.link_flags.HasLinkInfo:
self._link_info.write(lnk)
if self.link_flags.HasName:
write_sized_string(self.description, lnk,
self.link_flags.IsUnicode)
if self.link_flags.HasRelativePath:
write_sized_string(self.relative_path, lnk,
self.link_flags.IsUnicode)
if self.link_flags.HasWorkingDir:
write_sized_string(self.work_dir, lnk, self.link_flags.IsUnicode)
if self.link_flags.HasArguments:
write_sized_string(self.arguments, lnk, self.link_flags.IsUnicode)
if self.link_flags.HasIconLocation:
write_sized_string(self.icon, lnk, self.link_flags.IsUnicode)
if self.extra_data:
lnk.write(self.extra_data.bytes)
else:
lnk.write(b'\x00\x00\x00\x00')
def _get_shell_item_id_list(self):
return self._shell_item_id_list
def _set_shell_item_id_list(self, shell_item_id_list):
self._shell_item_id_list = shell_item_id_list
self.link_flags.HasLinkTargetIDList = shell_item_id_list is not None
shell_item_id_list = property(
_get_shell_item_id_list, _set_shell_item_id_list)
def _get_link_info(self):
return self._link_info
def _set_link_info(self, link_info):
self._link_info = link_info
self.link_flags.ForceNoLinkInfo = link_info is None
self.link_flags.HasLinkInfo = link_info is not None
link_info = property(_get_link_info, _set_link_info)
def _get_description(self):
return self._description
def _set_description(self, description):
self._description = description
self.link_flags.HasName = description is not None
description = property(_get_description, _set_description)
def _get_relative_path(self):
return self._relative_path
def _set_relative_path(self, relative_path):
self._relative_path = relative_path
self.link_flags.HasRelativePath = relative_path is not None
relative_path = property(_get_relative_path, _set_relative_path)
def _get_work_dir(self):
return self._work_dir
def _set_work_dir(self, work_dir):
self._work_dir = work_dir
self.link_flags.HasWorkingDir = work_dir is not None
work_dir = working_dir = property(_get_work_dir, _set_work_dir)
def _get_arguments(self):
return self._arguments
def _set_arguments(self, arguments):
self._arguments = arguments
self.link_flags.HasArguments = arguments is not None
arguments = property(_get_arguments, _set_arguments)
def _get_icon(self):
return self._icon
def _set_icon(self, icon):
self._icon = icon
self.link_flags.HasIconLocation = icon is not None
icon = property(_get_icon, _set_icon)
def _get_window_mode(self):
return self._show_command
def _set_window_mode(self, value):
if value not in list(_SHOW_COMMANDS.values()):
raise ValueError(
"Not a valid window mode: %s. Choose any of pylnk.WINDOW_*" % value)
self._show_command = value
window_mode = show_command = property(_get_window_mode, _set_window_mode)
@property
def path(self):
# lnk can contains several different paths at different structures
# here is some logic consistent with link properties at explorer (at least on test examples)
link_info_path = self._link_info.path if self._link_info and self._link_info.path else None
id_list_path = self._shell_item_id_list.get_path() if hasattr(
self, '_shell_item_id_list') else None
env_var_path = None
if self.extra_data and self.extra_data.blocks:
for block in self.extra_data.blocks:
if type(block) == ExtraData_EnvironmentVariableDataBlock:
env_var_path = block.target_unicode.strip(
'\x00') or block.target_ansi.strip('\x00')
break
if id_list_path and id_list_path.startswith('%MY_COMPUTER%'):
# full local path has priority
return id_list_path[14:]
if id_list_path and id_list_path.startswith('%USERPROFILE%\\::'):
# path to KNOWN_FOLDER also has priority over link_info
return id_list_path[14:]
if link_info_path:
# local path at link_info_path has priority over network path at id_list_path
# full local path at link_info_path has priority over partial path at id_list_path
return link_info_path
if env_var_path:
# some links in Recent folder contains path only at ExtraData_EnvironmentVariableDataBlock
return env_var_path
return id_list_path
def specify_local_location(self, path, drive_type=None, drive_serial=None, volume_label=None):
self._link_info.drive_type = drive_type or DRIVE_UNKNOWN
self._link_info.drive_serial = drive_serial or ''
self._link_info.volume_label = volume_label or ''
self._link_info.local_base_path = path
self._link_info.local = True
self._link_info.make_path()
def specify_remote_location(self, network_share_name, base_name):
self._link_info.network_share_name = network_share_name
self._link_info.base_name = base_name
self._link_info.remote = True
self._link_info.make_path()
def __str__(self):
s = "Target file:\n"
s += str(self.file_flags)
s += "\nCreation Time: %s" % self.creation_time
s += "\nModification Time: %s" % self.modification_time
s += "\nAccess Time: %s" % self.access_time
s += "\nFile size: %s" % self.file_size
s += "\nWindow mode: %s" % self._show_command
s += "\nHotkey: %s\n" % self.hot_key
s += str(self._link_info)
if self.link_flags.HasLinkTargetIDList:
s += "\n%s" % self.shell_item_id_list
if self.link_flags.HasName:
s += "\nDescription: %s" % self.description
if self.link_flags.HasRelativePath:
s += "\nRelative Path: %s" % self.relative_path
if self.link_flags.HasWorkingDir:
s += "\nWorking Directory: %s" % self.work_dir
if self.link_flags.HasArguments:
s += "\nCommandline Arguments: %s" % self.arguments
if self.link_flags.HasIconLocation:
s += "\nIcon: %s" % self.icon
if self._link_info:
s += "\nUsed Path: %s" % self.path
if self.extra_data:
s += str(self.extra_data)
return s
# ---- convenience functions
def parse(lnk):
return Lnk(lnk)
def create(f=None):
lnk = Lnk()
lnk.file = f
return lnk
def for_file(
target_file, lnk_name=None, arguments=None, description=None, icon_file=None, icon_index=0,
work_dir=None, window_mode=None,
):
lnk = create(lnk_name)
lnk.link_flags.IsUnicode = True
lnk.link_info = None
if target_file.startswith('\\\\'):
# remote link
lnk.link_info = LinkInfo()
lnk.link_info.remote = 1
# extract server + share name from full path
path_parts = target_file.split('\\')
share_name, base_name = '\\'.join(
path_parts[:4]), '\\'.join(path_parts[4:])
lnk.link_info.network_share_name = share_name.upper()
lnk.link_info.base_name = base_name
# somehow it requires EnvironmentVariableDataBlock & HasExpString flag
env_data_block = ExtraData_EnvironmentVariableDataBlock()
env_data_block.target_ansi = target_file
env_data_block.target_unicode = target_file
lnk.extra_data = ExtraData(blocks=[env_data_block])
lnk.link_flags.HasExpString = True
else:
# local link
levels = list(path_levels(target_file))
elements = [RootEntry(ROOT_MY_COMPUTER),
DriveEntry(levels[0])]
for level in levels[1:]:
segment = PathSegmentEntry.create_for_path(level)
elements.append(segment)
lnk.shell_item_id_list = LinkTargetIDList()
lnk.shell_item_id_list.items = elements
# lnk.link_flags.HasLinkInfo = True
if arguments:
lnk.link_flags.HasArguments = True
lnk.arguments = arguments
if description:
lnk.link_flags.HasName = True
lnk.description = description
if icon_file:
lnk.link_flags.HasIconLocation = True
lnk.icon = icon_file
lnk.icon_index = icon_index
if work_dir:
lnk.link_flags.HasWorkingDir = True
lnk.work_dir = work_dir
if window_mode:
lnk.window_mode = window_mode
if lnk_name:
lnk.save()
return lnk
def from_segment_list(data, lnk_name=None):
"""
Creates a lnk file from a list of path segments.
If lnk_name is given, the resulting lnk will be saved
to a file with that name.
The expected list for has the following format ("C:\\dir\\file.txt"):
['c:\\',
{'type': TYPE_FOLDER,
'size': 0, # optional for folders
'name': "dir",
'created': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476),
'modified': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476),
'accessed': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476)
},
{'type': TYPE_FILE,
'size': 823,
'name': "file.txt",
'created': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476),
'modified': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476),
'accessed': datetime.datetime(2012, 10, 12, 23, 28, 11, 8476)
}
]
For relative paths just omit the drive entry.
Hint: Correct dates really are not crucial for working lnks.
"""
if type(data) not in (list, tuple):
raise ValueError("Invalid data format, list or tuple expected")
lnk = Lnk()
entries = []
if is_drive(data[0]):
# this is an absolute link
entries.append(RootEntry(ROOT_MY_COMPUTER))
if not data[0].endswith('\\'):
data[0] += "\\"
drive = data.pop(0).encode("ascii")
entries.append(DriveEntry(drive))
for level in data:
segment = PathSegmentEntry()
segment.type = level['type']
if level['type'] == TYPE_FOLDER:
segment.file_size = 0
else:
segment.file_size = level['size']
segment.short_name = level['name']
segment.full_name = level['name']
segment.created = level['created']
segment.modified = level['modified']
segment.accessed = level['accessed']
entries.append(segment)
lnk.shell_item_id_list = LinkTargetIDList()
lnk.shell_item_id_list.items = entries
if data[-1]['type'] == TYPE_FOLDER:
lnk.file_flags.directory = True
if lnk_name:
lnk.save(lnk_name)
return lnk
def build_uwp(
package_family_name, target, location=None, logo44x44=None, lnk_name=None,
) -> Lnk:
"""
:param lnk_name: ex.: crafted_uwp.lnk
:param package_family_name: ex.: Microsoft.WindowsCalculator_10.1910.0.0_x64__8wekyb3d8bbwe
:param target: ex.: Microsoft.WindowsCalculator_8wekyb3d8bbwe!App
:param location: ex.: C:\\Program Files\\WindowsApps\\Microsoft.WindowsCalculator_10.1910.0.0_x64__8wekyb3d8bbwe
:param logo44x44: ex.: Assets\\CalculatorAppList.png
"""
lnk = Lnk()
lnk.link_flags.HasLinkTargetIDList = True
lnk.link_flags.IsUnicode = True
lnk.link_flags.EnableTargetMetadata = True
lnk.shell_item_id_list = LinkTargetIDList()
elements = [
RootEntry(ROOT_UWP_APPS),
UwpSegmentEntry.create(
package_family_name=package_family_name,
target=target,
location=location,
logo44x44=logo44x44,
)
]
lnk.shell_item_id_list.items = elements
if lnk_name:
lnk.file = lnk_name
lnk.save()
return lnk
def get_prop(obj, prop_queue):
attr = getattr(obj, prop_queue[0])
if len(prop_queue) > 1:
return get_prop(attr, prop_queue[1:])
return attr
def cli():
parser = argparse.ArgumentParser(add_help=False)
subparsers = parser.add_subparsers(dest='action', metavar='{p, c, d}')
parser.add_argument('--help', '-h', action='store_true')
parser_parse = subparsers.add_parser(
'parse', aliases=['p'], help='read lnk file')
parser_parse.add_argument('filename', help='lnk filename to read')
parser_parse.add_argument('props', nargs='*', help='props path to read')
parser_create = subparsers.add_parser(
'create', aliases=['c'], help='create new lnk file')
parser_create.add_argument('target', help='target path')
parser_create.add_argument('name', help='lnk filename to create')
parser_create.add_argument(
'--arguments', '-a', nargs='?', help='additional arguments')
parser_create.add_argument(
'--description', '-d', nargs='?', help='description')
parser_create.add_argument('--icon', '-i', nargs='?', help='icon filename')
parser_create.add_argument(
'--icon-index', '-ii', type=int, default=0, nargs='?', help='icon index')
parser_create.add_argument(
'--workdir', '-w', nargs='?', help='working directory')
parser_create.add_argument(
'--mode', '-m', nargs='?', choices=['Maximized', 'Normal', 'Minimized'], help='window mode')
parser_dup = subparsers.add_parser(
'duplicate', aliases=['d'], help='read and write lnk file')
parser_dup.add_argument('filename', help='lnk filename to read')
parser_dup.add_argument('new_filename', help='new filename to write')
args = parser.parse_args()
if args.help or not args.action:
print('''
Tool for read or create .lnk files
usage: pylnk3.py [p]arse / [c]reate ...
Examples:
pylnk3 p filename.lnk
pylnk3 c c:\\prog.exe shortcut.lnk
pylnk3 c \\\\192.168.1.1\\share\\file.doc doc.lnk
pylnk3 create c:\\1.txt text.lnk -m Minimized -d "Description"
for more info use help for each action (ex.: "pylnk3 create -h")
'''.strip())
exit(1)
if args.action in ['create', 'c']:
for_file(
args.target, args.name, arguments=args.arguments,
description=args.description, icon_file=args.icon,
icon_index=args.icon_index, work_dir=args.workdir,
window_mode=args.mode,
)
elif args.action in ['parse', 'p']:
lnk = parse(args.filename)
props = args.props
if len(props) == 0:
print(lnk)
else:
for prop in props:
print(get_prop(lnk, prop.split('.')))
elif args.action in ['d', 'duplicate']:
lnk = parse(args.filename)
new_filename = args.new_filename
print(lnk)
lnk.save(new_filename)
print('saved')
if __name__ == '__main__':
cli()