2023-02-27 18:54:48 +03:00
#!/usr/bin/env python3
# SPDX-License-Identifier: LGPL-2.1-or-later
# Convert ELF static PIE to PE/EFI image.
# To do so we simply copy desired ELF sections while preserving their memory layout to ensure that
# code still runs as expected. We then translate ELF relocations to PE relocations so that the EFI
# loader/firmware can properly load the binary to any address at runtime.
#
# To make this as painless as possible we only operate on static PIEs as they should only contain
# base relocations that are easy to handle as they have a one-to-one mapping to PE relocations.
#
# EDK2 does a similar process using their GenFw tool. The main difference is that they use the
# --emit-relocs linker flag, which emits a lot of different (static) ELF relocation types that have
# to be handled differently for each architecture and is overall more work than its worth.
#
# Note that on arches where binutils has PE support (x86/x86_64 mostly, aarch64 only recently)
# objcopy can be used to convert ELF to PE. But this will still not convert ELF relocations, making
# the resulting binary useless. gnu-efi relies on this method and contains a stub that performs the
# ELF dynamic relocations at runtime.
# pylint: disable=missing-docstring,invalid-name,attribute-defined-outside-init
import argparse
import hashlib
import io
import os
import pathlib
import time
from ctypes import (
c_char ,
c_uint8 ,
c_uint16 ,
c_uint32 ,
c_uint64 ,
LittleEndianStructure ,
sizeof ,
)
from elftools . elf . constants import SH_FLAGS
from elftools . elf . elffile import ELFFile , Section as ELFSection
from elftools . elf . enums import (
ENUM_DT_FLAGS_1 ,
ENUM_RELOC_TYPE_AARCH64 ,
ENUM_RELOC_TYPE_ARM ,
ENUM_RELOC_TYPE_i386 ,
ENUM_RELOC_TYPE_x64 ,
)
from elftools . elf . relocation import (
Relocation as ElfRelocation ,
RelocationTable as ElfRelocationTable ,
)
class PeCoffHeader ( LittleEndianStructure ) :
_fields_ = (
( " Machine " , c_uint16 ) ,
( " NumberOfSections " , c_uint16 ) ,
( " TimeDateStamp " , c_uint32 ) ,
( " PointerToSymbolTable " , c_uint32 ) ,
( " NumberOfSymbols " , c_uint32 ) ,
( " SizeOfOptionalHeader " , c_uint16 ) ,
( " Characteristics " , c_uint16 ) ,
)
class PeDataDirectory ( LittleEndianStructure ) :
_fields_ = (
( " VirtualAddress " , c_uint32 ) ,
( " Size " , c_uint32 ) ,
)
class PeRelocationBlock ( LittleEndianStructure ) :
_fields_ = (
( " PageRVA " , c_uint32 ) ,
( " BlockSize " , c_uint32 ) ,
)
def __init__ ( self , PageRVA : int ) :
super ( ) . __init__ ( PageRVA )
self . entries : list [ PeRelocationEntry ] = [ ]
class PeRelocationEntry ( LittleEndianStructure ) :
_fields_ = (
( " Offset " , c_uint16 , 12 ) ,
( " Type " , c_uint16 , 4 ) ,
)
class PeOptionalHeaderStart ( LittleEndianStructure ) :
_fields_ = (
( " Magic " , c_uint16 ) ,
( " MajorLinkerVersion " , c_uint8 ) ,
( " MinorLinkerVersion " , c_uint8 ) ,
( " SizeOfCode " , c_uint32 ) ,
( " SizeOfInitializedData " , c_uint32 ) ,
( " SizeOfUninitializedData " , c_uint32 ) ,
( " AddressOfEntryPoint " , c_uint32 ) ,
( " BaseOfCode " , c_uint32 ) ,
)
class PeOptionalHeaderMiddle ( LittleEndianStructure ) :
_fields_ = (
( " SectionAlignment " , c_uint32 ) ,
( " FileAlignment " , c_uint32 ) ,
( " MajorOperatingSystemVersion " , c_uint16 ) ,
( " MinorOperatingSystemVersion " , c_uint16 ) ,
( " MajorImageVersion " , c_uint16 ) ,
( " MinorImageVersion " , c_uint16 ) ,
( " MajorSubsystemVersion " , c_uint16 ) ,
( " MinorSubsystemVersion " , c_uint16 ) ,
( " Win32VersionValue " , c_uint32 ) ,
( " SizeOfImage " , c_uint32 ) ,
( " SizeOfHeaders " , c_uint32 ) ,
( " CheckSum " , c_uint32 ) ,
( " Subsystem " , c_uint16 ) ,
( " DllCharacteristics " , c_uint16 ) ,
)
class PeOptionalHeaderEnd ( LittleEndianStructure ) :
_fields_ = (
( " LoaderFlags " , c_uint32 ) ,
( " NumberOfRvaAndSizes " , c_uint32 ) ,
( " ExportTable " , PeDataDirectory ) ,
( " ImportTable " , PeDataDirectory ) ,
( " ResourceTable " , PeDataDirectory ) ,
( " ExceptionTable " , PeDataDirectory ) ,
( " CertificateTable " , PeDataDirectory ) ,
( " BaseRelocationTable " , PeDataDirectory ) ,
( " Debug " , PeDataDirectory ) ,
( " Architecture " , PeDataDirectory ) ,
( " GlobalPtr " , PeDataDirectory ) ,
( " TLSTable " , PeDataDirectory ) ,
( " LoadConfigTable " , PeDataDirectory ) ,
( " BoundImport " , PeDataDirectory ) ,
( " IAT " , PeDataDirectory ) ,
( " DelayImportDescriptor " , PeDataDirectory ) ,
( " CLRRuntimeHeader " , PeDataDirectory ) ,
( " Reserved " , PeDataDirectory ) ,
)
class PeOptionalHeader ( LittleEndianStructure ) :
pass
class PeOptionalHeader32 ( PeOptionalHeader ) :
_anonymous_ = ( " Start " , " Middle " , " End " )
_fields_ = (
( " Start " , PeOptionalHeaderStart ) ,
( " BaseOfData " , c_uint32 ) ,
( " ImageBase " , c_uint32 ) ,
( " Middle " , PeOptionalHeaderMiddle ) ,
( " SizeOfStackReserve " , c_uint32 ) ,
( " SizeOfStackCommit " , c_uint32 ) ,
( " SizeOfHeapReserve " , c_uint32 ) ,
( " SizeOfHeapCommit " , c_uint32 ) ,
( " End " , PeOptionalHeaderEnd ) ,
)
class PeOptionalHeader32Plus ( PeOptionalHeader ) :
_anonymous_ = ( " Start " , " Middle " , " End " )
_fields_ = (
( " Start " , PeOptionalHeaderStart ) ,
( " ImageBase " , c_uint64 ) ,
( " Middle " , PeOptionalHeaderMiddle ) ,
( " SizeOfStackReserve " , c_uint64 ) ,
( " SizeOfStackCommit " , c_uint64 ) ,
( " SizeOfHeapReserve " , c_uint64 ) ,
( " SizeOfHeapCommit " , c_uint64 ) ,
( " End " , PeOptionalHeaderEnd ) ,
)
class PeSection ( LittleEndianStructure ) :
_fields_ = (
( " Name " , c_char * 8 ) ,
( " VirtualSize " , c_uint32 ) ,
( " VirtualAddress " , c_uint32 ) ,
( " SizeOfRawData " , c_uint32 ) ,
( " PointerToRawData " , c_uint32 ) ,
( " PointerToRelocations " , c_uint32 ) ,
( " PointerToLinenumbers " , c_uint32 ) ,
( " NumberOfRelocations " , c_uint16 ) ,
( " NumberOfLinenumbers " , c_uint16 ) ,
( " Characteristics " , c_uint32 ) ,
)
def __init__ ( self ) :
super ( ) . __init__ ( )
self . data = bytearray ( )
N_DATA_DIRECTORY_ENTRIES = 16
assert sizeof ( PeSection ) == 40
assert sizeof ( PeCoffHeader ) == 20
assert sizeof ( PeOptionalHeader32 ) == 224
assert sizeof ( PeOptionalHeader32Plus ) == 240
# EFI mandates 4KiB memory pages.
SECTION_ALIGNMENT = 4096
FILE_ALIGNMENT = 512
# Nobody cares about DOS headers, so put the PE header right after.
PE_OFFSET = 64
def align_to ( x : int , align : int ) - > int :
return ( x + align - 1 ) & ~ ( align - 1 )
def use_section ( elf_s : ELFSection ) - > bool :
# These sections are either needed during conversion to PE or are otherwise not needed
# in the final PE image.
IGNORE_SECTIONS = [
" .ARM.exidx " ,
" .dynamic " ,
" .dynstr " ,
" .dynsym " ,
" .eh_frame_hdr " ,
" .eh_frame " ,
" .gnu.hash " ,
" .hash " ,
" .note.gnu.build-id " ,
" .rel.dyn " ,
" .rela.dyn " ,
]
# Known sections we care about and want to be in the final PE.
COPY_SECTIONS = [
" .data " ,
" .osrel " ,
" .rodata " ,
" .sbat " ,
" .sdmagic " ,
" .text " ,
]
# By only dealing with allocating sections we effectively filter out debug sections.
if not elf_s [ " sh_flags " ] & SH_FLAGS . SHF_ALLOC :
return False
if elf_s . name in IGNORE_SECTIONS :
return False
# For paranoia we only handle sections we know of. Any new sections that come up should
# be added to IGNORE_SECTIONS/COPY_SECTIONS and/or the linker script.
if elf_s . name not in COPY_SECTIONS :
raise RuntimeError ( f " Unknown section { elf_s . name } , refusing. " )
if elf_s [ " sh_addr " ] % SECTION_ALIGNMENT != 0 :
raise RuntimeError ( f " Section { elf_s . name } is not aligned. " )
if len ( elf_s . name ) > 8 :
raise RuntimeError ( f " ELF section name { elf_s . name } too long. " )
return True
def convert_elf_section ( elf_s : ELFSection ) - > PeSection :
pe_s = PeSection ( )
pe_s . Name = elf_s . name . encode ( )
pe_s . VirtualSize = elf_s . data_size
pe_s . VirtualAddress = elf_s [ " sh_addr " ]
pe_s . SizeOfRawData = align_to ( elf_s . data_size , FILE_ALIGNMENT )
pe_s . data = bytearray ( elf_s . data ( ) )
if elf_s [ " sh_flags " ] & SH_FLAGS . SHF_EXECINSTR :
pe_s . Characteristics = 0x60000020 # CNT_CODE|MEM_READ|MEM_EXECUTE
elif elf_s [ " sh_flags " ] & SH_FLAGS . SHF_WRITE :
pe_s . Characteristics = 0xC0000040 # CNT_INITIALIZED_DATA|MEM_READ|MEM_WRITE
else :
pe_s . Characteristics = 0x40000040 # CNT_INITIALIZED_DATA|MEM_READ
return pe_s
def copy_sections ( elf : ELFFile , opt : PeOptionalHeader ) - > list [ PeSection ] :
sections = [ ]
for elf_s in elf . iter_sections ( ) :
if not use_section ( elf_s ) :
continue
pe_s = convert_elf_section ( elf_s )
if pe_s . Name == b " .text " :
opt . BaseOfCode = pe_s . VirtualAddress
opt . SizeOfCode + = pe_s . VirtualSize
else :
opt . SizeOfInitializedData + = pe_s . VirtualSize
if pe_s . Name == b " .data " and isinstance ( opt , PeOptionalHeader32 ) :
opt . BaseOfData = pe_s . VirtualAddress
sections . append ( pe_s )
return sections
def apply_elf_relative_relocation (
reloc : ElfRelocation , image_base : int , sections : list [ PeSection ] , addend_size : int
) :
# fmt: off
[ target ] = [
pe_s for pe_s in sections
if pe_s . VirtualAddress < = reloc [ " r_offset " ] < pe_s . VirtualAddress + len ( pe_s . data )
]
# fmt: on
addend_offset = reloc [ " r_offset " ] - target . VirtualAddress
if reloc . is_RELA ( ) :
addend = reloc [ " r_addend " ]
else :
addend = target . data [ addend_offset : addend_offset + addend_size ]
addend = int . from_bytes ( addend , byteorder = " little " )
# This currently assumes that the ELF file has an image base of 0.
value = ( image_base + addend ) . to_bytes ( addend_size , byteorder = " little " )
target . data [ addend_offset : addend_offset + addend_size ] = value
def convert_elf_reloc_table (
elf : ELFFile ,
elf_reloc_table : ElfRelocationTable ,
image_base : int ,
sections : list [ PeSection ] ,
pe_reloc_blocks : dict [ int , PeRelocationBlock ] ,
) :
NONE_RELOC = {
" EM_386 " : ENUM_RELOC_TYPE_i386 [ " R_386_NONE " ] ,
" EM_AARCH64 " : ENUM_RELOC_TYPE_AARCH64 [ " R_AARCH64_NONE " ] ,
" EM_ARM " : ENUM_RELOC_TYPE_ARM [ " R_ARM_NONE " ] ,
2023-02-28 20:05:18 +03:00
" EM_LOONGARCH " : 0 ,
2023-02-27 18:54:48 +03:00
" EM_RISCV " : 0 ,
" EM_X86_64 " : ENUM_RELOC_TYPE_x64 [ " R_X86_64_NONE " ] ,
} [ elf [ " e_machine " ] ]
RELATIVE_RELOC = {
" EM_386 " : ENUM_RELOC_TYPE_i386 [ " R_386_RELATIVE " ] ,
" EM_AARCH64 " : ENUM_RELOC_TYPE_AARCH64 [ " R_AARCH64_RELATIVE " ] ,
" EM_ARM " : ENUM_RELOC_TYPE_ARM [ " R_ARM_RELATIVE " ] ,
2023-02-28 20:05:18 +03:00
" EM_LOONGARCH " : 3 ,
2023-02-27 18:54:48 +03:00
" EM_RISCV " : 3 ,
" EM_X86_64 " : ENUM_RELOC_TYPE_x64 [ " R_X86_64_RELATIVE " ] ,
} [ elf [ " e_machine " ] ]
for reloc in elf_reloc_table . iter_relocations ( ) :
if reloc [ " r_info_type " ] == NONE_RELOC :
continue
if reloc [ " r_info_type " ] == RELATIVE_RELOC :
apply_elf_relative_relocation (
reloc , image_base , sections , elf . elfclass / / 8
)
# Now that the ELF relocation has been applied, we can create a PE relocation.
block_rva = reloc [ " r_offset " ] & ~ 0xFFF
if block_rva not in pe_reloc_blocks :
pe_reloc_blocks [ block_rva ] = PeRelocationBlock ( block_rva )
entry = PeRelocationEntry ( )
entry . Offset = reloc [ " r_offset " ] & 0xFFF
# REL_BASED_HIGHLOW or REL_BASED_DIR64
entry . Type = 3 if elf . elfclass == 32 else 10
pe_reloc_blocks [ block_rva ] . entries . append ( entry )
continue
raise RuntimeError ( f " Unsupported relocation { reloc } " )
def convert_elf_relocations (
elf : ELFFile , opt : PeOptionalHeader , sections : list [ PeSection ]
) - > PeSection :
dynamic = elf . get_section_by_name ( " .dynamic " )
if dynamic is None :
raise RuntimeError ( " ELF .dynamic section is missing. " )
[ flags_tag ] = dynamic . iter_tags ( " DT_FLAGS_1 " )
if not flags_tag [ " d_val " ] & ENUM_DT_FLAGS_1 [ " DF_1_PIE " ] :
raise RuntimeError ( " ELF file is not a PIE. " )
pe_reloc_blocks : dict [ int , PeRelocationBlock ] = { }
for reloc_type , reloc_table in dynamic . get_relocation_tables ( ) . items ( ) :
if reloc_type not in [ " REL " , " RELA " ] :
raise RuntimeError ( " Unsupported relocation type {elf_reloc_type} . " )
convert_elf_reloc_table (
elf , reloc_table , opt . ImageBase , sections , pe_reloc_blocks
)
data = bytearray ( )
for rva in sorted ( pe_reloc_blocks ) :
block = pe_reloc_blocks [ rva ]
n_relocs = len ( block . entries )
# Each block must start on a 32-bit boundary. Because each entry is 16 bits
# the len has to be even. We pad by adding a none relocation.
if n_relocs % 2 != 0 :
n_relocs + = 1
block . entries . append ( PeRelocationEntry ( ) )
block . BlockSize = (
sizeof ( PeRelocationBlock ) + sizeof ( PeRelocationEntry ) * n_relocs
)
data + = block
for entry in sorted ( block . entries , key = lambda e : e . Offset ) :
data + = entry
pe_reloc_s = PeSection ( )
pe_reloc_s . Name = b " .reloc "
pe_reloc_s . data = data
pe_reloc_s . VirtualSize = len ( data )
pe_reloc_s . SizeOfRawData = align_to ( len ( data ) , FILE_ALIGNMENT )
pe_reloc_s . VirtualAddress = align_to (
sections [ - 1 ] . VirtualAddress + sections [ - 1 ] . VirtualSize , SECTION_ALIGNMENT
)
# CNT_INITIALIZED_DATA|MEM_READ|MEM_DISCARDABLE
pe_reloc_s . Characteristics = 0x42000040
sections . append ( pe_reloc_s )
opt . SizeOfInitializedData + = pe_reloc_s . VirtualSize
return pe_reloc_s
def write_pe (
file , coff : PeCoffHeader , opt : PeOptionalHeader , sections : list [ PeSection ]
) :
file . write ( b " MZ " )
file . seek ( 0x3C , io . SEEK_SET )
file . write ( PE_OFFSET . to_bytes ( 2 , byteorder = " little " ) )
file . seek ( PE_OFFSET , io . SEEK_SET )
file . write ( b " PE \0 \0 " )
file . write ( coff )
file . write ( opt )
offset = opt . SizeOfHeaders
for pe_s in sorted ( sections , key = lambda s : s . VirtualAddress ) :
if pe_s . VirtualAddress < opt . SizeOfHeaders :
# Linker script should make sure this does not happen.
raise RuntimeError ( f " Section { pe_s . Name } overlapping PE headers. " )
pe_s . PointerToRawData = offset
file . write ( pe_s )
offset = align_to ( offset + len ( pe_s . data ) , FILE_ALIGNMENT )
for pe_s in sections :
file . seek ( pe_s . PointerToRawData , io . SEEK_SET )
file . write ( pe_s . data )
file . truncate ( offset )
def elf2efi ( args : argparse . Namespace ) :
elf = ELFFile ( args . ELF )
if not elf . little_endian :
raise RuntimeError ( " ELF file is not little-endian. " )
if elf [ " e_type " ] not in [ " ET_DYN " , " ET_EXEC " ] :
raise RuntimeError ( " Unsupported ELF type. " )
pe_arch = {
" EM_386 " : 0x014C ,
" EM_AARCH64 " : 0xAA64 ,
" EM_ARM " : 0x01C2 ,
2023-02-28 20:05:18 +03:00
" EM_LOONGARCH " : 0x6232 if elf . elfclass == 32 else 0x6264 ,
" EM_RISCV " : 0x5032 if elf . elfclass == 32 else 0x5064 ,
2023-02-27 18:54:48 +03:00
" EM_X86_64 " : 0x8664 ,
} . get ( elf [ " e_machine " ] )
if pe_arch is None :
raise RuntimeError ( f " Unuspported ELF arch { elf [ ' e_machine ' ] } " )
coff = PeCoffHeader ( )
opt = PeOptionalHeader32 ( ) if elf . elfclass == 32 else PeOptionalHeader32Plus ( )
# We relocate to a unique image base to reduce the chances for runtime relocation to occur.
base_name = pathlib . Path ( args . PE . name ) . name . encode ( )
opt . ImageBase = int ( hashlib . sha1 ( base_name ) . hexdigest ( ) [ 0 : 8 ] , 16 )
if elf . elfclass == 32 :
opt . ImageBase = ( 0x400000 + opt . ImageBase ) & 0xFFFF0000
else :
opt . ImageBase = ( 0x100000000 + opt . ImageBase ) & 0x1FFFF0000
sections = copy_sections ( elf , opt )
pe_reloc_s = convert_elf_relocations ( elf , opt , sections )
coff . Machine = pe_arch
coff . NumberOfSections = len ( sections )
coff . TimeDateStamp = int ( os . environ . get ( " SOURCE_DATE_EPOCH " , time . time ( ) ) )
coff . SizeOfOptionalHeader = sizeof ( opt )
# EXECUTABLE_IMAGE|LINE_NUMS_STRIPPED|LOCAL_SYMS_STRIPPED|DEBUG_STRIPPED
# and (32BIT_MACHINE or LARGE_ADDRESS_AWARE)
coff . Characteristics = 0x30E if elf . elfclass == 32 else 0x22E
opt . AddressOfEntryPoint = elf [ " e_entry " ]
opt . SectionAlignment = SECTION_ALIGNMENT
opt . FileAlignment = FILE_ALIGNMENT
opt . MajorImageVersion = args . version_major
opt . MinorImageVersion = args . version_minor
opt . MajorSubsystemVersion = args . efi_major
opt . MinorSubsystemVersion = args . efi_minor
opt . Subsystem = args . subsystem
opt . Magic = 0x10B if elf . elfclass == 32 else 0x20B
opt . SizeOfImage = align_to (
sections [ - 1 ] . VirtualAddress + sections [ - 1 ] . VirtualSize , SECTION_ALIGNMENT
)
opt . SizeOfHeaders = align_to (
PE_OFFSET
+ coff . SizeOfOptionalHeader
+ sizeof ( PeSection ) * coff . NumberOfSections ,
FILE_ALIGNMENT ,
)
# DYNAMIC_BASE|NX_COMPAT|HIGH_ENTROPY_VA or DYNAMIC_BASE|NX_COMPAT
opt . DllCharacteristics = 0x160 if elf . elfclass == 64 else 0x140
# These values are taken from a natively built PE binary (although, unused by EDK2/EFI).
opt . SizeOfStackReserve = 0x100000
opt . SizeOfStackCommit = 0x001000
opt . SizeOfHeapReserve = 0x100000
opt . SizeOfHeapCommit = 0x001000
opt . NumberOfRvaAndSizes = N_DATA_DIRECTORY_ENTRIES
opt . BaseRelocationTable = PeDataDirectory (
pe_reloc_s . VirtualAddress , pe_reloc_s . VirtualSize
)
write_pe ( args . PE , coff , opt , sections )
def main ( ) :
parser = argparse . ArgumentParser ( description = " Convert ELF binaries to PE/EFI " )
parser . add_argument (
" --version-major " ,
type = int ,
default = 0 ,
help = " Major image version of EFI image " ,
)
parser . add_argument (
" --version-minor " ,
type = int ,
default = 0 ,
help = " Minor image version of EFI image " ,
)
parser . add_argument (
" --efi-major " ,
type = int ,
default = 0 ,
help = " Minimum major EFI subsystem version " ,
)
parser . add_argument (
" --efi-minor " ,
type = int ,
default = 0 ,
help = " Minimum minor EFI subsystem version " ,
)
parser . add_argument (
" --subsystem " ,
type = int ,
default = 10 ,
help = " PE subsystem " ,
)
parser . add_argument (
" ELF " ,
type = argparse . FileType ( " rb " ) ,
help = " Input ELF file " ,
)
parser . add_argument (
" PE " ,
type = argparse . FileType ( " wb " ) ,
help = " Output PE/EFI file " ,
)
elf2efi ( parser . parse_args ( ) )
if __name__ == " __main__ " :
main ( )