From 04d58745ecc829d52624c44bc96f397a55abada2 Mon Sep 17 00:00:00 2001 From: Yan Date: Mon, 17 Aug 2026 05:34:20 +0000 Subject: [PATCH] PE: Build base relocations on every architecture the backend resolves ALL_RELOCATIONS was keyed on "arm", "mips" and "RISCV", which archinfo does not produce, and had no AARCH64 entry, so get_relocation returned None for every type on those architectures and the whole .reloc directory was discarded. Key the table on arch.name instead. That alone is not safe. PEReloc.value falls through to self.resolvedby.rebased_addr, which is None for a base relocation, so making the ARM table reachable turns every IMAGE_REL_BASED_THUMB_MOV32 into an AttributeError during loading. Implement that relocation, and let the base class skip a recognised but unimplemented type with a warning rather than raising. --- cle/backends/pe/relocation/__init__.py | 11 ++- cle/backends/pe/relocation/arm.py | 37 ++++++++++ cle/backends/pe/relocation/pereloc.py | 13 +++- tests/test_pe_relocations.py | 97 ++++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 tests/test_pe_relocations.py diff --git a/cle/backends/pe/relocation/__init__.py b/cle/backends/pe/relocation/__init__.py index f80a361e9..96ed3928c 100644 --- a/cle/backends/pe/relocation/__init__.py +++ b/cle/backends/pe/relocation/__init__.py @@ -7,12 +7,17 @@ from .mips import relocation_table_mips from .riscv import relocation_table_riscv +# Keyed on archinfo's arch.name, which is what PE._make_reloc looks these up with. ALL_RELOCATIONS = { + "AARCH64": relocation_table_generic, "AMD64": relocation_table_generic, - "arm": relocation_table_generic | relocation_table_arm, + "ARMCortexM": relocation_table_generic | relocation_table_arm, + "ARMEL": relocation_table_generic | relocation_table_arm, + "ARMHF": relocation_table_generic | relocation_table_arm, + "MIPS32": relocation_table_generic | relocation_table_mips, + "PPC32": relocation_table_generic, + "RISCV64": relocation_table_generic | relocation_table_riscv, "X86": relocation_table_generic, - "mips": relocation_table_generic | relocation_table_mips, - "RISCV": relocation_table_generic | relocation_table_riscv, } log = logging.getLogger(name=__name__) diff --git a/cle/backends/pe/relocation/arm.py b/cle/backends/pe/relocation/arm.py index 500ae0c7f..5be4f9a14 100644 --- a/cle/backends/pe/relocation/arm.py +++ b/cle/backends/pe/relocation/arm.py @@ -1,5 +1,9 @@ from __future__ import annotations +import struct + +from cle.address_translator import AT + from .pereloc import PEReloc @@ -8,8 +12,41 @@ class IMAGE_REL_BASED_ARM_MOV32(PEReloc): class IMAGE_REL_BASED_THUMB_MOV32(PEReloc): + """ + The fixup site is a MOVW.W/MOVT.W pair that materialises a 32-bit address in a + register, one halfword per instruction. + """ + __slots__ = () + @property + def value(self): + org_bytes = self.owner.memory.load(self.relative_addr, 8) + movw, movt = struct.unpack("> 16) & 0xFFFF), + ) + + +def _thumb_mov_imm16(instr): + """ + Read the imm16 encoded across the two halfwords of a T32 MOVW or MOVT instruction, + which is held as imm4:i:imm3:imm8. + """ + first, second = instr & 0xFFFF, instr >> 16 + return ((first & 0xF) << 12) | (((first >> 10) & 1) << 11) | (((second >> 12) & 0x7) << 8) | (second & 0xFF) + + +def _thumb_mov_set_imm16(instr, imm16): + first, second = instr & 0xFFFF, instr >> 16 + first = (first & ~0x040F) | (((imm16 >> 11) & 1) << 10) | ((imm16 >> 12) & 0xF) + second = (second & ~0x70FF) | (((imm16 >> 8) & 0x7) << 12) | (imm16 & 0xFF) + return (second << 16) | first + relocation_table_arm = { 5: IMAGE_REL_BASED_ARM_MOV32, diff --git a/cle/backends/pe/relocation/pereloc.py b/cle/backends/pe/relocation/pereloc.py index dd0ee0265..e3776713d 100644 --- a/cle/backends/pe/relocation/pereloc.py +++ b/cle/backends/pe/relocation/pereloc.py @@ -5,6 +5,7 @@ from cle.backends.relocation import Relocation log = logging.getLogger(name=__name__) +unimplemented_log = set() # Reference: https://msdn.microsoft.com/en-us/library/ms809762.aspx @@ -43,7 +44,7 @@ def relocate(self): if self.symbol is None: # relocation described in the DIRECTORY_ENTRY_BASERELOC table value = self.value if value is None: - log.debug("Unresolved relocation with no symbol.") + log.debug("%s at %#x was not applied", type(self).__name__, self.relative_addr) return self.owner.memory.store(self.relative_addr, value) else: @@ -51,8 +52,18 @@ def relocate(self): @property def value(self): + if self.symbol is None: + # A base relocation rewrites the bytes at the fixup site, which only a subclass that + # knows the encoding can do. Reaching here means the type is recognized but has no + # implementation, so the fixup stays as the linker wrote it. + name = type(self).__name__ + if name not in unimplemented_log: + unimplemented_log.add(name) + log.warning("%s is not implemented, so base relocations of this type are not applied", name) + return None if self.resolved: return self.resolvedby.rebased_addr + return None @property def is_base_reloc(self): diff --git a/tests/test_pe_relocations.py b/tests/test_pe_relocations.py new file mode 100644 index 000000000..99a121a65 --- /dev/null +++ b/tests/test_pe_relocations.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +from __future__ import annotations + +import os +import struct +import unittest + +import cle +from cle.backends.pe.relocation.arm import IMAGE_REL_BASED_THUMB_MOV32 +from cle.backends.pe.relocation.generic import IMAGE_REL_BASED_DIR64, IMAGE_REL_BASED_HIGHLOW + +TEST_BASE = os.path.join(os.path.dirname(os.path.realpath(__file__)), os.path.join("..", "..", "binaries")) + +ARM64_PE = os.path.join(TEST_BASE, "tests", "aarch64", "windows", "pe_reloc_arm64.exe") +ARMNT_PE = os.path.join(TEST_BASE, "tests", "armel", "windows", "pe_reloc_armnt.exe") + +REBASE_DELTA = 0x10000 + + +def load(path, base_addr=None): + main_opts = {"backend": "pe"} + if base_addr is not None: + main_opts["base_addr"] = base_addr + return cle.Loader(path, auto_load_libs=False, main_opts=main_opts).main_object + + +def thumb_mov32_immediate(data): + """ + Read the 32-bit address a T32 MOVW/MOVT pair materialises. Each instruction carries one + halfword of it, encoded as imm4:i:imm3:imm8 across the instruction's two halfwords. + """ + halfwords = struct.unpack("> 10) & 1) << 11) | (((second >> 12) & 0x7) << 8) | (second & 0xFF) + ) + return (parts[1] << 16) | parts[0] + + +# pylint: disable=no-self-use +class TestPEBaseRelocations(unittest.TestCase): + """ + Base relocations of PE images built for architectures other than x86 and x86-64. + """ + + def test_aarch64(self): + obj = load(ARM64_PE) + assert obj.arch.name == "AARCH64" + relocs = [r for r in obj.relocs if isinstance(r, IMAGE_REL_BASED_DIR64)] + assert sorted(r.relative_addr for r in relocs) == [0x2000, 0x2008, 0x2010] + assert obj.memory.unpack_word(0x2000, size=8) == 0x1400011B8 + + def test_aarch64_rebased(self): + obj = load(ARM64_PE) + moved = load(ARM64_PE, obj.linked_base + REBASE_DELTA) + relocs = [r for r in obj.relocs if isinstance(r, IMAGE_REL_BASED_DIR64)] + assert relocs + for reloc in relocs: + linked = obj.memory.unpack_word(reloc.relative_addr, size=8) + rebased = moved.memory.unpack_word(reloc.relative_addr, size=8) + assert rebased == linked + REBASE_DELTA + + def test_armnt(self): + obj = load(ARMNT_PE) + assert obj.arch.name == "ARMEL" + highlow = [r for r in obj.relocs if isinstance(r, IMAGE_REL_BASED_HIGHLOW)] + thumb_mov32 = [r for r in obj.relocs if isinstance(r, IMAGE_REL_BASED_THUMB_MOV32)] + assert sorted(r.relative_addr for r in highlow) == [0x2000, 0x2004, 0x2008] + assert sorted(r.relative_addr for r in thumb_mov32) == [0x104C, 0x1130] + # the MOVW/MOVT pair at 0x104c materialises the address of the function pointer table + assert thumb_mov32_immediate(obj.memory.load(0x104C, 8)) == 0x402000 + + def test_armnt_rebased(self): + obj = load(ARMNT_PE) + moved = load(ARMNT_PE, obj.linked_base + REBASE_DELTA) + highlow = [r for r in obj.relocs if isinstance(r, IMAGE_REL_BASED_HIGHLOW)] + thumb_mov32 = [r for r in obj.relocs if isinstance(r, IMAGE_REL_BASED_THUMB_MOV32)] + assert highlow and thumb_mov32 + for reloc in highlow: + linked = obj.memory.unpack_word(reloc.relative_addr, size=4) + rebased = moved.memory.unpack_word(reloc.relative_addr, size=4) + assert rebased == linked + REBASE_DELTA + for reloc in thumb_mov32: + linked = thumb_mov32_immediate(obj.memory.load(reloc.relative_addr, 8)) + rebased = thumb_mov32_immediate(moved.memory.load(reloc.relative_addr, 8)) + assert rebased == linked + REBASE_DELTA + + def test_x86_64_unchanged(self): + exe = os.path.join(TEST_BASE, "tests", "x86_64", "windows", "sioctl.sys") + obj = load(exe) + assert obj.arch.name == "AMD64" + assert [r for r in obj.relocs if isinstance(r, IMAGE_REL_BASED_DIR64)] + + +if __name__ == "__main__": + unittest.main()