04 - ELF Hooking

The objective of this tutorial is to hook a library function.


In the previous tutorial, we saw how to swap symbol names in a shared library. We will now see the mechanism for hooking a function in a shared library.

The targeted library is the standard math library (libm.so), and we will insert a hook on the exp function so that \(\exp(x) = x + 1\). The source code of the sample that uses this function is provided in the following listing:

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

int main(int argc, char **argv) {
  if (argc != 2) {
    printf("Usage: %s <a> \n", argv[0]);
    exit(-1);
  }

  int a = atoi(argv[1]);
  printf("exp(%d) = %f\n", a, exp(a));
  return 0;
}

The hook function is as follows:

double hook(double x) {
  return x + 1;
}

Compiled with gcc -Os -nostdlib -nodefaultlibs -fPIC -Wl,-shared hook.c -o hook.

To inject this hook into the library, we use the add() (segment) method:

Binary.add(self, arg: lief._lief.ELF.DynamicEntry, /) lief._lief.ELF.DynamicEntry
Binary.add(self, section: lief._lief.ELF.Section, loaded: bool = True, pos: lief._lief.ELF.Binary.SEC_INSERT_POS = SEC_INSERT_POS.AUTO) lief._lief.ELF.Section
Binary.add(self, segment: lief._lief.ELF.Segment, base: int = 0) lief._lief.ELF.Segment
Binary.add(self, note: lief._lief.ELF.Note) lief._lief.ELF.Note

Overloaded function.

  1. add(self, arg: lief._lief.ELF.DynamicEntry, /) -> lief._lief.ELF.DynamicEntry

dynamic_entry

  1. add(self, section: lief._lief.ELF.Section, loaded: bool = True, pos: lief._lief.ELF.Binary.SEC_INSERT_POS = SEC_INSERT_POS.AUTO) -> lief._lief.ELF.Section

    Add the given Section to the binary.

    If the section does not aim at being loaded in memory, the loaded parameter has to be set to False (default: True)

  2. add(self, segment: lief._lief.ELF.Segment, base: int = 0) -> lief._lief.ELF.Segment

Add a new Segment in the binary

  1. add(self, note: lief._lief.ELF.Note) -> lief._lief.ELF.Note

Add a new Note in the binary

First, we find the code for our hook function and add it to the library:

import lief

libm = lief.parse("/usr/lib/libm.so.6")
hook = lief.parse("hook")

exp_symbol  = libm.get_symbol("exp")
hook_symbol = hook.get_symbol("hook")

code_segment = hook.segment_from_virtual_address(hook_symbol.value)
segment_added = libm.add(code_segment)

Once the stub is injected, we must calculate the new address for the exp symbol and update it:

new_address = segment_added.virtual_address + hook_symbol.value - code_segment.virtual_address
exp_symbol.value = new_address
exp_symbol.type  = lief.ELF.Symbol.TYPE.FUNC  # it might have been GNU_IFUNC

Note that we must update the symbol type to a regular FUNC because, on many distributions, libm.so is built with automatic hardware detection and exposes symbols as GNU_IFUNC, which uses a different dynamic binding protocol compared to regular functions.

Finally, we write the patched library to a file in the current directory:

libm.write("libm.so.6")

To test the patched library:

$ ./do_math.bin 1
exp(1) = 2.718282
$ LD_LIBRARY_PATH=. ./do_math.bin 1
exp(1) = 2.000000