diff --git a/README.md b/README.md index 328d235..7e4050f 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,13 @@ Coming soon. | | Linux x86 | Linux amd64 | Linux arm | Linux aarch64 | Linux PowerPC (32bit) | Linux PowerPC (64bit) | Linux MIPS (32bit) | Linux MIPS (64bit) | SPARCv8 (LEON3) | PowerPC (VLE) (IHEX) |-|-|-|-|-|-|-|-|-|-|-| -InsertDataPatch | 🟩 | 🟩 | 🟩 | 🟩 | 🟪 | 🟪 | 🟩 | 🟩 | ⬜ | ⬜ | 🟩 | 🟩 | -RemoveDataPatch | 🟩 | 🟩 | 🟩 | 🟩 | 🟪 | 🟪 | 🟩 | 🟩 | ⬜ | ⬜ | 🟩 | 🟩 | -ModifyDataPatch | 🟩 | 🟩 | 🟩 | 🟩 | 🟪 | 🟪 | 🟩 | 🟩 | ⬜ | ⬜ | 🟩 | 🟩 | -InsertInstructionPatch | 🟩 | 🟩 | 🟩 | 🟩 | 🟪 | 🟪 | 🟩 | 🟩 | ⬜ | ⬜ | 🟩 | 🟩 | -RemoveInstructionPatch | 🟩 | 🟩 | 🟩 | 🟩 | 🟪 | 🟪 | 🟩 | 🟩 | ⬜ | ⬜ | 🟩 | 🟩 | -ModifyInstructionPatch | 🟩 | 🟩 | 🟩 | 🟩 | 🟪 | 🟪 | 🟩 | 🟩 | ⬜ | ⬜ | 🟩 | 🟩 | -InsertFunctionPatch | ⬜ | ⬜ | ⬜ | ⬜ | 🟪 | 🟪 | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | -ModifyFunctionPatch | 🟨 | 🟩 | 🟩 | 🟩 | 🟪 | 🟪 | 🟨 | 🟨 | ⬜ | ⬜ | 🟩 | 🟩 | +InsertDataPatch | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟪 | 🟩 | 🟩 | ⬜ | ⬜ | 🟩 | 🟩 | +RemoveDataPatch | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟪 | 🟩 | 🟩 | ⬜ | ⬜ | 🟩 | 🟩 | +ModifyDataPatch | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟪 | 🟩 | 🟩 | ⬜ | ⬜ | 🟩 | 🟩 | +InsertInstructionPatch | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟪 | 🟩 | 🟩 | ⬜ | ⬜ | 🟩 | 🟩 | +RemoveInstructionPatch | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟪 | 🟩 | 🟩 | ⬜ | ⬜ | 🟩 | 🟩 | +ModifyInstructionPatch | 🟩 | 🟩 | 🟩 | 🟩 | 🟩 | 🟪 | 🟩 | 🟩 | ⬜ | ⬜ | 🟩 | 🟩 | +InsertFunctionPatch | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | 🟪 | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | +ModifyFunctionPatch | 🟨 | 🟩 | 🟩 | 🟩 | 🟨 | 🟪 | 🟨 | 🟨 | ⬜ | ⬜ | 🟩 | 🟩 | 🟩 Fully Functional, 🟨 Limited Functionality, 🟥 Not Working, ⬜ Not Tested, 🟪 Work in Progress diff --git a/src/patcherex2/targets/__init__.py b/src/patcherex2/targets/__init__.py index a270bff..3ec45db 100644 --- a/src/patcherex2/targets/__init__.py +++ b/src/patcherex2/targets/__init__.py @@ -6,6 +6,7 @@ from .elf_leon3_bare import ElfLeon3Bare from .elf_mips64_linux import ElfMips64Linux from .elf_mips_linux import ElfMipsLinux +from .elf_ppc_linux import ElfPpcLinux from .elf_x86_64_linux import ElfX8664Linux from .elf_x86_64_linux_recomp import ElfX8664LinuxRecomp from .ihex_ppc_bare import IHexPPCBare @@ -20,6 +21,7 @@ "ElfLeon3Bare", "ElfMips64Linux", "ElfMipsLinux", + "ElfPpcLinux", "ElfX8664Linux", "ElfX8664LinuxRecomp", "IHexPPCBare", diff --git a/src/patcherex2/targets/elf_ppc_linux.py b/src/patcherex2/targets/elf_ppc_linux.py new file mode 100644 index 0000000..1a9db27 --- /dev/null +++ b/src/patcherex2/targets/elf_ppc_linux.py @@ -0,0 +1,76 @@ +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 ElfPpcLinux(Target): + NOP_BYTES = b"\x60\x00\x00\x00" + NOP_SIZE = 4 + JMP_ASM = "b {dst}" + JMP_SIZE = 4 + + @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"\x00\x14", 0x12 + ): # EM_PPC + return True + return False + + def get_assembler(self, assembler): + assembler = assembler or "keystone" + if assembler == "keystone": + return Keystone( + self.p, + keystone.KS_ARCH_PPC, + keystone.KS_MODE_BIG_ENDIAN + keystone.KS_MODE_PPC32, + ) + 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=["-target", "powerpc-linux-gnu"]) + raise NotImplementedError() + + def get_disassembler(self, disassembler): + disassembler = disassembler or "capstone" + if disassembler == "capstone": + cs = Capstone( + capstone.CS_ARCH_PPC, capstone.CS_MODE_BIG_ENDIAN + capstone.CS_MODE_32 + ) + # NOTE: Doing this because keystone expects registers to just be numbers + cs.cs.syntax = capstone.CS_OPT_SYNTAX_NOREGNAME + return cs + 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/ppc/printf_nopie b/tests/test_binaries/ppc/printf_nopie new file mode 100755 index 0000000..9b9698e Binary files /dev/null and b/tests/test_binaries/ppc/printf_nopie differ diff --git a/tests/test_binaries/ppc/replace_function_patch b/tests/test_binaries/ppc/replace_function_patch new file mode 100755 index 0000000..10a0fff Binary files /dev/null and b/tests/test_binaries/ppc/replace_function_patch differ diff --git a/tests/test_ppc.py b/tests/test_ppc.py new file mode 100644 index 0000000..69d8772 --- /dev/null +++ b/tests/test_ppc.py @@ -0,0 +1,213 @@ +#!/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/ppc", + ) + ) + + def test_raw_file_patch(self): + self.run_one( + "printf_nopie", + [ModifyRawBytesPatch(0x6A4, b"No", addr_type="raw")], + expected_output=b"No", + expected_returnCode=0, + ) + + def test_raw_mem_patch(self): + self.run_one( + "printf_nopie", + [ModifyRawBytesPatch(0x100006A4, b"No")], + expected_output=b"No", + expected_returnCode=0, + ) + + def test_modify_instruction_patch(self): + self.run_one( + "printf_nopie", + [ + ModifyInstructionPatch(0x10000514, "addi 4, 9, 0x6ac"), + ], + expected_output=b"%s", + expected_returnCode=0, + ) + + def test_insert_instruction_patch(self): + instrs = """ + li 0, 0x4 + li 3, 1 + lis 9, 0x1000 + addi 4, 9, 0x6a4 + li 5, 0x3 + sc + """ + self.run_one( + "printf_nopie", + [InsertInstructionPatch(0x10000528, instrs)], + expected_output=b"Hi\x00Hi", + expected_returnCode=0, + ) + + def test_insert_instruction_patch_2(self): + instrs = """ + li 3, 0x32 + li 0, 0x1 + sc + """ + self.run_one( + "printf_nopie", + [ + InsertInstructionPatch("return_0x32", instrs), + ModifyInstructionPatch(0x10000528, "b {return_0x32}"), + ], + expected_returnCode=0x32, + ) + + def test_remove_instruction_patch(self): + self.run_one( + "printf_nopie", + [ + RemoveInstructionPatch(0x100006A5, num_bytes=4), + ], + expected_output=b"H\x60", + expected_returnCode=0, + ) + + def test_modify_data_patch(self): + self.run_one( + "printf_nopie", + [ModifyDataPatch(0x100006A4, 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 = """ + li 0, 0x4 + li 3, 0x1 + lis 9, {added_data}@h + addi 4, 9, {added_data}@l + li 5, %s + sc + """ % hex(tlen) + p2 = InsertInstructionPatch(0x10000528, 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(0x100006A5, 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(0x100004FC, 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(0x100005AC, code)], + expected_output=b"-21-21", + expected_returnCode=0, + ) + + @pytest.mark.skip(reason="waiting for cle relocation support") + def test_replace_function_patch_with_function_reference_and_rodata(self): + code = """ + extern int printf(const char *format, ...); + int multiply(int a, int b){ printf("%sWorld %s %s %s %d\\n", "Hello ", "Hello ", "Hello ", "Hello ", a * b);printf("%sWorld\\n", "Hello "); return a * b; } + """ + self.run_one( + "replace_function_patch", + [ModifyFunctionPatch(0x100005AC, code)], + expected_output=b"Hello World Hello Hello Hello 21\nHello World\n2121", + 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( + ["qemu-ppc", "-L", "/usr/powerpc-linux-gnu", 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()