Skip to content

Instantly share code, notes, and snippets.

@jborean93
Created March 15, 2021 06:15
Show Gist options
  • Save jborean93/89dd5b606a29e23a1641ed934bd4d5b6 to your computer and use it in GitHub Desktop.
Save jborean93/89dd5b606a29e23a1641ed934bd4d5b6 to your computer and use it in GitHub Desktop.
Get information about a Windows shortcut (lnk) files
# 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