1

To be specific, I cannot recompile this binary, nor do I have access to the sourcecode.

The functions are not defined within a shared library.

So, how can I go about changing a function, or preferably detouring it to a new function?

And if it's possible, use dlsym/dlopen to get my new modified code from a shared library, so I don't have to edit the binary by hand every time I want to change something.

Oh, one more thing, editing the actual binary is something I can do/I have access to.

And just for extra info, I have Radare2 installed, as well as GDB with pwndbg, so any solutions using those tools, or builtin GNU/Linux debugging tools would be appreciated.

EDIT:

I am a beginner to reverse engineering things, but not to the point I have zero idea what I'm doing.

0

2 Answers 2

6

You have several options here, but I like this one.

Since the code is already inside the executable, so you have to patch the entrypoint to jump to a new function. To do so, I suggest to use a shared object (.so). In this module you will both: the new function and a 'patcher function'. The latter function must be defined with __attribute__((constructor)), this attribute will force the ELF loader to call this function before the main function from your original executable is called.

The patcher function will set write access at the entrypoint of the old function with mprotect (2), encode the jump instruction, and it's probably better to remove the write access you needed for the patch.

Once you have your shared object, you can inject it using the LD_PRELOAD variable.

Edit with an example, since you didn't mentioned the architecture, I assumed this is for x86-64:

First this is a target, we want to change the function my_rand to return 0xdeadbeef.

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

int my_rand(void)
{
        srand(time(NULL));
        return rand() + rand() ^ rand();
}

int main(void)
{
        printf("random value: %08x\n", my_rand());
}

To find the RVA, we can use nm because we have the symbol. You might have to find it manually.

nm target|grep my_rand
0000000000001169 T my_rand

Finally, the code of the patcher:

#include <sys/mman.h>
#include <unistd.h>
#include <dlfcn.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <link.h>

static int new_random(void)
{
        return 0xdeadbeef;
}

static void emit_jump_to_address(uint64_t address, uint64_t jump_destination)
{
        long page_size = sysconf(_SC_PAGESIZE);
        void* aligned_address = (void*)(address & ~(page_size - 1));
        if (mprotect(aligned_address, page_size, PROT_READ|PROT_WRITE|PROT_EXEC) < 0)
        {
                perror("mprotect");
                return;
        }
        *(uint16_t*)(address + 0x0) = 0xb848;           // mov rax, Iv
        *(uint64_t*)(address + 0x2) = jump_destination; // mov rax, jump_destination
        *(uint16_t*)(address + 0xa) = 0xe0ff;           // jmp rax
        if (mprotect(aligned_address, page_size, PROT_READ|PROT_EXEC) < 0)
        {
                perror("mprotect");
                return;
        }
}

// ref: https://stackoverflow.com/questions/19451791/get-loaded-address-of-a-elf-binary-dlopen-is-not-working-as-expected
uint64_t get_image_base(void)
{
        struct link_map* lm = dlopen(NULL, RTLD_NOW);
        return (uint64_t)lm->l_addr;
}

__attribute__((constructor)) void patcher(void)
{
        uint64_t target;

        target = get_image_base();
        target += 0x1169; // RVA
        printf("[!] targeted function is at %p\n", (void const*)target);
        printf("[!] new function is at %p\n", (void const*)&new_random);
        emit_jump_to_address(target, (uint64_t)&new_random);
}

This source code was compiled with clang and injected using LD_PRELOAD:

$ clang patcher.c  -o patcher.so -shared -ldl
$ env LD_PRELOAD=./patcher.so ./target
[!] targeted function is at 0x7fe05f907169
[!] new function is at 0x7fe05f8c11f0
random value: deadbeef

Note, I'm using fish, so the env is required.

4
  • Thanks, I really love this answer. It's a really creative way to go about doing this. One thing though, isn't attribute a GCC specific macro? It's not a deal breaker, as I'm pretty sure Clang can handle those macros, but if it can't, is there another way to achieve the same effect?
    – Walaryne
    Commented Jan 17, 2019 at 17:03
  • Also, if it's not too much trouble, do you think you could write up some example code? Just a simple theoretical function in a binary, and what the shared library code could look like. Even some ASM representation somewhere, if that's possible. I mostly need clarification on how to use the "mprotect() then encode jmp" portion.
    – Walaryne
    Commented Jan 17, 2019 at 17:45
  • 1
    __attribute__((constructor)) is supported by clang. And I'll try to write a quick example.
    – wisk
    Commented Jan 21, 2019 at 16:18
  • I had tagged the question x86_64, should have said it explicitly in the question though, sorry. Thanks for the example.
    – Walaryne
    Commented Jan 21, 2019 at 21:00
2

First off, I would like to thank @wisk for his answer. We've been trying to patch some software running in an experiment on the ISS and building from his answer we were finally able to figure this out.

So, this is a reworked version of @wisk's solution with some additional features:

  1. the ability to fetch the symbol with dlsym (our case could not use this, but it was helpful for testing)
  2. the trampoline code uses inline assembly that is copied into place, instead of hard-coding the assembly instructions
  3. the ability to run on either AMD64 or ARM64

patch code

gcc patcher.c -o libpatcher.so -shared -ldl -fPIC:

#define _GNU_SOURCE
#include <string.h>
#include <byteswap.h>
#include <stdint.h>
#include <dlfcn.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <link.h>
#include <stdio.h>

static void (*__original_routine)();

static void __patch_routine(void)
{
    fprintf(stderr, "patch routine!\n");
}

#if defined(__x86_64__)
uint64_t function_start_offset = 8;
uint64_t fn_padding_size = 3;
#elif defined(__aarch64__)
uint64_t function_start_offset = 0;
uint64_t fn_padding_size = 4;
#endif

static inline void trampoline()
{
asm volatile (
"_trampoline_begin:\n"
#if defined(__x86_64__)
    "mov 0x2(%rip), %rax\n" // <-- fetch data after trampoline
    "jmp *%rax\n"
#elif defined(__aarch64__)
    "ldr x3, .+(_trampoline_end-_trampoline_begin)\n" // <-- fetch data after trampoline
    "br  x3\n"
#endif
"_trampoline_end:\n"
);
}
static inline void trampoline_end() { }

static void patch_function(uint64_t address, uint64_t jump_destination)
{
    long page_size = sysconf(_SC_PAGESIZE);
    void* aligned_address = (void*)(address & ~(page_size - 1));
    if (mprotect(aligned_address, page_size, PROT_READ|PROT_WRITE|PROT_EXEC) < 0)
    {
        perror("mprotect");
        return;
    }
    uint8_t *ptr = (uint8_t *)(address);
    uint64_t fn_size = (uint64_t)&trampoline_end - (uint64_t)&trampoline - fn_padding_size;
    fn_size -= function_start_offset;
    uint64_t trampoline_begin = (uint64_t)&trampoline + function_start_offset;
    memcpy(ptr, (void*)trampoline_begin, fn_size);
    ptr += fn_size;
    /* store the jump destination here */
    *((uint64_t *)ptr) = jump_destination;
    /* turn write protection back on */
    if (mprotect(aligned_address, page_size, PROT_READ|PROT_EXEC) < 0)
    {
        perror("mprotect");
        return;
    }
}
static uint64_t get_address(char *function)
{
    uint64_t target;
#if 0
    // ref: https://stackoverflow.com/questions/19451791/get-loaded-address-of-a-elf-binary-dlopen-is-not-working-as-expected
    struct link_map* lm = (struct link_map*)dlopen(NULL, RTLD_NOW);
    target = (uint64_t)lm->l_addr;
    printf("[!] image base is at %p\n", (void const*)target);
    target += 0x400910; // RVA found by using readelf
#else
    target = (uint64_t)dlsym(RTLD_DEFAULT, function);
#endif
    return target;
}

__attribute__((constructor))
static void patcher(void)
{
    uint64_t target;
    target = get_address("original_routine");
    if (!target)
    {
        fprintf(stderr, "failed to fetch routine to replace.\n");
        exit(1);
    }
    printf("[!] targeted function is at %p\n", (void const*)target);
    printf("[!] new function is at %p\n", (void const*)&__patch_routine);
    __original_routine = (typeof(__original_routine))target;
    patch_function((uint64_t)__original_routine, (uint64_t)&__patch_routine);
}

test program code

gcc -rdynamic -fPIC -o program program.c:

#include <stdio.h>

void original_routine(void)
{
    fprintf(stderr, "original.\n");
}

int main(int argc, char *argv[])
{
    original_routine();
    fprintf(stderr, "routine done.\n");
    return 0;
}

execution

With these two pieces in place, we can test on amd64:

$ uname -m; LD_PRELOAD=$PWD/libpatcher.so ./program
x86_64
[!] targeted function is at 0x557a2be7e149
[!] new function is at 0x7f84757d31f9
patch routine!
routine done.

and on arm64:

$ uname -m; LD_PRELOAD=$PWD/libpatcher.so ./program
aarch64
[!] targeted function is at 0x400910
[!] new function is at 0x5500832a0c
patch routine!
routine done.
root@d75affb8f124:/

Not the answer you're looking for? Browse other questions tagged or ask your own question.