#!/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=attribute-defined-outside-init # mypy: untyped-calls-exclude=elftools import argparse import hashlib import io import os import pathlib import sys import time import typing from ctypes import ( LittleEndianStructure, c_char, c_uint8, c_uint16, c_uint32, c_uint64, sizeof, ) from elftools import elf from elftools.elf import elffile 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), ) # fmt: skip class PeDataDirectory(LittleEndianStructure): _fields_ = ( ('VirtualAddress', c_uint32), ('Size', c_uint32), ) # fmt: skip class PeRelocationBlock(LittleEndianStructure): _fields_ = ( ('PageRVA', c_uint32), ('BlockSize', c_uint32), ) # fmt: skip def __init__(self, PageRVA: int): super().__init__(PageRVA) self.entries: list[PeRelocationEntry] = [] class PeRelocationEntry(LittleEndianStructure): _fields_ = ( ('Offset', c_uint16, 12), ('Type', c_uint16, 4), ) # fmt: skip 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), ) # fmt: skip 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), ) # fmt: skip 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), ) # fmt: skip 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), ) # fmt: skip 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), ) # fmt: skip 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), ) # fmt: skip def __init__(self) -> None: 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 # fmt: off PE_CHARACTERISTICS_RX = 0x60000020 # CNT_CODE|MEM_READ|MEM_EXECUTE PE_CHARACTERISTICS_RW = 0xC0000040 # CNT_INITIALIZED_DATA|MEM_READ|MEM_WRITE PE_CHARACTERISTICS_R = 0x40000040 # CNT_INITIALIZED_DATA|MEM_READ # fmt: on IGNORE_SECTIONS = [ '.eh_frame', '.eh_frame_hdr', '.ARM.exidx', '.relro_padding', '.sframe', ] IGNORE_SECTION_TYPES = [ 'SHT_DYNAMIC', 'SHT_DYNSYM', 'SHT_GNU_ATTRIBUTES', 'SHT_GNU_HASH', 'SHT_HASH', 'SHT_NOTE', 'SHT_REL', 'SHT_RELA', 'SHT_RELR', 'SHT_STRTAB', 'SHT_SYMTAB', ] # 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 PE_MAGIC = b'PE\0\0' def align_to(x: int, align: int) -> int: return (x + align - 1) & ~(align - 1) def align_down(x: int, align: int) -> int: return x & ~(align - 1) def next_section_address(sections: list[PeSection]) -> int: return align_to(sections[-1].VirtualAddress + sections[-1].VirtualSize, SECTION_ALIGNMENT) class BadSectionError(ValueError): "One of the sections is in a bad state" def iter_copy_sections(file: elffile.ELFFile) -> typing.Iterator[PeSection]: pe_s = None # This is essentially the same as copying by ELF load segments, except that we assemble them # manually, so that we can easily strip unwanted sections. We try to only discard things we know # about so that there are no surprises. relro = None for elf_seg in file.iter_segments(): if elf_seg['p_type'] == 'PT_LOAD' and elf_seg['p_align'] != SECTION_ALIGNMENT: raise BadSectionError( f'ELF segment {elf_seg["p_type"]} is not properly aligned ({elf_seg["p_align"]} != {SECTION_ALIGNMENT})' ) if elf_seg['p_type'] == 'PT_GNU_RELRO': relro = elf_seg for elf_s in file.iter_sections(): if ( elf_s['sh_flags'] & elf.constants.SH_FLAGS.SHF_ALLOC == 0 or elf_s['sh_type'] in IGNORE_SECTION_TYPES or elf_s.name in IGNORE_SECTIONS or elf_s['sh_size'] == 0 ): continue if elf_s['sh_type'] not in ['SHT_PROGBITS', 'SHT_NOBITS']: raise BadSectionError(f'Unknown section {elf_s.name} with type {elf_s["sh_type"]}') if elf_s.name == '.got': # FIXME: figure out why those sections are inserted print('WARNING: Non-empty .got section', file=sys.stderr) if elf_s['sh_flags'] & elf.constants.SH_FLAGS.SHF_EXECINSTR: rwx = PE_CHARACTERISTICS_RX elif elf_s['sh_flags'] & elf.constants.SH_FLAGS.SHF_WRITE: rwx = PE_CHARACTERISTICS_RW else: rwx = PE_CHARACTERISTICS_R # PE images are always relro. if relro and relro.section_in_segment(elf_s): rwx = PE_CHARACTERISTICS_R if pe_s and pe_s.Characteristics != rwx: yield pe_s pe_s = None if pe_s: # Insert padding to properly align the section. pad_len = elf_s['sh_addr'] - pe_s.VirtualAddress - len(pe_s.data) pe_s.data += bytearray(pad_len) + elf_s.data() else: pe_s = PeSection() pe_s.VirtualAddress = elf_s['sh_addr'] pe_s.Characteristics = rwx pe_s.data = elf_s.data() if pe_s: yield pe_s def convert_sections( file: elffile.ELFFile, opt: PeOptionalHeader, ) -> list[PeSection]: last_vma = (0, 0) sections = [] for pe_s in iter_copy_sections(file): # Truncate the VMA to the nearest page and insert appropriate padding. This should not # cause any overlap as this is pretty much how ELF *segments* are loaded/mmapped anyways. # The ELF sections inside should also be properly aligned as we reuse the ELF VMA layout # for the PE image. vma = pe_s.VirtualAddress pe_s.VirtualAddress = align_down(vma, SECTION_ALIGNMENT) pe_s.data = bytearray(vma - pe_s.VirtualAddress) + pe_s.data pe_s.VirtualSize = len(pe_s.data) pe_s.SizeOfRawData = align_to(len(pe_s.data), FILE_ALIGNMENT) pe_s.Name = { PE_CHARACTERISTICS_RX: b'.text', PE_CHARACTERISTICS_RW: b'.data', PE_CHARACTERISTICS_R: b'.rodata', }[pe_s.Characteristics] # This can happen if not building with '-z separate-code'. if pe_s.VirtualAddress < sum(last_vma): raise BadSectionError( f'Section {pe_s.Name.decode()!r} @{pe_s.VirtualAddress:#x} overlaps previous section @{last_vma[0]:#x}+{last_vma[1]:#x}=@{sum(last_vma):#x}' ) last_vma = (pe_s.VirtualAddress, pe_s.VirtualSize) 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 copy_sections( file: elffile.ELFFile, opt: PeOptionalHeader, input_names: str, sections: list[PeSection], ) -> None: for name in input_names.split(','): elf_s = file.get_section_by_name(name) if not elf_s: continue if elf_s.data_alignment > 1 and SECTION_ALIGNMENT % elf_s.data_alignment != 0: raise BadSectionError(f'ELF section {name} is not aligned') if elf_s['sh_flags'] & (elf.constants.SH_FLAGS.SHF_EXECINSTR | elf.constants.SH_FLAGS.SHF_WRITE) != 0: # fmt: skip raise BadSectionError(f'ELF section {name} is not read-only data') pe_s = PeSection() pe_s.Name = name.encode() pe_s.data = elf_s.data() pe_s.VirtualAddress = next_section_address(sections) pe_s.VirtualSize = len(elf_s.data()) pe_s.SizeOfRawData = align_to(len(elf_s.data()), FILE_ALIGNMENT) pe_s.Characteristics = PE_CHARACTERISTICS_R opt.SizeOfInitializedData += pe_s.VirtualSize sections.append(pe_s) def apply_elf_relative_relocation( reloc: elf.relocation.Relocation, image_base: int, sections: list[PeSection], addend_size: int, ) -> None: [target] = [ pe_s for pe_s in sections if pe_s.VirtualAddress <= reloc['r_offset'] < pe_s.VirtualAddress + len(pe_s.data) ] 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') value = (image_base + addend).to_bytes(addend_size, byteorder='little') target.data[addend_offset : addend_offset + addend_size] = value def convert_elf_reloc_table( file: elffile.ELFFile, elf_reloc_table: elf.relocation.RelocationTable, elf_image_base: int, sections: list[PeSection], pe_reloc_blocks: dict[int, PeRelocationBlock], ) -> None: NONE_RELOC = { 'EM_386': elf.enums.ENUM_RELOC_TYPE_i386['R_386_NONE'], 'EM_AARCH64': elf.enums.ENUM_RELOC_TYPE_AARCH64['R_AARCH64_NONE'], 'EM_ARM': elf.enums.ENUM_RELOC_TYPE_ARM['R_ARM_NONE'], 'EM_LOONGARCH': 0, 'EM_RISCV': 0, 'EM_X86_64': elf.enums.ENUM_RELOC_TYPE_x64['R_X86_64_NONE'], }[file['e_machine']] # fmt: skip RELATIVE_RELOC = { 'EM_386': elf.enums.ENUM_RELOC_TYPE_i386['R_386_RELATIVE'], 'EM_AARCH64': elf.enums.ENUM_RELOC_TYPE_AARCH64['R_AARCH64_RELATIVE'], 'EM_ARM': elf.enums.ENUM_RELOC_TYPE_ARM['R_ARM_RELATIVE'], 'EM_LOONGARCH': 3, 'EM_RISCV': 3, 'EM_X86_64': elf.enums.ENUM_RELOC_TYPE_x64['R_X86_64_RELATIVE'], }[file['e_machine']] # fmt: skip 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, elf_image_base, sections, file.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 file.elfclass == 32 else 10 pe_reloc_blocks[block_rva].entries.append(entry) continue raise BadSectionError(f'Unsupported relocation {reloc}') def convert_elf_relocations( file: elffile.ELFFile, opt: PeOptionalHeader, sections: list[PeSection], minimum_sections: int, ) -> typing.Optional[PeSection]: dynamic = file.get_section_by_name('.dynamic') if dynamic is None: raise BadSectionError('ELF .dynamic section is missing') [flags_tag] = dynamic.iter_tags('DT_FLAGS_1') if not flags_tag['d_val'] & elf.enums.ENUM_DT_FLAGS_1['DF_1_PIE']: raise ValueError('ELF file is not a PIE') # This checks that the ELF image base is 0. symtab = file.get_section_by_name('.symtab') if symtab: exe_start = symtab.get_symbol_by_name('__executable_start') if exe_start and exe_start[0]['st_value'] != 0: raise ValueError('Unexpected ELF image base') opt.SizeOfHeaders = align_to( PE_OFFSET + len(PE_MAGIC) + sizeof(PeCoffHeader) + sizeof(opt) + sizeof(PeSection) * max(len(sections) + 1, minimum_sections), FILE_ALIGNMENT, ) # We use the basic VMA layout from the ELF image in the PE image. This could cause the first # section to overlap the PE image headers during runtime at VMA 0. We can simply apply a fixed # offset relative to the PE image base when applying/converting ELF relocations. Afterwards we # just have to apply the offset to the PE addresses so that the PE relocations work correctly on # the ELF portions of the image. segment_offset = 0 if sections[0].VirtualAddress < opt.SizeOfHeaders: segment_offset = align_to(opt.SizeOfHeaders - sections[0].VirtualAddress, SECTION_ALIGNMENT) opt.AddressOfEntryPoint = file['e_entry'] + segment_offset opt.BaseOfCode += segment_offset if isinstance(opt, PeOptionalHeader32): opt.BaseOfData += segment_offset 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 BadSectionError(f'Unsupported relocation type {reloc_type}') convert_elf_reloc_table(file, reloc_table, opt.ImageBase + segment_offset, sections, pe_reloc_blocks) for pe_s in sections: pe_s.VirtualAddress += segment_offset if len(pe_reloc_blocks) == 0: return None 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.PageRVA += segment_offset 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.VirtualAddress = next_section_address(sections) pe_reloc_s.VirtualSize = len(data) pe_reloc_s.SizeOfRawData = align_to(len(data), FILE_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: typing.IO[bytes], coff: PeCoffHeader, opt: PeOptionalHeader, sections: list[PeSection], ) -> None: 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(PE_MAGIC) 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: raise BadSectionError( f'Section {pe_s.Name} @{pe_s.VirtualAddress:#x} overlaps PE headers ending at {opt.SizeOfHeaders:#x}' ) pe_s.PointerToRawData = offset file.write(pe_s) offset = align_to(offset + len(pe_s.data), FILE_ALIGNMENT) assert file.tell() <= opt.SizeOfHeaders 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) -> None: file = elffile.ELFFile(args.ELF) if not file.little_endian: raise ValueError('ELF file is not little-endian') if file['e_type'] not in ['ET_DYN', 'ET_EXEC']: raise ValueError(f'Unsupported ELF type {file["e_type"]}') pe_arch = { 'EM_386': 0x014C, 'EM_AARCH64': 0xAA64, 'EM_ARM': 0x01C2, 'EM_LOONGARCH': 0x6232 if file.elfclass == 32 else 0x6264, 'EM_RISCV': 0x5032 if file.elfclass == 32 else 0x5064, 'EM_X86_64': 0x8664, }.get(file['e_machine']) # fmt: skip if pe_arch is None: raise ValueError(f'Unsupported ELF architecture {file["e_machine"]}') coff = PeCoffHeader() opt = PeOptionalHeader32() if file.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 file.elfclass == 32: opt.ImageBase = (0x400000 + opt.ImageBase) & 0xFFFF0000 else: opt.ImageBase = (0x100000000 + opt.ImageBase) & 0x1FFFF0000 sections = convert_sections(file, opt) copy_sections(file, opt, args.copy_sections, sections) pe_reloc_s = convert_elf_relocations(file, opt, sections, args.minimum_sections) coff.Machine = pe_arch coff.NumberOfSections = len(sections) coff.TimeDateStamp = int(os.environ.get('SOURCE_DATE_EPOCH') or 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 file.elfclass == 32 else 0x22E 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 file.elfclass == 32 else 0x20B opt.SizeOfImage = next_section_address(sections) # DYNAMIC_BASE|NX_COMPAT|HIGH_ENTROPY_VA or DYNAMIC_BASE|NX_COMPAT opt.DllCharacteristics = 0x160 if file.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 if pe_reloc_s: opt.BaseRelocationTable = PeDataDirectory(pe_reloc_s.VirtualAddress, pe_reloc_s.VirtualSize) write_pe(args.PE, coff, opt, sections) def create_parser() -> argparse.ArgumentParser: 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', ) parser.add_argument( '--minimum-sections', type=int, default=0, help='Minimum number of sections to leave space for', ) parser.add_argument( '--copy-sections', type=str, default='', help='Copy these sections if found', ) return parser def main() -> None: parser = create_parser() elf2efi(parser.parse_args()) if __name__ == '__main__': main()