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()