systemd/tools/elf2efi.py

579 lines
18 KiB
Python
Raw Normal View History

#!/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"],
"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"],
"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,
"EM_RISCV": 0x5064,
"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()