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.

Write-Host “SHA-1`t”   $peHeader.SHA1

Write-Host “SHA-256`t” $peHeader.SHA256

Write-Host “MD5`t`t”   $peHeader.MD5


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.

Write-Host “ImpHash`t” $peHeader.ImpHash


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.


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



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.


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.


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



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 



The output for the first two sections with resolved name and resolved flags:


Number: 1

Name:  .text


PhysicalAddress:             16B0

VirtualSize:                 16B0

VirtualAddress:              1000

SizeOfRawData:               1800

PointerToRawData:            400

PointerToRelocations:        0

PointerToLinenumbers:        0

NumberOfRelocations:         0

NumberOfLinenumbers:         0

Characteristics:             60000020




Number: 2

Name:  .rdata


PhysicalAddress:            151A

VirtualSize:                151A

VirtualAddress:             3000

SizeOfRawData:              1600

PointerToRawData:           1C00

PointerToRelocations:       0

PointerToLinenumbers:       0

NumberOfRelocations:        0

NumberOfLinenumbers:        0

Characteristics:            40000040



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.


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



VirtualAddress:             0

Size:                       0


VirtualAddress:             3D68

Size:                       DC


VirtualAddress:             7000

Size:                       4718


VirtualAddress:             6000

Size:                       180


VirtualAddress:             0

Size:                       0


VirtualAddress:             C000

Size:                       48


VirtualAddress:             3590

Size:                       38


VirtualAddress:             0

Size:                       0



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”)





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.


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.”



    “Image has exports.”



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



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.


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.


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.