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 Import
s 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
:
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)
builder.build_imports(True)
builder.build()
builder.write("pe_from_scratch.exe")
You can now enjoy the newly created binary.