E9Patch is a low-level static binary rewriting tool for x86-64 Linux ELF executables and shared objects. This document is intended for tool/frontend developers. If you are a user and merely wish to use E9Patch to rewrite binaries, we recommend you read the E9Tool User Guide instead.
There are three main ways to integrate E9Patch into your project:
- E9Tool Call Trampolines [simple, high-level, rigid, recommended method]
- E9Patch JSON-RPC Interface [advanced, low-level, flexible]
- E9Tool Plugin API [advanced, low-level, flexible]
If performance is not an issue, then we recommend using E9Tool call instrumentation. For serious/optimized applications, we recommend using an E9Tool plugin or the E9Patch JSON-RPC interface.
E9Tool supports a generic instruction patching capability in the form of call trampolines. This is by far the simplest way to build a new application using E9Patch, and is also the recommended method unless you are specifically trying to generate optimized code, or if your application requires maximum flexibility.
Call trampolines allow patch code to be implemented as ordinary functions
using a supported programming language, such as C
, C++
or assembly.
The patch code can then be compiled into a patch binary and will be injected
into the rewritten binary.
The patch functions can then be called from the desired locations.
For example, the following code defines a function that increments a
counter.
Once the counter exceeds some predefined maximum value, the function
will execute the int3
instruction, causing SIGTRAP
to be sent to
the program.
static unsigned long counter = 0;
static unsigned long max = 100000;
void entry(void)
{
counter++;
if (counter >= max)
asm volatile ("int3");
}
Once defined, the patch code can then be compiled using the special
e9compile.sh
script.
This script invokes gcc
with the correct options necessary to be
compatible with E9Tool:
./e9compile.sh counter.c
This command will build a special ELF executable file named counter
.
To rewrite a binary using counter
, we use E9Tool and the --patch
/-P
options.
For example, to instrument all jump instructions in the xterm
binary
with a call to the entry()
function, we can use the following
command-line syntax:
./e9tool -M 'asm=/j.*/' -P 'entry()@counter' xterm
The syntax is as follows:
-M
: Selects the E9Tool "match" command-line option. This specifies which instructions should be instrumented.asm=/j.*/
: Specifies that we want to patch/instrument all instructions whose assembly syntax matches the regular expressionj.*
(delimited by slash/
characters). For thex86_64
, only jump instructions begin withj
, so this syntax selects all jump instructions, e.g.jmp
,jnz
,jg
, etc.-P
: Selects the E9Tool "patch" command-line option. This tells E9Tool how to patch the matching instruction(s).entry()@counter
: Specifies that the trampoline should call the functionentry()
in thecounter
binary.
By default, call trampolines will handle all low-level details, such as
saving and restoring the CPU state, and
injecting the counter
executable into the rewritten binary.
By default the modified binary will be written to a.out
.
The instrumented a.out
file will call the counter
function each
time a jump instruction is executed.
After 100000 jumps, the program will terminate with SIGTRAP
.
Call trampolines support many features, such as function arguments, selectable ABIs, and conditional control-flow redirection. More information about call trampolines can be found in the E9Tool User Guide.
Additional examples of call trampolines are also available here.
The E9Patch tool uses the JSON-RPC (version 2.0) as its API. Basically, the E9Patch tool expects a stream of JSON-RPC messages which describe which binary to rewrite and how to rewrite it. These JSON-RPC messages are fed from a frontend tool, such as E9Tool, but this design means that multiple different frontends can be supported. The choice of JSON-RPC as the API also means that the frontend can be implemented in any programming language, including C++, python or Rust.
By design, E9Patch tool will do very little parsing or analysis of the input binary file. Instead, the analysis/parsing is left to the frontend, and E9Patch relies on the frontend to supply all necessary information in order to rewrite the binary. Specifically, the frontend must specify:
- The file offsets, virtual addresses and size of instructions in the input binary.
- The file offsets of the patch location.
- The templates for the trampolines to be used by the rewritten binary.
- Any additional data/code to be inserted into the rewritten binary.
The main JSON-RPC messages are:
- Binary Message
- Trampoline Message
- Reserve Message
- Instruction Message
- Patch Message
- Options Message
- Emit Message
The E9Patch JSON-RPC parser does not yet support the full JSON syntax, but implements a reasonable subset. The parser also implements an extension in the form of support for hexadecimal numbers in a string format. For example, the following are equivalent:
"address": 4245300
"address": "0x40c734"
The string format can also be used to represent numbers larger than those representable in 32bits.
Note that implementing a new frontend from scratch may require a lot of boilerplate code. An alternative is to implement an E9Tool plugin which is documented here.
The "binary"
message begins the patching process.
It must be the first message sent to E9Patch.
"filename"
: the path to the binary file that is to be patched."mode"
: the type of the binary file. Valid values include"elf.exe"
for ELF executables and"elf.so"
for ELF shared objects.
{
"jsonrpc": "2.0",
"method": "binary",
"params":
{
"filename": "/usr/bin/xterm",
"mode": "elf.exe"
},
"id": 0
}
The "trampoline"
message sends a trampoline template specification
to E9Patch that can be used to patch instructions.
"name"
: the name of the trampoline. Valid names must start with the dollar character."template"
: the template of the trampoline.
The template essentially specifies what bytes should be placed in memory when the trampoline is used by a patched instruction. The template can consist of both instructions and data, depending on the application.
The template specification language is low-level, and is essentially a sequence of bytes, but can also include other data types (integers, strings), macros and labels. Any desired instruction sequence should therefore be specified in machine code.
The template is specified as a list of elements, where each element can be any of the following:
- A byte: represented by an integer 0..255
- A macro: represented by a string beginning with a dollar character,
e.g.
"$asmStr
. Macros are expanded with metadata values provided by the"patch"
message (see below) or a template from another "trampoline" message. There are also some builtin macros (see below). - A label: represented by a string beginning with the string
".L"
, e.g.".Llabel"
. There are also some builtin labels (see below). - An integer: represented by a type/value tuple, e.g.
{"int32": 1000}
, where valid types are:"int8"
: a single byte signed integer"int16"
: a 16bit little-endian signed integer"int32"
: a 32bit little-endian signed integer"int64"
: a 64bit little-endian signed integer
- A string: represented by a type/value where the type is
"string"
, e.g.{"string": "hello\n"}
- A relative offset: represented by a type/value tuple, e.g.
{"rel8": ".Llabel"}
. where valid types are:"rel8"
: an 8bit relative offset"rel32"
: a 32bit relative offset
- An empty value: represented by the
null
token.
Several builtin macros are implicitly defined, including:
"$bytes"
: The original byte sequence of the instruction that was patched"$instr"
: A byte sequence of instructions that emulates the patched instruction. Note that this is usually equivalent to"$bytes"
except for instructions that are position-dependent."$break"
(thin break): A byte sequence of instructions that will return control-flow to the next instruction after the matching instruction. The"$break"
builtin macro is guaranteed to be implemented as a singlejmpq rel32
instruction."$BREAK"
(fat break): Semantically equivalent to"$break"
, but the implementation is not limited to a single jump instruction. This allows for a more aggressive optimization that is possible using a thin"$break"
, but at the cost of more space usage. For optimal results, each trampoline should use exactly one fat"$BREAK"
, with thin"$break"
used for the rest (if applicable)."$take"
: Similar to"$break"
, but for the branch-taken case of conditional jumps.
Several builtin labels are also implicitly defined, including:
".Lbreak"
: The address corresponding to"$break"
".Ltake"
: The address corresponding to$take"
".Linstr"
: The address of the matching instruction.".Lconfig"
: The address of the internal E9Patch configuration structure.
{
"jsonrpc": "2.0",
"method": "trampoline",
"params":
{
"name": "$print",
"template": [
72, 141, 164, 36, 0, 192, 255, 255,
87,
86,
80,
81,
82,
65, 83,
72, 141, 53, {"rel32": ".LasmStr"},
186, "$asmStrLen",
191, 2, 0, 0, 0,
184, 1, 0, 0, 0,
15, 5,
65, 91,
90,
89,
88,
94,
95,
72, 141, 164, 36, 0, 64, 0, 0,
"$instr",
"$BREAK",
".LasmStr",
"$asmStr"
]
},
"id":1
}
This is a representation of the following trampoline in assembly syntax:
# Save registers:
lea -0x4000(%rsp),%rsp
push %rdi
push %rsi
push %rax
push %rcx
push %rdx
push %r11
# Setup and execute a SYS_write system call:
leaq .LasmStr(%rip),%rsi
mov $asmStrlen,%edx
mov $0x2,%edi # stderr
mov $0x1,%eax # SYS_write
syscall
# Restore registers:
pop %r11
pop %rdx
pop %rcx
pop %rax
pop %rsi
pop %rdi
lea 0x4000(%rsp),%rsp
# Execute the displaced instruction:
$instr
# Return from the trampoline
$BREAK
# Store the asm String here:
.LasmStr:
$asmStr
Note that the interface is very low-level.
E9Patch does not have a builtin assembler so instructions must be specified
in machine code.
Furthermore, it is up to the trampoline template specification to
save/restore the CPU state as necessary.
In the example above, the trampoline saves several registers to the
stack before restoring them before returning.
Note that under the System V ABI, up to 128bytes below the stack
pointer may be used (the stack red zone), hence a pair of
lea
instructions must adjust the stack pointer to skip this
region, else the patched program may crash or misbehave.
In general, the saving/restoring of the CPU state is solely the responsibility
of the frontend, and E9Patch will simply execute the template "as-is".
The above code uses two user-defined macros, namely "$asmStr"
and
"$asmStrLen"
.
The values of these macros depend on the matching instruction, so will be
instantiated by metadata defined by the "patch" message (see below).
The "reserve"
message is useful for reserving sections of the
patched program's virtual address space and (optionally) initializing
it with data.
The reserved address range will not be used to host trampolines.
Note that the reserved address range will be implicitly rounded to the nearest page boundary. This means that trampoline and reserved memory will be disjoint at the page level.
"absolute"
: [optional] iftrue
then the address is interpreted as an absolute address."address"
: the base address of the virtual address space region. For PIC, this is a relative address unless"absolute"
is set totrue
."bytes"
: [optional] bytes to initialize the memory with, using the trampoline template syntax. This is mandatory if"length"
is unspecified."length"
: [optional] the length of the reservation. This is mandatory if"bytes"
is unspecified."init"
: [optional] the address of an initialization routine that will be called when the patched program is loaded into memory."fini"
: [optional] the address of a finalization routine that will be called when the patched program exits normally."mmap"
: [optional] the address of a replacement implementation ofmmap()
that will be used during the patched program's initialization. This is for advanced applications only."preinit"
: [optional] raw bytes to be called before loader initialization."postinit"
[optional] raw bytes to be called after loader initialization."protection"
: [optional] the page permissions represented as a string, e.g.,"rwx"
,"r-x"
,"r--"
, etc. The default is"r-x"
.
{
"jsonrpc": "2.0",
"method": "reserve",
"params":
{
"address": 2097152,
"length": 65536
},
"id": 1
}
{
"jsonrpc": "2.0",
"method": "reserve",
"params":
{
"address": 23687168,
"protection": "r-x",
"bytes": [127, 69, 76, 70, 2, 1, 1, ..., 0]
},
"id": 1
}
The "instruction"
message sends information about a single instruction
in the binary file.
"address"
: the virtual address of the instruction. This can be a relative address for Position Independent Code (PIC) binaries, or an absolute address for non-PIC binaries."length"
: the length of the instruction in bytes."offset"
: the file offset of instruction in the input binary.
Note that it is not necessary to send an "instruction" message for every instruction in the input binary. Instead, only send an "instruction" message for patch locations, and instructions within the x86-64 short jump distance of a patch location. This is all instructions within the range of [-128..127] bytes of a patch location instruction.
The E9Patch tool does not validate the information and simply trusts the information to be correct.
{
"jsonrpc": "2.0",
"method": "instruction",
"params":
{
"address":4533271,
"length":3,
"offset":338967
},
"id": 10
}
The "patch"
message tells E9Patch to patch a given instruction.
"offset"
: the file-offset that identifies an instruction previously sent via an "instruction" message."trampoline"
: a trampoline name (sent by a previous "trampoline" message) or a trampoline template."metadata"
: a set of key-value pairs mapping macro names to data represented in the trampoline template format. This metadata will be used to instantiate the trampoline before it is emitted in the rewritten binary.
Note that patch messages should be sent in reverse order as they appear in the binary file. That is, the patch location with the highest file offset should be sent first, then the second highest should be sent second, and so forth. This is to implement the reverse execution order strategy which is necessary to manage the complex dependencies between patch locations.
{
"jsonrpc": "2.0",
"method": "patch",
"params":
{
"trampoline": "$print",
"metadata":
{
"$asmStr": "jmp 0x406ac0\n",
"$asmStrLen": {"int32":13}
},
"offset":338996
},
"id": 43
}
The "options"
message allows E9Patch command-line options to be passed using
the JSON-RPC interface.
The new options will be applied to subsequent messages.
For the complete list of command-line options, see:
./e9patch --help
"argv"
: a list of command-line options.
{
"jsonrpc": "2.0",
"method": "options",
"params":
{
"argv": ["--tactic-T3=false", "--mem-mapping-size=4096"]
},
"id": 777
}
The "emit"
message instructs E9Patch to emit the patched binary file.
"filename"
: the path where the patched binary file is to be written to."format"
: the format of the patched binary. Supported values include"binary"
(an ELF binary)"patch"
(a binary diff) and"patch.gz"
/"patch.bz2"
/"patch.xz"
(a compressed binary diff).
{
"jsonrpc": "2.0",
"method": "emit",
"params":
{
"filename": "a.out",
"format": "binary"
},
"id": 82535
}
E9Tool is the default frontend for E9Patch. Although it is possible to create new frontends for E9Patch, this can be quite complicated and require a lot of code. An alternative is the plugin mechanism for E9Tool, as documented below.
An E9Tool plugin is a shared object that exports specific functions. These functions will be invoked by E9Tool at different stages of the patching process. Some tasks, such as disassembly, will be automatically handled by the E9Tool frontend.
The E9Tool plugin API is simple and consists of the following functions:
e9_plugin_init(const Context *cxt)
: Called once before the binary is disassembled.e9_plugin_event(const Context *cxt, Event event)
: Called once for each event (see theEvent
enum).e9_plugin_match(const Context *cxt)
: Called once for each match location.e9_plugin_code(const Context *cxt)
: Called once per trampoline template (code).e9_plugin_data(const Context *cxt)
: Called once per trampoline template (data).e9_plugin_patch(const Context *cxt)
: Called for each patch location.e9_plugin_fini(const Context *cxt)
: Called once after all instructions have been patched.
Note that each function is optional, and the plugin can choose not to define it. However, the plugin must define at least one function to be considered valid.
Each function takes a cxt
argument of type Context
defined in e9plugin.h
.
The Context
structure contains several fields, including:
out
: is the JSON-RPC output stream that is sent to the E9Patch backend. The plugin can directly emit messages to this stream.argv
: is a vector of all command-line options passed in using E9Tool's--plugin
option.context
: is the plugin-defined context, which is the return value of thee9_plugin_init()
function.elf
: is the input ELF file.Is
: is a vector containing all disassembled instructions, sorted by address.idx
: is the index (intoIs
) of the instruction being matched/patched.I
: is detailed information about the instruction being matched/patched.id
: is the current patch ID.
Note that:
- Not all
Context
fields are defined for each operation. For example, theIs
field will be undefined before the input binary has been disassembled. Undefined fields will have the valueNULL
/-1
, depending on the type. - The
I
structure (if defined) is temporary, and will be immediately destroyed once the plugin function returns. Plugins must not store references to this object. - The
Is
array (if defined) is persistent, and the plugin may safely store references to this object untile9_plugin_fini()
returns.
The API is designed to be highly flexible. Basically, the plugin API
functions are expected to send JSON-RPC messages (or parts of messages)
directly to the E9Patch backend by writing to the out
output stream.
See the E9Patch JSON-RPC interface for more
information.
Also see the e9tool.h
header file for useful functions that can be used by
plugins.
Plugins are invoked using the E9Tool --match
/-M
or --patch
/-P
options.
For example:
g++ -std=c++11 -fPIC -shared -o myPlugin.so myPlugin.cpp -I src/e9tool/
./e9tool -M 'plugin(myPlugin).match() > 0x333' -P 'plugin(myPlugin).patch()' xterm
The syntax is as follows:
-M
: Selects the E9Tool "match" command-line option. This tells E9Tool which instructions should be patched.-P
: Selects the E9Tool "patch" command-line option. This tells E9Tool how each matching instruction should be patched.plugin(myPlugin).match() > 0x333
: Specifies that we should only rewrite instructions for which thee9_plugin_match()
(see below) function returns a value greater than0x333
.plugin(myPlugin).patch()
: Specifies that instrument the program using thee9_plugin_code()
/e9_plugin_patch()
functions (see below).
For an example plugin, see examples/plugin/example.cpp
.
The e9_plugin_init()
function is called once when the plugin is first
loaded, and before the input binary is disassembled.
Typically, the e9_plugin_init()
function is used to complete the
following tasks, as required:
- Initialize the plugin (if necessary)
- Setup one (or more) trampoline templates
(e.g., using
e9tool::sendTrapTrampolineMessage()
) - Reserve parts of the virtual address space
(e.g., using
e9tool::sendReserveMessage()
) - Load ELF binaries into the virtual address space
(e.g., using
e9tool::sendELFFileMessage()
) - Etc.
The e9_plugin_init()
function returns an optional context
(of type
void *
) that will be passed to all other API calls through the
cxt->context
field.
If not needed, the e9_plugin_init()
function can simply return NULL
.
The e9_plugin_event()
function is called on certain events, as indicated
by the Event
enum.
The Event
enum has the following values:
EVENT_DISASSEMBLY_COMPLETE
: Disassembly completed.EVENT_MATCHING_COMPLETE
: Matching completed.EVENT_PATCHING_COMPLETE
: Patching completed.
The e9_plugin_match()
function is called whenever the plugin is
invoked during matching using the plugin(NAME).match()
syntax.
Here, cxt->I
will indicate the instruction being matched, and the
e9_plugin_match()
should return an integer value (of type intptr_t
)
that will be used in evaluation of the matching expression.
The e9_plugin_code()
function specifies code (in the template
specification language) that will be invoked when a matching instruction is
patched using the plugin(NAME).patch()
syntax.
The code emitted by the e9_plugin_code()
function must be
instruction independent (i.e., cxt->I
will be NULL
).
If instruction-dependent code is necessary, then emit a macro name
(e.g., "$myCode"
) and instantiate the macro using the e9_plugin_patch()
function (see below).
The e9_plugin_code()
function may be invoked more than once to support
trampoline composition.
The e9_plugin_data()
function is similar to e9_plugin_code()
,
except it is used to specify data (e.g., the assembly string, etc.) rather
than executable code.
This function can be omitted if no data is necessary.
Any data that is emitted here can be referenced by the trampoline code
using labels.
As with e9_plugin_code()
, the e9_plugin_data()
function is
instruction independent.
If instruction-dependent data is necessary, then emit a macro name
(e.g., "$myData"
) and instantiate the macro using e9_plugin_patch()
function (see below).
As with e9_plugin_code()
, the e9_plugin_code()
function may be
invoked more than once.
The e9_plugin_patch()
function is called once per matching instruction
(stored in cxt->I
), and can be used to instantiate macros with
instruction-specific code or data.
Each macro is instantiated by writing key:value
pairs to cxt->out
,
where key
is a macro name and value
is a value specified in trampoline
template format.
For example, if the e9_plugin_code()
and e9_plugin_data()
functions
emitted the "$myCode"
and "$myData"
macro names respectively, then these
can be instantiated by emitting the following to cxt->out
:
"$myCode":CODE, "$myData":DATA
Here, CODE
and DATA
are the instruction-specific code and data
(in trampoline template format) respectively.
The e9_plugin_patch()
function can also be used to instantiate macros
defined by trampoline template messages.
The e9_plugin_fini()
function is called once after the patching process
is complete, and can be used for any cleanup if necessary.