Created
March 15, 2021 06:15
-
-
Save jborean93/89dd5b606a29e23a1641ed934bd4d5b6 to your computer and use it in GitHub Desktop.
Get information about a Windows shortcut (lnk) files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Copyright: (c) 2021, Jordan Borean (@jborean93) <[email protected]> | |
# MIT License (see LICENSE or https://opensource.org/licenses/MIT) | |
Function Get-Shortcut { | |
[CmdletBinding(DefaultParameterSetName='Path')] | |
param ( | |
[Parameter( | |
Mandatory = $true, | |
Position = 0, | |
ValueFromPipeline = $true, | |
ValueFromPipelineByPropertyName = $true, | |
ParameterSetName = 'Path' | |
)] | |
[SupportsWildcards()] | |
[ValidateNotNullOrEmpty()] | |
[String[]] | |
$Path, | |
[Parameter( | |
Mandatory = $true, | |
Position = 0, | |
ValueFromPipelineByPropertyName = $true, | |
ParameterSetName = 'LiteralPath' | |
)] | |
[Alias('PSPath')] | |
[ValidateNotNullOrEmpty()] | |
[String[]] | |
$LiteralPath | |
) | |
begin { | |
[Flags()] enum LinkFlags { | |
HasLinkTargetIDList = 0x00000001 | |
HasLinkInfo = 0x00000002 | |
HasName = 0x00000004 | |
HasRelativePath = 0x00000008 | |
HasWorkingDir = 0x00000010 | |
HasArguments = 0x00000020 | |
HasIconLocation = 0x00000040 | |
IsUnicode = 0x00000080 | |
ForceNoLinkInfo = 0x00000100 | |
HasExpString = 0x00000200 | |
RunInSeparateProcess = 0x00000400 | |
Unused1 = 0x00000800 | |
HasDarwinID = 0x00001000 | |
RunAsUser = 0x00002000 | |
HasExpIcon = 0x00004000 | |
NoPidlAlias = 0x00008000 | |
Unused2 = 0x00010000 | |
RunWithShimLayer = 0x00020000 | |
ForceNoLinkTrack = 0x00040000 | |
EnableTargetMetadata = 0x00080000 | |
DisableLinkPathTracking = 0x00100000 | |
DisableKnownFolderTracking = 0x00200000 | |
DisableKnownFolderAlias = 0x00400000 | |
AllowLinkToLink = 0x00800000 | |
UnaliasOnSave = 0x01000000 | |
PreferEnvironmentPath = 0x02000000 | |
KeepLocalIDListForUNCTarget = 0x04000000 | |
Unused3 = 0x08000000 | |
Unused4 = 0x10000000 | |
Unused5 = 0x20000000 | |
Unused6 = 0x40000000 | |
Unused7 = 0x80000000 | |
} | |
enum ShowCommand { | |
Normal = 0x00000001 | |
Maximized = 0x00000003 | |
MinNoActive = 0x00000007 | |
} | |
[Flags()] enum HotKeys { | |
None = 0x0000 | |
Menu = 0x0012 | |
Pause = 0x0013 | |
CapsLock = 0x0014 | |
Escape = 0x001B | |
Convert = 0x001C | |
NonConvert = 0x001D | |
Accept = 0x001E | |
ModeChange = 0x001F | |
Space = 0x0020 | |
PageUp = 0x0021 | |
PageDown = 0x0022 | |
End = 0x0023 | |
Home = 0x0024 | |
Left = 0x0025 | |
Up = 0x0026 | |
Right = 0x0027 | |
Down = 0x0028 | |
Select = 0x0029 | |
Print = 0x002A | |
Execute = 0x002B | |
Snapshot = 0x002C | |
Insert = 0x002D | |
Del = 0x002E | |
Help = 0x002F | |
Num0 = 0x0030 | |
Num1 = 0x0031 | |
Num2 = 0x0032 | |
Num3 = 0x0033 | |
Num4 = 0x0034 | |
Num5 = 0x0035 | |
Num6 = 0x0036 | |
Num7 = 0x0037 | |
Num8 = 0x0038 | |
Num9 = 0x0039 | |
A = 0x0041 | |
B = 0x0042 | |
C = 0x0043 | |
D = 0x0044 | |
E = 0x0045 | |
F = 0x0046 | |
G = 0x0047 | |
H = 0x0048 | |
I = 0x0049 | |
J = 0x004A | |
K = 0x004B | |
L = 0x004C | |
M = 0x004D | |
N = 0x004E | |
O = 0x004F | |
P = 0x0050 | |
Q = 0x0051 | |
R = 0x0052 | |
S = 0x0053 | |
T = 0x0054 | |
U = 0x0055 | |
V = 0x0056 | |
W = 0x0057 | |
X = 0x0058 | |
Y = 0x0059 | |
Z = 0x005A | |
NumPad0 = 0x0060 | |
NumPad1 = 0x0061 | |
NumPad2 = 0x0062 | |
NumPad3 = 0x0063 | |
NumPad4 = 0x0064 | |
NumPad5 = 0x0065 | |
NumPad6 = 0x0066 | |
NumPad7 = 0x0067 | |
NumPad8 = 0x0068 | |
NumPad9 = 0x0069 | |
NumPadMultiply = 0x006A | |
NumPadPlus = 0x006B | |
NumPadMinus = 0x006D | |
NumPadDel = 0x006E | |
NumPadDivide = 0x006F | |
F1 = 0x0070 | |
F2 = 0x0071 | |
F3 = 0x0072 | |
F4 = 0x0073 | |
F5 = 0x0074 | |
F6 = 0x0075 | |
F7 = 0x0076 | |
F8 = 0x0077 | |
F9 = 0x0078 | |
F10 = 0x0079 | |
F11 = 0x007A | |
F12 = 0x007B | |
F13 = 0x007C | |
F14 = 0x007D | |
F15 = 0x007E | |
F16 = 0x007F | |
F17 = 0x0080 | |
F18 = 0x0081 | |
F19 = 0x0082 | |
F20 = 0x0083 | |
F21 = 0x0084 | |
F22 = 0x0085 | |
F23 = 0x0086 | |
F24 = 0x0087 | |
Numlock = 0x0090 | |
Scroll = 0x0091 | |
Tilde = 0x00C0 | |
Shift = 0x0100 | |
Control = 0x0200 | |
Alt = 0x0400 | |
UnknownModifier = 0x0800 # TODO: Cannot find any docs on this one | |
} | |
[Flags()] enum LinkInfoFlags { | |
VolumeIDAndLocalBasePath = 0x00000001 | |
CommonNetworkRelativeLinkAndPathSuffix = 0x00000002 | |
} | |
enum DriveType { | |
Unknown = 0x00000000 | |
NoRootDir = 0x00000001 | |
Removable = 0x00000002 | |
Fixed = 0x00000003 | |
Remote = 0x00000004 | |
CDRom = 0x00000005 | |
Ramdisk = 0x00000006 | |
} | |
enum BlockSignature { | |
Console = 0xA0000002 | |
ConsoleFE = 0xA0000004 | |
Darwin = 0xA0000006 | |
Environment = 0xA0000001 | |
IconEnvironment = 0xA0000007 | |
KnownFolder = 0xA000000B | |
PropertyStore = 0xA000000B | |
Shim = 0xA0000008 | |
SpecialFolder = 0xA0000005 | |
Tracker = 0xA0000003 | |
VistaAndAboveIDList = 0xA000000C | |
} | |
$readBuffer = { | |
param ( | |
[Parameter(Mandatory)] | |
[IO.Stream] | |
$Stream, | |
[Parameter(Mandatory)] | |
[int] | |
$Length | |
) | |
$buffer = [byte[]]::new($Length) | |
$read = 0 | |
while ($read -lt $buffer.Length) { | |
$read += $Stream.Read($buffer, $read, $buffer.Length) | |
} | |
,$buffer | |
} | |
$readString = { | |
param ( | |
[byte[]] | |
$Buffer, | |
[int] | |
$Offset, | |
[Text.Encoding] | |
$Encoding = [Text.Encoding]::Default | |
) | |
$temp = $buffer[$Offset..$buffer.Length] | |
$str = $Encoding.GetString($temp) | |
$nullIdx = $str.IndexOf("`0") | |
if ($nullIdx -ge 0) { | |
$str = $str[0..$nullIdx] -join '' | |
} | |
$str.TrimEnd("`0") | |
} | |
} | |
process { | |
if ($PSCmdlet.ParameterSetName -eq 'Path') { | |
$allPaths = $Path | ForEach-Object -Process { | |
$provider = $null | |
$PSCmdlet.SessionState.Path.GetResolvedProviderPathFromPSPath($_, [ref]$provider) | |
} | |
} | |
elseif ($PSCmdlet.ParameterSetName -eq 'LiteralPath') { | |
$allPaths = $LiteralPath | ForEach-Object -Process { | |
$PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($_) | |
} | |
} | |
foreach ($linkPath in $allPaths) { | |
$fs = [IO.File]::OpenRead($linkPath) | |
try { | |
[void]$fs.Seek(4, 'Begin') # first 4 bytes are the HeaderSize | |
$buffer = &$readBuffer -Stream $fs -Length 72 | |
$linkClsid = [Guid]::new(([byte[]]$buffer[0..15])) | |
$linkFlags = [LinkFlags][BitConverter]::ToInt32($buffer, 16) | |
$fileAttributes = [IO.FileAttributes][BitConverter]::ToInt32($buffer, 20) | |
$creationTime = [DateTime]::FromFileTimeUtc([BitConverter]::ToInt64($buffer, 24)) | |
$accessTime = [DateTime]::FromFileTimeUtc([BitConverter]::ToInt64($buffer, 32)) | |
$writeTime = [DateTime]::FromFileTimeUtc([BitConverter]::ToInt64($buffer, 40)) | |
$fileSize = [BitConverter]::ToUInt32($buffer, 48) | |
$iconIndex = [BitConverter]::ToInt32($buffer, 52) | |
$showCommand = [BitConverter]::ToInt32($buffer, 56) | |
if ($showCommand -notin @([ShowCommand]::Maximized, [ShowCommand]::MinNoActive)) { | |
$showCommand = [ShowCommand]::Normal | |
} | |
$hotKey = [HotKeys][BitConverter]::ToInt16($buffer, 60) | |
$linkTargetList = @(if ($linkFlags.HasFlag([LinkFlags]::HasLinkTargetIDList)) { | |
$buffer = &$readBuffer -Stream $fs -Length 2 | |
$listSize = [BitConverter]::ToInt16($buffer, 0) | |
$buffer = &$readBuffer -Stream $fs -Length $listSize | |
$offset = 0 | |
while (( | |
$buffer.Length -ge ($offset + 2) -and | |
-not ($buffer[$offset] -eq [byte]0 -and $buffer[$offset + 1] -eq [byte]0) | |
)) { | |
$itemSize = [BitConverter]::ToUInt16($buffer, $offset) | |
# https://github.com/libyal/libfwsi/blob/main/documentation/Windows%20Shell%20Item%20format.asciidoc | |
[Convert]::ToBase64String([byte[]]$buffer[($offset + 2)..($offset + ($itemSize - 1))]) | |
$offset += $itemSize | |
} | |
}) | |
$linkInfo = if ($linkFlags.HasFlag([LinkFlags]::HasLinkInfo)) { | |
$buffer = &$readBuffer -Stream $fs -Length 4 | |
$linkInfoSize = [BitConverter]::ToUInt32($buffer, 0) | |
$buffer = &$readBuffer -Stream $fs -Length ($linkInfoSize - 4) | |
$linkHeaderSize = [BitConverter]::ToUInt32($buffer, 0) | |
$linkInfoFlags = [LinkInfoFlags][BitConverter]::ToInt32($buffer, 4) | |
$volumeIDOffset = [BitConverter]::ToUInt32($buffer, 8) | |
$localBasePathOffset = [BitConverter]::ToUInt32($buffer, 12) | |
$commonNetworkRelativeLinkOffset = [BitConverter]::ToUInt32($buffer, 16) | |
$commonPathSuffixOffset = [BitConverter]::ToUInt32($buffer, 20) | |
if ($linkHeaderSize -ge 0x24) { | |
$localBasePathOffsetUni = [BitConverter]::ToUInt32($buffer, 24) | |
$commonPathSuffixOffsetUni = [BitConverter]::ToUInt32($buffer, 28) | |
} | |
else { | |
$localBasePathOffsetUni = 0 | |
$commonPathSuffixOffsetUni = 0 | |
} | |
$volumeID, $localBasePath = if ($linkInfoFlags.HasFlag([LinkInfoFlags]::VolumeIDAndLocalBasePath)) { | |
$driveType = [DriveType][BitConverter]::ToInt32($buffer, $volumeIDOffset) | |
$serialNumber = [BitConverter]::ToUInt32($buffer, $volumeIDOffset + 4) | |
$labelOffset = [BitConverter]::ToUInt32($buffer, $volumeIDOffset + 8) | |
if ($labelOffset -eq 0x14) { | |
$labelOffset = [BitConverter]::ToUInt32($buffer, $volumeIDOffset + 12) | |
$encoding = [Text.Encoding]::Unicode | |
} | |
else { | |
$encoding = [Text.Encoding]::Default | |
} | |
$volumeLabelOffset = ($volumeIDOffset - 4) + $labelOffset | |
$label = &$readString -Buffer $buffer -Offset $volumeLabelOffset -Encoding $encoding | |
# VolumeID | |
[PSCustomObject]@{ | |
DriveType = $driveType | |
SerialNumber = $serialNumber | |
Label = $label | |
} | |
# LocalBasePath | |
&$readString -Buffer $buffer -Offset ($localBasePathOffset - 4) | |
} | |
$commonNetworkRelativeLink = if ($commonNetworkRelativeLinkOffset) { | |
&$readString -Buffer $buffer -Offset ($commonNetworkRelativeLinkOffset - 4) | |
} | |
$commonPathSuffix = &$readString -Buffer $buffer -Offset ($commonPathSuffixOffset - 4) | |
if ($localBasePathOffsetUni) { | |
$localBasePath = &$readString -Buffer $buffer -Offset ($localBasePathOffsetUni - 4) -Encoding ([Text.Encoding]::Unicode) | |
} | |
if ($commonPathSuffixOffsetUni) { | |
$commonPathSuffix = &$readString -Buffer $buffer -Offset ($commonPathSuffixOffsetUni - 4) -Encoding ([Text.Encoding]::Unicode) | |
} | |
[PSCustomObject]@{ | |
LinkInfoFlags = $linkInfoFlags | |
VolumeID = $volumeID | |
LocalBasePath = $localBasePath | |
CommonNetworkRelativeLink = $commonNetworkRelativeLink | |
CommonPathSuffix = $commonPathSuffix | |
} | |
} | |
$encoding = if ($linkFlags.HasFlag([LinkFlags]::IsUnicode)) { | |
[Text.Encoding]::Unicode | |
} | |
else { | |
[Text.Encoding]::Default | |
} | |
$characterBytes = $encoding.GetByteCount("`0") | |
$buffer = &$readBuffer -Stream $fs -Length ($fs.Length - $fs.Position) | |
$offset = 0 | |
$comment = if ($linkFlags.HasFlag([LinkFlags]::HasName)) { | |
$count = [BitConverter]::ToUInt16($buffer, $offset) * $characterBytes | |
$encoding.GetString($buffer, $offset + 2, $count) | |
$offset += $count + 2 | |
} | |
$relativePath = if ($linkFlags.HasFlag([LinkFlags]::HasRelativePath)) { | |
$count = [BitConverter]::ToUInt16($buffer, $offset) * $characterBytes | |
$encoding.GetString($buffer, $offset + 2, $count) | |
$offset += $count + 2 | |
} | |
$workingDir = if ($linkFlags.HasFlag([LinkFlags]::HasWorkingDir)) { | |
$count = [BitConverter]::ToUInt16($buffer, $offset) * $characterBytes | |
$encoding.GetString($buffer, $offset + 2, $count) | |
$offset += $count + 2 | |
} | |
$arguments = if ($linkFlags.HasFlag([LinkFlags]::HasArguments)) { | |
$count = [BitConverter]::ToUInt16($buffer, $offset) * $characterBytes | |
$encoding.GetString($buffer, $offset + 2, $count) | |
$offset += $count + 2 | |
} | |
$iconLocation = if ($linkFlags.HasFlag([LinkFlags]::HasIconLocation)) { | |
$count = [BitConverter]::ToUInt16($buffer, $offset) * $characterBytes | |
[Environment]::ExpandEnvironmentVariables($encoding.GetString($buffer, $offset + 2, $count)) | |
$offset += $count + 2 | |
} | |
$buffer = $buffer[$offset..$buffer.Length] | |
$offset = 0 | |
$extradata = @(while (( | |
$buffer.Length -ge ($offset + 4) -and | |
-not ($buffer[$offset] -eq [byte]0 -and $buffer[$offset + 1] -eq [byte]0 -and | |
$buffer[$offset + 2] -eq [byte]0 -and $buffer[$offset + 3] -eq [byte]0) | |
)) { | |
$blockSize = [BitConverter]::ToUInt32($buffer, $offset) | |
$blockSig = [BitConverter]::ToInt32($buffer, $offset + 4) | |
switch ($blockSig) { | |
([int][BlockSignature]::Console) { | |
[PSCustomObject]@{ | |
Signature = [BlockSignature]$blockSig | |
Data = [Convert]::ToBase64String([byte[]]$buffer[$offset..($offset + $blockSize)]) | |
} | |
} | |
([int][BlockSignature]::ConsoleFE) { | |
$codePage = [BitConverter]::ToUInt32($buffer, $offset + 8) | |
[PSCustomObject]@{ | |
Signature = [BlockSignature]$blockSig | |
CodePage = $codePage | |
} | |
} | |
([int][BlockSignature]::Darwin) { | |
[PSCustomObject]@{ | |
Signature = [BlockSignature]$blockSig | |
Data = [Convert]::ToBase64String([byte[]]$buffer[$offset..($offset + $blockSize)]) | |
} | |
} | |
([int][BlockSignature]::Environment) { | |
[PSCustomObject]@{ | |
Signature = [BlockSignature]$blockSig | |
Data = [Convert]::ToBase64String([byte[]]$buffer[$offset..($offset + $blockSize)]) | |
} | |
} | |
([int][BlockSignature]::IconEnvironment) { | |
[PSCustomObject]@{ | |
Signature = [BlockSignature]$blockSig | |
Data = [Convert]::ToBase64String([byte[]]$buffer[$offset..($offset + $blockSize)]) | |
} | |
} | |
([int][BlockSignature]::KnownFolder) { | |
[PSCustomObject]@{ | |
Signature = [BlockSignature]$blockSig | |
Data = [Convert]::ToBase64String([byte[]]$buffer[$offset..($offset + $blockSize)]) | |
} | |
} | |
([int][BlockSignature]::PropertyStore) { | |
[PSCustomObject]@{ | |
Signature = [BlockSignature]$blockSig | |
Data = [Convert]::ToBase64String([byte[]]$buffer[$offset..($offset + $blockSize)]) | |
} | |
} | |
([int][BlockSignature]::Shim) { | |
$layerName = &$readString -Buffer $buffer -Offset ($offset + 8) -Encoding ([Text.Encoding]::Unicode) | |
[PSCustomObject]@{ | |
Signature = [BlockSignature]$blockSig | |
LayerName = $layerName | |
} | |
} | |
([int][BlockSignature]::SpecialFolder) { | |
$folderId = [BitConverter]::ToUInt32($buffer, $offset + 8) | |
$idListOffset = [System.BitConverter]::ToUInt32($buffer, $offset + 12) | |
[PSCustomObject]@{ | |
Signature = [BlockSignature]$blockSig | |
FolderId = $folderID | |
Offset = $idListOffset | |
} | |
} | |
([int][BlockSignature]::Tracker) { | |
$version = [BitConverter]::ToUInt32($buffer, $offset + 12) | |
$machineID = &$readString -Buffer $buffer -Offset ($offset + 16) | |
$volumeID = [Guid]::new([byte[]]$buffer[($offset + 32)..($offset + 47)]) | |
$fileID = [Guid]::new([byte[]]$buffer[($offset + 48)..($offset + 63)]) | |
$birthVolumeID = [Guid]::new([byte[]]$buffer[($offset + 64)..($offset + 79)]) | |
$birthFileID = [Guid]::new([byte[]]$buffer[($offset + 80)..($offset + 95)]) | |
[PSCustomObject]@{ | |
Signature = [BlockSignature]$blockSig | |
Version = $version | |
MachineID = $machineID | |
VolumeID = $volumeID | |
FileID = $fileID | |
BirthVolumeID = $birthVolumeID | |
BirthFileID = $birthFileID | |
} | |
} | |
([int][BlockSignature]::VistaAndAboveIDList) { | |
[PSCustomObject]@{ | |
Signature = [BlockSignature]$blockSig | |
Data = [Convert]::ToBase64String([byte[]]$buffer[$offset..($offset + $blockSize)]) | |
} | |
} | |
default { | |
[PSCUstomObject]@{ | |
Signature = 'Unknown {0:X8}' -f $blockSig | |
Data = [Convert]::ToBase64String([byte[]]$buffer[$offset..($offset + $blockSize)]) | |
} | |
} | |
} | |
$offset += $blockSize | |
}) | |
[PSCustomObject]@{ | |
Path = $linkPath | |
LinkCLSID = $linkClsid | |
LinkFlags = $linkFlags | |
FileAttributes = $fileAttributes | |
CreationTime = $creationTime | |
AccessTime = $accessTime | |
WriteTime = $writeTime | |
FileSize = $fileSize | |
IconIndex = $iconIndex | |
ShowCommand = [ShowCommand]$showCommand | |
HotKey = $hotKey | |
LinkTargetList = $linkTargetList | |
LinkInfo = $linkInfo | |
Comment = $comment | |
RelativePath = $relativePath | |
WorkingDir = $workingDir | |
Arguments = $arguments | |
IconLocation = $iconLocation | |
ExtraData = $extraData | |
} | |
} | |
finally { | |
$fs.Dispose() | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment