02 - Create a PE from scratch

In this tutorial we introduce LIEF API to create a simple PE executable from scratch

Scripts and materials are available here: materials

By Romain Thomas - @rh0main

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

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, the binary’s type: PE32 or PE32_PLUS (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 can 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")

Same for ExitProcess (kernel32.dll):

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

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: lief._lief.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': 0x00306a
Address of 'MessageBoxA': 0x00305c

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)

You can now enjoy the newly created binary.