Skip to content

Patcherex

InsertDataPatch

Bases: Patch

Patch that inserts data into the binary.

Source code in src/patcherex2/patches/data_patches.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class InsertDataPatch(Patch):
    """
    Patch that inserts data into the binary.
    """

    def __init__(self, addr_or_name: int | str, data: bytes) -> None:
        """
        Constructor.

        :param addr_or_name: If an integer, data is inserted at the address.
                             If a string, it is placed in a free spot in the binary and added as a symbol (with this as its name).
        :param data: New data to place in binary.
        """
        self.addr = None
        self.name = None
        if isinstance(addr_or_name, int):
            self.addr = addr_or_name
        elif isinstance(addr_or_name, str):
            self.name = addr_or_name
        self.data = data

    def apply(self, p: Patcherex) -> None:
        """
        Applies the patch to the binary, intended to be called by a Patcherex instance.

        :param p: Patcherex instance.
        """
        if self.addr:
            p.binfmt_tool.update_binary_content(self.addr, self.data)
        elif self.name:
            block = p.allocation_manager.allocate(
                len(self.data), flag=MemoryFlag.RWX
            )  # FIXME: why RW not work?
            p.symbols[self.name] = block.mem_addr
            p.binfmt_tool.update_binary_content(block.file_addr, self.data)

__init__(addr_or_name, data)

Constructor.

Parameters:

Name Type Description Default
addr_or_name int | str

If an integer, data is inserted at the address. If a string, it is placed in a free spot in the binary and added as a symbol (with this as its name).

required
data bytes

New data to place in binary.

required
Source code in src/patcherex2/patches/data_patches.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def __init__(self, addr_or_name: int | str, data: bytes) -> None:
    """
    Constructor.

    :param addr_or_name: If an integer, data is inserted at the address.
                         If a string, it is placed in a free spot in the binary and added as a symbol (with this as its name).
    :param data: New data to place in binary.
    """
    self.addr = None
    self.name = None
    if isinstance(addr_or_name, int):
        self.addr = addr_or_name
    elif isinstance(addr_or_name, str):
        self.name = addr_or_name
    self.data = data

apply(p)

Applies the patch to the binary, intended to be called by a Patcherex instance.

Parameters:

Name Type Description Default
p Patcherex

Patcherex instance.

required
Source code in src/patcherex2/patches/data_patches.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def apply(self, p: Patcherex) -> None:
    """
    Applies the patch to the binary, intended to be called by a Patcherex instance.

    :param p: Patcherex instance.
    """
    if self.addr:
        p.binfmt_tool.update_binary_content(self.addr, self.data)
    elif self.name:
        block = p.allocation_manager.allocate(
            len(self.data), flag=MemoryFlag.RWX
        )  # FIXME: why RW not work?
        p.symbols[self.name] = block.mem_addr
        p.binfmt_tool.update_binary_content(block.file_addr, self.data)

InsertFunctionPatch

Bases: Patch

Inserts a function into the binary.

Source code in src/patcherex2/patches/function_patches.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
class InsertFunctionPatch(Patch):
    """
    Inserts a function into the binary.
    """

    def __init__(
        self,
        addr_or_name: int | str,
        code: str,
        force_insert=False,
        detour_pos=-1,
        symbols: dict[str, int] | None = None,
        is_thumb=False,
        **kwargs,
    ) -> None:
        """
        Constructor.

        :param addr_or_name: If an integer, an intermediate function is created in a free spot in the binary,
                             and at that address, a jump to the function is made with necessary context saves.
                             If a string, the function is created in a free spot in the binary with that name.
        :param code: C code for the new function. "SAVE_CONTEXT" and "RESTORE_CONTEXT" can be used to save and restore context.
        :param force_insert: If Patcherex should ignore whether instructions can be moved when inserting, defaults to False
        :param detour_pos: If address is used, this is the address to place trampoline code for jumping to function.
                           If name is used, this is where the new function will be placed, defaults to -1
        :param symbols: Symbols to include when compiling/assembling, in format {symbol name: memory address}, defaults to None
        :param is_thumb: Whether the instructions given are thumb, defaults to False
        :param kwargs: Extra options. Can include "prefunc" and "postfunc", instructions to go before or after your function if you give an address.
                         Can also have "save_context" for whether context should be saved and "compile_opts" for extra compile options.
        """
        self.addr = None
        self.name = None
        if isinstance(addr_or_name, int):
            self.addr = addr_or_name
        elif isinstance(addr_or_name, str):
            self.name = addr_or_name
        self.code = code
        self.detour_pos = detour_pos
        self.symbols = symbols if symbols else {}
        self.is_thumb = is_thumb
        self.force_insert = force_insert
        self.prefunc = kwargs["prefunc"] if "prefunc" in kwargs else None
        self.postfunc = kwargs["postfunc"] if "postfunc" in kwargs else None
        self.compile_opts = kwargs["compile_opts"] if "compile_opts" in kwargs else {}
        self.save_context = (
            kwargs["save_context"] if "save_context" in kwargs else False
        )

    def apply(self, p: Patcherex) -> None:
        """
        Applies the patch to the binary, intended to be called by a Patcherex instance.

        :param p: Patcherex instance.
        """
        if self.addr:
            if self.prefunc:
                if "SAVE_CONTEXT" in self.prefunc:
                    self.prefunc = self.prefunc.replace(
                        "SAVE_CONTEXT", f"\n{p.archinfo.save_context_asm}\n"
                    )
                if "RESTORE_CONTEXT" in self.prefunc:
                    self.prefunc = self.prefunc.replace(
                        "RESTORE_CONTEXT", f"\n{p.archinfo.restore_context_asm}\n"
                    )
            if self.postfunc:
                if "SAVE_CONTEXT" in self.postfunc:
                    self.postfunc = self.postfunc.replace(
                        "SAVE_CONTEXT", f"\n{p.archinfo.save_context_asm}\n"
                    )
                if "RESTORE_CONTEXT" in self.postfunc:
                    self.postfunc = self.postfunc.replace(
                        "RESTORE_CONTEXT", f"\n{p.archinfo.restore_context_asm}\n"
                    )
            ifp = InsertFunctionPatch(
                f"__patcherex_{hex(self.addr)}",
                self.code,
                is_thumb=p.binary_analyzer.is_thumb(self.addr),
                symbols=self.symbols,
            )
            ifp.apply(p)
            instrs = ""
            instrs += p.archinfo.save_context_asm if self.save_context else ""
            instrs += self.prefunc if self.prefunc else ""
            instrs += "\n"
            # NOTE: ↓ this is hardcoded to bl, not blx, but it should be fine for this use case
            instrs += p.archinfo.call_asm.format(
                dst=f"{{__patcherex_{hex(self.addr)}}}"
            )
            instrs += "\n"
            instrs += self.postfunc if self.postfunc else ""
            instrs += p.archinfo.restore_context_asm if self.save_context else ""
            p.utils.insert_trampoline_code(
                self.addr,
                instrs,
                force_insert=self.force_insert,
                detour_pos=self.detour_pos,
                symbols=self.symbols,
            )
        elif self.name:
            compiled_size = len(
                p.compiler.compile(
                    self.code,
                    symbols=self.symbols,
                    is_thumb=self.is_thumb,
                    **self.compile_opts,
                )
            )
            if self.detour_pos == -1:
                block = p.allocation_manager.allocate(
                    compiled_size + 0x20, align=p.archinfo.alignment, flag=MemoryFlag.RX
                )  # TODO: adjust that 0x20 part
                mem_addr = block.mem_addr
                file_addr = block.file_addr
            else:
                mem_addr = self.detour_pos
                file_addr = p.binary_analyzer.mem_addr_to_file_offset(mem_addr)
            p.sypy_info["patcherex_added_functions"].append(hex(mem_addr))
            p.symbols[self.name] = mem_addr
            p.binfmt_tool.update_binary_content(
                file_addr,
                p.compiler.compile(
                    self.code,
                    mem_addr,
                    symbols=self.symbols,
                    is_thumb=self.is_thumb,
                    **self.compile_opts,
                ),
            )

__init__(addr_or_name, code, force_insert=False, detour_pos=-1, symbols=None, is_thumb=False, **kwargs)

Constructor.

Parameters:

Name Type Description Default
addr_or_name int | str

If an integer, an intermediate function is created in a free spot in the binary, and at that address, a jump to the function is made with necessary context saves. If a string, the function is created in a free spot in the binary with that name.

required
code str

C code for the new function. "SAVE_CONTEXT" and "RESTORE_CONTEXT" can be used to save and restore context.

required
force_insert

If Patcherex should ignore whether instructions can be moved when inserting, defaults to False

False
detour_pos

If address is used, this is the address to place trampoline code for jumping to function. If name is used, this is where the new function will be placed, defaults to -1

-1
symbols dict[str, int] | None

Symbols to include when compiling/assembling, in format {symbol name: memory address}, defaults to None

None
is_thumb

Whether the instructions given are thumb, defaults to False

False
kwargs

Extra options. Can include "prefunc" and "postfunc", instructions to go before or after your function if you give an address. Can also have "save_context" for whether context should be saved and "compile_opts" for extra compile options.

{}
Source code in src/patcherex2/patches/function_patches.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def __init__(
    self,
    addr_or_name: int | str,
    code: str,
    force_insert=False,
    detour_pos=-1,
    symbols: dict[str, int] | None = None,
    is_thumb=False,
    **kwargs,
) -> None:
    """
    Constructor.

    :param addr_or_name: If an integer, an intermediate function is created in a free spot in the binary,
                         and at that address, a jump to the function is made with necessary context saves.
                         If a string, the function is created in a free spot in the binary with that name.
    :param code: C code for the new function. "SAVE_CONTEXT" and "RESTORE_CONTEXT" can be used to save and restore context.
    :param force_insert: If Patcherex should ignore whether instructions can be moved when inserting, defaults to False
    :param detour_pos: If address is used, this is the address to place trampoline code for jumping to function.
                       If name is used, this is where the new function will be placed, defaults to -1
    :param symbols: Symbols to include when compiling/assembling, in format {symbol name: memory address}, defaults to None
    :param is_thumb: Whether the instructions given are thumb, defaults to False
    :param kwargs: Extra options. Can include "prefunc" and "postfunc", instructions to go before or after your function if you give an address.
                     Can also have "save_context" for whether context should be saved and "compile_opts" for extra compile options.
    """
    self.addr = None
    self.name = None
    if isinstance(addr_or_name, int):
        self.addr = addr_or_name
    elif isinstance(addr_or_name, str):
        self.name = addr_or_name
    self.code = code
    self.detour_pos = detour_pos
    self.symbols = symbols if symbols else {}
    self.is_thumb = is_thumb
    self.force_insert = force_insert
    self.prefunc = kwargs["prefunc"] if "prefunc" in kwargs else None
    self.postfunc = kwargs["postfunc"] if "postfunc" in kwargs else None
    self.compile_opts = kwargs["compile_opts"] if "compile_opts" in kwargs else {}
    self.save_context = (
        kwargs["save_context"] if "save_context" in kwargs else False
    )

apply(p)

Applies the patch to the binary, intended to be called by a Patcherex instance.

Parameters:

Name Type Description Default
p Patcherex

Patcherex instance.

required
Source code in src/patcherex2/patches/function_patches.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
def apply(self, p: Patcherex) -> None:
    """
    Applies the patch to the binary, intended to be called by a Patcherex instance.

    :param p: Patcherex instance.
    """
    if self.addr:
        if self.prefunc:
            if "SAVE_CONTEXT" in self.prefunc:
                self.prefunc = self.prefunc.replace(
                    "SAVE_CONTEXT", f"\n{p.archinfo.save_context_asm}\n"
                )
            if "RESTORE_CONTEXT" in self.prefunc:
                self.prefunc = self.prefunc.replace(
                    "RESTORE_CONTEXT", f"\n{p.archinfo.restore_context_asm}\n"
                )
        if self.postfunc:
            if "SAVE_CONTEXT" in self.postfunc:
                self.postfunc = self.postfunc.replace(
                    "SAVE_CONTEXT", f"\n{p.archinfo.save_context_asm}\n"
                )
            if "RESTORE_CONTEXT" in self.postfunc:
                self.postfunc = self.postfunc.replace(
                    "RESTORE_CONTEXT", f"\n{p.archinfo.restore_context_asm}\n"
                )
        ifp = InsertFunctionPatch(
            f"__patcherex_{hex(self.addr)}",
            self.code,
            is_thumb=p.binary_analyzer.is_thumb(self.addr),
            symbols=self.symbols,
        )
        ifp.apply(p)
        instrs = ""
        instrs += p.archinfo.save_context_asm if self.save_context else ""
        instrs += self.prefunc if self.prefunc else ""
        instrs += "\n"
        # NOTE: ↓ this is hardcoded to bl, not blx, but it should be fine for this use case
        instrs += p.archinfo.call_asm.format(
            dst=f"{{__patcherex_{hex(self.addr)}}}"
        )
        instrs += "\n"
        instrs += self.postfunc if self.postfunc else ""
        instrs += p.archinfo.restore_context_asm if self.save_context else ""
        p.utils.insert_trampoline_code(
            self.addr,
            instrs,
            force_insert=self.force_insert,
            detour_pos=self.detour_pos,
            symbols=self.symbols,
        )
    elif self.name:
        compiled_size = len(
            p.compiler.compile(
                self.code,
                symbols=self.symbols,
                is_thumb=self.is_thumb,
                **self.compile_opts,
            )
        )
        if self.detour_pos == -1:
            block = p.allocation_manager.allocate(
                compiled_size + 0x20, align=p.archinfo.alignment, flag=MemoryFlag.RX
            )  # TODO: adjust that 0x20 part
            mem_addr = block.mem_addr
            file_addr = block.file_addr
        else:
            mem_addr = self.detour_pos
            file_addr = p.binary_analyzer.mem_addr_to_file_offset(mem_addr)
        p.sypy_info["patcherex_added_functions"].append(hex(mem_addr))
        p.symbols[self.name] = mem_addr
        p.binfmt_tool.update_binary_content(
            file_addr,
            p.compiler.compile(
                self.code,
                mem_addr,
                symbols=self.symbols,
                is_thumb=self.is_thumb,
                **self.compile_opts,
            ),
        )

InsertInstructionPatch

Bases: Patch

Source code in src/patcherex2/patches/instruction_patches.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
class InsertInstructionPatch(Patch):
    class CConfig:
        def __init__(
            self,
            c_forward_header: str = "",
            scratch_regs: Iterable[str] = None,
            regs_sort: Iterable[str | tuple[str, int] | tuple[str, str]] | None = None,
            asm_header: str = "",
            asm_footer: str = "",
        ):
            """
            Used to configure an InsertInstructionPatch when language == "C"

            :param c_forward_header: C code that will be inserted before the micropatch code. This is useful when you want\
            to use C types, C headers, and C function forward declarations.
            :param scratch_regs: It is generally a good idea to mark some registers as scratch to give the compiler\
            breathing room for allocating registers to use for intermediate variables in your micropatch.\
            All of the registers that we mark as scratch can be freely clobbered by the compiler\
            Note that you can still read from scratch registers stored in the variables. What the scratch\
            register denotation will indicate however is that the register can be re-used after the variable\
            is no longer live.
            :param regs_sort: Some architectures have subregisters\
            which allow access to a portion of a register, such as the lower 32 bits of a 64 bit register. If you want\
            to use a subregister instead of the full register you can request this by passing in a list of subregisters\
            here. Note that if you specify a subregister here, the full parent register is not available in the C\
            patch. Some registers allow values of smaller types to be stored in larger registers. For example, you can\
            store float values in xmm0 on x64, but there is no 32 bit subregister. To indicate that a register\
            should be treated as a specific size, you may specify register size in bits in the second element of the\
            tuple. Alternatively, you may specify the type as a string. An example of a valid list for x64 is\
            ['eax', ('xmm0', 32), ('xmm1', 'double')].
            :param asm_header: The asm_header is inserted in the main body of the patch before the C code. This header is\
            primarily useful for gaining access to the stack pointer, which is a register that is typically unavailable in\
            our C patch code.
            :param asm_footer: The asm_footer is the same as the asm_header, except that it runs after the C code executes.\
            This is typically less useful than asm_header, but is still available here if you need to do any cleanup in\
            assembly.
            """
            self.c_forward_header = c_forward_header
            self.scratch_regs = scratch_regs
            self.regs_sort = regs_sort
            self.asm_header = asm_header
            self.asm_footer = asm_footer

    """
    Patch that allows instructions to be inserted into binary. These instructions are inserted at a free place in the binary.
    Then, At the address given, an instruction is inserted that jumps to this block (also in the block are the instructions this overwrites).
    At the end of the block, it jumps back to right after the initial jump. The initial jump must be able to be inserted within the basic block
    of the given address.
    """

    def __init__(
        self,
        addr_or_name: int | str,
        instr: str,
        force_insert=False,
        detour_pos=-1,
        symbols: dict[str, int] | None = None,
        is_thumb=False,
        language: str = "ASM",
        c_config: CConfig | None = None,
        **kwargs,
    ) -> None:
        """
        Constructor.

        :param addr_or_name: If an integer, the new instructions are placed in a free spot in the binary and the jump to them is inserted at that memory address.
                             If a string, the new instructions are placed in a free spot in the binary and added as a symbol (with this as its name).
        :param instr: Instructions to insert. You can use "SAVE_CONTEXT" and "RESTORE_CONTEXT" wherever you want to save and restore program context. If you want to use any symbols from the program or from previous patches, you must surround them with curly braces.
        :param force_insert: If Patcherex should ignore whether instructions can be moved when inserting, defaults to False
        :param detour_pos: If given a name, specifies the file address to place the new instructions, defaults to -1
        :param symbols: Symbols to include when assembling, in format {symbol name: memory address}, defaults to None
        :param is_thumb: Whether the instructions given are thumb, defaults to False
        :param language: The language of the patch, can be either "ASM" or "C"
        :param c_config: Configuration options for when language == "C"
        :param **kwargs: Extra options. Can have a boolean "save_context" for whether context should be saved.
        """
        self.addr = None
        self.name = None
        if isinstance(addr_or_name, int):
            self.addr = addr_or_name
        elif isinstance(addr_or_name, str):
            self.name = addr_or_name
        self.instr = instr
        self.force_insert = force_insert
        self.detour_pos = detour_pos
        self.symbols = symbols if symbols else {}
        self.is_thumb = is_thumb
        self.language = language.upper()
        self.c_config = self.CConfig() if c_config is None else c_config
        self.save_context = (
            kwargs["save_context"] if "save_context" in kwargs else False
        )
        self.compile_opts = kwargs["compile_opts"] if "compile_opts" in kwargs else {}

    def apply(self, p):
        """
        Applies the patch to the binary, intended to be called by a Patcherex instance.

        :param p: Patcherex instance.
        :type p: Patcherex
        """
        if self.language == "ASM":
            self._apply_asm(p)
        elif self.language == "C":
            self._apply_c(p)

    def _apply_c(self, p) -> None:
        if self.addr is None:
            raise ValueError("An address must be provided for a C instruction patch")

        c_forward_header = self.c_config.c_forward_header
        c_scratch_regs = (
            frozenset()
            if self.c_config.scratch_regs is None
            else frozenset(self.c_config.scratch_regs)
        )
        c_regs_sort = (
            [] if self.c_config.regs_sort is None else list(self.c_config.regs_sort)
        )

        # TODO: make it os agnostic?
        calling_convention = (
            p.archinfo.cc["LinuxPreserveNone"]
            if p.compiler.preserve_none
            else p.archinfo.cc["Linux"]
        )
        subregister_table = p.archinfo.subregisters
        subregister_float_table = p.archinfo.subregisters_float

        # Figure out if there are any extra registers that we need to expose to the user
        # that aren't part of the calling convention. For x64 preserve_none, this will be
        # registers r10 and rbx.
        extra_saved = set(p.archinfo.regs)
        # Note that we cannot control callee saved registers. If we attempt to define
        # some registers via 'register uint64_t rbx asm("rbx");', the compiler will insert
        # push and pop instructions to save these registers.
        extra_saved = (
            extra_saved
            - set(calling_convention)
            - set(p.archinfo.callee_saved["Linux"])
        )
        extra_saved_in = list(extra_saved)
        # We don't want to necessarily output registers that have been marked as scratch
        # However we always want to make them available as input
        extra_saved_out = list(extra_saved - c_scratch_regs)

        def uint_converter(size: int):
            return f"uint{size}_t"

        extra_saved_in_converted = convert_to_subregisters(
            extra_saved_in, subregister_table, c_regs_sort, uint_converter
        )
        extra_saved_out_converted = convert_to_subregisters(
            extra_saved_out, subregister_table, c_regs_sort, uint_converter
        )

        calling_convention_float: list[str] = p.archinfo.cc_float["Linux"]
        extra_saved_float = set(p.archinfo.regs_float)
        extra_saved_float = (
            extra_saved_float
            - set(calling_convention_float)
            - set(p.archinfo.callee_saved_float["Linux"])
        )
        extra_saved_float_in = list(extra_saved_float)
        extra_saved_float_out = list(extra_saved_float - c_scratch_regs)

        def float_converter(size: int):
            if size in p.archinfo.float_types:
                return p.archinfo.float_types[size]
            else:
                raise ValueError(
                    f"Unable to determine type of float with size of {size} bits"
                )

        extra_saved_float_in_converted = convert_to_subregisters(
            extra_saved_float_in, subregister_float_table, c_regs_sort, float_converter
        )
        extra_saved_float_out_converted = convert_to_subregisters(
            extra_saved_float_out, subregister_float_table, c_regs_sort, float_converter
        )

        attribute = "__attribute__((preserve_none))" if p.compiler.preserve_none else ""

        int_args = convert_to_subregisters(
            calling_convention, subregister_table, c_regs_sort, uint_converter
        )
        int_args_str = [f"{itype} {name}" for (name, itype) in int_args]
        float_args: list[tuple[str, str]] = convert_to_subregisters(
            calling_convention_float,
            subregister_float_table,
            c_regs_sort,
            float_converter,
        )
        float_args_str = [f"{ftype} {name}" for (name, ftype) in float_args]
        args_str = ", ".join(int_args_str + float_args_str)

        callback_forward_decl = f"extern void {attribute} _CALLBACK({args_str});"

        # Stupid macro tricks to make coding the patch a little bit nicer. This allows the user
        # to write 'return;' instead of having to understand how to call the callback
        # reg_name if reg_name in self.c_out_regs else '_dummy'
        return_macro_lines = [
            "#define return do {",
        ]

        for reg, _ in extra_saved_out_converted:
            # Make sure the variables are live just before the return statement
            return_macro_lines.append(f'    asm ("" : : "r"({reg}) :);')
        for reg, _ in extra_saved_float_out_converted:
            # Make sure the variables are live just before the return statement
            return_macro_lines.append(f'    asm ("" : : "r"({reg}) :);')

        callback_args = [
            "_dummy" if reg_name in c_scratch_regs else reg_name
            for (reg_name, bits) in int_args
        ]
        callback_args += [
            "_dummyFloat" if reg_name in c_scratch_regs else reg_name
            for (reg_name, ftype) in float_args
        ]
        return_macro_lines += [
            "    __attribute__((musttail)) return _CALLBACK({});".format(
                ", ".join(callback_args)
            ),
            "} while(0)",
        ]

        return_macro = "\\\n".join(return_macro_lines)

        lines = [
            "#include <stdint.h>",
            "",
            callback_forward_decl,
            "",
            c_forward_header,
            "",
            return_macro,
            "",
            f"void {attribute} _MICROPATCH({args_str}) {{",
            f"    uint{p.archinfo.bits}_t _dummy;",
            "    float _dummyFloat;",
        ]
        for reg, reg_type in extra_saved_in_converted:
            # Force the variables to live in a specific register using the register C extension
            lines.append(f'    register {reg_type} {reg} asm("{reg}");')
        for reg, ftype in extra_saved_float_in_converted:
            lines.append(f'    register {ftype} {reg} asm("{reg}");')
        for reg, _ in extra_saved_in_converted:
            # Trick the C compiler into thinking that the variables we just defined are actually live
            lines.append(f'    asm ("" : "=r"({reg}) : : );')
        for reg, _ in extra_saved_float_in_converted:
            lines.append(f'    asm ("" : "=r"({reg}) : : );')
        lines += [
            self.instr,
            # Make sure we actually do the callback in case the user forgets to put in a return
            "    return;",
            "}",
            "#undef return",
        ]
        code = "\n".join(lines)
        logger.info("InsertInstructionPatch generated C code:\n" + code)
        p.utils.insert_trampoline_code(
            self.addr,
            code,
            force_insert=self.force_insert,
            detour_pos=self.detour_pos,
            symbols=self.symbols,
            language="C",
            asm_header=self.c_config.asm_header,
            asm_footer=self.c_config.asm_footer,
        )

    def _apply_asm(self, p) -> None:
        if self.addr:
            if "SAVE_CONTEXT" in self.instr:
                self.instr = self.instr.replace(
                    "SAVE_CONTEXT", f"\n{p.archinfo.save_context_asm}\n"
                )
            if "RESTORE_CONTEXT" in self.instr:
                self.instr = self.instr.replace(
                    "RESTORE_CONTEXT", f"\n{p.archinfo.restore_context_asm}\n"
                )
            if self.save_context:
                self.instr = f"{p.archinfo.save_context_asm}\n{self.instr}\n{p.archinfo.restore_context_asm}"
            p.utils.insert_trampoline_code(
                self.addr,
                self.instr,
                force_insert=self.force_insert,
                detour_pos=self.detour_pos,
                symbols=self.symbols,
            )
        elif self.name:
            assembled_size = len(
                p.assembler.assemble(
                    self.instr, symbols=self.symbols, is_thumb=self.is_thumb
                )
            )
            if self.detour_pos == -1:
                block = p.allocation_manager.allocate(
                    assembled_size, align=p.archinfo.alignment, flag=MemoryFlag.RX
                )
                p.symbols[self.name] = block.mem_addr
                p.binfmt_tool.update_binary_content(
                    block.file_addr,
                    p.assembler.assemble(
                        self.instr,
                        block.mem_addr,
                        symbols=self.symbols,
                        is_thumb=self.is_thumb,
                    ),
                )
            else:
                p.symbols[self.name] = self.detour_pos
                p.binfmt_tool.update_binary_content(
                    self.detour_pos,
                    p.assembler.assemble(
                        self.instr,
                        self.detour_pos,
                        symbols=self.symbols,
                        is_thumb=self.is_thumb,
                    ),
                )

CConfig

Source code in src/patcherex2/patches/instruction_patches.py
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
class CConfig:
    def __init__(
        self,
        c_forward_header: str = "",
        scratch_regs: Iterable[str] = None,
        regs_sort: Iterable[str | tuple[str, int] | tuple[str, str]] | None = None,
        asm_header: str = "",
        asm_footer: str = "",
    ):
        """
        Used to configure an InsertInstructionPatch when language == "C"

        :param c_forward_header: C code that will be inserted before the micropatch code. This is useful when you want\
        to use C types, C headers, and C function forward declarations.
        :param scratch_regs: It is generally a good idea to mark some registers as scratch to give the compiler\
        breathing room for allocating registers to use for intermediate variables in your micropatch.\
        All of the registers that we mark as scratch can be freely clobbered by the compiler\
        Note that you can still read from scratch registers stored in the variables. What the scratch\
        register denotation will indicate however is that the register can be re-used after the variable\
        is no longer live.
        :param regs_sort: Some architectures have subregisters\
        which allow access to a portion of a register, such as the lower 32 bits of a 64 bit register. If you want\
        to use a subregister instead of the full register you can request this by passing in a list of subregisters\
        here. Note that if you specify a subregister here, the full parent register is not available in the C\
        patch. Some registers allow values of smaller types to be stored in larger registers. For example, you can\
        store float values in xmm0 on x64, but there is no 32 bit subregister. To indicate that a register\
        should be treated as a specific size, you may specify register size in bits in the second element of the\
        tuple. Alternatively, you may specify the type as a string. An example of a valid list for x64 is\
        ['eax', ('xmm0', 32), ('xmm1', 'double')].
        :param asm_header: The asm_header is inserted in the main body of the patch before the C code. This header is\
        primarily useful for gaining access to the stack pointer, which is a register that is typically unavailable in\
        our C patch code.
        :param asm_footer: The asm_footer is the same as the asm_header, except that it runs after the C code executes.\
        This is typically less useful than asm_header, but is still available here if you need to do any cleanup in\
        assembly.
        """
        self.c_forward_header = c_forward_header
        self.scratch_regs = scratch_regs
        self.regs_sort = regs_sort
        self.asm_header = asm_header
        self.asm_footer = asm_footer

__init__(c_forward_header='', scratch_regs=None, regs_sort=None, asm_header='', asm_footer='')

Used to configure an InsertInstructionPatch when language == "C"

Parameters:

Name Type Description Default
c_forward_header str

C code that will be inserted before the micropatch code. This is useful when you want to use C types, C headers, and C function forward declarations.

''
scratch_regs Iterable[str]

It is generally a good idea to mark some registers as scratch to give the compiler breathing room for allocating registers to use for intermediate variables in your micropatch. All of the registers that we mark as scratch can be freely clobbered by the compiler Note that you can still read from scratch registers stored in the variables. What the scratch register denotation will indicate however is that the register can be re-used after the variable is no longer live.

None
regs_sort Iterable[str | tuple[str, int] | tuple[str, str]] | None

Some architectures have subregisters which allow access to a portion of a register, such as the lower 32 bits of a 64 bit register. If you want to use a subregister instead of the full register you can request this by passing in a list of subregisters here. Note that if you specify a subregister here, the full parent register is not available in the C patch. Some registers allow values of smaller types to be stored in larger registers. For example, you can store float values in xmm0 on x64, but there is no 32 bit subregister. To indicate that a register should be treated as a specific size, you may specify register size in bits in the second element of the tuple. Alternatively, you may specify the type as a string. An example of a valid list for x64 is ['eax', ('xmm0', 32), ('xmm1', 'double')].

None
asm_header str

The asm_header is inserted in the main body of the patch before the C code. This header is primarily useful for gaining access to the stack pointer, which is a register that is typically unavailable in our C patch code.

''
asm_footer str

The asm_footer is the same as the asm_header, except that it runs after the C code executes. This is typically less useful than asm_header, but is still available here if you need to do any cleanup in assembly.

''
Source code in src/patcherex2/patches/instruction_patches.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def __init__(
    self,
    c_forward_header: str = "",
    scratch_regs: Iterable[str] = None,
    regs_sort: Iterable[str | tuple[str, int] | tuple[str, str]] | None = None,
    asm_header: str = "",
    asm_footer: str = "",
):
    """
    Used to configure an InsertInstructionPatch when language == "C"

    :param c_forward_header: C code that will be inserted before the micropatch code. This is useful when you want\
    to use C types, C headers, and C function forward declarations.
    :param scratch_regs: It is generally a good idea to mark some registers as scratch to give the compiler\
    breathing room for allocating registers to use for intermediate variables in your micropatch.\
    All of the registers that we mark as scratch can be freely clobbered by the compiler\
    Note that you can still read from scratch registers stored in the variables. What the scratch\
    register denotation will indicate however is that the register can be re-used after the variable\
    is no longer live.
    :param regs_sort: Some architectures have subregisters\
    which allow access to a portion of a register, such as the lower 32 bits of a 64 bit register. If you want\
    to use a subregister instead of the full register you can request this by passing in a list of subregisters\
    here. Note that if you specify a subregister here, the full parent register is not available in the C\
    patch. Some registers allow values of smaller types to be stored in larger registers. For example, you can\
    store float values in xmm0 on x64, but there is no 32 bit subregister. To indicate that a register\
    should be treated as a specific size, you may specify register size in bits in the second element of the\
    tuple. Alternatively, you may specify the type as a string. An example of a valid list for x64 is\
    ['eax', ('xmm0', 32), ('xmm1', 'double')].
    :param asm_header: The asm_header is inserted in the main body of the patch before the C code. This header is\
    primarily useful for gaining access to the stack pointer, which is a register that is typically unavailable in\
    our C patch code.
    :param asm_footer: The asm_footer is the same as the asm_header, except that it runs after the C code executes.\
    This is typically less useful than asm_header, but is still available here if you need to do any cleanup in\
    assembly.
    """
    self.c_forward_header = c_forward_header
    self.scratch_regs = scratch_regs
    self.regs_sort = regs_sort
    self.asm_header = asm_header
    self.asm_footer = asm_footer

__init__(addr_or_name, instr, force_insert=False, detour_pos=-1, symbols=None, is_thumb=False, language='ASM', c_config=None, **kwargs)

Constructor.

Parameters:

Name Type Description Default
addr_or_name int | str

If an integer, the new instructions are placed in a free spot in the binary and the jump to them is inserted at that memory address. If a string, the new instructions are placed in a free spot in the binary and added as a symbol (with this as its name).

required
instr str

Instructions to insert. You can use "SAVE_CONTEXT" and "RESTORE_CONTEXT" wherever you want to save and restore program context. If you want to use any symbols from the program or from previous patches, you must surround them with curly braces.

required
force_insert

If Patcherex should ignore whether instructions can be moved when inserting, defaults to False

False
detour_pos

If given a name, specifies the file address to place the new instructions, defaults to -1

-1
symbols dict[str, int] | None

Symbols to include when assembling, in format {symbol name: memory address}, defaults to None

None
is_thumb

Whether the instructions given are thumb, defaults to False

False
language str

The language of the patch, can be either "ASM" or "C"

'ASM'
c_config CConfig | None

Configuration options for when language == "C"

None
**kwargs

Extra options. Can have a boolean "save_context" for whether context should be saved.

{}
Source code in src/patcherex2/patches/instruction_patches.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
def __init__(
    self,
    addr_or_name: int | str,
    instr: str,
    force_insert=False,
    detour_pos=-1,
    symbols: dict[str, int] | None = None,
    is_thumb=False,
    language: str = "ASM",
    c_config: CConfig | None = None,
    **kwargs,
) -> None:
    """
    Constructor.

    :param addr_or_name: If an integer, the new instructions are placed in a free spot in the binary and the jump to them is inserted at that memory address.
                         If a string, the new instructions are placed in a free spot in the binary and added as a symbol (with this as its name).
    :param instr: Instructions to insert. You can use "SAVE_CONTEXT" and "RESTORE_CONTEXT" wherever you want to save and restore program context. If you want to use any symbols from the program or from previous patches, you must surround them with curly braces.
    :param force_insert: If Patcherex should ignore whether instructions can be moved when inserting, defaults to False
    :param detour_pos: If given a name, specifies the file address to place the new instructions, defaults to -1
    :param symbols: Symbols to include when assembling, in format {symbol name: memory address}, defaults to None
    :param is_thumb: Whether the instructions given are thumb, defaults to False
    :param language: The language of the patch, can be either "ASM" or "C"
    :param c_config: Configuration options for when language == "C"
    :param **kwargs: Extra options. Can have a boolean "save_context" for whether context should be saved.
    """
    self.addr = None
    self.name = None
    if isinstance(addr_or_name, int):
        self.addr = addr_or_name
    elif isinstance(addr_or_name, str):
        self.name = addr_or_name
    self.instr = instr
    self.force_insert = force_insert
    self.detour_pos = detour_pos
    self.symbols = symbols if symbols else {}
    self.is_thumb = is_thumb
    self.language = language.upper()
    self.c_config = self.CConfig() if c_config is None else c_config
    self.save_context = (
        kwargs["save_context"] if "save_context" in kwargs else False
    )
    self.compile_opts = kwargs["compile_opts"] if "compile_opts" in kwargs else {}

apply(p)

Applies the patch to the binary, intended to be called by a Patcherex instance.

Parameters:

Name Type Description Default
p Patcherex

Patcherex instance.

required
Source code in src/patcherex2/patches/instruction_patches.py
230
231
232
233
234
235
236
237
238
239
240
def apply(self, p):
    """
    Applies the patch to the binary, intended to be called by a Patcherex instance.

    :param p: Patcherex instance.
    :type p: Patcherex
    """
    if self.language == "ASM":
        self._apply_asm(p)
    elif self.language == "C":
        self._apply_c(p)

ModifyDataPatch

Bases: ModifyRawBytesPatch

Extends ModifyRawBytesPatch to only be used for memory addresses, used for modifying data in binary.

Source code in src/patcherex2/patches/data_patches.py
17
18
19
20
21
22
23
24
25
26
class ModifyDataPatch(ModifyRawBytesPatch):
    """
    Extends ModifyRawBytesPatch to only be used for memory addresses, used for modifying data in binary.
    """

    def __init__(self, addr: int, new_bytes: bytes) -> None:
        """
        Same as ModifyRawBytesPatch constructor, but address type of memory is assumed.
        """
        super().__init__(addr, new_bytes, addr_type="mem")

__init__(addr, new_bytes)

Same as ModifyRawBytesPatch constructor, but address type of memory is assumed.

Source code in src/patcherex2/patches/data_patches.py
22
23
24
25
26
def __init__(self, addr: int, new_bytes: bytes) -> None:
    """
    Same as ModifyRawBytesPatch constructor, but address type of memory is assumed.
    """
    super().__init__(addr, new_bytes, addr_type="mem")

ModifyFunctionPatch

Bases: Patch

Patch that replaces an existing function in the binary with your own. If there is enough room in the existing function, your code is compiled and placed there. If not, your code is placed in a free spot in the binary, and the function will jump there instead.

Source code in src/patcherex2/patches/function_patches.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class ModifyFunctionPatch(Patch):
    """
    Patch that replaces an existing function in the binary with your own. If there is enough room in the existing
    function, your code is compiled and placed there. If not, your code is placed in a free spot in the binary, and
    the function will jump there instead.
    """

    def __init__(
        self,
        addr_or_name: int | str,
        code: str,
        detour_pos=-1,
        symbols: dict[str, int] | None = None,
        **kwargs,
    ) -> None:
        """
        Constructor.

        :param addr_or_name: The name or file address of the function.
        :param code: C code to replace the function.
        :param detour_pos: If original function is not big enough, file address to place the given code, defaults to -1
        :param symbols: Symbols to include when compiling, in format {symbol name: memory address}, defaults to None
        """
        self.code = code
        self.detour_pos = detour_pos
        self.addr_or_name = addr_or_name
        self.symbols = symbols if symbols else {}
        self.compile_opts = kwargs["compile_opts"] if "compile_opts" in kwargs else {}

    def apply(self, p: Patcherex) -> None:
        """
        Applies the patch to the binary, intended to be called by a Patcherex instance.

        :param p: Patcherex instance.
        """
        func = p.binary_analyzer.get_function(self.addr_or_name)
        compiled_size = len(
            p.compiler.compile(
                self.code,
                symbols=self.symbols,
                is_thumb=p.binary_analyzer.is_thumb(func["addr"]),
                **self.compile_opts,
            )
        )
        if compiled_size <= func["size"]:
            mem_addr = func["addr"]
            file_addr = p.binary_analyzer.mem_addr_to_file_offset(mem_addr)
        else:
            # TODO: mark the function as free (exclude jump instr)
            if self.detour_pos == -1:
                block = p.allocation_manager.allocate(
                    compiled_size + 0x20, align=0x4, flag=MemoryFlag.RX
                )
                mem_addr = block.mem_addr
                file_addr = block.file_addr
            else:
                mem_addr = self.detour_pos
                file_addr = p.binary_analyzer.mem_addr_to_file_offset(mem_addr)
            jmp_instr = p.archinfo.jmp_asm.format(dst=hex(mem_addr))
            jmp_bytes = p.assembler.assemble(
                jmp_instr,
                func["addr"],
                is_thumb=p.binary_analyzer.is_thumb(func["addr"]),
            )
            p.binfmt_tool.update_binary_content(
                p.binary_analyzer.mem_addr_to_file_offset(func["addr"]),
                jmp_bytes,
            )
        p.binfmt_tool.update_binary_content(
            file_addr,
            p.compiler.compile(
                self.code,
                mem_addr,
                symbols=self.symbols,
                is_thumb=p.binary_analyzer.is_thumb(func["addr"]),
                **self.compile_opts,
            ),
        )

__init__(addr_or_name, code, detour_pos=-1, symbols=None, **kwargs)

Constructor.

Parameters:

Name Type Description Default
addr_or_name int | str

The name or file address of the function.

required
code str

C code to replace the function.

required
detour_pos

If original function is not big enough, file address to place the given code, defaults to -1

-1
symbols dict[str, int] | None

Symbols to include when compiling, in format {symbol name: memory address}, defaults to None

None
Source code in src/patcherex2/patches/function_patches.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def __init__(
    self,
    addr_or_name: int | str,
    code: str,
    detour_pos=-1,
    symbols: dict[str, int] | None = None,
    **kwargs,
) -> None:
    """
    Constructor.

    :param addr_or_name: The name or file address of the function.
    :param code: C code to replace the function.
    :param detour_pos: If original function is not big enough, file address to place the given code, defaults to -1
    :param symbols: Symbols to include when compiling, in format {symbol name: memory address}, defaults to None
    """
    self.code = code
    self.detour_pos = detour_pos
    self.addr_or_name = addr_or_name
    self.symbols = symbols if symbols else {}
    self.compile_opts = kwargs["compile_opts"] if "compile_opts" in kwargs else {}

apply(p)

Applies the patch to the binary, intended to be called by a Patcherex instance.

Parameters:

Name Type Description Default
p Patcherex

Patcherex instance.

required
Source code in src/patcherex2/patches/function_patches.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def apply(self, p: Patcherex) -> None:
    """
    Applies the patch to the binary, intended to be called by a Patcherex instance.

    :param p: Patcherex instance.
    """
    func = p.binary_analyzer.get_function(self.addr_or_name)
    compiled_size = len(
        p.compiler.compile(
            self.code,
            symbols=self.symbols,
            is_thumb=p.binary_analyzer.is_thumb(func["addr"]),
            **self.compile_opts,
        )
    )
    if compiled_size <= func["size"]:
        mem_addr = func["addr"]
        file_addr = p.binary_analyzer.mem_addr_to_file_offset(mem_addr)
    else:
        # TODO: mark the function as free (exclude jump instr)
        if self.detour_pos == -1:
            block = p.allocation_manager.allocate(
                compiled_size + 0x20, align=0x4, flag=MemoryFlag.RX
            )
            mem_addr = block.mem_addr
            file_addr = block.file_addr
        else:
            mem_addr = self.detour_pos
            file_addr = p.binary_analyzer.mem_addr_to_file_offset(mem_addr)
        jmp_instr = p.archinfo.jmp_asm.format(dst=hex(mem_addr))
        jmp_bytes = p.assembler.assemble(
            jmp_instr,
            func["addr"],
            is_thumb=p.binary_analyzer.is_thumb(func["addr"]),
        )
        p.binfmt_tool.update_binary_content(
            p.binary_analyzer.mem_addr_to_file_offset(func["addr"]),
            jmp_bytes,
        )
    p.binfmt_tool.update_binary_content(
        file_addr,
        p.compiler.compile(
            self.code,
            mem_addr,
            symbols=self.symbols,
            is_thumb=p.binary_analyzer.is_thumb(func["addr"]),
            **self.compile_opts,
        ),
    )

ModifyInstructionPatch

Bases: Patch

Patch that directly modifies instructions in a binary (overwrites them) starting at address given. If ISA is variable length, then if there are remaining bytes in the last overwritten instruction, it will fill them with nops, but it will fail if remaining bytes are not divisible by nop length.

Source code in src/patcherex2/patches/instruction_patches.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class ModifyInstructionPatch(Patch):
    """
    Patch that directly modifies instructions in a binary (overwrites them) starting at address given.
    If ISA is variable length, then if there are remaining bytes in the last overwritten instruction,
    it will fill them with nops, but it will fail if remaining bytes are not divisible by nop length.
    """

    def __init__(
        self, addr: int, instr: str, symbols: dict[str, int] | None = None
    ) -> None:
        """
        Constructor.

        :param addr: Memory address of instruction(s) to overwrite.
        :param instr: Assembly instruction(s) to place in binary. If you want to use any symbols from the program or from previous patches, you must surround them with curly braces.
        :param symbols: Symbols to include when assembling, in format {symbol name: memory address}, defaults to None
        """
        self.addr = addr
        self.instr = instr
        self.symbols = symbols if symbols else {}

    def apply(self, p) -> None:
        """
        Applies the patch to the binary, intended to be called by a Patcherex instance.

        :param p: Patcherex instance.
        :type p: Patcherex
        """
        asm_bytes = p.assembler.assemble(
            self.instr,
            self.addr,
            symbols=self.symbols,
            is_thumb=p.binary_analyzer.is_thumb(self.addr),
        )
        if p.archinfo.is_variable_length_isa:
            asm_size = len(asm_bytes)
            overwritten_size = 0
            num_instrs = 1
            while overwritten_size < asm_size:
                overwritten_size = len(
                    p.binary_analyzer.get_instr_bytes_at(
                        self.addr, num_instr=num_instrs
                    )
                )
                num_instrs += 1
            remaining_size = overwritten_size - asm_size
            assert (
                remaining_size % p.archinfo.nop_size == 0
            ), f"Cannot fill in {remaining_size} bytes when modifying instruction, must be a multiple of {p.archinfo.nop_size}"
            asm_bytes += p.archinfo.nop_bytes * (remaining_size // p.archinfo.nop_size)
        offset = p.binary_analyzer.mem_addr_to_file_offset(self.addr)
        p.binfmt_tool.update_binary_content(offset, asm_bytes)

__init__(addr, instr, symbols=None)

Constructor.

Parameters:

Name Type Description Default
addr int

Memory address of instruction(s) to overwrite.

required
instr str

Assembly instruction(s) to place in binary. If you want to use any symbols from the program or from previous patches, you must surround them with curly braces.

required
symbols dict[str, int] | None

Symbols to include when assembling, in format {symbol name: memory address}, defaults to None

None
Source code in src/patcherex2/patches/instruction_patches.py
28
29
30
31
32
33
34
35
36
37
38
39
40
def __init__(
    self, addr: int, instr: str, symbols: dict[str, int] | None = None
) -> None:
    """
    Constructor.

    :param addr: Memory address of instruction(s) to overwrite.
    :param instr: Assembly instruction(s) to place in binary. If you want to use any symbols from the program or from previous patches, you must surround them with curly braces.
    :param symbols: Symbols to include when assembling, in format {symbol name: memory address}, defaults to None
    """
    self.addr = addr
    self.instr = instr
    self.symbols = symbols if symbols else {}

apply(p)

Applies the patch to the binary, intended to be called by a Patcherex instance.

Parameters:

Name Type Description Default
p Patcherex

Patcherex instance.

required
Source code in src/patcherex2/patches/instruction_patches.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def apply(self, p) -> None:
    """
    Applies the patch to the binary, intended to be called by a Patcherex instance.

    :param p: Patcherex instance.
    :type p: Patcherex
    """
    asm_bytes = p.assembler.assemble(
        self.instr,
        self.addr,
        symbols=self.symbols,
        is_thumb=p.binary_analyzer.is_thumb(self.addr),
    )
    if p.archinfo.is_variable_length_isa:
        asm_size = len(asm_bytes)
        overwritten_size = 0
        num_instrs = 1
        while overwritten_size < asm_size:
            overwritten_size = len(
                p.binary_analyzer.get_instr_bytes_at(
                    self.addr, num_instr=num_instrs
                )
            )
            num_instrs += 1
        remaining_size = overwritten_size - asm_size
        assert (
            remaining_size % p.archinfo.nop_size == 0
        ), f"Cannot fill in {remaining_size} bytes when modifying instruction, must be a multiple of {p.archinfo.nop_size}"
        asm_bytes += p.archinfo.nop_bytes * (remaining_size // p.archinfo.nop_size)
    offset = p.binary_analyzer.mem_addr_to_file_offset(self.addr)
    p.binfmt_tool.update_binary_content(offset, asm_bytes)

ModifyRawBytesPatch

Bases: Patch

Patch that modifies bytes of the binary.

Source code in src/patcherex2/patches/raw_patches.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class ModifyRawBytesPatch(Patch):
    """
    Patch that modifies bytes of the binary.
    """

    def __init__(self, addr: int, new_bytes: bytes, addr_type="mem") -> None:
        """
        Constructor.

        :param addr: Starting address of bytes you want to change.
        :param new_bytes: New bytes to replace original ones.
        :param addr_type: The type of address given, "mem" (memory address) or "raw" (file address), defaults to "mem"
        """
        self.addr = addr
        self.new_bytes = new_bytes
        self.addr_type = addr_type

    def apply(self, p) -> None:
        """
        Applies the patch to the binary, intended to be called by a Patcherex instance.

        :param p: Patcherex instance.
        :raises NotImplementedError: Raised if an address type other than "raw" or "mem" is specified.
        """
        if self.addr_type == "raw":
            offset = self.addr
        elif self.addr_type == "mem":
            offset = p.binary_analyzer.mem_addr_to_file_offset(self.addr)
            if not offset:
                logger.warning(
                    "failed to convert mem addr to file offset, will just default to raw addr"
                )
                offset = self.addr
        else:
            raise NotImplementedError()
        p.binfmt_tool.update_binary_content(offset, self.new_bytes)

__init__(addr, new_bytes, addr_type='mem')

Constructor.

Parameters:

Name Type Description Default
addr int

Starting address of bytes you want to change.

required
new_bytes bytes

New bytes to replace original ones.

required
addr_type

The type of address given, "mem" (memory address) or "raw" (file address), defaults to "mem"

'mem'
Source code in src/patcherex2/patches/raw_patches.py
17
18
19
20
21
22
23
24
25
26
27
def __init__(self, addr: int, new_bytes: bytes, addr_type="mem") -> None:
    """
    Constructor.

    :param addr: Starting address of bytes you want to change.
    :param new_bytes: New bytes to replace original ones.
    :param addr_type: The type of address given, "mem" (memory address) or "raw" (file address), defaults to "mem"
    """
    self.addr = addr
    self.new_bytes = new_bytes
    self.addr_type = addr_type

apply(p)

Applies the patch to the binary, intended to be called by a Patcherex instance.

Parameters:

Name Type Description Default
p

Patcherex instance.

required

Raises:

Type Description
NotImplementedError

Raised if an address type other than "raw" or "mem" is specified.

Source code in src/patcherex2/patches/raw_patches.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def apply(self, p) -> None:
    """
    Applies the patch to the binary, intended to be called by a Patcherex instance.

    :param p: Patcherex instance.
    :raises NotImplementedError: Raised if an address type other than "raw" or "mem" is specified.
    """
    if self.addr_type == "raw":
        offset = self.addr
    elif self.addr_type == "mem":
        offset = p.binary_analyzer.mem_addr_to_file_offset(self.addr)
        if not offset:
            logger.warning(
                "failed to convert mem addr to file offset, will just default to raw addr"
            )
            offset = self.addr
    else:
        raise NotImplementedError()
    p.binfmt_tool.update_binary_content(offset, self.new_bytes)

Patcherex

The main class of the library. This is how you are intended to interact with patches.

Source code in src/patcherex2/patcherex.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
class Patcherex:
    """
    The main class of the library. This is how you are intended to interact with patches.
    """

    def __init__(
        self,
        binary_path: str,
        target_cls: type[Target] | None = None,
        target_opts: dict[str, str] | None = None,
        components_opts: dict[str, dict[str, str]] | None = None,
    ) -> None:
        """
        Constructor.

        :param binary_path: The path of the binary to patch.
        :param target_cls: Specified architecture class to use, otherwise it is automatically detected, defaults to None
        :param target_opts: Options to specify components for the target, defaults to None
        :param components_opts: Options for configuring each component for the target, defaults to None
        """
        if target_opts is None:
            target_opts = {}
        if components_opts is None:
            components_opts = {}
        self.binary_path = binary_path
        if target_cls is None:
            self.target = Target.detect_target(self, binary_path)
        else:
            self.target = target_cls(self, binary_path)

        self.symbols = {}
        self.sypy_info = {"patcherex_added_functions": []}
        self.patches = []

        # Initialize components
        components = [
            "assembler",
            "disassembler",
            "compiler",
            "binary_analyzer",
            "allocation_manager",
            "binfmt_tool",
            "utils",
            "archinfo",
        ]
        for component in components:
            setattr(
                self,
                component,
                self.target.get_component(
                    component,
                    target_opts.get(component),
                    components_opts.get(component),
                ),
            )

        # Chosen patch order, making sure all are accounted for
        self.patch_order = (
            ModifyRawBytesPatch,
            RemoveDataPatch,
            InsertDataPatch,
            ModifyDataPatch,
            RemoveLabelPatch,
            ModifyLabelPatch,
            InsertLabelPatch,
            RemoveInstructionPatch,
            InsertInstructionPatch,
            ModifyInstructionPatch,
            RemoveFunctionPatch,
            InsertFunctionPatch,
            ModifyFunctionPatch,
        )
        assert len(self.patch_order) == len(all_patches)

    def shutdown(self):
        """
        Shuts down any resources used by Patcherex2.
        This needs to be called when using Ghidra as the binary analyzer when done patching.
        """
        if isinstance(self.binary_analyzer, Ghidra):
            self.binary_analyzer.shutdown()

    def apply_patches(self) -> None:
        """
        Applies all added patches to the binary. Call this when you have added all the patches you want.
        """
        # TODO: sort patches properly
        # self.patches.sort(key=lambda x: self.patch_order.index(type(x)))
        self.patches.sort(
            key=lambda x: not isinstance(
                x, (ModifyDataPatch, InsertDataPatch, RemoveDataPatch)
            )
        )
        logger.debug(f"Applying patches: {self.patches}")
        for patch in self.patches:
            patch.apply(self)
        self.binfmt_tool.finalize()

    def save_binary(self, filename: str = None) -> None:
        """
        Save the patched binary to a file.

        :param filename: Name of file to save to, defaults to '<filename>.patched'
        """
        self.binfmt_tool.save_binary(filename)

__init__(binary_path, target_cls=None, target_opts=None, components_opts=None)

Constructor.

Parameters:

Name Type Description Default
binary_path str

The path of the binary to patch.

required
target_cls type[Target] | None

Specified architecture class to use, otherwise it is automatically detected, defaults to None

None
target_opts dict[str, str] | None

Options to specify components for the target, defaults to None

None
components_opts dict[str, dict[str, str]] | None

Options for configuring each component for the target, defaults to None

None
Source code in src/patcherex2/patcherex.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def __init__(
    self,
    binary_path: str,
    target_cls: type[Target] | None = None,
    target_opts: dict[str, str] | None = None,
    components_opts: dict[str, dict[str, str]] | None = None,
) -> None:
    """
    Constructor.

    :param binary_path: The path of the binary to patch.
    :param target_cls: Specified architecture class to use, otherwise it is automatically detected, defaults to None
    :param target_opts: Options to specify components for the target, defaults to None
    :param components_opts: Options for configuring each component for the target, defaults to None
    """
    if target_opts is None:
        target_opts = {}
    if components_opts is None:
        components_opts = {}
    self.binary_path = binary_path
    if target_cls is None:
        self.target = Target.detect_target(self, binary_path)
    else:
        self.target = target_cls(self, binary_path)

    self.symbols = {}
    self.sypy_info = {"patcherex_added_functions": []}
    self.patches = []

    # Initialize components
    components = [
        "assembler",
        "disassembler",
        "compiler",
        "binary_analyzer",
        "allocation_manager",
        "binfmt_tool",
        "utils",
        "archinfo",
    ]
    for component in components:
        setattr(
            self,
            component,
            self.target.get_component(
                component,
                target_opts.get(component),
                components_opts.get(component),
            ),
        )

    # Chosen patch order, making sure all are accounted for
    self.patch_order = (
        ModifyRawBytesPatch,
        RemoveDataPatch,
        InsertDataPatch,
        ModifyDataPatch,
        RemoveLabelPatch,
        ModifyLabelPatch,
        InsertLabelPatch,
        RemoveInstructionPatch,
        InsertInstructionPatch,
        ModifyInstructionPatch,
        RemoveFunctionPatch,
        InsertFunctionPatch,
        ModifyFunctionPatch,
    )
    assert len(self.patch_order) == len(all_patches)

apply_patches()

Applies all added patches to the binary. Call this when you have added all the patches you want.

Source code in src/patcherex2/patcherex.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def apply_patches(self) -> None:
    """
    Applies all added patches to the binary. Call this when you have added all the patches you want.
    """
    # TODO: sort patches properly
    # self.patches.sort(key=lambda x: self.patch_order.index(type(x)))
    self.patches.sort(
        key=lambda x: not isinstance(
            x, (ModifyDataPatch, InsertDataPatch, RemoveDataPatch)
        )
    )
    logger.debug(f"Applying patches: {self.patches}")
    for patch in self.patches:
        patch.apply(self)
    self.binfmt_tool.finalize()

save_binary(filename=None)

Save the patched binary to a file.

Parameters:

Name Type Description Default
filename str

Name of file to save to, defaults to '.patched'

None
Source code in src/patcherex2/patcherex.py
112
113
114
115
116
117
118
def save_binary(self, filename: str = None) -> None:
    """
    Save the patched binary to a file.

    :param filename: Name of file to save to, defaults to '<filename>.patched'
    """
    self.binfmt_tool.save_binary(filename)

shutdown()

Shuts down any resources used by Patcherex2. This needs to be called when using Ghidra as the binary analyzer when done patching.

Source code in src/patcherex2/patcherex.py
88
89
90
91
92
93
94
def shutdown(self):
    """
    Shuts down any resources used by Patcherex2.
    This needs to be called when using Ghidra as the binary analyzer when done patching.
    """
    if isinstance(self.binary_analyzer, Ghidra):
        self.binary_analyzer.shutdown()

RemoveDataPatch

Bases: ModifyRawBytesPatch

Extends ModifyRawBytesPatch for removing data in the binary (fills it with null bytes starting at address given). Expects a memory address.

Source code in src/patcherex2/patches/data_patches.py
66
67
68
69
70
71
72
73
74
75
76
77
78
class RemoveDataPatch(ModifyRawBytesPatch):
    """
    Extends ModifyRawBytesPatch for removing data in the binary (fills it with null bytes starting at address given).
    Expects a memory address.
    """

    def __init__(self, addr: int, size: int) -> None:
        """
        Same as ModifyRawBytes Patch constructor, but adds size parameter and assumes memory address.

        :param size: The number of bytes to remove.
        """
        super().__init__(addr, b"\x00" * size, addr_type="mem")

__init__(addr, size)

Same as ModifyRawBytes Patch constructor, but adds size parameter and assumes memory address.

Parameters:

Name Type Description Default
size int

The number of bytes to remove.

required
Source code in src/patcherex2/patches/data_patches.py
72
73
74
75
76
77
78
def __init__(self, addr: int, size: int) -> None:
    """
    Same as ModifyRawBytes Patch constructor, but adds size parameter and assumes memory address.

    :param size: The number of bytes to remove.
    """
    super().__init__(addr, b"\x00" * size, addr_type="mem")

RemoveFunctionPatch

Bases: Patch

Patch that removes a function from the binary. Not implemented.

Source code in src/patcherex2/patches/function_patches.py
229
230
231
232
233
234
235
236
237
238
class RemoveFunctionPatch(Patch):
    """
    Patch that removes a function from the binary. Not implemented.
    """

    def __init__(self, parent=None) -> None:
        """
        Constructor.
        """
        raise NotImplementedError()

__init__(parent=None)

Constructor.

Source code in src/patcherex2/patches/function_patches.py
234
235
236
237
238
def __init__(self, parent=None) -> None:
    """
    Constructor.
    """
    raise NotImplementedError()

RemoveInstructionPatch

Bases: Patch

Patch that removes instructions in the binary. Currently only takes in a number of bytes and an starting address. The number of bytes must be divisible by the nop size of the architecture, otherwise it will fail.

Source code in src/patcherex2/patches/instruction_patches.py
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
class RemoveInstructionPatch(Patch):
    """
    Patch that removes instructions in the binary. Currently only takes in a number of bytes and an starting address.
    The number of bytes must be divisible by the nop size of the architecture, otherwise it will fail.
    """

    def __init__(
        self,
        addr: int,
        num_instr: int | None = None,
        num_bytes: int | None = None,
    ) -> None:
        """
        Constructor.

        :param addr: Memory address to remove instructions at.
        :param num_instr: Number of instructions to remove, currently not used, defaults to None
        :param num_bytes: Number of bytes to remove, must be divisible by nop size, defaults to None
        """
        self.addr = addr
        self.num_instr = num_instr
        self.num_bytes = num_bytes
        if self.num_instr is None and self.num_bytes is None:
            self.num_instr = 1

    def apply(self, p: Patcherex) -> None:
        """
        Applies the patch to the binary, intended to be called by a Patcherex instance.

        :param p: Patcherex instance.
        """
        if self.num_bytes is None:
            raise NotImplementedError()
        if self.num_bytes and self.num_bytes % p.archinfo.nop_size != 0:
            raise Exception(
                f"Cannot remove {self.num_bytes} bytes, must be a multiple of {p.archinfo.nop_size}"
            )
        num_nops = self.num_bytes // p.archinfo.nop_size
        offset = p.binary_analyzer.mem_addr_to_file_offset(self.addr)
        p.binfmt_tool.update_binary_content(offset, p.archinfo.nop_bytes * num_nops)

__init__(addr, num_instr=None, num_bytes=None)

Constructor.

Parameters:

Name Type Description Default
addr int

Memory address to remove instructions at.

required
num_instr int | None

Number of instructions to remove, currently not used, defaults to None

None
num_bytes int | None

Number of bytes to remove, must be divisible by nop size, defaults to None

None
Source code in src/patcherex2/patches/instruction_patches.py
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
def __init__(
    self,
    addr: int,
    num_instr: int | None = None,
    num_bytes: int | None = None,
) -> None:
    """
    Constructor.

    :param addr: Memory address to remove instructions at.
    :param num_instr: Number of instructions to remove, currently not used, defaults to None
    :param num_bytes: Number of bytes to remove, must be divisible by nop size, defaults to None
    """
    self.addr = addr
    self.num_instr = num_instr
    self.num_bytes = num_bytes
    if self.num_instr is None and self.num_bytes is None:
        self.num_instr = 1

apply(p)

Applies the patch to the binary, intended to be called by a Patcherex instance.

Parameters:

Name Type Description Default
p Patcherex

Patcherex instance.

required
Source code in src/patcherex2/patches/instruction_patches.py
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
def apply(self, p: Patcherex) -> None:
    """
    Applies the patch to the binary, intended to be called by a Patcherex instance.

    :param p: Patcherex instance.
    """
    if self.num_bytes is None:
        raise NotImplementedError()
    if self.num_bytes and self.num_bytes % p.archinfo.nop_size != 0:
        raise Exception(
            f"Cannot remove {self.num_bytes} bytes, must be a multiple of {p.archinfo.nop_size}"
        )
    num_nops = self.num_bytes // p.archinfo.nop_size
    offset = p.binary_analyzer.mem_addr_to_file_offset(self.addr)
    p.binfmt_tool.update_binary_content(offset, p.archinfo.nop_bytes * num_nops)