Detour hooking

Index | Up


Table of Contents

1. What does “hooking” mean?

Hooking consists in intercepting and altering function calls or event passed between software components. In this case, we will be focusing on function hooking for the x86 and x86_64 architectures.

Imagine the following foo function, called from main:

#include <stdio.h>

double foo(double a, double b) {
    printf("foo: %.1f + %.1f = %.1f\n", a, b, a + b);
    return a + b;
}

int main(void) {
    foo(5.0, 2.0);
    return 0;
}

Imagine this was a very important function that we wanted to intercept of modify. The idea is that when main calls foo, we somehow intercept the call and the control is passed to our hook function. From there, we could call the original function, change the behavior, spoof the returned value, etc.

2. Detour hooking

Detour hooking is a hooking technique where we overwrite the first bytes of the target function (foo) to introduce a jump to our hook function.

First, let’s have a look at the disassembly of this foo function using rizin.

; CALL XREF from main @ 0x11b5
fcn.00001139 ; fcn.00001139(int64_t arg7, int64_t arg8);
fcn.00001139 ;  arg int64_t arg7 @ xmm0
fcn.00001139 ;  arg int64_t arg8 @ xmm1
fcn.00001139 ;  var int64_t var_18h @ stack - 0x18
fcn.00001139 ;  var int64_t var_10h @ stack - 0x10
fcn.00001139         55                      push    rbp
fcn.00001139+0x1     48 89 e5                mov     rbp, rsp
fcn.00001139+0x4     48 83 ec 10             sub     rsp, 0x10
fcn.00001139+0x8     f2 0f 11 45 f8          movsd   qword [rbp - 8], xmm0 ; arg7
fcn.00001139+0xd     f2 0f 11 4d f0          movsd   qword [rbp - 0x10], xmm1 ; arg8
fcn.00001139+0x12    f2 0f 10 45 f8          movsd   xmm0, qword [rbp - 8]
fcn.00001139+0x17    66 0f 28 c8             movapd  xmm1, xmm0
fcn.00001139+0x1b    f2 0f 58 4d f0          addsd   xmm1, qword [rbp - 0x10]
fcn.00001139+0x20    f2 0f 10 45 f0          movsd   xmm0, qword [rbp - 0x10]
fcn.00001139+0x25    48 8b 45 f8             mov     rax, qword [rbp - 8]
fcn.00001139+0x29    66 0f 28 d1             movapd  xmm2, xmm1
fcn.00001139+0x2d    66 0f 28 c8             movapd  xmm1, xmm0
fcn.00001139+0x31    66 48 0f 6e c0          movq    xmm0, rax
fcn.00001139+0x36    48 8d 05 92 0e 00 00    lea     rax, [rip + str.foo]
fcn.00001139+0x3d    48 89 c7                mov     rdi, rax          ; const char *format
fcn.00001139+0x40    b8 03 00 00 00          mov     eax, 3
fcn.00001139+0x45    e8 ad fe ff ff          call    sym.imp.printf    ; sym.imp.printf ; int printf(const char *format)
fcn.00001139+0x4a    f2 0f 10 45 f8          movsd   xmm0, qword [rbp - 8]
fcn.00001139+0x4f    f2 0f 58 45 f0          addsd   xmm0, qword [rbp - 0x10]
fcn.00001139+0x54    66 48 0f 7e c0          movq    rax, xmm0
fcn.00001139+0x59    66 48 0f 6e c0          movq    xmm0, rax
fcn.00001139+0x5e    c9                      leave
fcn.00001139+0x5f    c3                      ret

At the right of the instructions, you can see their corresponding bytes. The idea is to overwrite the first N bytes of the function in memory, so instead of those push and mov instructions, we jump to our function.

Let’s see how we can do that from assembly. Note that the byte order of the addresses is “reversed” because of endianness.

; x86
mov     eax, 0x11223344          ; b8 44 33 22 11
jmp     eax                      ; ff e0

; x86_64
movabs  rax, 0x1122334455667788  ; 48 b8 88 77 66 55 44 33 22 11
jmp     rax                      ; ff e0

Now we know that we need to overwrite 7 bytes for 32-bit programs and 12 bytes for 64-bit programs.

After overwriting, assuming our hook function is at 0xDEADBEEF our function would be:

fcn.00001139         48 b8 ef be ad de 00    movabs  rax, 0xDEADBEEF
fcn.00001139         00 00 00
fcn.00001139+0x1     ff e0                   jmp     rax
fcn.00001139+0x4     f8                      clc
fcn.00001139+0x8     f2 0f 11 4d f0          movsd   qword [rbp - 0x10], xmm1 ; arg8
; ...

3. Hooking from C

The basic process from C would be:

  1. Get the address of the foo and hook functions.
  2. Store the first N bytes of foo, so we can restore them when necessary.
  3. Create the final jump bytes by placing the address of our hook function.
  4. Change the permissions of the foo function to make sure we can write to them.
  5. Overwrite the first N bytes of foo with our jump bytes.
  6. Restore the old permissions for foo.

First, we need to declare the placeholder for the jump bytes. We will change the declaration depending on the architecture with #ifdef.

#include <stdint.h>

#ifdef __i386__
static uint8_t jmp_bytes[] = { 0xB8, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0 };
#define JMP_BYTES_OFF 1 /* Offset inside the array where the ptr should go */
#else
static uint8_t jmp_bytes[] = { 0x48, 0xB8, 0x00, 0x00, 0x00, 0x00,
                               0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0 };
#define JMP_BYTES_OFF 2 /* Offset inside the array where the ptr should go */
#endif

We will use JMP_BYTES_OFF to specify the position inside the jmp_bytes array where the hook address will be.

We will also declare a protect_addr function for changing the permission of the memory page where the ptr is. Since I am on linux, I will use mprotect, but if you are on windows you should use something like VirtualProtect (Link).

#include <stdint.h>
#include <stdbool.h>
#include <unistd.h>   /* getpagesize */
#include <sys/mman.h> /* mprotect */

static bool protect_addr(void* ptr, int new_flags) {
    long page_size      = sysconf(_SC_PAGESIZE);
    long page_mask      = ~(page_size - 1);
    uintptr_t next_page = ((uintptr_t)ptr + page_size - 1) & page_mask;
    uintptr_t prev_page = next_page - page_size;
    void* page          = (void*)prev_page;

    if (mprotect(page, page_size, new_flags) == -1)
        return false;

    return true;
}

First, we get the next page by masking the address plus the page size minus one with ~(page_size - 1). We need to add the page size minus one to make sure we don’t align an already aligned address. Then we subtract the page size to get the address of the previous page. For more information see this StackOverflow answer.

Ptr:                 0x12345    0b10010001101000101
Page size:           0x01000    0b00001000000000000
Page size - 1:       0x00FFF    0b00000111111111111
After NOT (Mask):    0xFF000    0b11111000000000000
After adding to ptr: 0x13344    0b10011001101000100
Ptr & Mask:          0x13000    0b10011000000000000
Minus page size:     0x12000    0b10010000000000000

Let’s declare our sample hook.

double hook(double a, double b) {
    printf("hook: intercepted %.1f and %.1f\n", a, b);
    printf("hook: overwriting return value...\n");
    return 420;
}

Now, we need to get the function pointers and store the first N bytes of foo. This is important since these bytes will be used for unhooking and for calling the original function from our hook.

#include <string.h>

void* orig_ptr = &foo;  /* foo(...) */
void* hook_ptr = &hook; /* hook(...) */

/* Store first N bytes of `foo' into `saved_bytes' */
#define N sizeof(jmp_bytes)
static uint8_t saved_bytes[N];
memcpy(saved_bytes, orig_ptr, N);

Then, we place the pointer of our hook into the jmp_bytes array. Note that we pass &hook_ptr instead of hook_ptr directly because we want to copy the function address, not the first 8 bytes of our hook function.

memcpy(&jmp_bytes[JMP_BYTES_OFF], &hook_ptr, sizeof(void*));

Now that we are set up, we can actually hook our function.

/* Try to add WRITE permissions to `orig_ptr' */
if (!protect_addr(orig_ptr, PROT_READ | PROT_WRITE | PROT_EXEC))
    return;

/* Overwrite the first N bytes of `foo' with our jmp instruction */
memcpy(orig_ptr, jmp_bytes, sizeof(jmp_bytes));

/* Restore old protection, assuming it was r-x */
if (!protect_addr(orig_ptr, PROT_READ | PROT_EXEC))
    return;

And with that, our function is hooked. Every time foo gets called, our hook function will get called instead.

Unhooking the function is easy, we just need to restore saved_bytes.

if (!protect_addr(orig_ptr, PROT_READ | PROT_WRITE | PROT_EXEC))
    return;

/* Restore the first N bytes of `foo' */
memcpy(orig_ptr, saved_bytes, sizeof(saved_bytes));

if (!protect_addr(orig_ptr, PROT_READ | PROT_EXEC))
    return;

Calling the original function from our hook is as simple as unhooking, calling with the intercepted parameters and hooking again.

typedef double (*orig_t)(double, double);

double result;

unhook(orig_ptr, saved_bytes);
result = (orig_t)orig_ptr(a, b);
hook(orig_ptr, jmp_bytes);

4. Detour hooking library

I made a simple detour hooking library in pure C for both GNU/Linux and Windows. The platform-specific function is protect_addr().

To use it, you just need to:

  1. Use the LIBDETOUR_DECL_TYPE macro to specify the type of your original function.
  2. Declare a Detour Context, and initialize it by calling libdetour_init with the original and hook function pointers.
  3. To add the hook, call libdetour_add with the context you just declared.
  4. To call the original function, use the LIBDETOUR_ORIG_CALL or LIBDETOUR_ORIG_GET macros, depending on if you want to store the returned value.
  5. When you are done, remove the hook by passing the Detour Context to the libdetour_del function.

You can find the code, full usage and an example in the GitHub repository.