💾 Archived View for aphrack.org › issues › phrack58 › 8.gmi captured on 2022-03-01 at 15:51:22. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2021-12-03)
-=-=-=-=-=-=-
==Phrack Inc.== Volume 0x0b, Issue 0x3a, Phile #0x08 of 0x0e |=-----------------=[ IA32 ADVANCED FUNCTION HOOKING ]=------------------=| |=-----------------------------------------------------------------------=| |=-------------------=[ mayhem <mayhem@hert.org> ]=---------------------=| |=-----------------------=[ December 08th 2001 ]=------------------------=| --[ Contents 1 - Introduction 1.1 - History 1.2 - New requirements 2 - Hooking basics 2.1 - Usual techniques 2.2 - Things not to forget 3 - The code explained 4 - Using the library 4.1 - The API 4.2 - Kernel symbol resolution 4.3 - The hook_t object 5 - Testing the code 5.1 - Loading the module 5.2 - Playing around a bit 5.3 - The code 6 - References --[ 1 - Introduction Abusing, logging , patching , or even debugging : obvious reasons to think that hooking matters . We will try to understand how it works . The demonstration context is the Linux kernel environment . The articles ends with a general purpose hooking library the linux kernel 2.4 serie, developped on 2.4.5 and running on IA32, it's called LKH, the Linux Kernel Hooker. ----[ 1.1 - History One of the reference on the function hijacking subject subject has been released in November 1999 and is written by Silvio Cesare (hi dude ;-). This implementation was pretty straightforward since the hooking was consisting in modifying the first bytes of the function jumping to another code , in order to filter access on the acct_process function of the kernel, keeping specific processes from beeing accounted . ----[ 1.2 - New requirements Some work has been done since that time : - Pragmatic use of redirection often (always ?) need to access the original parameters, whatever their number and their size (for example if we want to modify and forward IP packets) . - We may need to disable the hook on demand, which is perfect for runtime kernel configuration . We may want to call the original functions (discrete hooking, used by monitoring programs) or not (aggressive hooking, used by security patches to manage ACL - Access Control Lists - ) on kernel ojects . - In some cases, we may also want to destroy the hook just after the first call, for example to do statistics (we can hook one time every seconds or every minuts) . --[ 2 - Hooking basics ----[ 2.1 Usual techniques Of course, the core hooking code must be done in assembly language, but the hooking wrapping code is done in C . The LKH high level interface is described in the API section . May we first understand some hooking basics . This is basicaly what is hooking : - Modify the begin of a function code to points to another code (called the 'hooking code') . This is a very old and efficient way to do what we want . The other way to do this is to patch every calls in the code segment referencing the function . This second method has some advantages (it's very stealth) but the implementation is a bit complex (memory area blocks parsing, then code scanning) and not very fast . - Modify in runtime the function return address to takes control when the hooked function execution is over . - The hook code must have two different parts, the first one must be executed before the function (prepare the stack for accessing para- meters, launch callbacks, restore the old function code) , the second one must be executed after (reset the hook again if needed) - Default parameters (defining the hook behaviour) must be set during the hook creation (before modifying the function code) . Function dependant parameters must be fixed now . - Add callbacks . Each callback can access and even modify the original function parameters . - Enable, disable, change parameters, add or remove callbacks when we want . ----[ 2.2 - Things not to forget -> Functions without frame pointer: A important feature is the capability to hook functions compiled with the -fomit-frame-pointer gcc option . This feature requires the hooking code to be %ebp free , that's why we will only %esp is used for stack operations. We also have to update some part (Some bytes here and there) to fix %ebp relative offsets in the hook code . Look at khook_create() in lkh.c for more details on that subject . The hook code also has to be position independant . That's why so many offsets in the hookcode are fixed in runtime (Since we are in the kernel, offsets have to be fixed during the hook creation, but very similar techniques can be used for function hooking in *runtime* processes). -> Recursion We must be able to call the original function from a callback, so the original code has t be restored before the execution of any callback . -> Return values We must returns the correct value in %eax, wether we have callbacks or no, wether the original function is called or no . In the demonstration, the return value of the last executed callback is returned if the original function is not called . If no callbacks and no original function is called, the return value is beyond control. -> POST callbacks You cannot access function parameters if you execute callbacks after the original function . That's why it's a bad idea . However, here is the technique to do it : - Set the hook as aggressive - Call the PRE callbacks . - Call the original function from a callback with its own parameters . - Call the POST callbacks . --[ 3 - The code explained . First we install the hook. A - Overwrite the first 7 bytes of the hijacked routine with an indirect jump pointing to the hook code area . The offset put in %eax is the obsolute address of the hook code, so each time we'll call the hijack_me() function, the hook code will takes control . Before hijack: 0x80485ec <hijack_me>: mov 0x4(%esp,1),%eax 0x80485f0 <hijack_me+4>: push %eax 0x80485f1 <hijack_me+5>: push $0x8048e00 0x80485f6 <hijack_me+10>: call 0x80484f0 <printf> 0x80485fb <hijack_me+15>: add $0x8,%esp After the hijack: 0x80485ec <hijack_me>: mov $0x804a323,%eax 0x80485f1 <hijack_me+5>: jmp *%eax 0x80485f3 <hijack_me+7>: movl (%eax,%ecx,1),%es 0x80485f6 <hijack_me+10>: call 0x80484f0 <printf> 0x80485fb <hijack_me+15>: add $0x8,%esp The 3 instructions displayed after the jmp dont means anything , since gdb is fooled by our hook . B - Reset the original bytes of the hooked function, we need that if we want to call the original function without breaking things . pusha movl $0x00, %esi (1) movl $0x00, %edi (2) push %ds pop %es cld xor %ecx, %ecx movb $0x07, %cl rep movsl The two NULL offsets have actually been modified during the hook creation (since their values depends on the hooked function offset, we have to patch the hook code in runtime) . (1) is fixed with the offset of the buffer containing the first 7 saved bytes of the original function . (2) is fixed with the original function address. If you are familiar with the x86 assembly langage, you should know that these instructions will copy %ecx bytes from %ds:%esi to %es:%edi . Refers to [2] for the INTEL instructions specifications. C - Initialise the stack to allow parameters read/write access and launch our callbacks . We move the first original parameter address in %eax then we push it . leal 8(%esp), %eax push %eax nop; nop; nop; nop; nop nop; nop; nop; nop; nop nop; nop; nop; nop; nop nop; nop; nop; nop; nop nop; nop; nop; nop; nop nop; nop; nop; nop; nop nop; nop; nop; nop; nop nop; nop; nop; nop; nop Note that empty slots are full of NOP instruction (opcode 0x90) . This mean no operation . When a slot is filled (using khook_add_entry function) , 5 bytes are used : - The call opcode (opcode 0xE8) - The calback offset (4 bytes relative address) We choose to set a maximum of 8 callbacks . Each of the inserted callbacks are called with one parameter (the %eax pushed value contains the address of the original function parameters, reposing the stack). D - Reset the stack . add $0x04, %esp We now remove the original function's parameter address pushed in (C) . That way, %esp is reset to its old value (the one before entering the step C). At this moment, the stack does not contains the original function's stack frame since it was overwritten on step (A) . E - Modify the return address of the original function on the stack . On INTEL processors, functions return addresses are saved on the stack, which is not a very good idea for security reasons ;-) . This modification makes us return where we want (to the hook-code) after the original function execution. Then we call the original function. On return, the hook code regains control . Let's look at that carefully : -> First we get our actual %eip and save it in %esi (the end labels points to some code you can easily identify on step E5). This trick is always used in position independant code. 1. jmp end begin: pop %esi -> Then we retreive the old return address reposing at 4(%esp) and save it in %eax . 2. movl 4(%esp), %eax -> We use that saved return address as an 4 bytes offset at the end of the hook code (see the NULL pointer in step H), so we could return to the right place at the end of the hooking process . 3. movl %eax, 20(%esi) -> We modify the return address of the original function so we could return just after the 'call begin' instruction . 4. movl %esi, 4(%esp) movl $0x00, %eax -> We call the original function . The 'end' label is used in step 1, and the 'begin' label points the code just after the "jmp end" (still in step 1) . The original function will return just after the 'call begin' instruction since we changed its return address . 5. jmp *%eax end: call begin F - Back to the hooking code . We set again the 7 evil bytes in the original function 's code . These bytes were reset to their original values before calling the function, so we need to hook the function again (like in step A) . This step is noped (replaced by NOP instructions) if the hook is single-shot (not permanent), so the 7 bytes of our evil indirect jump (step A) are not copied again . This step is very near from step (B) since it use the same copy mechanism (using rep movs* instructions), so refers tothis step for explainations . NULL offsets in the code must be fixed during the hook creation : - The first one (the source buffer) is replaced by the evil bytes buffer . - The second one (the destination buffer) is replaced by the original function entry point address . movl $0x00, %esi movl $0x00, %edi push %ds pop %es cld xor %ecx, %ecx movb $0x07, %cl rep movsb G - Use the original return address (saved on step E2) and get back to the original calling function . The NULL offset you can see (*) must be fixed in step E2 with the original function return address . The %ecx value is then pushed on the stack so the next ret instruction will use it like if it was a saved %eip register on the stack . This returns to the (correct) original place . movl $0x00, %ecx * pushl %ecx ret --[ 4 - Using the library ----[ 4.1 - The API The LKH API is pretty easy to use : hook_t *khook_create(int addr, int mask); Create a hook on the address 'addr'. Give also the default type (HOOK_PERMANENT or HOOK_SINGLESHOT) , the default state (HOOK_ENABLED or HOOK_DISABLED) and the default mode (HOOK_AGGRESSIVE or HOOK_DISCRETE) . The type, state and mode are OR'd in the 'mask' parameter . void khook_destroy(hook_t *h); Disable, destroy, and free the hook ressources . int khook_add_entry(hook_t *h, char *routine, int range); Add a callback to the hook, at the 'range' rank . Return -1 if the given rank is invalid . Otherwise, return 0 . int khook_remove_entry(hook_t *h, int range); Remove the callback put in slot 'range', return -1 if the given rank is invalid . Otherwise return 0 . void khook_purge(hook_t *h); Remove all callbacks on this hook . int khook_set_type(hook_t *h, char type); Change the type for the hook 'h' . The type can be HOOK_PERMANENT (the hookcode is executed each time the hooked function is called) or HOOK_SINGLESHOT (the hookcode is executed only for 1 hijack, then the hook is cleanly removed . int khook_set_state(hook_t *h, char state); Change the state for the hook 'h' . The state can be HOOK_ENABLED (the hook is enabled) or HOOK_DISABLED (the hook is disabled) . int khook_set_mode(hook_t *h, char mode); Change the mode for the hook 'h' . The mode can be HOOK_AGGRESSIVE (the hook does not call the hijacked function) or HOOK_DISCRETE (the hook calls the hijacked function after having executed the callback routines) . Some part of the hook code is nop'ed (overwritten by no operation instructions) if the hook is aggressive (step E and step H) . int khook_set_attr(hook_t *h, int mask); Change the mode, state, and/or type using a unique function call. The function returns 0 in case of success or -1 if the specified mask contains incompatible options . Note that you can add or remove entries whenever you want, whatever the state , type and mode of the used hook . ----[ 4.2 - Kernel symbol resolution A symbol resolution function has been added to LKH, allowing you to access exported functions values . int ksym_lookup(char *name); Note that it returns NULL if the symbol remains unresolved . This lookup can resolve symbols contained in the __ksymtab section of the kernel, an exhaustive list of these symbols is printed when executing 'ksyms -a' : bash-2.03# ksyms -a | wc -l 1136 bash-2.03# wc -l /boot/System.map 14647 /boot/System.map bash-2.03# elfsh -f /usr/src/linux/vmlinux -s # displaying sections [SECTION HEADER TABLE] (nil) --- foffset: (nil) 0 bytes [*Unknown*] (...) 0xc024d9e0 a-- __ex_table foffset: 0x14e9e0 5520 bytes [Program data] 0xc024ef70 a-- __ksymtab foffset: 0x14ff70 9008 bytes [Program data] 0xc02512a0 aw- .data foffset: 0x1522a0 99616 bytes [Program data] (...) (nil) --- .shstrtab foffset: 0x1ad260 216 bytes [String table] (nil) --- .symtab foffset: 0x1ad680 245440 bytes [Symbol table] (nil) --- .strtab foffset: 0x1e9540 263805 bytes [String table] [END] As a matter of fact, the memory mapped section __ksymtab does not contains every kernel symbols we would like to hijack. In the other hand, the non-mapped section .symtab is definitely bigger (245440 bytes vs 9008 bytes). When using 'ksyms', the __NR_query_module syscall (or __NR_get_kernel_syms for older kernels) is used internaly, this syscall can only access the __ksymtab section since the complete kernel symbol table contained in __ksymtab is not loaded in memory. The solution to access to whole symbol table is to pick up offsets in our System.map file (create it using `nm -a vmlinux > System.map`) . bash-2.03# ksyms -a | grep sys_fork bash-2.03# grep sys_fork /boot/System.map c0105898 T sys_fork bash-2.03# #define SYS_FORK 0xc0105898 if ((s = khook_create((int) SYS_FORK, HOOK_PERMANENT, HOOK_ENABLED)) == NULL) KFATAL("init_module: Cant set hook on function *sys_fork* ! \n", -1); khook_add_entry(s, (int) fork_callback, 0); #undef SYS_FORK For systems not having System.map or uncompressed kernel image (vmlinux), it is acceptable to uncompress the vmlinuz file (take care, its not a standard gzip format! [3] contains very useful information about this) and create manually a new System.map file . Another way to go concerning kernel non-exported symbols resolution could be a statistic based lookup : Analysing references in the kernel hexadecimal code could allow us to predict the symbol values (fetching call or jmp instructions), the difficulty of this tool would be the portability, since the kernel code changes from a version to another. Dont forgett t change SYS_FORK to your own sys_fork offset value. ----[ 4.3 - LKH Internals: the hook_t object Let's look at the hook_t structure (the hook entity in memory) : typedef struct s_hook { int addr; int offset; char saved_bytes[7]; char voodoo_bytes[7]; char hook[HOOK_SIZE]; char cache1[CACHE1_SIZE]; char cache2[CACHE2_SIZE]; } hook_t; h->addr The address of the original function, used to enable or disable the hook . h->offset This field contains the offset from h->addr where to begin overwrite to set the hijack . Its value is 3 or 0 , it depends if the function has a stack frame or not . h->original_bytes The seven overwritten bytes of the original function . h->voodoo_bytes The seven bytes we need to put at the beginning of the function to redirect it (contains the indirect jump code seen in step A on paragraph 3) . h->hook The opcodes buffer contaning the hooking code, where we insert callback reference using khook_add_entry() . The cache1 and cache2 buffers are used to backup some hook code when we set the mode HOOK_AGGRESSIVE (since we have to nop the original function call, saving this code is necessary , for eventually reset the hook as discrete after) Each time you create a hook, an instance of hook_t is declared and allocated . You have to create one hook per function you want to hijack . ----[ 5 - Testing the code Please check http://www.devhell.org/~mayhem/ for fresh code first. The package (version 1.1) is given at the end of the article) . Just do #include "lkh.c" and play ! In this example module using LKH, we wants to hook : - the hijack_me() function, here you can check the good parameters passing and their well done modification throught the callbacks . - the schedule() function, SINGLESHOT hijack . - the sys_fork() function, PERMANENT hijack . ------[ 5.1 - Loading the module bash-2.03# make load insmod lkh.o Testing a permanent, aggressive, enabled hook with 3 callbacks: A in hijack_one = 0 -OK- B in hijack_one = 1 -OK- A in hijack_zero = 1 -OK- B in hijack_zero = 2 -OK- A in hijack_two = 2 -OK- B in hijack_two = 3 -OK- -------------------- Testing a disabled hook: A in HIJACKME!!! = 10 -OK- B in HIJACKME!!! = 20 -OK- -------------------- Calling hijack_me after the hook destruction A in HIJACKME!!! = 1 -OK- B in HIJACKME!!! = 2 -OK- SCHEDULING! ------[ 5.2 - Playing around a bit bash-2.05# ls FORKING! Makefile doc example.c lkh.c lkh.h lkh.o user user.c user.h user.o bash-2.05# pwd /usr/src/coding/LKH (Did not printed FORKING! since pwd is a shell builtin command :) bash-2.05# make unload FORKING! rmmod lkh; LKH unloaded - sponsorized by the /dev/hell crew! bash-2.05# ls Makefile doc example.c lkh.c lkh.h lkh.o user user.c user.h user.o bash-2.05# You can see "FORKING!" each time the sys_fork() kernel function is called (the hook is permanent) and "SCHEDULING!" when the schedule() kernel function is called for the first time (since this hook is SINGLESHOT, the schedule() function is hijacked only one time, then the hook is removed) . Here is the commented code for this demo : ------[ 5.3 - The code /*