Shofel2_T124_python/venv/lib/python3.10/site-packages/qiling/loader/elf.py

674 lines
28 KiB
Python

#!/usr/bin/env python3
#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
import io
import os
from enum import IntEnum
from typing import Optional, Sequence, Mapping, Tuple
from elftools.common.utils import preserve_stream_pos
from elftools.elf.constants import P_FLAGS, SH_FLAGS
from elftools.elf.elffile import ELFFile
from elftools.elf.relocation import RelocationHandler
from elftools.elf.sections import Symbol, SymbolTableSection
from elftools.elf.descriptions import describe_reloc_type
from unicorn.unicorn_const import UC_PROT_NONE, UC_PROT_READ, UC_PROT_WRITE, UC_PROT_EXEC
from qiling import Qiling
from qiling.const import QL_ARCH, QL_ENDIAN, QL_OS
from qiling.exception import QlErrorELFFormat, QlMemoryMappedError
from qiling.loader.loader import QlLoader, Image
from qiling.os.linux.function_hook import FunctionHook
from qiling.os.linux.syscall_nums import SYSCALL_NR
from qiling.os.linux.kernel_api.hook import *
from qiling.os.linux.kernel_api.kernel_api import hook_sys_open, hook_sys_read, hook_sys_write
# auxiliary vector types
# see: https://man7.org/linux/man-pages/man3/getauxval.3.html
class AUXV(IntEnum):
AT_NULL = 0
AT_IGNORE = 1
AT_EXECFD = 2
AT_PHDR = 3
AT_PHENT = 4
AT_PHNUM = 5
AT_PAGESZ = 6
AT_BASE = 7
AT_FLAGS = 8
AT_ENTRY = 9
AT_NOTELF = 10
AT_UID = 11
AT_EUID = 12
AT_GID = 13
AT_EGID = 14
AT_PLATFORM = 15
AT_HWCAP = 16
AT_CLKTCK = 17
AT_SECURE = 23
AT_BASE_PLATFORM = 24
AT_RANDOM = 25
AT_HWCAP2 = 26
AT_EXECFN = 31
# start area memory for API hooking
# we will reserve 0x1000 bytes for this (which contains multiple slots of 4/8 bytes, each for one api)
API_HOOK_MEM = 0x1000000
# memory for syscall table
SYSCALL_MEM = API_HOOK_MEM + 0x1000
class QlLoaderELF(QlLoader):
def __init__(self, ql: Qiling):
super().__init__(ql)
def run(self):
if self.ql.code:
self.ql.mem.map(self.ql.os.entry_point, self.ql.os.code_ram_size, info="[shellcode_stack]")
shellcode_base = self.ql.os.entry_point + 0x200000 - 0x1000
self.ql.mem.write(shellcode_base, self.ql.code)
self.ql.arch.regs.arch_sp = shellcode_base
self.ql.os.entry_point = shellcode_base
self.load_address = shellcode_base
return
self.profile = self.ql.os.profile[f'OS{self.ql.arch.bits}']
# setup program stack
stack_address = self.profile.getint('stack_address')
stack_size = self.profile.getint('stack_size')
self.ql.mem.map(stack_address, stack_size, info='[stack]')
self.path = self.ql.path
with open(self.path, 'rb') as infile:
fstream = io.BytesIO(infile.read())
elffile = ELFFile(fstream)
elftype = elffile['e_type']
# is it a driver?
if elftype == 'ET_REL':
self.load_driver(elffile, stack_address + stack_size, loadbase=0x8000000)
self.ql.hook_code(hook_kernel_api)
# is it an executable?
elif elftype == 'ET_EXEC':
load_address = 0
self.load_with_ld(elffile, stack_address + stack_size, load_address, self.argv, self.env)
# is it a shared object?
elif elftype == 'ET_DYN':
load_address = self.profile.getint('load_address')
self.load_with_ld(elffile, stack_address + stack_size, load_address, self.argv, self.env)
else:
raise QlErrorELFFormat(f'unexpected elf type value (e_type = {elftype})')
self.is_driver = (elftype == 'ET_REL')
self.ql.arch.regs.arch_sp = self.stack_address
# No idea why.
if self.ql.os.type == QL_OS.FREEBSD:
# self.ql.arch.regs.rbp = self.stack_address + 0x40
self.ql.arch.regs.rdi = self.stack_address
self.ql.arch.regs.r14 = self.stack_address
@staticmethod
def seg_perm_to_uc_prot(perm: int) -> int:
"""Translate ELF segment perms to Unicorn protection constants.
"""
prot = UC_PROT_NONE
if perm & P_FLAGS.PF_X:
prot |= UC_PROT_EXEC
if perm & P_FLAGS.PF_W:
prot |= UC_PROT_WRITE
if perm & P_FLAGS.PF_R:
prot |= UC_PROT_READ
return prot
def load_with_ld(self, elffile: ELFFile, stack_addr: int, load_address: int, argv: Sequence[str] = [], env: Mapping[str, str] = {}):
def load_elf_segments(elffile: ELFFile, load_address: int, info: str):
# get list of loadable segments; these segments will be loaded to memory
load_segments = sorted(elffile.iter_segments(type='PT_LOAD'), key=lambda s: s['p_vaddr'])
# determine the memory regions that need to be mapped in order to load the segments.
# note that region boundaries are aligned to page, which means they may be larger than
# the segment they contain. to reduce mapping clutter, adjacent regions with the same
# perms are consolidated into one contigous memory region
load_regions: Sequence[Tuple[int, int, int]] = []
# iterate over loadable segments
for seg in load_segments:
lbound = self.ql.mem.align(load_address + seg['p_vaddr'])
ubound = self.ql.mem.align_up(load_address + seg['p_vaddr'] + seg['p_memsz'])
perms = QlLoaderELF.seg_perm_to_uc_prot(seg['p_flags'])
if load_regions:
prev_lbound, prev_ubound, prev_perms = load_regions[-1]
# new region starts where the previous one ended
if lbound == prev_ubound:
# same perms? extend previous memory region
if perms == prev_perms:
load_regions[-1] = (prev_lbound, ubound, prev_perms)
# different perms? start a new one
else:
load_regions.append((lbound, ubound, perms))
# start a new memory region
elif lbound > prev_ubound:
load_regions.append((lbound, ubound, perms))
# overlapping segments? something probably went wrong
elif lbound < prev_ubound:
# EDL ELF files use 0x400 bytes pages, which might make some segments look as if they
# start at the same segment as their predecessor. though that is fixable, unicorn
# supports only 0x1000 bytes pages; this becomes problematic when using mem.protect
#
# this workaround unifies such "overlapping" segments, which may apply more permissive
# protection flags to that memory region.
if self.ql.arch.type == QL_ARCH.ARM64:
load_regions[-1] = (prev_lbound, ubound, prev_perms | perms)
continue
raise RuntimeError
else:
load_regions.append((lbound, ubound, perms))
# map the memory regions
for lbound, ubound, perms in load_regions:
size = ubound - lbound
# there might be a region with zero size. in this case, do not mmap it
if size:
try:
self.ql.mem.map(lbound, size, perms, os.path.basename(info))
except QlMemoryMappedError:
self.ql.log.exception(f'Failed to map {lbound:#x}-{ubound:#x}')
else:
self.ql.log.debug(f'Mapped {lbound:#x}-{ubound:#x}')
# load loadable segments contents to memory
for seg in load_segments:
self.ql.mem.write(load_address + seg['p_vaddr'], seg.data())
return load_regions[0][0], load_regions[-1][1]
mem_start, mem_end = load_elf_segments(elffile, load_address, self.path)
self.elf_entry = entry_point = load_address + elffile['e_entry']
self.ql.log.debug(f'mem_start : {mem_start:#x}')
self.ql.log.debug(f'mem_end : {mem_end:#x}')
# by convention the loaded binary is first on the list
self.images.append(Image(mem_start, mem_end, os.path.abspath(self.path)))
# note: 0x2000 is the size of [hook_mem]
self.brk_address = mem_end + 0x2000
# determine interpreter path
interp_seg = next(elffile.iter_segments(type='PT_INTERP'), None)
interp_path = str(interp_seg.get_interp_name()) if interp_seg else ''
interp_address = 0
# load the interpreter, if there is one
if interp_path:
interp_vpath = self.ql.os.path.virtual_abspath(interp_path)
interp_hpath = self.ql.os.path.virtual_to_host_path(interp_path)
self.ql.log.debug(f'Interpreter path: {interp_vpath}')
if not self.ql.os.path.is_safe_host_path(interp_hpath):
raise PermissionError(f'unsafe path: {interp_hpath}')
with open(interp_hpath, 'rb') as infile:
interp = ELFFile(infile)
min_vaddr = min(seg['p_vaddr'] for seg in interp.iter_segments(type='PT_LOAD'))
# determine interpreter base address
# some old interpreters may not be PIE: p_vaddr of the first LOAD segment is not zero
# we should load interpreter at the address p_vaddr specified in such situation
interp_address = self.profile.getint('interp_address') if min_vaddr == 0 else 0
self.ql.log.debug(f'Interpreter addr: {interp_address:#x}')
# load interpreter segments data to memory
interp_start, interp_end = load_elf_segments(interp, interp_address, interp_vpath)
# add interpreter to the loaded images list
self.images.append(Image(interp_start, interp_end, interp_hpath))
# determine entry point
entry_point = interp_address + interp['e_entry']
# set mmap addr
mmap_address = self.profile.getint('mmap_address')
self.ql.log.debug(f'mmap_address is : {mmap_address:#x}')
# set info to be used by gdb
self.mmap_address = mmap_address
# set elf table
elf_table = bytearray()
new_stack = stack_addr
def __push_str(top: int, s: str) -> int:
"""Write a string to stack memory and adjust the top of stack accordingly.
Top of stack remains aligned to pointer size
"""
data = s.encode('latin') + b'\x00'
top = self.ql.mem.align(top - len(data), self.ql.arch.pointersize)
self.ql.mem.write(top, data)
return top
# write argc
elf_table.extend(self.ql.pack(len(argv)))
# write argv
for s in argv:
new_stack = __push_str(new_stack, s)
elf_table.extend(self.ql.pack(new_stack))
# add a nullptr sentinel
elf_table.extend(self.ql.pack(0))
# write env
for k, v in env.items():
new_stack = __push_str(new_stack, f'{k}={v}')
elf_table.extend(self.ql.pack(new_stack))
# add a nullptr sentinel
elf_table.extend(self.ql.pack(0))
new_stack = randstraddr = __push_str(new_stack, 'a' * 16)
new_stack = cpustraddr = __push_str(new_stack, 'i686')
new_stack = execfn = __push_str(new_stack, argv[0])
# store aux vector data for gdb use
elf_phdr = elffile['e_phoff'] + mem_start
elf_phent = elffile['e_phentsize']
elf_phnum = elffile['e_phnum']
if self.ql.arch.bits == 64:
elf_hwcap = 0x078bfbfd
elif self.ql.arch.bits == 32:
elf_hwcap = 0x1fb8d7
if self.ql.arch.endian == QL_ENDIAN.EB:
# FIXME: considering this is a 32 bits value, it is not a big-endian version of the
# value above like it is meant to be, since the one above has an implied leading zero
# byte (i.e. 0x001fb8d7) which the EB value didn't take into account
elf_hwcap = 0xd7b81f
# setup aux vector
auxv_entries = (
(AUXV.AT_HWCAP, elf_hwcap),
(AUXV.AT_PAGESZ, self.ql.mem.pagesize),
(AUXV.AT_CLKTCK, 100),
(AUXV.AT_PHDR, elf_phdr),
(AUXV.AT_PHENT, elf_phent),
(AUXV.AT_PHNUM, elf_phnum),
(AUXV.AT_BASE, interp_address),
(AUXV.AT_FLAGS, 0),
(AUXV.AT_ENTRY, self.elf_entry),
(AUXV.AT_UID, self.ql.os.uid),
(AUXV.AT_EUID, self.ql.os.euid),
(AUXV.AT_GID, self.ql.os.gid),
(AUXV.AT_EGID, self.ql.os.egid),
(AUXV.AT_SECURE, 0),
(AUXV.AT_RANDOM, randstraddr),
(AUXV.AT_HWCAP2, 0),
(AUXV.AT_EXECFN, execfn),
(AUXV.AT_PLATFORM, cpustraddr),
(AUXV.AT_NULL, 0)
)
bytes_before_auxv = len(elf_table)
# add all auxv entries
for key, val in auxv_entries:
elf_table.extend(self.ql.pack(key))
elf_table.extend(self.ql.pack(val))
new_stack = self.ql.mem.align(new_stack - len(elf_table), 0x10)
self.ql.mem.write(new_stack, bytes(elf_table))
self.auxv = new_stack + bytes_before_auxv
self.stack_address = new_stack
self.load_address = load_address
self.init_sp = self.ql.arch.regs.arch_sp
self.ql.os.entry_point = self.entry_point = entry_point
self.ql.os.elf_entry = self.elf_entry
self.ql.os.function_hook = FunctionHook(self.ql, elf_phdr, elf_phnum, elf_phent, load_address, mem_end)
# If there is a loader, we ignore exit
self.skip_exit_check = (self.elf_entry != self.entry_point)
# map vsyscall section for some specific needs
if self.ql.arch.type == QL_ARCH.X8664 and self.ql.os.type == QL_OS.LINUX:
vsyscall_addr = self.profile.getint('vsyscall_address')
vsyscall_ids = (
SYSCALL_NR.gettimeofday,
SYSCALL_NR.time,
SYSCALL_NR.getcpu
)
# each syscall should be 1KiB away
entry_size = 1024
vsyscall_size = self.ql.mem.align_up(len(vsyscall_ids) * entry_size)
if self.ql.mem.is_available(vsyscall_addr, vsyscall_size):
# initialize with int3 instructions then insert syscall entry
self.ql.mem.map(vsyscall_addr, vsyscall_size, info="[vsyscall]")
assembler = self.ql.arch.assembler
def __assemble(asm: str) -> bytes:
bs, _ = assembler.asm(asm)
return bytes(bs)
for i, scid in enumerate(vsyscall_ids):
entry = __assemble(f'mov rax, {scid:#x}; syscall; ret')
self.ql.mem.write(vsyscall_addr + i * entry_size, entry.ljust(entry_size, b'\xcc'))
def lkm_get_init(self, elffile: ELFFile) -> int:
"""Get file offset of the init_module function.
"""
symbol_tables = (sec for sec in elffile.iter_sections() if type(sec) is SymbolTableSection)
for sec in symbol_tables:
syms = sec.get_symbol_by_name('init_module')
if syms:
sym = syms[0]
addr = sym['st_value'] + elffile.get_section(sym['st_shndx'])['sh_offset']
return addr
raise QlErrorELFFormat('invalid module: symbol init_module not found')
def lkm_dynlinker(self, elffile: ELFFile, mem_start: int) -> Mapping[str, int]:
def __get_symbol(name: str) -> Optional[Symbol]:
_symtab = elffile.get_section_by_name('.symtab')
_sym = _symtab.get_symbol_by_name(name)
return _sym[0] if _sym else None
ql = self.ql
all_symbols = []
self.ql.os.hook_addr = API_HOOK_MEM
# map address to symbol name
self.import_symbols = {}
# reverse dictionary to map symbol name -> address
rev_reloc_symbols = {}
rh = RelocationHandler(elffile)
sections = [sec for sec in elffile.iter_sections() if sec['sh_flags'] & SH_FLAGS.SHF_ALLOC]
for sec in sections:
reloc_sec = rh.find_relocations_for_section(sec)
if reloc_sec and reloc_sec.name != '.rela.gnu.linkonce.this_module':
# get the symbol table section pointed in sh_link
symtab = elffile.get_section(reloc_sec['sh_link'])
assert isinstance(symtab, SymbolTableSection)
for rel in reloc_sec.iter_relocations():
# if reloc['r_info_sym'] == 0:
# continue
symbol = symtab.get_symbol(rel['r_info_sym'])
assert symbol
# Some symbols have zero 'st_name', so instead what's used is
# the name of the section they point at.
if symbol['st_name'] == 0:
symsec = elffile.get_section(symbol['st_shndx'])
symbol_name = symsec.name
sym_offset = symsec['sh_offset']
rev_reloc_symbols[symbol_name] = sym_offset + mem_start
else:
symbol_name = symbol.name
# get info about related section to be patched
info_section = elffile.get_section(reloc_sec['sh_info'])
sym_offset = info_section['sh_offset']
if symbol_name in all_symbols:
sym_offset = rev_reloc_symbols[symbol_name] - mem_start
else:
all_symbols.append(symbol_name)
_symbol = __get_symbol(symbol_name)
if _symbol['st_shndx'] == 'SHN_UNDEF':
# external symbol
# only save symbols of APIs
# we need to lookup from address to symbol, so we can find the right callback
# for sys_xxx handler for syscall, the address must be aligned to pointer size
if symbol_name.startswith('sys_'):
self.ql.os.hook_addr = self.ql.mem.align_up(self.ql.os.hook_addr, self.ql.arch.pointersize)
self.import_symbols[self.ql.os.hook_addr] = symbol_name
# FIXME: this is for rootkit to scan for syscall table from page_offset_base
# write address of syscall table to this slot, so syscall scanner can quickly find it
if symbol_name == "page_offset_base":
ql.mem.write_ptr(self.ql.os.hook_addr, SYSCALL_MEM)
# we also need to do reverse lookup from symbol to address
rev_reloc_symbols[symbol_name] = self.ql.os.hook_addr
sym_offset = self.ql.os.hook_addr - mem_start
self.ql.os.hook_addr += self.ql.arch.pointersize
elif _symbol['st_shndx'] == 'SHN_ABS':
rev_reloc_symbols[symbol_name] = _symbol['st_value']
else:
# local symbol
_section = elffile.get_section(_symbol['st_shndx'])
rev_reloc_symbols[symbol_name] = _section['sh_offset'] + _symbol['st_value'] + mem_start
# ql.log.info(f'relocating: {symbol_name} -> {rev_reloc_symbols[symbol_name]:#010x}')
# FIXME: using the rh.apply_section_relocations method for the following relocation work
# seems to be cleaner.
loc = elffile.get_section(reloc_sec['sh_info'])['sh_offset'] + rel['r_offset']
loc += mem_start
desc = describe_reloc_type(rel['r_info_type'], elffile)
if desc in ('R_X86_64_32S', 'R_X86_64_32'):
# patch this reloc
if rel['r_addend']:
val = sym_offset + rel['r_addend']
val += mem_start
else:
val = rev_reloc_symbols[symbol_name]
ql.mem.write_ptr(loc, (val & 0xFFFFFFFF), 4)
elif desc == 'R_X86_64_64':
val = sym_offset + rel['r_addend']
val += 0x2000000 # init_module position: FIXME
ql.mem.write_ptr(loc, val, 8)
elif desc == 'R_X86_64_PC64':
val = rel['r_addend'] - loc
val += rev_reloc_symbols[symbol_name]
ql.mem.write_ptr(loc, val, 8)
elif desc in ('R_X86_64_PC32', 'R_X86_64_PLT32'):
val = rel['r_addend'] - loc
val += rev_reloc_symbols[symbol_name]
ql.mem.write_ptr(loc, (val & 0xFFFFFFFF), 4)
elif desc in ('R_386_PC32', 'R_386_PLT32'):
val = ql.mem.read_ptr(loc, 4)
val += rev_reloc_symbols[symbol_name] - loc
ql.mem.write_ptr(loc, (val & 0xFFFFFFFF), 4)
elif desc in ('R_386_32', 'R_MIPS_32'):
val = ql.mem.read_ptr(loc, 4)
val += rev_reloc_symbols[symbol_name]
ql.mem.write_ptr(loc, (val & 0xFFFFFFFF), 4)
elif desc == 'R_MIPS_HI16':
# actual relocation is done in R_MIPS_LO16
prev_mips_hi16_loc = loc
elif desc == 'R_MIPS_LO16':
val = ql.mem.read_ptr(prev_mips_hi16_loc + 2, 2) << 16 | ql.mem.read_ptr(loc + 2, 2)
val = rev_reloc_symbols[symbol_name] + val
# *(word)(mips_lo16_loc + 2) is treated as signed
if (val & 0xFFFF) >= 0x8000:
val += (1 << 16)
ql.mem.write_ptr(prev_mips_hi16_loc + 2, (val >> 16), 2)
ql.mem.write_ptr(loc + 2, (val & 0xFFFF), 2)
else:
raise NotImplementedError(f'Relocation type {desc} not implemented')
return rev_reloc_symbols
def load_driver(self, elffile: ELFFile, stack_addr: int, loadbase: int = 0) -> None:
elfdata_mapping = self.get_elfdata_mapping(elffile)
# FIXME: determine true memory boundaries, taking relocation into account (if requested)
mem_start = 0
mem_end = mem_start + self.ql.mem.align_up(len(elfdata_mapping), 0x1000)
# map some memory to intercept external functions of Linux kernel
self.ql.mem.map(API_HOOK_MEM, 0x1000, info="[api_mem]")
self.ql.log.debug(f'loadbase : {loadbase:#x}')
self.ql.log.debug(f'mem_start : {mem_start:#x}')
self.ql.log.debug(f'mem_end : {mem_end:#x}')
self.ql.mem.map(loadbase + mem_start, mem_end - mem_start, info=self.ql.path)
self.ql.mem.write(loadbase + mem_start, elfdata_mapping)
init_module = self.lkm_get_init(elffile) + loadbase + mem_start
self.ql.log.debug(f'init_module : {init_module:#x}')
self.brk_address = mem_end + loadbase
# Set MMAP addr
mmap_address = self.profile.getint('mmap_address')
self.ql.log.debug(f'mmap_address is : {mmap_address:#x}')
# self.ql.os.elf_entry = self.elf_entry = loadbase + elfhead['e_entry']
self.ql.os.entry_point = self.entry_point = init_module
self.elf_entry = self.ql.os.elf_entry = self.ql.os.entry_point
self.stack_address = self.ql.mem.align(stack_addr, self.ql.arch.pointersize)
self.load_address = loadbase
# remember address of syscall table, so external tools can access to it
# self.ql.os.syscall_addr = SYSCALL_MEM
# setup syscall table
self.ql.mem.map(SYSCALL_MEM, 0x1000, info="[syscall_mem]")
self.ql.mem.write(SYSCALL_MEM, b'\x00' * 0x1000)
rev_reloc_symbols = self.lkm_dynlinker(elffile, mem_start + loadbase)
# iterate over relocatable symbols, but pick only those who start with 'sys_'
for sc, addr in rev_reloc_symbols.items():
if sc.startswith('sys_') and sc != 'sys_call_table':
tmp_sc = sc[4:]
if hasattr(SYSCALL_NR, tmp_sc):
syscall_id = getattr(SYSCALL_NR, tmp_sc).value
dest = SYSCALL_MEM + syscall_id * self.ql.arch.pointersize
self.ql.log.debug(f'Writing syscall {tmp_sc} to {dest:#x}')
self.ql.mem.write_ptr(dest, addr)
# write syscall addresses into syscall table
self.ql.mem.write_ptr(SYSCALL_MEM + 0 * self.ql.arch.pointersize, self.ql.os.hook_addr + 0 * self.ql.arch.pointersize)
self.ql.mem.write_ptr(SYSCALL_MEM + 1 * self.ql.arch.pointersize, self.ql.os.hook_addr + 1 * self.ql.arch.pointersize)
self.ql.mem.write_ptr(SYSCALL_MEM + 2 * self.ql.arch.pointersize, self.ql.os.hook_addr + 2 * self.ql.arch.pointersize)
# setup hooks for read/write/open syscalls
self.import_symbols[self.ql.os.hook_addr + 0 * self.ql.arch.pointersize] = hook_sys_read
self.import_symbols[self.ql.os.hook_addr + 1 * self.ql.arch.pointersize] = hook_sys_write
self.import_symbols[self.ql.os.hook_addr + 2 * self.ql.arch.pointersize] = hook_sys_open
def get_elfdata_mapping(self, elffile: ELFFile) -> bytes:
# from io import BytesIO
#
# rh = RelocationHandler(elffile)
#
# for sec in elffile.iter_sections():
# rs = rh.find_relocations_for_section(sec)
#
# if rs is not None:
# ss = BytesIO(sec.data())
# rh.apply_section_relocations(ss, rs)
#
# # apply changes to stream
# elffile.stream.seek(sec['sh_offset'])
# elffile.stream.write(ss.getbuffer())
#
# TODO: need to patch hooked symbols with their hook targets
# (e.g. replace calls to 'printk' with the hooked address that
# was allocate for it)
elfdata_mapping = bytearray()
# pick up elf header
with preserve_stream_pos(elffile.stream):
elffile.stream.seek(0)
elf_header = elffile.stream.read(elffile['e_ehsize'])
elfdata_mapping.extend(elf_header)
# FIXME: normally the address of a section would be determined by its 'sh_addr' value.
# in case of a relocatable object all its sections' sh_addr will be set to zero, so
# the value in 'sh_offset' should be used to determine the final address.
# see: https://refspecs.linuxbase.org/elf/gabi4+/ch4.sheader.html
#
# here we presume this a relocatable object and don't do any relocation (that is, it
# is relocated to 0)
# pick up loadable sections
for sec in elffile.iter_sections():
if sec['sh_flags'] & SH_FLAGS.SHF_ALLOC:
# pad aggregated elf data to the offset of the current section
elfdata_mapping.extend(b'\x00' * (sec['sh_offset'] - len(elfdata_mapping)))
# aggregate section data
elfdata_mapping.extend(sec.data())
return bytes(elfdata_mapping)