Presenting PeNet: a native .NET library for analyzing PE Headers with PowerShell
The last years have seen a strong rise in PowerShell and .NET malware, so in this article we go native and show how PowerShell and the PeNet library can be leveraged to analyze Portable Executable (PE) headers of Windows executables, motivated by, but not limited to .NET binaries. PeNet is published under Apache License Version 2.0 and maintained by the author. Find an API description for C#, VB, C++ and F# in the PeNet API Reference.
Why PowerShell
PowerShell is an object-oriented programming language with full access to all .NET and Windows libraries. This is one of the reasons why PowerShell became more and more popular in malware and exploitation. The syntax makes it easy to use for everyone familiar with C/C++ or an equivalent modern programming language. Since most malware analysis is done automated to process huge amounts of malicious executables, a scripting language comes handy in this context. As every Windows PC has PowerShell and an IDE for it installed, it is easy to start coding and you can be sure that your code runs basically on every computer running Windows.
About PeNet Library
The PeNet library is a .NET library written in C# which parses PE headers of Windows executables. It can show and interpret values, flags, resources and so on, and is also capable of changing the values. The library is under the Apache 2 license and can be used by everyone in their projects.
Analyzing a PE Header
In the following section we walk through and example to demonstrate how to analyze the PE header of a Windows executable using PeNet. First, we need to import the PeNet library in PowerShell:
Import the PeNet DLL
[System.Reflection.Assembly]::LoadFrom("C:\PeNet.dll") | Out-Null
Now, we define a path to an executable and a new object which represents the PE header.
Windows Executable to analyze
$executable = 'C:\Windows\System32\calc.exe'
# Create a new PE header object with PeNet
$peHeader = New-Object PeNet.PeFile -ArgumentList $executable
The $peHeader object contains all information we can get from the PeNet library.
Basic File Information
Now, let us check, if the file we are analyzing is a PE file or not and display some information about the PE file.
Write-Host "Is PE File`t" $peHeader.IsValidPeFile
Write-Host "Is 32 Bit`t" $peHeader.Is32Bit
Write-Host "Is 64 Bit`t" $peHeader.Is64Bit
Write-Host "Is DLL`t`t" $peHeader.IsDLL
Write-Host "File Size`t" $peHeader.FileSize "Bytes"
These lines are accessing flags and properties of the $peHeader object. The output for these lines is:``` Is PE File True Is 32 Bit False Is 64 Bit True Is DLL False File Size 32768 Bytes
The output shows that the executable we are analyzing is a 64 Bit PE file and an executable but no DLL. The file size is 32768 bytes. To identify files, hashes are common in malware analysis, so let us display the most common hashes.
```powershell
Write-Host "SHA-1`t" $peHeader.SHA1
Write-Host "SHA-256`t" $peHeader.SHA256
Write-Host "MD5`t`t" $peHeader.MD5
Output:
SHA-1 7270d8b19e3b13973ee905f89d02cc2f33a63aa5
SHA-256 700ee0e9c1e9dc18114ae2798847824577e587813c72b413127fb70b9cb042dd
MD5 c4cb4fdf6369dd1342d2666171866ce5
```One more interesting hash is the Import-Hash (ImpHash), which is a hash over the imported functions of the PE file. The ImpHash can be used to identify similar malware, since the same ImpHash is a strong indicator for similar behavior, or at least the same packer.```powershell
Write-Host "ImpHash`t" $peHeader.ImpHash
Output:
ImpHash 251a86d312a56bf3a543b34a1b34d4cc
The ImpHash is used by VirusTotal, too, and besides the .NET implementation in PeNet, a Python library exists which can be used to compute the hash. To check if the PE file is signed with a valid signature we can use the following PS code:
Write-Host "Signature Information" -ForegroundColor Yellow
Write-Host "Is Signed`t`t`t" $peHeader.IsSigned
Write-Host "Is Chain Valid`t`t" $peHeader.IsValidCertChain($True)
Write-Host "Is Signature Valid`t" ([PeNet.Utility]::IsSignatureValid($executable))
The parameter $True
indicates that PeNet should check the certificate chain online. If we set the parameter to $False
, the check would be done offline, which could lead to outdated results.
The output for our example is:
Is Signed False
Is Chain Valid False
Is Signature Valid False
The executable isn’t signed at all, so obviously the chain and the signature are invalid.
DOS/NT Header
We have two options to get the values of each structure in the PE header. The first is
by value, where you can access every single structure member. The second methods
returns the whole structure with all value containing it as a string for easier representation in console output.
To access single values, let’s have a look how we can obtain the e_magic value from the
IMAGE_DOS_HEADER
and how to access the Signature from the IMAGE_NT_HEADERS
.
"e_magic`t {0:X0}" -f $peHeader.ImageDosHeader.e_magic
"NT signature {0:X0}" -f $peHeader.ImageNtHeaders.Signature
We start at the PE header and select the structure and value we want. The values are printed to the console in hexadecimal. Output:
e_magic 5A4D
NT signature 4550
The PeNet library overrides a ToString() method for each PE structure. This means that we can print the whole IMAGE_DOS_HEADER (and any other header) directly to the console without caring for formatting or without the knowledge of the members of the structure.
Write-Host $peHeader.ImageDosHeader
Output:
IMAGE_DOS_HEADER
e_magic : 5A4D
e_cblp : 90
e_cp : 3
e_crlc : 0
e_cparhdr : 4
e_minalloc: 0
e_maxalloc: FFFF
e_ss : 0
e_sp : B8
e_csum : 0
e_ip : 0
e_cs : 0
e_lfarlc : 40
e_ovno : 0
e_oemid : 0
e_oeminfo : 0
e_lfanew : F8
File Header
The IMAGE_FILE_HEADER
is a structure in the IMAGE_NT_HEADERS
and can be accessed again as single values or as a whole.
For example, let’s access the machine type of the file header.
"Machine {0:X0}" -f $peHeader.ImageNtHeaders.FileHeader.Machine
Write-Host "Resolved Machine`t ([PeNet.Utility]::ResolveTargetMachine($peHeader.ImageNtHeaders.FileHeader.Machine))
In the first line, we print the machine field as a hex value. Since this value doesn’t say much we can use the PeNet library to resolve the value to a meaningful string as shown in the second line. Output:
Machine 8664
Resolved Machine AMD AMD64
Interesting in the file header are the file characteristics which can be accessed and resolved like this:
"`nCharacteristics {0:X0}" -f $peHeader.ImageNtHeaders.FileHeader.Characteristics
Write-Host ([PeNet.Utility]::ResolveFileCharacteristics($peHeader.ImageNtHeaders.FileHeader.Characteristics))
The first line prints the file characteristics as a hex value and the second line resolves them to different flags. All flags can be access on their own, but we just print them all at once. Output:
Characteristics 22
File Characteristics
RelocStripped : False
ExecutableImage : True
LineNumbersStripped : False
LocalSymbolsStripped : False
AggressiveWsTrim : False
LargeAddressAware : True
BytesReversedLo : False
Machine32Bit : False
DebugStripped : False
RemovableRunFromSwap : False
NetRunFroMSwap : False
System : False
DLL : False
UpSystemOnly : False
BytesReversedHi : False
And again to print the whole header just type:
Write-Host $peHeader.ImageNtHeaders.FileHeader
Output:
IMAGE_FILE_HEADER
Machine: 8664
NumberOfSections: 6
TimeDateStamp: 5632D8CE
PointerToSymbolTable: 0
NumberOfSymbols: 0
SizeOfOptionalHeader: F0
Characteristics: 22
Section Header
To access the sections of the executable we parse the section header and resolve the section characteristics to get an overview of the rights of each section.
$num = 1;
foreach($sec in $peHeader.ImageSectionHeaders)
{
Write-Host "`nNumber:" $num
Write-Host "Name:" ([PeNet.Utility]::ResolveSectionName($sec.Name))
Write-Host $sec
$flags = ([PeNet.Utility]::ResolveSectionFlags($sec.Characteristics))
Write-Host "Flags:" $flags
$num++;
}
The output for the first two sections with resolved name and resolved flags:
Number: 1
Name: .text
IMAGE_SECTION_HEADER
PhysicalAddress: 16B0
VirtualSize: 16B0
VirtualAddress: 1000
SizeOfRawData: 1800
PointerToRawData: 400
PointerToRelocations: 0
PointerToLinenumbers: 0
NumberOfRelocations: 0
NumberOfLinenumbers: 0
Characteristics: 60000020
Flags: IMAGE_SCN_CNT_CODE
IMAGE_SCN_MEM_EXECUTE IMAGE_SCN_MEM_READ
Number: 2
Name: .rdata
IMAGE_SECTION_HEADER
PhysicalAddress: 151A
VirtualSize: 151A
VirtualAddress: 3000
SizeOfRawData: 1600
PointerToRawData: 1C00
PointerToRelocations: 0
PointerToLinenumbers: 0
NumberOfRelocations: 0
NumberOfLinenumbers: 0
Characteristics: 40000040
Flags:
IMAGE_SCN_CNT_INITIALIZED_DATA IMAGE_SCN_MEM_READ
Optional Header / Data Directory
The Optional Header is, like the File Header, a structure in the NT header and can be accessed by each member or as a whole. In the following we just print the whole header. Since the Optional Header includes the Data Directory structure it is printed (cut off), too.
IMAGE_OPTIONAL_HEADER
Magic: 20B
MajorLinkerVersion: C
MinorLinkerVersion: A
SizeOfCode: 1800
SizeOfInitializedData: 6A00
SizeOfUninitializedData: 0
AddressOfEntryPoint: 22E0
BaseOfCode: 1000
BaseOfData: 0
ImageBase: 140000000
SectionAlignment: 1000
FileAlignment: 200
MajorOperatingSystemVersion: A
MinorOperatingSystemVersion: 0
MajorImageVersion: A
MinorImageVersion: 0
MajorSubsystemVersion: A
MinorSubsystemVersion: 0
Win32VersionValue: 0
SizeOfImage: D000
SizeOfHeaders: 400
CheckSum: FB66
Subsystem: 2
DllCharacteristics: C160
SizeOfStackReserve: 80000
SizeOfStackCommit: 2000
SizeOfHeapReserve: 100000
SizeOfHeapCommit: 1000
LoaderFlags: 0
NumberOfRvaAndSizes: 10
IMAGE_DATA_DIRECTORY
VirtualAddress: 0
Size: 0
IMAGE_DATA_DIRECTORY
VirtualAddress: 3D68
Size: DC
IMAGE_DATA_DIRECTORY
VirtualAddress: 7000
Size: 4718
IMAGE_DATA_DIRECTORY
VirtualAddress: 6000
Size: 180
IMAGE_DATA_DIRECTORY
VirtualAddress: 0
Size: 0
IMAGE_DATA_DIRECTORY
VirtualAddress: C000
Size: 48
IMAGE_DATA_DIRECTORY
VirtualAddress: 3590
Size: 38
IMAGE_DATA_DIRECTORY
VirtualAddress: 0
Size: 0
IMAGE_DATA_DIRECTORY...
Imports, Exports and Resources
We have seen how the Optional Header and the Data Directory can be accessed. The most interesting parts of the Data Directory are the Imports, Exports and Resources. PeNet parses all these directories for us and shows us the content. Let’s start with the imports. Since listing all imported functions would be too much for this article we will have a look at all function imported from Kernel32.dll
foreach($import in $peHeader.ImportedFunctions)
{
if($import.DLL -eq "KERNEL32.DLL")
{
$import
}
}
This code iterates over all imported functions and checks if the DLL from which the function should be imported is the Kernel32.dll. If so, the imported function is printed.
Output:
Name DLL Hint
---- --- ----
GetLastError KERNEL32.dll 599
CreateEventExW KERNEL32.dll 179
WaitForSingleObject KERNEL32.dll 1483
SetEvent KERNEL32.dll 1291
FindPackagesByPackageFamily KERNEL32.dll 395
QueryPerformanceCounter KERNEL32.dll 1081
GetCurrentProcessId KERNEL32.dll 529
GetCurrentThreadId KERNEL32.dll 533
CloseHandle KERNEL32.dll 124
GetTickCount KERNEL32.dll 765
RtlCaptureContext KERNEL32.dll 1210
RtlLookupFunctionEntry KERNEL32.dll 1217
RtlVirtualUnwind KERNEL32.dll 1224
UnhandledExceptionFilter KERNEL32.dll 1441
SetUnhandledExceptionFilter KERNEL32.dll 1377
GetCurrentProcess KERNEL32.dll 528
TerminateProcess KERNEL32.dll 1407
RaiseException KERNEL32.dll 1103
GetSystemTimeAsFileTime KERNEL32.dll 736
Since we are analyzing an executable and not a DLL there are most likely no exports, but to be sure let us check.
if($peHeader.ExportedFunctions -eq $null)
{
"Image has no exports."
}
else
{
"Image has exports."
}
Output:
Image has no exports.
Nothing to see here, let’s move on and check for resources. We print the root elements of this executable and no subdirectories to keep the output readable.
foreach($de in $peHeader.ImageResourceDirectory.DirectoryEntries)
{
if($de.IsIdEntry -eq $True)
{
Write-Host "ID Entry" $de.ID " Resolved Name: " ([PeNet.Utility]::ResolveResourceId($de.ID))
}
elseif($de.IsNamedEntry -eq $True)
{
Write-Host "Named Entry: " $de.ResolvedName
}
}
Output:
ID Entry 3 Resolved Name: Icon
ID Entry 14 Resolved Name: GroupIcon
ID Entry 16 Resolved Name: Version
ID Entry 24 Resolved Name: Manifest
Again, we used a resolve function to map the IDs of directory entries to meaningful names.
Pattern matching
Often, we don’t search for a structure member to have a specific value, but instead want to match some byte or string signature over samples. That’s why PeNet comes with a build-in Aho-Corasick pattern matching algorithm for byte arrays and strings.
The idea is to construct a trie containing all signatures once and to use this trie to match multiple executables against the signatures. We will see an example below.
$trie = New-Object PeNet.PatternMatching.Trie
$trie.Add("MicrosoftCalculator", ([System.Text.Encoding]::ASCII), "pattern1")
$trie.Add("not in the binary", ([System.Text.Encoding]::ASCII), "pattern2")
$trie.Add("<assemblyIdentity", ([System.Text.Encoding]::ASCII), "pattern3")
$matches = $trie.Find($peHeader.Buff)
foreach($match in $matches)
{
Write-Host "Pattern" $match.Item1 "matched at offset" $match.Item2
}
A new Trie
object is created and then filled with some strings to search for. The encoding is of the string is given, because string in binaries are often in Unicode. The pattern2 is not in the binary and should not be found, the other two patterns should.
The output is:
Pattern pattern1 matched at offset 9410
Pattern pattern3 matched at offset 14498
Pattern pattern3 matched at offset 14725
We see that pattern1 matched once and pattern3 matched two times at different offsets.
Conclusion
We’ve seen how PE headers can be analyzed with PowerShell and PeNet. PeNet is a .NET library, i.e., any other .NET language can be used, too. The library is still under development and a lot of PE features are still missing, but they will follow soon. None the less, it’s a mighty tool for malware analysis, if building large-scale, automated analysis systems with the .NET framework is what you aim for. Feel free to contribute to the PeNet library or fork it at GitHub.