Warning
This tutorial is no longer functional or accurate for LIEF version >= 0.17.0.
In this tutorial, we introduce the LIEF API for creating a simple PE executable from scratch.
LIEF enables the creation of a simple PE from scratch. The aim of this tutorial is to create an executable that shows a “Hello World” MessageBoxA.
First, we must create a Binary:
from lief import PE
binary32 = PE.Binary("pe_from_scratch", PE.PE_TYPE.PE32)
The first parameter is the binary name, and the second is the binary type: PE32 or PE32_PLUS (see PE_TYPE). The Binary constructor automatically creates DosHeader, Header, OptionalHeader, and an empty DataDirectory.
Now that we have a minimal binary, we must 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 section 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 provided in the following 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 must push the virtual addresses of these strings. In the PE format, a section’s virtual address is actually a relative virtual address (relative to OptionalHeader.imagebase when ASLR is not enabled). By default, the Binary 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 entries and add 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")
The same applies to ExitProcess (kernel32.dll):
kernel32 = binary32.add_library("kernel32.dll")
kernel32.add_entry("ExitProcess")
Once the necessary libraries and functions have been added to the binary, we must determine their addresses (Import Address Table).
To do so, we can use the lief.PE.Binary.predict_function_rva method, which returns the IAT address set by the Builder:
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 must 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 use the newly created binary.