Private/Core.PE.ps1
|
function ConvertTo-OIPEImage { <# Parses a PE (Portable Executable) header from a byte buffer that contains at least the DOS header, PE header, and section table. Returns machine, bitness, entry point, image base, and the section table. Throws if the buffer is not a PE image or is truncated before the section table. Pure and cross-platform. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [byte[]]$Bytes ) $requireBytes = { param([int64]$Needed) if ($Needed -gt $Bytes.Length) { throw "The header buffer is truncated before offset $Needed; provide more header bytes." } } & $requireBytes 0x40 if ($Bytes[0] -ne 0x4D -or $Bytes[1] -ne 0x5A) { throw 'Not a PE image: missing MZ (DOS) signature.' } $peHeaderOffset = [BitConverter]::ToInt32($Bytes, 0x3C) if ($peHeaderOffset -le 0) { throw 'Not a PE image: invalid PE header offset.' } & $requireBytes ($peHeaderOffset + 24) if ($Bytes[$peHeaderOffset] -ne 0x50 -or $Bytes[$peHeaderOffset + 1] -ne 0x45 -or $Bytes[$peHeaderOffset + 2] -ne 0x00 -or $Bytes[$peHeaderOffset + 3] -ne 0x00) { throw 'Not a PE image: missing PE signature.' } $coffOffset = $peHeaderOffset + 4 $machineId = [BitConverter]::ToUInt16($Bytes, $coffOffset) $sectionCount = [BitConverter]::ToUInt16($Bytes, $coffOffset + 2) $optionalHeaderSize = [BitConverter]::ToUInt16($Bytes, $coffOffset + 16) $optionalOffset = $coffOffset + 20 & $requireBytes ($optionalOffset + 28) $magic = [BitConverter]::ToUInt16($Bytes, $optionalOffset) $isPe32Plus = ($magic -eq 0x20B) $entryPointRva = [BitConverter]::ToUInt32($Bytes, $optionalOffset + 16) $imageBase = if ($isPe32Plus) { [BitConverter]::ToUInt64($Bytes, $optionalOffset + 24) } else { [uint64][BitConverter]::ToUInt32($Bytes, $optionalOffset + 28) } # Data directories live after the fixed optional-header fields: offset 96 for # PE32, 112 for PE32+, with NumberOfRvaAndSizes in the uint32 just before them. $dataDirectoryBase = if ($isPe32Plus) { $optionalOffset + 112 } else { $optionalOffset + 96 } $rvaAndSizeCount = 0 if (($dataDirectoryBase - 4 + 4) -le $Bytes.Length) { $rvaAndSizeCount = [BitConverter]::ToUInt32($Bytes, $dataDirectoryBase - 4) } $readDataDirectory = { param([int]$Index) $offset = $dataDirectoryBase + ($Index * 8) if ($Index -lt $rvaAndSizeCount -and ($offset + 8) -le $Bytes.Length) { return [pscustomobject]@{ Rva = [BitConverter]::ToUInt32($Bytes, $offset); Size = [BitConverter]::ToUInt32($Bytes, $offset + 4) } } return [pscustomobject]@{ Rva = [uint32]0; Size = [uint32]0 } }.GetNewClosure() $exportDirectory = & $readDataDirectory 0 $importDirectory = & $readDataDirectory 1 $resourceDirectory = & $readDataDirectory 2 $machineName = switch ($machineId) { 0x014C { 'x86 (I386)' } 0x8664 { 'x64 (AMD64)' } 0xAA64 { 'ARM64' } 0x01C0 { 'ARM' } 0x01C4 { 'ARMNT' } default { '0x{0:X4}' -f $machineId } } $sectionTableOffset = $optionalOffset + $optionalHeaderSize $sections = New-Object 'System.Collections.Generic.List[object]' for ($i = 0; $i -lt $sectionCount; $i++) { $entry = $sectionTableOffset + ($i * 40) & $requireBytes ($entry + 40) $rawName = [System.Text.Encoding]::ASCII.GetString($Bytes, $entry, 8).TrimEnd([char]0) $sections.Add([pscustomobject]@{ Name = $rawName VirtualSize = [BitConverter]::ToUInt32($Bytes, $entry + 8) VirtualAddress = [BitConverter]::ToUInt32($Bytes, $entry + 12) SizeOfRawData = [BitConverter]::ToUInt32($Bytes, $entry + 16) PointerToRawData = [BitConverter]::ToUInt32($Bytes, $entry + 20) }) } return [pscustomobject]@{ IsPE = $true MachineId = $machineId Machine = $machineName IsPE32Plus = $isPe32Plus EntryPointRva = $entryPointRva ImageBase = $imageBase SectionCount = [int]$sectionCount Sections = $sections.ToArray() ExportRva = $exportDirectory.Rva ExportSize = $exportDirectory.Size ImportRva = $importDirectory.Rva ImportSize = $importDirectory.Size ResourceRva = $resourceDirectory.Rva ResourceSize = $resourceDirectory.Size } } function Get-OIPESectionForOffset { <# Returns the name of the section whose raw data contains the given file offset, 'headers' when the offset falls before the first section's raw data, or $null when it maps to no section. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] $Image, [Parameter(Mandatory = $true)] [int64]$Offset ) foreach ($section in $Image.Sections) { $rawStart = [int64]$section.PointerToRawData $rawEnd = $rawStart + [int64]$section.SizeOfRawData if ($section.SizeOfRawData -gt 0 -and $Offset -ge $rawStart -and $Offset -lt $rawEnd) { return $section.Name } } $firstRaw = @($Image.Sections | Where-Object { $_.PointerToRawData -gt 0 } | Sort-Object PointerToRawData | Select-Object -First 1) if ($firstRaw.Count -gt 0 -and $Offset -lt [int64]$firstRaw[0].PointerToRawData) { return 'headers' } return $null } function ConvertFrom-OIRvaToOffset { # Maps a relative virtual address to a raw file offset using the section table, # or returns $null when the RVA falls outside every section. [CmdletBinding()] param( [Parameter(Mandatory = $true)] $Image, [Parameter(Mandatory = $true)] [int64]$Rva ) foreach ($section in $Image.Sections) { $virtualAddress = [int64]$section.VirtualAddress $virtualSpan = [int64][Math]::Max([int64]$section.VirtualSize, [int64]$section.SizeOfRawData) if ($Rva -ge $virtualAddress -and $Rva -lt ($virtualAddress + $virtualSpan)) { return ($Rva - $virtualAddress + [int64]$section.PointerToRawData) } } return $null } function Read-OINullTerminatedAscii { # Reads an ASCII string terminated by a null byte, starting at a file offset. [CmdletBinding()] param( [Parameter(Mandatory = $true)] [System.IO.FileStream]$Stream, [Parameter(Mandatory = $true)] [int64]$Offset, [ValidateRange(1, 4096)] [int]$MaxLength = 256 ) $bytes = Read-OIFileRange -Stream $Stream -Start $Offset -Length $MaxLength if ($bytes.Length -eq 0) { return '' } $terminator = [array]::IndexOf($bytes, [byte]0) $length = if ($terminator -ge 0) { $terminator } else { $bytes.Length } if ($length -le 0) { return '' } return [System.Text.Encoding]::ASCII.GetString($bytes, 0, $length) } function Get-OIPEOverlayRange { # An overlay is file data appended after the last section's raw data - common # in installers and some malware. Returns its offset and size. [CmdletBinding()] param( [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [object[]]$Section, [Parameter(Mandatory = $true)] [int64]$FileSize ) $overlayStart = [int64]0 foreach ($entry in $Section) { if ($entry.SizeOfRawData -gt 0) { $end = [int64]$entry.PointerToRawData + [int64]$entry.SizeOfRawData if ($end -gt $overlayStart) { $overlayStart = $end } } } $overlaySize = if ($FileSize -gt $overlayStart) { $FileSize - $overlayStart } else { [int64]0 } return [pscustomobject]@{ HasOverlay = ($overlaySize -gt 0) OverlayOffset = if ($overlaySize -gt 0) { $overlayStart } else { $null } OverlaySize = $overlaySize } } function Get-OIImpHash { # Imphash = MD5 of the comma-joined, lower-cased "library.function" import list # (library extension stripped). Ordinal-only imports are rendered as ordNNN; # unlike pefile this does not resolve ordinals to names for special libraries, # so imphashes of ordinal-heavy binaries may differ from pefile's. [CmdletBinding()] param( [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [string[]]$Entry ) if ($Entry.Count -eq 0) { return $null } $joined = ($Entry -join ',') $md5 = [System.Security.Cryptography.MD5]::Create() try { $hashBytes = $md5.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($joined)) return (($hashBytes | ForEach-Object { $_.ToString('x2') }) -join '') } finally { $md5.Dispose() } } function Get-OIPEImport { # Parses the PE import directory from the stream (reads on demand at mapped # RVAs, so imports outside the header region are handled) and computes the # imphash. Returns @{ Imports; ImpHash }. [CmdletBinding()] param( [Parameter(Mandatory = $true)] $Image, [Parameter(Mandatory = $true)] [System.IO.FileStream]$Stream ) $imports = New-Object 'System.Collections.Generic.List[object]' $imphashEntries = New-Object 'System.Collections.Generic.List[string]' if ([int64]$Image.ImportRva -le 0) { return [pscustomobject]@{ Imports = @(); ImpHash = $null } } $descriptorOffset = ConvertFrom-OIRvaToOffset -Image $Image -Rva ([int64]$Image.ImportRva) if ($null -eq $descriptorOffset) { return [pscustomobject]@{ Imports = @(); ImpHash = $null } } $pointerSize = if ($Image.IsPE32Plus) { 8 } else { 4 } # Build the ordinal-import flag via a hex string to avoid the 0x8000000000000000 # literal overflowing Int64 during parsing. $ordinalFlag = if ($Image.IsPE32Plus) { [System.Convert]::ToUInt64('8000000000000000', 16) } else { [uint64]0x80000000 } $descriptorIndex = 0 while ($descriptorIndex -lt 4096) { $descriptor = Read-OIFileRange -Stream $Stream -Start ($descriptorOffset + ($descriptorIndex * 20)) -Length 20 if ($descriptor.Length -lt 20) { break } $originalFirstThunk = [BitConverter]::ToUInt32($descriptor, 0) $nameRva = [BitConverter]::ToUInt32($descriptor, 12) $firstThunk = [BitConverter]::ToUInt32($descriptor, 16) if ($originalFirstThunk -eq 0 -and $nameRva -eq 0 -and $firstThunk -eq 0) { break } $libraryName = '' $nameOffset = ConvertFrom-OIRvaToOffset -Image $Image -Rva ([int64]$nameRva) if ($null -ne $nameOffset) { $libraryName = Read-OINullTerminatedAscii -Stream $Stream -Offset $nameOffset } $libraryBase = $libraryName.ToLowerInvariant() foreach ($extension in @('.dll', '.ocx', '.sys')) { if ($libraryBase.EndsWith($extension)) { $libraryBase = $libraryBase.Substring(0, $libraryBase.Length - $extension.Length) break } } $functions = New-Object 'System.Collections.Generic.List[string]' $thunkRva = if ($originalFirstThunk -ne 0) { $originalFirstThunk } else { $firstThunk } $thunkOffset = ConvertFrom-OIRvaToOffset -Image $Image -Rva ([int64]$thunkRva) if ($null -ne $thunkOffset) { $thunkIndex = 0 while ($thunkIndex -lt 100000) { $thunkBytes = Read-OIFileRange -Stream $Stream -Start ($thunkOffset + ($thunkIndex * $pointerSize)) -Length $pointerSize if ($thunkBytes.Length -lt $pointerSize) { break } $thunkValue = if ($pointerSize -eq 8) { [BitConverter]::ToUInt64($thunkBytes, 0) } else { [uint64][BitConverter]::ToUInt32($thunkBytes, 0) } if ($thunkValue -eq 0) { break } if (($thunkValue -band $ordinalFlag) -ne 0) { $functionName = 'ord' + [int]($thunkValue -band 0xFFFF) } else { $byNameOffset = ConvertFrom-OIRvaToOffset -Image $Image -Rva ([int64]($thunkValue -band 0xFFFFFFFF)) $functionName = if ($null -ne $byNameOffset) { Read-OINullTerminatedAscii -Stream $Stream -Offset ($byNameOffset + 2) } else { '' } } if (-not [string]::IsNullOrEmpty($functionName)) { $functions.Add($functionName) $imphashEntries.Add($libraryBase + '.' + $functionName.ToLowerInvariant()) } $thunkIndex++ } } $imports.Add([pscustomobject]@{ Dll = $libraryName; Functions = $functions.ToArray() }) $descriptorIndex++ } return [pscustomobject]@{ Imports = $imports.ToArray() ImpHash = (Get-OIImpHash -Entry $imphashEntries.ToArray()) } } |