Adding New Target Support
Patcherex2 has been designed with extensibility in mind, making it easy to add support for new targets. This document will walk you through the process of defining a new target in Patcherex2.
Defining a New Target
The first step is to define a new target class that inherits from the Target
base class. This class should specify the required components to support the target. Here's an example of the existing elf_amd64_linux
target definition.
from ..components.allocation_managers.allocation_manager import AllocationManager
from ..components.archinfo.amd64 import Amd64Info
from ..components.assemblers.keystone import Keystone, keystone
from ..components.binary_analyzers.angr import Angr
from ..components.binary_analyzers.ghidra import Ghidra
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 ElfAmd64Linux(Target):
@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"\x3e\x00", 0x12
): # EM_X86_64
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_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)
elif compiler == "clang19":
return Clang(self.p, clang_version=19)
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_64,
)
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, **kwargs):
binary_analyzer = binary_analyzer or "angr"
if binary_analyzer == "angr":
return Angr(self.binary_path, **kwargs)
if binary_analyzer == "ghidra":
return Ghidra(self.binary_path, **kwargs)
raise NotImplementedError()
def get_utils(self, utils):
utils = utils or "default"
if utils == "default":
return Utils(self.p, self.binary_path)
raise NotImplementedError()
def get_archinfo(self, archinfo):
archinfo = archinfo or "default"
if archinfo == "default":
return Amd64Info()
raise NotImplementedError()
detect_target
Method
The detect_target
static method is responsible for automatically detecting if a given binary is supported by this target. It should return True
if the binary matches the target criteria, or False
otherwise. In the example above, it checks if the file is an ELF binary for the AMD64 architecture.
get_{component}
Methods
The target definition should define methods to get the required components. The method names should be in the format get_{component}
. The following are the list of components that must be defined for a target:
- assembler
- disassembler
- compiler
- binary_analyzer (Extract extra information from the binary file)
- allocation_manager (Find free space or allocate new space in the binary)
- binfmt_tool (Parse and modify binary formats, such as ELF, PE, IHEX, etc.)
- utils
- archinfo (Architecture specific information, such as register names, sizes, etc.)
These methods allow you to specify the appropriate implementation for each component based on the target's requirements. Patcherex2 provides multiple implementations for common components that you can choose from.
Adding New Components
If your target requires custom components not provided by Patcherex2, you can define new component classes that inherit from the respective base component classes. These custom components should implement the necessary methods to support your target's specific needs.
Registering the New Target
Once you have defined your target class, Patcherex2 will automatically register it if it is defined before creating a Patcherex2 instance (p = Patcherex("/path/to/bin")
). Patcherex2 will call the detect_target
method of each registered target to determine the appropriate target for the given binary.
Manually Selecting the Target
If your target is designed for manual selection only (i.e., detect_target
always returns False
), or if you want to override the automatic target detection, you can specify the target class when creating the Patcherex2 instance:
p = Patcherex("/path/to/binary", target_cls=MyCustomTarget)
Configuring the Target
Selecting Component Implementations
Some targets may support multiple implementations for a given component, allowing you to choose the desired implementation. You can configure the target by passing a configuration dictionary to the Patcherex2 constructor.
For example, if your target's get_assembler
method supports multiple assemblers:
def get_assembler(self, assembler):
assembler = assembler or "keystone"
if assembler == "keystone":
return Keystone()
elif assembler == "gas":
return Gas()
raise NotImplementedError()
You can select the assembler like this:
p = Patcherex("/path/to/binary", target_opts={"assembler": "gas"})
This will use the Gas
assembler instead of the default Keystone
assembler.
Configuring Components
Some component implementations accept additional keyword arguments for configuration. You can pass these options through the component_opts
parameter when creating the Patcherex2 instance.
For example, if your target's get_assembler
method accepts keyword arguments:
def get_assembler(self, assembler, **kwargs):
assembler = assembler or "some_assembler"
if assembler == "some_assembler":
return SomeAssembler(**kwargs)
raise NotImplementedError()
You can configure the assembler options like this:
p = Patcherex("/path/to/binary", component_opts={"assembler": {"arch": "x86", "mode": "64"}})
This will create the SomeAssembler
instance with the provided keyword arguments:
SomeAssembler(arch="x86", mode="64")
By following these steps and leveraging the extensible architecture of Patcherex2, you can easily add support for new targets and customize their behavior to suit your specific requirements.