Public/Get-DllInfo.ps1
|
function Get-DllInfo { <# .SYNOPSIS Read-only inspector for Windows PE files (DLL, OCX, EXE, SYS). .DESCRIPTION Parses the PE/COFF headers, version resource, and (optionally) the Authenticode signature of a Windows binary, and reports whether it is a self-registering COM server and/or a managed (.NET) assembly. Emits one PSCustomObject per input path; output serializes cleanly with ConvertTo-Json (suitable for Ansible). The inspector NEVER calls LoadLibrary. The file is opened read-only with FileShare.ReadWrite and parsed as raw bytes. Consequences: * 32-bit DLLs can be inspected from 64-bit PowerShell and vice versa. * Old / corrupt / unsigned binaries can be examined without side effects. * DllMain is never executed. .PARAMETER Path One or more paths to PE files. Accepts pipeline input (e.g. from Get-ChildItem) and the FullName / PSPath / FilePath property aliases. .PARAMETER IncludeImports Include the full imports table — modules and per-module function lists (with name+hint or ordinal). Reads the IDT and ILT/IAT from the PE. .PARAMETER IncludeExports Include the full exports table — name, ordinal, RVA, and forwarder info (when an export points back into the export directory and resolves to "OtherModule.OtherFunction" or "OtherModule.#ordinal"). .PARAMETER IncludeResources Include a flat list of resource entries (Type, Name, Language, Size, CodePage). Walks the full 3-level resource directory tree. .PARAMETER IncludeTypeLib When the binary carries a TYPELIB resource, load it via oleaut32! LoadTypeLibEx (REGKIND_NONE — no registration) and emit the COM type library: LibId, version, name, plus every CoClass / interface / dispinterface / enum / alias with their GUIDs, parents, methods, params and members. .PARAMETER IncludeComRegistration Cross-reference the DLL's CoClasses against HKCR\CLSID to determine whether the COM server is correctly registered. Walks HKLM and HKCU in both 64-bit and 32-bit registry views via Microsoft.Win32.RegistryKey (no PowerShell registry provider — much faster). Reports every CLSID whose InprocServer32 points at the inspected DLL plus, for each CoClass declared in the TypeLib, whether it is Registered, DeclaredOnly (declared but never registered) or PathMismatch (registered but InprocServer32 points to a different file). CLSIDs registered to this DLL but absent from the TypeLib are reported as RegisteredOnly (common and not necessarily a problem — TypeLibs do not always expose every creatable class). Strictly read-only — no regsvr32, no LoadLibrary. Implies TypeLib parsing internally even if -IncludeTypeLib is not specified, but only emits the TypeLib payload when -IncludeTypeLib is set. .PARAMETER IncludeDotNetTypes For managed assemblies, additionally load the file via Assembly.ReflectionOnlyLoadFrom and enumerate every type with its attributes (IsComVisible, Guid, ProgId, base type, kind). The cheap fields (RuntimeVersion, PEKind, CorFlags, AssemblyName, Version, Culture, PublicKeyToken) are reported always when the binary has a CLR header. .PARAMETER IncludeSignature Include Authenticode signature info (signer, issuer, validity, status). Slower since it goes through CryptoAPI. .PARAMETER IncludeHash Include the SHA-256 hash of the file. .PARAMETER Detailed Convenience switch — turns on every Include* switch. .EXAMPLE Get-DllInfo -Path C:\Windows\System32\scrrun.dll | ConvertTo-Json -Depth 12 .EXAMPLE Get-ChildItem C:\App -Filter *.dll -Recurse | Get-DllInfo -IncludeHash -IncludeSignature .EXAMPLE # Find every COM-registrable DLL under a directory Get-ChildItem C:\Legacy -Include *.dll,*.ocx -Recurse | Get-DllInfo | Where-Object { $_.Com.IsComServer } .EXAMPLE # Audit which DLLs in a directory import a given API Get-ChildItem C:\App -Filter *.dll | Get-DllInfo -IncludeImports | Where-Object { $_.Imports.Functions.Name -contains 'CreateRemoteThread' } | Select-Object Path .EXAMPLE # Verify a COM DLL is correctly registered and list every CLSID it owns $info = Get-DllInfo -Path C:\App\foo.dll -IncludeComRegistration $info.Com.Registration.Status # OK / Partial / Unregistered $info.Com.Registration.Clsids | Format-Table Clsid, ProgId, Status, View .EXAMPLE # Full inventory (all sections + signature + hash) for one binary Get-DllInfo -Path .\foo.dll -Detailed | ConvertTo-Json -Depth 12 .NOTES PowerShell 5.1+ on Windows. #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('FullName', 'PSPath', 'FilePath')] [string[]]$Path, [switch]$IncludeImports, [switch]$IncludeExports, [switch]$IncludeResources, [switch]$IncludeTypeLib, [switch]$IncludeComRegistration, [switch]$IncludeDotNetTypes, [switch]$IncludeSignature, [switch]$IncludeHash, [switch]$Detailed ) process { if ($Detailed) { $IncludeImports = $true $IncludeExports = $true $IncludeResources = $true $IncludeTypeLib = $true $IncludeComRegistration = $true $IncludeDotNetTypes = $true $IncludeSignature = $true $IncludeHash = $true } foreach ($p in $Path) { try { $resolved = (Resolve-Path -LiteralPath $p -ErrorAction Stop).ProviderPath } catch { Write-Error "Failed to resolve '$p': $($_.Exception.Message)" continue } $fi = Get-Item -LiteralPath $resolved $result = [ordered]@{ Path = $resolved FileSize = $fi.Length LastWrite = $fi.LastWriteTimeUtc IsValidPE = $false ParseError = $null PE = $null Version = $null Com = $null DotNet = $null Imports = $null Exports = $null Resources = $null Signature = $null Sha256 = $null } $pe = $null try { $pe = Read-PEImage -FilePath $resolved $result.IsValidPE = $true $arch = $script:MachineMap[[int]$pe.Machine] if (-not $arch) { $arch = ('Unknown(0x{0:X4})' -f $pe.Machine) } $sub = $script:SubsystemMap[[int]$pe.Subsystem] if (-not $sub) { $sub = ('Unknown({0})' -f $pe.Subsystem) } # Some compilers emit a deterministic "reproducible build" hash in # TimeDateStamp instead of a real time_t. We surface the decoded # value as-is and let the caller decide. $tsUtc = $null if ($pe.TimeDateStamp -gt 0) { try { $tsUtc = ([System.DateTimeOffset]::FromUnixTimeSeconds([int64]$pe.TimeDateStamp)).UtcDateTime } catch { $tsUtc = $null } } $result.PE = [pscustomobject]@{ Architecture = $arch MachineRaw = ('0x{0:X4}' -f $pe.Machine) Is64Bit = $pe.Is64Bit Subsystem = $sub IsDll = (($pe.Characteristics -band 0x2000) -ne 0) IsExecutable = (($pe.Characteristics -band 0x0002) -ne 0) Characteristics = (ConvertTo-Flags -Value $pe.Characteristics -Map $script:CharacteristicsMap) DllCharacteristics = (ConvertTo-Flags -Value $pe.DllCharacteristics -Map $script:DllCharacteristicsMap) TimestampUtc = $tsUtc NumberOfSections = $pe.NumberOfSections Sections = $pe.Sections | Select-Object Name, VirtualSize, VirtualAddress, SizeOfRawData, PointerToRawData } $result.Version = Get-PEVersionInfoSafe -FilePath $resolved # Exports — always read (cheap), used both for COM detection and # for the Exports field when -IncludeExports is set. $expData = @{ Names = @(); Exports = @() } try { $expData = Get-PEExportsFull -Pe $pe } catch { } $exportNames = $expData.Names $selfReg = @($script:ComSelfRegSymbols | Where-Object { $exportNames -contains $_ }) # Resources — when requested, do a full walk and derive HasTypeLib # from it; otherwise the cheap level-1 detector is enough. $resources = $null $hasTlb = $false if ($IncludeResources) { try { $resources = @(Get-PEResources -Pe $pe) } catch { $resources = @() } $hasTlb = [bool]($resources | Where-Object { $_.Type -ieq 'TYPELIB' -or $_.TypeRaw -ieq 'TYPELIB' }) } else { try { $hasTlb = Test-PEHasTypeLibResource -Pe $pe } catch { } } # TypeLib is needed both for the public TypeLib payload AND # internally for COM registration cross-referencing. Load it # once if either path needs it. $tlibInfo = $null if ($hasTlb -and ($IncludeTypeLib -or $IncludeComRegistration)) { $tlibInfo = Get-TypeLibInfoSafe -FilePath $resolved } $registration = $null if ($IncludeComRegistration) { try { $registration = Get-ComRegistrationInfo -FilePath $resolved -TypeLibInfo $tlibInfo } catch { $registration = [pscustomobject]@{ Scanned = $false Status = 'Error' Issues = @($_.Exception.Message) } } } $result.Com = [pscustomobject]@{ IsComServer = (($selfReg -contains 'DllGetClassObject') -and ($selfReg -contains 'DllRegisterServer')) HasTypeLib = $hasTlb SelfRegExports = @($selfReg) TypeLib = if ($IncludeTypeLib) { $tlibInfo } else { $null } Registration = $registration } # .NET — cheap path: CLR header + MetaData root + AssemblyName. $dotNet = Get-DotNetCheapInfo -Pe $pe -FilePath $resolved if ($null -eq $dotNet) { $dotNet = [pscustomobject]@{ IsManaged = $false } } elseif ($IncludeDotNetTypes) { $deep = Get-DotNetDeepInfo -FilePath $resolved -IncludeTypes $true if ($deep) { $dotNet.IsComVisible = $deep.IsComVisible $dotNet.Types = $deep.Types } } $result.DotNet = $dotNet if ($IncludeImports) { try { $result.Imports = @(Get-PEImports -Pe $pe) } catch { $result.Imports = @() } } if ($IncludeExports) { $result.Exports = @($expData.Exports) } if ($IncludeResources) { $result.Resources = @($resources) } } catch { $result.ParseError = $_.Exception.Message } finally { if ($pe) { try { $pe.Reader.Dispose() } catch {} try { $pe.Stream.Dispose() } catch {} } } if ($IncludeSignature) { $result.Signature = Get-PESignatureInfoSafe -FilePath $resolved } if ($IncludeHash) { try { $result.Sha256 = (Get-FileHash -LiteralPath $resolved -Algorithm SHA256).Hash } catch { $result.Sha256 = $null } } [pscustomobject]$result } } } |