02 - Create a PE from scratch

In this tutorial we will see how to create a simple PE executable from scratch

Scripts and materials are available here: materials

By Romain Thomas - @rh0main


LIEF enables to create simple PE from scratch. The aim of this tutorial is to create an executable which pop a MessageBoxA with “Hello Word”

First we have to create a Binary :

from lief import PE

binary32 = PE.Binary("pe_from_scratch", PE.PE_TYPE.PE32)

The first parameter is the binary’s name and the second one is the type: PE32 or PE64 (see PE_TYPE). The Binary’s constructor creates automatically DosHeader, Header, OptionalHeader an empty DataDirectory.

Now that we have a minimal binary, we have to add sections. We will have a first section holding assembly code (.text) and a second one containing strings (.data):

section_text                 = PE.Section(".text")
section_text.content         = code
section_text.virtual_address = 0x1000

section_data                 = PE.Section(".data")
section_data.content         = data
section_data.virtual_address = 0x2000

A MessageBoxA is composed of a title and a message. These two strings will be stored in the .data as follows:

title   = "LIEF is awesome\0"
message = "Hello World\0"

data =  list(map(ord, title))
data += list(map(ord, message))

The pseudo assembly code of the .text section is given in next listing:

push 0x00              ; uType
push "LIEF is awesome" ; Title
push "Hello World"     ; Message
push 0                 ; hWnd
call MessageBoxA       ;
push 0                 ; uExitCode
call ExitProcess       ;

Instead of pushing strings we have to push the virtual address of these strings. In the PE format a section’s virtual address is in fact a relative virtual address (relative to OptionalHeader.imagebase when the ASLR is not enabled). By default the Binary’s constructor sets the imagebase to 0x400000.

As a result, the virtual addresses of the strings are:

push 0x00              ; uType
push 0x402000          ; Title
push 0x402010          ; Message
push 0                 ; hWnd
call MessageBoxA       ;
push 0                 ; uExitCode
call ExitProcess       ;

As the code uses MessageBoxA, we need to import user32.dll into the binary’s Imports and the MessageBoxA ImportEntry. To do so we can use the add_library() method combined with add_entry():

user32 = binary32.add_library("user32.dll")
user32.add_entry("MessageBoxA")

Same for ExitProcess (kernel32.dll):

kernel32 = binary32.add_library("kernel32.dll")
kernel32.add_entry("ExitProcess")

Once needed libraries and functions are added to the binary, we have to determine their addresses (Import Address Table).

For that we can use the predict_function_rva() method which will return the IAT address set by the Builder:

Binary.predict_function_rva(self: _pylief.PE.Binary, library: str, function: str) → int

Try to predict the RVA of the given function name in the given import library name

ExitProcess_addr = binary32.predict_function_rva("kernel32.dll", "ExitProcess")
MessageBoxA_addr = binary32.predict_function_rva("user32.dll", "MessageBoxA")
print("Address of 'ExitProcess': 0x{:06x} ".format(ExitProcess_addr))
print("Address of 'MessageBoxA': 0x{:06x} ".format(MessageBoxA_addr))
Address of 'ExitProcess': 0x00304c
Address of 'MessageBoxA': 0x003054

Thus the absolute virtual addresses of MessageBoxA and ExitProcess are:

And the associated assembly code:

push 0x00              ; uType
push 0x402000          ; Title
push 0x402010          ; Message
push 0                 ; hWnd
call 0x40306a          ;
push 0                 ; uExitCode
call 0x40305c          ;

The transformation of the Binary into an executable is performed by the Builder class.

By default the import table is not rebuilt so we have to configure the builder to rebuild it:

builder = lief.PE.Builder(binary32)
builder.build_imports(True)
builder.build()
builder.write("pe_from_scratch.exe")

You can now enjoy the newly created binary.