Private/PEParser.ps1
|
# PE/COFF/Optional/Section parser plus directory readers (imports, exports, # resources). Pure byte reading — never calls LoadLibrary, so cross-bitness # inspection works and DllMain never executes. function ConvertTo-Flags { # Decodes a bitmask into the matching string names from a map. # Uses GetEnumerator() so that with [ordered]@{} dictionaries we read by # key — the int indexer on OrderedDictionary would silently return the # value at a positional index instead. param([uint32]$Value, [System.Collections.IDictionary]$Map) $out = New-Object System.Collections.Generic.List[string] foreach ($entry in $Map.GetEnumerator()) { $k = [uint32]$entry.Key if ($k -ne 0 -and (($Value -band $k) -eq $k)) { [void]$out.Add([string]$entry.Value) } } , $out.ToArray() } function Read-PEImage { # Opens the file (shared read/write), parses DOS+PE+COFF+Optional+section # table, and returns a hashtable. Caller must dispose Reader and Stream. param([string]$FilePath) $fs = [System.IO.File]::Open($FilePath, 'Open', 'Read', 'ReadWrite') $br = New-Object System.IO.BinaryReader($fs) try { if ($fs.Length -lt 64) { throw 'File too small to be a PE image' } if ($br.ReadUInt16() -ne 0x5A4D) { throw "Not a PE file (missing 'MZ' DOS signature)" } $fs.Position = 0x3C $eLfanew = $br.ReadUInt32() if ($eLfanew -le 0 -or $eLfanew -ge ($fs.Length - 24)) { throw "Invalid e_lfanew offset ($eLfanew)" } $fs.Position = $eLfanew if ($br.ReadUInt32() -ne 0x00004550) { throw "Not a PE file (missing 'PE\0\0' signature at $eLfanew)" } # IMAGE_FILE_HEADER (COFF), 20 bytes $machine = $br.ReadUInt16() $numberOfSections = $br.ReadUInt16() $timeDateStamp = $br.ReadUInt32() $null = $br.ReadUInt32() # PointerToSymbolTable $null = $br.ReadUInt32() # NumberOfSymbols $sizeOptHdr = $br.ReadUInt16() $characteristics = $br.ReadUInt16() # IMAGE_OPTIONAL_HEADER — magic word tells us PE32 vs PE32+ $optStart = $fs.Position $magic = $br.ReadUInt16() $is64 = switch ($magic) { 0x10b { $false } 0x20b { $true } 0x107 { $false } # ROM image (rare) default { throw ("Unknown optional header magic 0x{0:X4}" -f $magic) } } # Subsystem and DllCharacteristics live at fixed offsets relative to # the start of the optional header (same for PE32 and PE32+): # +68 Subsystem (uint16) # +70 DllCharacteristics (uint16) $fs.Position = $optStart + 68 $subsystem = $br.ReadUInt16() $dllCharacteristics = $br.ReadUInt16() # NumberOfRvaAndSizes — different offset between PE32 (+92) and # PE32+ (+108). The field counts how many data directory entries # follow. $fs.Position = $optStart + ($(if ($is64) { 108 } else { 92 })) $numRvaSizes = $br.ReadUInt32() if ($numRvaSizes -gt 32) { $numRvaSizes = 32 } # sanity clamp $dataDirs = New-Object 'object[]' $numRvaSizes for ($i = 0; $i -lt $numRvaSizes; $i++) { $dataDirs[$i] = [pscustomobject]@{ Rva = $br.ReadUInt32() Size = $br.ReadUInt32() } } # Section table — IMAGE_SECTION_HEADER * NumberOfSections, 40 bytes each. $fs.Position = $optStart + $sizeOptHdr $sections = New-Object 'object[]' $numberOfSections for ($i = 0; $i -lt $numberOfSections; $i++) { $nameBytes = $br.ReadBytes(8) $name = [Text.Encoding]::ASCII.GetString($nameBytes).TrimEnd([char]0) $sections[$i] = [pscustomobject]@{ Name = $name VirtualSize = $br.ReadUInt32() VirtualAddress = $br.ReadUInt32() SizeOfRawData = $br.ReadUInt32() PointerToRawData = $br.ReadUInt32() PointerToRelocs = $br.ReadUInt32() PointerToLineNum = $br.ReadUInt32() NumberOfRelocs = $br.ReadUInt16() NumberOfLineNum = $br.ReadUInt16() Characteristics = $br.ReadUInt32() } } return @{ Stream = $fs Reader = $br Machine = $machine Is64Bit = $is64 NumberOfSections = $numberOfSections TimeDateStamp = $timeDateStamp Characteristics = $characteristics Subsystem = $subsystem DllCharacteristics = $dllCharacteristics DataDirectories = $dataDirs Sections = $sections } } catch { $br.Dispose() $fs.Dispose() throw } } function ConvertTo-FileOffset { # RVA -> file offset using the section table. Returns $null when the RVA # is outside any mapped section. param([uint32]$Rva, [object[]]$Sections) foreach ($s in $Sections) { $vaEnd = $s.VirtualAddress + [Math]::Max($s.VirtualSize, $s.SizeOfRawData) if ($Rva -ge $s.VirtualAddress -and $Rva -lt $vaEnd) { return ($Rva - $s.VirtualAddress + $s.PointerToRawData) } } return $null } function Read-CStringAt { param([System.IO.BinaryReader]$Reader, [long]$Offset) $Reader.BaseStream.Position = $Offset $sb = New-Object System.Text.StringBuilder while ($true) { $b = $Reader.BaseStream.ReadByte() if ($b -le 0) { break } [void]$sb.Append([char]$b) } $sb.ToString() } function Get-PEExportsFull { # Reads the full export directory: ordinal, name, RVA, and forwarder # info (when an export's RVA falls within the export directory range, # the value at that location is an ASCII "OtherDll.OtherFunc" string). # Returns @{ Names = string[]; Exports = pscustomobject[] }. The Names # list is also used for the shallow COM-self-reg detector. param($Pe) $empty = @{ Names = @(); Exports = @() } $dd = $Pe.DataDirectories[$script:DD_EXPORT] if (-not $dd -or $dd.Size -eq 0) { return $empty } $expOff = ConvertTo-FileOffset -Rva $dd.Rva -Sections $Pe.Sections if ($null -eq $expOff) { return $empty } $br = $Pe.Reader $br.BaseStream.Position = $expOff # IMAGE_EXPORT_DIRECTORY (40 bytes) $null = $br.ReadUInt32() # Characteristics $null = $br.ReadUInt32() # TimeDateStamp $null = $br.ReadUInt16() # MajorVersion $null = $br.ReadUInt16() # MinorVersion $null = $br.ReadUInt32() # NameRva (DLL name) $ordinalBase = $br.ReadUInt32() $numFuncs = $br.ReadUInt32() $numNames = $br.ReadUInt32() $rvaFuncs = $br.ReadUInt32() $rvaNames = $br.ReadUInt32() $rvaOrdinals = $br.ReadUInt32() # Address-of-functions table -> RVA per ordinal slot $funcs = @() if ($numFuncs -gt 0 -and $rvaFuncs -ne 0) { $foff = ConvertTo-FileOffset -Rva $rvaFuncs -Sections $Pe.Sections if ($null -ne $foff) { $br.BaseStream.Position = $foff $funcs = for ($i = 0; $i -lt $numFuncs; $i++) { $br.ReadUInt32() } } } # Names + name-ordinals tables (parallel arrays of length NumberOfNames). # NameOrdinals[i] is the index into AddressOfFunctions that Names[i] # refers to. $nameRvasArr = @() $nameOrdsArr = @() if ($numNames -gt 0 -and $rvaNames -ne 0 -and $rvaOrdinals -ne 0) { $nFOff = ConvertTo-FileOffset -Rva $rvaNames -Sections $Pe.Sections $nOOff = ConvertTo-FileOffset -Rva $rvaOrdinals -Sections $Pe.Sections if ($null -ne $nFOff -and $null -ne $nOOff) { $br.BaseStream.Position = $nFOff $nameRvasArr = for ($i = 0; $i -lt $numNames; $i++) { $br.ReadUInt32() } $br.BaseStream.Position = $nOOff $nameOrdsArr = for ($i = 0; $i -lt $numNames; $i++) { $br.ReadUInt16() } } } # Map ordinal slot index -> name $namesByIndex = @{} $namesList = New-Object System.Collections.Generic.List[string] for ($i = 0; $i -lt $numNames; $i++) { $idx = [int]$nameOrdsArr[$i] $rva = [uint32]$nameRvasArr[$i] $off = ConvertTo-FileOffset -Rva $rva -Sections $Pe.Sections if ($null -ne $off) { $nm = Read-CStringAt -Reader $br -Offset $off $namesByIndex[$idx] = $nm [void]$namesList.Add($nm) } } # Build full exports list $exports = New-Object System.Collections.Generic.List[object] $expDirEnd = $dd.Rva + $dd.Size for ($i = 0; $i -lt $numFuncs; $i++) { $rva = [uint32]$funcs[$i] if ($rva -eq 0) { continue } # gap in the ordinal range $name = if ($namesByIndex.ContainsKey($i)) { $namesByIndex[$i] } else { $null } $ordinal = [int]($ordinalBase + $i) $isFwd = ($rva -ge $dd.Rva -and $rva -lt $expDirEnd) $fwd = $null if ($isFwd) { $fwdOff = ConvertTo-FileOffset -Rva $rva -Sections $Pe.Sections if ($null -ne $fwdOff) { $fwd = Read-CStringAt -Reader $br -Offset $fwdOff } } [void]$exports.Add([pscustomobject]@{ Name = $name Ordinal = $ordinal Rva = $rva IsForwarder = $isFwd ForwardsTo = $fwd }) } # NOTE: do NOT use @($exports) here — PowerShell 5.1 fails with # "Argument types do not match" when @() coerces a # System.Collections.Generic.List[object] of PSCustomObject items. # Use the .NET ToArray() method directly. @{ Names = $namesList.ToArray(); Exports = $exports.ToArray() } } function Get-PEImports { # Reads the IMAGE_IMPORT_DESCRIPTOR array and walks each module's ILT # (preferred) or IAT to enumerate imported functions. Returns # [{ Module, Functions: [{ Name, Ordinal, Hint, ByOrdinal }] }, ...]. param($Pe) $dd = $Pe.DataDirectories[$script:DD_IMPORT] if (-not $dd -or $dd.Size -eq 0) { return @() } $impOff = ConvertTo-FileOffset -Rva $dd.Rva -Sections $Pe.Sections if ($null -eq $impOff) { return @() } $br = $Pe.Reader $is64 = $Pe.Is64Bit $modules = New-Object System.Collections.Generic.List[object] $cursor = [long]$impOff $maxDescriptors = 4096 # sanity cap for malformed PEs for ($mi = 0; $mi -lt $maxDescriptors; $mi++) { $br.BaseStream.Position = $cursor $oft = $br.ReadUInt32() # OriginalFirstThunk (ILT) $null = $br.ReadUInt32() # TimeDateStamp $null = $br.ReadUInt32() # ForwarderChain $name = $br.ReadUInt32() # Module name RVA $ft = $br.ReadUInt32() # FirstThunk (IAT) if ($oft -eq 0 -and $name -eq 0 -and $ft -eq 0) { break } $cursor += 20 $modName = '' $nOff = ConvertTo-FileOffset -Rva $name -Sections $Pe.Sections if ($null -ne $nOff) { $modName = Read-CStringAt -Reader $br -Offset $nOff } # Prefer ILT (not patched at runtime); fall back to IAT when bound. $thunkRva = if ($oft -ne 0) { $oft } else { $ft } $functions = New-Object System.Collections.Generic.List[object] $thunkOff = ConvertTo-FileOffset -Rva $thunkRva -Sections $Pe.Sections if ($null -ne $thunkOff) { # First pass: collect raw thunk values until terminator. $thunks = New-Object System.Collections.Generic.List[uint64] $br.BaseStream.Position = $thunkOff while ($thunks.Count -lt 65536) { $t = if ($is64) { $br.ReadUInt64() } else { [uint64]$br.ReadUInt32() } if ($t -eq 0) { break } [void]$thunks.Add($t) } # Second pass: resolve names/ordinals. foreach ($t in $thunks) { $isOrd = if ($is64) { (($t -band $script:OrdinalFlag64) -ne 0) } else { (($t -band 0x80000000) -ne 0) } if ($isOrd) { [void]$functions.Add([pscustomobject]@{ Name = $null Ordinal = [int]($t -band 0xFFFF) Hint = $null ByOrdinal = $true }) } else { $rvaByName = [uint32]($t -band 0xFFFFFFFF) $byNameOff = ConvertTo-FileOffset -Rva $rvaByName -Sections $Pe.Sections $hint = $null; $fname = $null if ($null -ne $byNameOff) { $br.BaseStream.Position = $byNameOff $hint = $br.ReadUInt16() $fname = Read-CStringAt -Reader $br -Offset ($byNameOff + 2) } [void]$functions.Add([pscustomobject]@{ Name = $fname Ordinal = $null Hint = $hint ByOrdinal = $false }) } } } [void]$modules.Add([pscustomobject]@{ Module = $modName Functions = $functions.ToArray() }) } # Emit the modules as a stream — caller wraps with @() to materialize. $modules.ToArray() } function Read-ResourceString { # Reads a UTF-16 length-prefixed name starting at the given absolute # file offset. Used for level entries with the high bit set on NameOrId. param([System.IO.BinaryReader]$Reader, [long]$Offset) $Reader.BaseStream.Position = $Offset $len = $Reader.ReadUInt16() $bytes = $Reader.ReadBytes([int]$len * 2) [Text.Encoding]::Unicode.GetString($bytes) } function Read-ResourceDirectory { # Recursively walks one IMAGE_RESOURCE_DIRECTORY and pushes leaf data # entries into $Acc. $Path is a 1..3 hashtable keyed by level (1=Type, # 2=Name, 3=Language); values are int IDs or strings. param( $Pe, [long]$RootOff, [long]$DirOff, [int]$Level, [hashtable]$Path, [System.Collections.IList]$Acc ) $br = $Pe.Reader $br.BaseStream.Position = $DirOff $null = $br.ReadUInt32() # Characteristics $null = $br.ReadUInt32() # TimeDateStamp $null = $br.ReadUInt16() # MajorVersion $null = $br.ReadUInt16() # MinorVersion $named = $br.ReadUInt16() $idCnt = $br.ReadUInt16() $total = [int]$named + [int]$idCnt $entries = for ($i = 0; $i -lt $total; $i++) { [pscustomobject]@{ NameOrId = $br.ReadUInt32() OffsetToData = $br.ReadUInt32() } } foreach ($e in $entries) { $key = if (($e.NameOrId -band 0x80000000) -ne 0) { Read-ResourceString -Reader $br ` -Offset ($RootOff + ($e.NameOrId -band 0x7FFFFFFF)) } else { [int]$e.NameOrId } $newPath = @{} + $Path $newPath[$Level] = $key if (($e.OffsetToData -band 0x80000000) -ne 0) { $subOff = $RootOff + ($e.OffsetToData -band 0x7FFFFFFF) Read-ResourceDirectory -Pe $Pe -RootOff $RootOff -DirOff $subOff ` -Level ($Level + 1) -Path $newPath -Acc $Acc } else { # IMAGE_RESOURCE_DATA_ENTRY (16 bytes) $deOff = $RootOff + $e.OffsetToData $br.BaseStream.Position = $deOff $dataRva = $br.ReadUInt32() $size = $br.ReadUInt32() $codepage = $br.ReadUInt32() $null = $br.ReadUInt32() # Reserved [void]$Acc.Add([pscustomobject]@{ TypeRaw = $newPath[1] Name = $newPath[2] Language = $newPath[3] DataRva = $dataRva Size = $size CodePage = $codepage }) } } } function Get-PEResources { # Walks the full resource tree and returns a flat list with friendly # type names (decoded from ResourceTypeMap when numeric). param($Pe) $dd = $Pe.DataDirectories[$script:DD_RESOURCE] if (-not $dd -or $dd.Size -eq 0) { return @() } $rootOff = ConvertTo-FileOffset -Rva $dd.Rva -Sections $Pe.Sections if ($null -eq $rootOff) { return @() } $list = New-Object System.Collections.Generic.List[object] Read-ResourceDirectory -Pe $Pe -RootOff $rootOff -DirOff $rootOff ` -Level 1 -Path @{} -Acc $list $list | ForEach-Object { $typeName = if ($_.TypeRaw -is [int]) { $script:ResourceTypeMap[[int]$_.TypeRaw] } else { [string]$_.TypeRaw } if (-not $typeName) { $typeName = "ID:$($_.TypeRaw)" } [pscustomobject]@{ Type = $typeName TypeRaw = $_.TypeRaw Name = $_.Name Language = $_.Language Size = $_.Size CodePage = $_.CodePage DataRva = $_.DataRva } } } function Test-PEHasTypeLibResource { # Walks the level-1 (Type) entries of the resource directory and returns # $true when a string-named "TYPELIB" type entry exists. param($Pe) $dd = $Pe.DataDirectories[$script:DD_RESOURCE] if (-not $dd -or $dd.Size -eq 0) { return $false } $rootOff = ConvertTo-FileOffset -Rva $dd.Rva -Sections $Pe.Sections if ($null -eq $rootOff) { return $false } $br = $Pe.Reader $br.BaseStream.Position = $rootOff # IMAGE_RESOURCE_DIRECTORY $null = $br.ReadUInt32() # Characteristics $null = $br.ReadUInt32() # TimeDateStamp $null = $br.ReadUInt16() # MajorVersion $null = $br.ReadUInt16() # MinorVersion $namedCount = $br.ReadUInt16() $idCount = $br.ReadUInt16() $total = $namedCount + $idCount $entries = for ($i = 0; $i -lt $total; $i++) { [pscustomobject]@{ NameOrId = $br.ReadUInt32() OffsetToData = $br.ReadUInt32() } } foreach ($e in $entries) { if (($e.NameOrId -band 0x80000000) -ne 0) { $strOff = $rootOff + ($e.NameOrId -band 0x7FFFFFFF) $br.BaseStream.Position = $strOff $strLen = $br.ReadUInt16() $strBytes = $br.ReadBytes([int]$strLen * 2) $name = [Text.Encoding]::Unicode.GetString($strBytes) if ($name -ieq 'TYPELIB') { return $true } } } return $false } function Get-PEVersionInfoSafe { param([string]$FilePath) try { $vi = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($FilePath) [pscustomobject]@{ FileVersion = $vi.FileVersion ProductVersion = $vi.ProductVersion FileVersionRaw = "$($vi.FileMajorPart).$($vi.FileMinorPart).$($vi.FileBuildPart).$($vi.FilePrivatePart)" ProductVersionRaw = "$($vi.ProductMajorPart).$($vi.ProductMinorPart).$($vi.ProductBuildPart).$($vi.ProductPrivatePart)" CompanyName = $vi.CompanyName ProductName = $vi.ProductName FileDescription = $vi.FileDescription OriginalFilename = $vi.OriginalFilename InternalName = $vi.InternalName LegalCopyright = $vi.LegalCopyright LegalTrademarks = $vi.LegalTrademarks Comments = $vi.Comments Language = $vi.Language IsDebug = $vi.IsDebug IsPreRelease = $vi.IsPreRelease IsPatched = $vi.IsPatched IsPrivateBuild = $vi.IsPrivateBuild IsSpecialBuild = $vi.IsSpecialBuild } } catch { $null } } function Get-PESignatureInfoSafe { param([string]$FilePath) try { $sig = Get-AuthenticodeSignature -FilePath $FilePath -ErrorAction Stop $cert = $sig.SignerCertificate [pscustomobject]@{ Status = "$($sig.Status)" StatusMessage = $sig.StatusMessage SignatureType = "$($sig.SignatureType)" IsOSBinary = [bool]$sig.IsOSBinary Subject = if ($cert) { $cert.Subject } else { $null } Issuer = if ($cert) { $cert.Issuer } else { $null } NotBefore = if ($cert) { $cert.NotBefore } else { $null } NotAfter = if ($cert) { $cert.NotAfter } else { $null } Thumbprint = if ($cert) { $cert.Thumbprint } else { $null } SerialNumber = if ($cert) { $cert.SerialNumber } else { $null } } } catch { [pscustomobject]@{ Status = 'Error' StatusMessage = $_.Exception.Message } } } |