Skip to content

Latest commit

 

History

History
771 lines (630 loc) · 27.5 KB

e9patch-programming-guide.md

File metadata and controls

771 lines (630 loc) · 27.5 KB

E9Patch Programming Guide

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:

  1. E9Tool Call Trampolines [simple, high-level, rigid, recommended method]
  2. E9Patch JSON-RPC Interface [advanced, low-level, flexible]
  3. 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.


Contents


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 expression j.* (delimited by slash / characters). For the x86_64, only jump instructions begin with j, 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 function entry() in the counter 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:

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.

Parameters:

  • "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.

Example:

    {
        "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.

Parameters:

  • "name": the name of the trampoline. Valid names must start with the dollar character.
  • "template": the template of the trampoline.

Notes:

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 single jmpq 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.

Example:

    {
        "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.

Parameters:

  • "absolute": [optional] if true 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 to true.
  • "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 of mmap() 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".

Example:

    {
        "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.

Parameters:

  • "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.

Notes:

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.

Example:

    {
        "jsonrpc": "2.0",
        "method": "instruction",
        "params":
        {
            "address":4533271,
            "length":3,
            "offset":338967
        },
        "id": 10
    }

The "patch" message tells E9Patch to patch a given instruction.

Parameters:

  • "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.

Notes:

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.

Example:

    {
        "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

Parameters:

  • "argv": a list of command-line options.

Example:

    {
        "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.

Parameters:

  • "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).

Example:

    {
        "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:

  1. e9_plugin_init(const Context *cxt): Called once before the binary is disassembled.
  2. e9_plugin_event(const Context *cxt, Event event): Called once for each event (see the Event enum).
  3. e9_plugin_match(const Context *cxt): Called once for each match location.
  4. e9_plugin_code(const Context *cxt): Called once per trampoline template (code).
  5. e9_plugin_data(const Context *cxt): Called once per trampoline template (data).
  6. e9_plugin_patch(const Context *cxt): Called for each patch location.
  7. 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 the e9_plugin_init() function.
  • elf: is the input ELF file.
  • Is: is a vector containing all disassembled instructions, sorted by address.
  • idx: is the index (into Is) 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, the Is field will be undefined before the input binary has been disassembled. Undefined fields will have the value NULL/-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 until e9_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 the e9_plugin_match() (see below) function returns a value greater than 0x333.
  • plugin(myPlugin).patch(): Specifies that instrument the program using the e9_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:

  1. Initialize the plugin (if necessary)
  2. Setup one (or more) trampoline templates (e.g., using e9tool::sendTrapTrampolineMessage())
  3. Reserve parts of the virtual address space (e.g., using e9tool::sendReserveMessage())
  4. Load ELF binaries into the virtual address space (e.g., using e9tool::sendELFFileMessage())
  5. 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.