diff --git a/src/patcherex2/components/utils/utils.py b/src/patcherex2/components/utils/utils.py index 467a457..837d1b3 100644 --- a/src/patcherex2/components/utils/utils.py +++ b/src/patcherex2/components/utils/utils.py @@ -15,12 +15,20 @@ def insert_trampoline_code(self, addr, instrs, force_insert=False, detour_pos=-1 assert force_insert or self.is_valid_insert_point( addr ), f"Cannot insert instruction at {hex(addr)}" + moved_instrs = self.get_instrs_to_be_moved(addr) + moved_instrs_len = len( + self.p.assembler.assemble( + moved_instrs, + addr, # TODO: we don't really need this addr, but better than 0x0 because 0x0 is too far away from the code + is_thumb=self.p.binary_analyzer.is_thumb(addr), + ) + ) trempoline_instrs_with_jump_back = ( instrs + "\n" - + self.get_instrs_to_be_moved(addr) + + moved_instrs + "\n" - + self.p.target.JMP_ASM.format(dst=hex(addr + self.p.target.JMP_SIZE)) + + self.p.target.JMP_ASM.format(dst=hex(addr + moved_instrs_len)) ) trempoline_size = ( len( diff --git a/src/patcherex2/targets/__init__.py b/src/patcherex2/targets/__init__.py index 25fd2b1..649fc73 100644 --- a/src/patcherex2/targets/__init__.py +++ b/src/patcherex2/targets/__init__.py @@ -1,6 +1,7 @@ from .elf_aarch64_linux import ElfAArch64Linux from .elf_arm_linux import ElfArmLinux from .elf_arm_mimxrt1052 import ElfArmMimxrt1052 +from .elf_i386_linux import ElfI386Linux from .elf_leon3_bare import ElfLeon3Bare from .elf_x86_64_linux import ElfX8664Linux from .ihex_ppc_bare import IHexPPCBare @@ -10,6 +11,7 @@ "ElfAArch64Linux", "ElfArmLinux", "ElfArmMimxrt1052", + "ElfI386Linux", "ElfLeon3Bare", "ElfX8664Linux", "IHexPPCBare", diff --git a/src/patcherex2/targets/elf_i386_linux.py b/src/patcherex2/targets/elf_i386_linux.py new file mode 100644 index 0000000..7497fbd --- /dev/null +++ b/src/patcherex2/targets/elf_i386_linux.py @@ -0,0 +1,74 @@ +from ..components.allocation_managers.allocation_manager import AllocationManager +from ..components.assemblers.keystone import Keystone, keystone +from ..components.binary_analyzers.angr import Angr +from ..components.binfmt_tools.elf import ELF +from ..components.compilers.clang import Clang +from ..components.disassemblers.capstone import Capstone, capstone +from ..components.utils.utils import Utils +from .target import Target + + +class ElfI386Linux(Target): + NOP_BYTES = b"\x90" + NOP_SIZE = 1 + JMP_ASM = "jmp {dst}" + JMP_SIZE = 5 + + @staticmethod + def detect_target(binary_path): + with open(binary_path, "rb") as f: + magic = f.read(0x14) + if magic.startswith(b"\x7fELF") and magic.startswith( + b"\x03\x00", 0x12 + ): # EM_386 + return True + return False + + def get_assembler(self, assembler): + assembler = assembler or "keystone" + if assembler == "keystone": + return Keystone( + self.p, + keystone.KS_ARCH_X86, + keystone.KS_MODE_LITTLE_ENDIAN + keystone.KS_MODE_32, + ) + raise NotImplementedError() + + def get_allocation_manager(self, allocation_manager): + allocation_manager = allocation_manager or "default" + if allocation_manager == "default": + return AllocationManager(self.p) + raise NotImplementedError() + + def get_compiler(self, compiler): + compiler = compiler or "clang" + if compiler == "clang": + return Clang(self.p, compiler_flags=["-m32"]) + raise NotImplementedError() + + def get_disassembler(self, disassembler): + disassembler = disassembler or "capstone" + if disassembler == "capstone": + return Capstone( + capstone.CS_ARCH_X86, + capstone.CS_MODE_LITTLE_ENDIAN + capstone.CS_MODE_32, + ) + raise NotImplementedError() + + def get_binfmt_tool(self, binfmt_tool): + binfmt_tool = binfmt_tool or "pyelftools" + if binfmt_tool == "pyelftools": + return ELF(self.p, self.binary_path) + raise NotImplementedError() + + def get_binary_analyzer(self, binary_analyzer): + binary_analyzer = binary_analyzer or "angr" + if binary_analyzer == "angr": + return Angr(self.binary_path) + raise NotImplementedError() + + def get_utils(self, utils): + utils = utils or "default" + if utils == "default": + return Utils(self.p, self.binary_path) + raise NotImplementedError() diff --git a/tests/test_binaries/i386/printf_nopie b/tests/test_binaries/i386/printf_nopie new file mode 100755 index 0000000..0b9567c Binary files /dev/null and b/tests/test_binaries/i386/printf_nopie differ diff --git a/tests/test_binaries/i386/replace_function_patch b/tests/test_binaries/i386/replace_function_patch new file mode 100755 index 0000000..c7d26b6 Binary files /dev/null and b/tests/test_binaries/i386/replace_function_patch differ diff --git a/tests/test_i386.py b/tests/test_i386.py new file mode 100644 index 0000000..0eeb559 --- /dev/null +++ b/tests/test_i386.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python + +# ruff: noqa +import logging +import os +import shutil +import subprocess +import tempfile +import unittest +import pytest + +from patcherex2 import * + +logging.getLogger("patcherex2").setLevel("DEBUG") + + +class Tests(unittest.TestCase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bin_location = str( + os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "./test_binaries/i386", + ) + ) + + def test_raw_file_patch(self): + self.run_one( + "printf_nopie", + [ModifyRawBytesPatch(0x2008, b"No", addr_type="raw")], + expected_output=b"No", + expected_returnCode=0, + ) + + def test_raw_mem_patch(self): + self.run_one( + "printf_nopie", + [ModifyRawBytesPatch(0x804A008, b"No")], + expected_output=b"No", + expected_returnCode=0, + ) + + def test_modify_instruction_patch(self): + self.run_one( + "printf_nopie", + [ + ModifyInstructionPatch(0x8049192, "lea edx, [0x804a00b]"), + ModifyInstructionPatch(0x8049198, "push edx"), + ], + expected_output=b"%s", + expected_returnCode=0, + ) + + def test_insert_instruction_patch(self): + instrs = """ + mov eax, 0x4 + mov ebx, 0x1 + lea ecx, [0x804a008] + mov edx, 0x3 + int 0x80 + """ + self.run_one( + "printf_nopie", + [InsertInstructionPatch(0x80491A7, instrs)], + expected_output=b"Hi\x00Hi", + expected_returnCode=0, + ) + + def test_insert_instruction_patch_2(self): + instrs = """ + mov eax, 0x32 + leave + ret + """ + self.run_one( + "printf_nopie", + [ + InsertInstructionPatch("return_0x32", instrs), + ModifyInstructionPatch(0x80491A7, "jmp {return_0x32}"), + ], + expected_returnCode=0x32, + ) + + def test_remove_instruction_patch(self): + self.run_one( + "printf_nopie", + [ + RemoveInstructionPatch(0x804A009, num_bytes=1), + ], + expected_output=b"H\x90", + expected_returnCode=0, + ) + + def test_modify_data_patch(self): + self.run_one( + "printf_nopie", + [ModifyDataPatch(0x804A008, b"No")], + expected_output=b"No", + expected_returnCode=0, + ) + + def test_insert_data_patch(self, tlen=5): + p1 = InsertDataPatch("added_data", b"A" * tlen) + instrs = """ + mov eax, 0x4 + mov ebx, 0x1 + lea ecx, [{added_data}] + mov edx, %s + int 0x80 + """ % hex(tlen) + p2 = InsertInstructionPatch(0x80491A7, instrs) + self.run_one( + "printf_nopie", + [p1, p2], + expected_output=b"A" * tlen + b"Hi", + expected_returnCode=0, + ) + + def test_remove_data_patch(self): + self.run_one( + "printf_nopie", + [RemoveDataPatch(0x804A009, 1)], + expected_output=b"H", + expected_returnCode=0, + ) + + def test_replace_function_patch(self): + code = """ + int add(int a, int b){ for(;; b--, a+=2) if(b <= 0) return a; } + """ + self.run_one( + "replace_function_patch", + [ModifyFunctionPatch(0x119D, code)], + expected_output=b"70707070", + expected_returnCode=0, + ) + + @pytest.mark.skip(reason="waiting for cle relocation support") + def test_replace_function_patch_with_function_reference(self): + code = """ + extern int add(int, int); + extern int subtract(int, int); + int multiply(int a, int b){ for(int c = 0;; b = subtract(b, 1), c = subtract(c, a)) if(b <= 0) return c; } + """ + self.run_one( + "replace_function_patch", + [ModifyFunctionPatch(0x11C9, code)], + expected_output=b"-21-21", + expected_returnCode=0, + ) + + def run_one( + self, + filename, + patches, + set_oep=None, + inputvalue=None, + expected_output=None, + expected_returnCode=None, + ): + filepath = os.path.join(self.bin_location, filename) + pipe = subprocess.PIPE + + with tempfile.TemporaryDirectory() as td: + tmp_file = os.path.join(td, "patched") + p = Patcherex(filepath) + for patch in patches: + p.patches.append(patch) + p.apply_patches() + p.binfmt_tool.save_binary(tmp_file) + # os.system(f"readelf -hlS {tmp_file}") + + p = subprocess.Popen( + [tmp_file], + stdin=pipe, + stdout=pipe, + stderr=pipe, + ) + res = p.communicate(inputvalue) + if expected_output: + if res[0] != expected_output: + self.fail( + f"AssertionError: {res[0]} != {expected_output}, binary dumped: {self.dump_file(tmp_file)}" + ) + # self.assertEqual(res[0], expected_output) + if expected_returnCode: + if p.returncode != expected_returnCode: + self.fail( + f"AssertionError: {p.returncode} != {expected_returnCode}, binary dumped: {self.dump_file(tmp_file)}" + ) + # self.assertEqual(p.returncode, expected_returnCode) + + def dump_file(self, file): + shutil.copy(file, "/tmp/patcherex_failed_binary") + return "/tmp/patcherex_failed_binary" + + +if __name__ == "__main__": + unittest.main()