purseclab / patcherex2 Goto Github PK
View Code? Open in Web Editor NEWA versatile and easy-to-use static binary patching tool.
Home Page: https://purseclab.github.io/Patcherex2/
License: BSD 2-Clause "Simplified" License
A versatile and easy-to-use static binary patching tool.
Home Page: https://purseclab.github.io/Patcherex2/
License: BSD 2-Clause "Simplified" License
I've been working on aarch64 support for the copy and micropatch fork and I've come across an issue with the use of the -relocatable
flag during the linking process. This flag is used in compiler.py
and llvm_recomp.py
.
It seems that relative relocations are not resolved when the -relocatable
flag is used. See this Stackoverflow post for more details: https://stackoverflow.com/questions/35324636/resolve-relative-relocations-in-partial-link
Merely removing the -relocatable
flag causes a bunch of other errors. When the base=0
, we get an error trace like this:
ERROR | 2024-05-02 11:25:19,643 | patcherex2.components.compilers.compiler | ld.lld-15: error: /tmp/tmp_uu_rkga/linker.ld:1: unable to move location counter backward for: .patcherex2
ld.lld-15: error: /tmp/tmp_uu_rkga/linker.ld:1: unable to move location counter backward for: .patcherex2
ld.lld-15: error: section .patcherex2 at 0x34 of size 0xFFFFFFFFFFFFFFF0 exceeds available address space
ld.lld-15: error: section .eh_frame file range overlaps with .comment
>>> .eh_frame range is [0x10000, 0x10033]
>>> .comment range is [0x10024, 0x1005A]
ld.lld-15: error: section .comment file range overlaps with .patcherex2
>>> .comment range is [0x10024, 0x1005A]
>>> .patcherex2 range is [0x10034, 0x10023]
Traceback (most recent call last):
File "/home/caleb/Documents/vibesproject/Patcherex2/tests/test_binaries/aarch64/iip_c.py", line 26, in <module>
p.apply_patches()
File "/home/caleb/Documents/vibesproject/Patcherex2/src/patcherex2/patcherex.py", line 100, in apply_patches
patch.apply(self)
File "/home/caleb/Documents/vibesproject/Patcherex2/src/patcherex2/patches/instruction_patches.py", line 224, in apply
self._apply_c(p)
File "/home/caleb/Documents/vibesproject/Patcherex2/src/patcherex2/patches/instruction_patches.py", line 340, in _apply_c
p.utils.insert_trampoline_code(
File "/home/caleb/Documents/vibesproject/Patcherex2/src/patcherex2/components/utils/utils.py", line 70, in insert_trampoline_code
self.p.compiler.compile(
File "/home/caleb/Documents/vibesproject/Patcherex2/src/patcherex2/components/compilers/compiler.py", line 96, in compile
raise e
File "/home/caleb/Documents/vibesproject/Patcherex2/src/patcherex2/components/compilers/compiler.py", line 93, in compile
print(subprocess.run(args, check=True, capture_output=True))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.11/subprocess.py", line 571, in run
raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '['ld.lld-15', '/tmp/tmp_uu_rkga/obj.o', '-T', '/tmp/tmp_uu_rkga/linker.ld', '-o', '/tmp/tmp_uu_rkga/obj_linked.o']' returned non-zero exit status 1.
For the nonzero base
, the linking process completes successfully but results in a different error:
Traceback (most recent call last):
File "/home/caleb/Documents/vibesproject/Patcherex2/tests/test_binaries/aarch64/iip_c.py", line 26, in <module>
p.apply_patches()
File "/home/caleb/Documents/vibesproject/Patcherex2/src/patcherex2/patcherex.py", line 100, in apply_patches
patch.apply(self)
File "/home/caleb/Documents/vibesproject/Patcherex2/src/patcherex2/patches/instruction_patches.py", line 224, in apply
self._apply_c(p)
File "/home/caleb/Documents/vibesproject/Patcherex2/src/patcherex2/patches/instruction_patches.py", line 340, in _apply_c
p.utils.insert_trampoline_code(
File "/home/caleb/Documents/vibesproject/Patcherex2/src/patcherex2/components/utils/utils.py", line 124, in insert_trampoline_code
compiled_code = self.p.compiler.compile(
^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/caleb/Documents/vibesproject/Patcherex2/src/patcherex2/components/compilers/compiler.py", line 108, in compile
compiled = ld.memory.load(
^^^^^^^^^^^^^^^
File "/home/caleb/Documents/vibesproject/Patcherex2/venv/lib/python3.11/site-packages/cle/memory.py", line 391, in load
raise KeyError(addr)
KeyError: 203024
This issue crops up when we attempt to compile a copy-and-micropatch C file which has an extern _CALLBACK
function. The _CALLBACK
symbol ends up being a 1 instruction relative jump. On aarch64
this will cause the program to jump to the jump instruction, which causes an infinite loop. I have an example to reproduce this issue, however it uses the Draper branch. It is based off of the iip_c
test case found in the tests/test_binaries/aarch64
folder. I have attached the relevant compiled iip_c
test case and the InsertInstructionPatch C script: iip_c.zip
If you look in iip_c.patched you will see the infinite loop (highlighted in green) in Ghidra:
The correct assembly code should be 01 00 00 14
. If you don't use the -relocatable
flag with nonzero base and look at obj_linked.o
as created in the /tmp
directory, you will see that the linker does correctly resolve this branch instruction. Even though the linking succeeds, the program then fails with the KeyError
that I posted previously.
Some other possibly helpful links:
My coworker, Phil Zucker, came up with a new clever method of creating micropatches using an ordinary C compiler. The strategy is called "copy and micropatch" and operates similarly to copy and patch JITs. The strategy is based around abuse of the calling convention to force values into certain registers.
The easiest way to illustrate the concept is with an example (taken from Phil's blog):
#include <stdint.h>
uint64_t CALLBACK(uint64_t rdi, uint64_t rsi, uint64_t rdx, uint64_t rcx, uint64_t r8, uint64_t r9);
uint64_t PATCHCODE(uint64_t rdi, uint64_t rsi, uint64_t rdx, uint64_t rcx, uint64_t r8, uint64_t r9){
// Some random patch code here
if(rcx >= r8){
rdi = rsi * rdx;
}
// End patchcode
return CALLBACK(rdi, rsi, rdx, rcx, r8, r9);
}
The calling convention for this snippet ensures that the PATCHCODE
receives certain registers as inputs, and the CALLBACK
at the end ensures that the variables are placed into the correct registers once the function terminates.
The code is passed through an ordinary C compiler, and the body of PATCHCODE
is extracted and inserted somewhere where there is space. This process requires tail-call optimization turned on, which turns the call to CALLBACK
into a jump. Through the use of a linker script we could set the CALLBACK symbol to be placed at the detour return point.
With the __attribute__((preserve_none))
tag built into the latest version of Clang, we can get control over many registers (at least on x64). Note that the preserve_none
is brand new, I don't think it has landed into any release versions of Clang yet. As an alternative to preserve_none
, we could add shims to push/pop registers to ensure the data gets to the right place.
For more info, see Phil's blog here: https://www.philipzucker.com/permutation_compile/
I'm willing to put the time into developing this target for integration into patcherex2. Is there anything that we need to know before forking and getting started? Using the version of Clang with support for preserve_none
would be highly desirable.
See also:
def is_movable_instruction(self, addr: int) -> bool:
is_thumb = self.p.binary_analyzer.is_thumb(addr)
insn = self.p.binary_analyzer.get_instr_bytes_at(addr)
asm = self.p.disassembler.disassemble(insn, addr, is_thumb=is_thumb)[0]
asm = self.p.disassembler.to_asm_string(asm)
for addr in [0x0, 0x7F00000, 0xFE000000]:
if self.p.assembler.assemble(asm, addr, is_thumb=is_thumb) != insn:
return False
return True
i don't believe the above check is sufficient. Some examples found in the arm printf pie binary are
0000050e 064b ldr r3, [pc, #0x18] {data_528}
00000510 7b44 add r3, pc {data_5cc, "Hi"}
which would pass the above check since the disassembly does no change if the base address changes, but these aren't safe to move since they depend on the value of the PC.
When working on the InsertInstructionPatch lang="C" code, I ran across some scenarios where the .eh_frame
section was getting tacked onto the output from Compiler.compile
. Here is what was causing the error for me:
base=0
was passed to Compiler.compile
, the .eh_frame
section was not directly adjacent to the .text
section, so Compiler.comple
did not return that section.base=0x5edb
was passed to Compiler.compile
, the .eh_frame
was placed directly adjacent to the .text
section, and was returned by Compiler.compile
..eh_frame
is completely useless for us, it can be safely omitted.-fno-asynchronous-unwind-tables
to stop the section from being outputted from the compiler in the first place.Compiler.compile
.I fixed this issue by passing the compiler flag -fno-asynchronous-unwind-tables
here:
I have attached the files in the compiler /tmp
directory for both the version where -fno-asynchronous-unwind-tables
was passed, and the version where it wasn't.
We may want to add -fno-asynchronous-unwind-tables
as a default flag to pass to the compiler, or fix the section extraction code.
Here are two screenshots from Ghidra showing the adjacency of the text section:
At base 0x5edb (notice that .eh_frame
is now directly adjacent):
I have an ELF file for one of the upcoming AMP Hackathon problems that I am attempting to patch. The ELF file in question is ARM, and it contains a section .ARM.exidx
, which from my understanding is related to stack unwinding. Attempting either an InsertFunctionPatch or a InsertInstructionPatch results in failure due to some sort of relative addressing related to this section.
Here is the stacktrace and error message that I obtained:
ERROR | 2024-03-01 14:52:42,847 | patcherex2.components.compilers.compiler | ld.lld-15: error: /tmp/tmpnykktgjr/obj.o:(.ARM.exidx+0x0): relocation R_ARM_PREL31 out of range: 1610719160 is not in [-1073741824, 1073741823]
>>> referenced by code.c
Traceback (most recent call last):
File "/home/caleb/Documents/vibesproject/cozy/test_programs/GridIDPS/build/micropatch.py", line 40, in <module>
apply_badpatch()
File "/home/caleb/Documents/vibesproject/cozy/test_programs/GridIDPS/build/micropatch.py", line 37, in apply_badpatch
proj.apply_patches()
File "/home/caleb/Documents/vibesproject/VIBES-internal/experiments/cozy/venv/lib/python3.11/site-packages/patcherex2/patcherex.py", line 57, in apply_patches
patch.apply(self)
File "/home/caleb/Documents/vibesproject/VIBES-internal/experiments/cozy/venv/lib/python3.11/site-packages/patcherex2/patches/function_patches.py", line 134, in apply
p.compiler.compile(
File "/home/caleb/Documents/vibesproject/VIBES-internal/experiments/cozy/venv/lib/python3.11/site-packages/patcherex2/components/compilers/clang_arm.py", line 37, in compile
compiled = super().compile(
^^^^^^^^^^^^^^^^
File "/home/caleb/Documents/vibesproject/VIBES-internal/experiments/cozy/venv/lib/python3.11/site-packages/patcherex2/components/compilers/compiler.py", line 83, in compile
raise e
File "/home/caleb/Documents/vibesproject/VIBES-internal/experiments/cozy/venv/lib/python3.11/site-packages/patcherex2/components/compilers/compiler.py", line 80, in compile
subprocess.run(args, check=True, capture_output=True)
File "/usr/lib/python3.11/subprocess.py", line 571, in run
raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '['ld.lld-15', '-relocatable', '/tmp/tmpnykktgjr/obj.o', '-T', '/tmp/tmpnykktgjr/linker.ld', '-o', '/tmp/tmpnykktgjr/obj_linked.o']' returned non-zero exit status 1.
Here is my Python script:
import patcherex2
proj = patcherex2.Patcherex("amp_challenge_arm.ino_unstripped.elf")
# Make sure InsertFunctionPatch is working
# Applying the InsertFunctionPatch fails even if we comment out the body of flushSerial()
flushSerial_code = '''
void flushSerial() {
while(usb_serial_available() > 0) {
char t = usb_serial_getchar();
}
}
'''
flushSerial_patch = patcherex2.InsertFunctionPatch("flushSerial", flushSerial_code)
proj.patches.append(flushSerial_patch)
# Make sure InsertInstructionPatch is working
instructionPatch_code = '''
nop
nop
'''
loop_addr = proj.binary_analyzer.get_function('loop')['addr']
instruction_patch = patcherex2.InsertInstructionPatch(loop_addr, instructionPatch_code)
proj.patches.append(instruction_patch)
proj.apply_patches()
proj.binfmt_tool.save_binary("amp_challenge_arm.ino_unstripped-draper-badpatch.elf")
Some information I have found on this exidx section:
The linker script that patcherex2 generates does not seem to have anything about the exidx. Unfortunately I am not well versed enough in the ELF format or linker scripts to make a determination on how to fix this issue.
I have attached the ELF file and the Python script to this issue.
arm_exidx_bug.zip
As I've been writing some test cases for InsertInstructionPatch
when language="C"
, I've come across segmentation faults that occur when data sections are not aligned properly. As an example, take a look at the following program in Ghidra:
The highlighted instruction is accessing a chunk of readable data, and the argument needs to be 16 byte aligned. Running this program results in a segfault due to misalignment. Other instructions in this micropatch seem to have a similar problem.
Here is the error in gef:
There seems to be a chicken and egg problem with the patch, where you don't know the true size of the patch until you know its insertion address, but to get the address of the patch you need the size. Right now I believe we first get the size by assuming the compiled function starts at address 0. Could this be related to the alignment bug?
Although this problem cropped up with the InsertIntructionPatch
, I assume this issue also crops up in InsertFunctionPatch
.
I have attached the binaries (before and after patch) and the Python script needed to reproduce the issue (you need to run the Python script on the draperlaboratory fork). I have also attached the log of running the Python script, which prints out what the MICROPATCH
function is:
On line 32 of utils.py, moved_instrs_len
is set to 0, which means that the trampoline will jump back to the instruction that jumped to the trampoline in the first place. This will either cause an infinite loop or a crash.
My workaround is to set moved_instrs_len = self.p.target.JMP_SIZE
, but for variable length instruction sets such as x64 this may be incorrect. A better solution would be to somehow get the length of the first jump and use that to compute where to jump back.
I have attached the programs and patcherex Python script to this issue. The target3 binary was produced after changing line 32 to be moved_instrs_len = self.p.target.JMP_SIZE
. In this case using JMP_SIZE
is okay, because I've noped out the instructions in the trampoline start area.
Relevant source files: patcherex_bug.zip
Expected output of test should be:
patched failed
Expected output of test3 should be:
ched failed
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.