The objective of this tutorial is to hook an imported function in an ELF binary.
Scripts and materials are available here: materials
By Romain Thomas - @rh0main
Hooking imported functions by infecting the .got
section is a well-known technique [1] [2] and this tutorial will be focused
on its implementation using LIEF.
These figures illustrate the plt/got
mechanism:
With lazy binding, the first time that the function is called the got
entry redirects to the plt instruction.¶
The Second time, got
entry holds the address in the shared library.¶
Basically the infection is done in two steps:
Firstly, we inject our hook
Secondly, we redirect the targeted function to our hook by patching the
got
It can be summed up by the following figure:
As example, we will use a basic crackme which performs a memcmp(3) on the flag and user’s input.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Damn_YoU_Got_The_Flag
char password[] = "\x18\x3d\x31\x32\x03\x05\x33\x09\x03\x1b\x33\x28\x03\x08\x34\x39\x03\x1a\x30\x3d\x3b";
inline int check(char* input);
int check(char* input) {
for (int i = 0; i < sizeof(password) - 1; ++i) {
password[i] ^= 0x5c;
}
return memcmp(password, input, sizeof(password) - 1);
}
int main(int argc, char **argv) {
if (argc != 2) {
printf("Usage: %s <password>\n", argv[0]);
return EXIT_FAILURE;
}
if (strlen(argv[1]) == (sizeof(password) - 1) && check(argv[1]) == 0) {
puts("You got it !!");
return EXIT_SUCCESS;
}
puts("Wrong");
return EXIT_FAILURE;
}
The flag is xored with 0x5C
. To validate the crackme, the user has to enter Damn_YoU_Got_The_Flag
:
$ crackme.bin foo
Wrong
$ crackme.bin Damn_YoU_Got_The_Flag
You got it !!
The hook will consist in printing arguments of memcmp
and returning 0
:
#include "arch/x86_64/syscall.c"
#define stdout 1
int my_memcmp(const void* lhs, const void* rhs, int n) {
const char msg[] = "Hook memcmp\n";
_write(stdout, msg, sizeof(msg));
_write(stdout, (const char*)lhs, n);
_write(stdout, "\n", 2);
_write(stdout, (const char*)rhs, n);
_write(stdout, "\n", 2);
return 0;
}
As the hook is going to be injected into the crackme, it must have the following requirements:
Assembly code must be position independant (compiled with -fPIC
or -pie/-fPIE
flags)
Don’t use external libraries such as libc.so
(-nostdlib -nodefaultlibs
flags)
Due to the requirements, the hook is compiled with: gcc -nostdlib -nodefaultlibs -fPIC -Wl,-shared hook.c -o hook
.
The first step is to inject the hook into the binary. To do so we will add a Segment
:
import lief
crackme = lief.parse("crackme.bin")
hook = lief.parse("hook")
segment_added = crackme.add(hook.segments[0])
All assembly code of the hook stands in the first LOAD
segment of hook
.
Once the hook added, its virtual address is virtual_address
of segment_added
and we can processed to the got
patching.
got
¶LIEF provides a function to easily patch the got
entry associated with a Symbol
:
Overloaded function.
patch_pltgot(self, symbol_name: str, address: int) -> None
Patch the imported symbol’s name with the address
patch_pltgot(self, symbol: lief._lief.ELF.Symbol, address: int) -> None
Patch the imported Symbol
with the address
The offset of the memcmp
function is stored in the value
attribute of the associated dynamic symbol. Thus its virtual address will be:
my_memcpy
: value
+ segment_added.virtual_address
my_memcmp = hook.get_symbol("my_memcmp")
my_memcmp_addr = segment_added.virtual_address + my_memcmp.value
Finally we can patch the memcmp
from the crakme with this value:
crackme.patch_pltgot('memcmp', my_memcmp_addr)
And rebuild it:
crackme.write("crackme.hooked")
As a check on the input size is performed before checking the flag value, we have to provide an input with the correct length (no matter its content):
$ crackme.hooked XXXXXXXXXXXXXXXXXXXXX
Hook add
Damn_YoU_Got_The_Flag
XXXXXXXXXXXXXXXXXXXXX
You got it !!
References