diff --git a/src/patcherex2/targets/__init__.py b/src/patcherex2/targets/__init__.py index 8a36771..6c256ec 100644 --- a/src/patcherex2/targets/__init__.py +++ b/src/patcherex2/targets/__init__.py @@ -5,6 +5,7 @@ from .elf_i386_linux import ElfI386Linux from .elf_leon3_bare import ElfLeon3Bare from .elf_mips64_linux import ElfMips64Linux +from .elf_mips64el_linux import ElfMips64elLinux from .elf_mips_linux import ElfMipsLinux from .elf_mipsel_linux import ElfMipselLinux from .elf_ppc64_linux import ElfPpc64Linux @@ -22,6 +23,7 @@ "ElfI386Linux", "ElfLeon3Bare", "ElfMips64Linux", + "ElfMips64elLinux", "ElfMipsLinux", "ElfMipselLinux", "ElfPpc64Linux", diff --git a/src/patcherex2/targets/elf_mips64el_linux.py b/src/patcherex2/targets/elf_mips64el_linux.py new file mode 100644 index 0000000..4a493d3 --- /dev/null +++ b/src/patcherex2/targets/elf_mips64el_linux.py @@ -0,0 +1,75 @@ +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 ElfMips64elLinux(Target): + NOP_BYTES = b"\x00\x00\x00\x00" + NOP_SIZE = 4 + JMP_ASM = "j {dst}" + # NOTE: keystone will always add nop for branch delay slot, so include it in size + JMP_SIZE = 8 + + @staticmethod + def detect_target(binary_path): + with open(binary_path, "rb") as f: + magic = f.read(0x14) + if magic.startswith(b"\x7fELF\x02") and magic.startswith( + b"\x08\x00", 0x12 + ): # EM_MIPS + return True + return False + + def get_assembler(self, assembler): + assembler = assembler or "keystone" + if assembler == "keystone": + return Keystone( + self.p, + keystone.KS_ARCH_MIPS, + keystone.KS_MODE_LITTLE_ENDIAN + keystone.KS_MODE_64, + ) + 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=mips64el-linux-gnuabi64"]) + raise NotImplementedError() + + def get_disassembler(self, disassembler): + disassembler = disassembler or "capstone" + if disassembler == "capstone": + return Capstone( + capstone.CS_ARCH_MIPS, + capstone.CS_MODE_LITTLE_ENDIAN + capstone.CS_MODE_MIPS64, + ) + 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/mips64el/printf_nopie b/tests/test_binaries/mips64el/printf_nopie new file mode 100755 index 0000000..3a3bd81 Binary files /dev/null and b/tests/test_binaries/mips64el/printf_nopie differ diff --git a/tests/test_binaries/mips64el/replace_function_patch b/tests/test_binaries/mips64el/replace_function_patch new file mode 100755 index 0000000..4d4dae0 Binary files /dev/null and b/tests/test_binaries/mips64el/replace_function_patch differ diff --git a/tests/test_mips64el.py b/tests/test_mips64el.py new file mode 100644 index 0000000..c11c721 --- /dev/null +++ b/tests/test_mips64el.py @@ -0,0 +1,231 @@ +#!/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/mips64el", + ) + ) + + def test_raw_file_patch(self): + self.run_one( + "printf_nopie", + [ModifyRawBytesPatch(0xBD3, b"No", addr_type="raw")], + expected_output=b"No", + expected_returnCode=0, + ) + + def test_raw_mem_patch(self): + self.run_one( + "printf_nopie", + [ModifyRawBytesPatch(0x120000BD3, b"No")], + expected_output=b"No", + expected_returnCode=0, + ) + + def test_modify_instruction_patch(self): + self.run_one( + "printf_nopie", + [ + ModifyInstructionPatch(0x120000AB0, "daddiu $a1, $at, 0xbd0"), + ], + expected_output=b"%s", + expected_returnCode=0, + ) + + def test_insert_instruction_patch(self): + instrs = """ + li $v0, 0x1389 + li $a0, 0x1 + ld $at, -0x7fc0($gp) + daddiu $a1, $at, 0xbd3 + li $a2, 0x3 + syscall + """ + self.run_one( + "printf_nopie", + [InsertInstructionPatch(0x120000AC0, instrs)], + expected_output=b"Hi\x00Hi", + expected_returnCode=0, + ) + + def test_insert_instruction_patch_2(self): + instrs = """ + li $v0, 0x13c2 + li $a0, 0x32 + syscall + """ + self.run_one( + "printf_nopie", + [ + InsertInstructionPatch("return_0x32", instrs), + ModifyInstructionPatch(0x120000AC0, "j {return_0x32}"), + ], + expected_returnCode=0x32, + ) + + def test_remove_instruction_patch(self): + self.run_one( + "printf_nopie", + [ + RemoveInstructionPatch(0x120000BD4, num_bytes=4), + ], + expected_output=b"H", + expected_returnCode=0, + ) + + def test_modify_data_patch(self): + self.run_one( + "printf_nopie", + [ModifyDataPatch(0x120000BD3, 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 $v0, 0x1389 + li $a0, 0x1 + dla $a1, {added_data} + li $a2, %s + syscall + """ % hex(tlen) + p2 = InsertInstructionPatch(0x120000AC0, 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(0x120000BD4, 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(0xC00, 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(0xD00, 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(0xD00, code)], + expected_output=b"Hello World Hello Hello Hello 21\nHello World\n2121", + expected_returnCode=0, + ) + + @pytest.mark.skip(reason="waiting for cle relocation support") + def test_insert_function_patch(self): + insert_code = """ + int min(int a, int b) { return (a < b) ? a : b; } + """ + replace_code = """ + extern int min(int, int); + int max(int a, int b) { return min(a, b); } + """ + self.run_one( + "replace_function_patch", + [ + InsertFunctionPatch("min", insert_code), + ModifyFunctionPatch(0xE88, replace_code), + ], + expected_output=b"2121212121", + 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-mips64el", "-L", "/usr/mips64el-linux-gnuabi64", 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()