📜 ⬆️ ⬇️

Redirecting Functions in Shared ELF Libraries

We all use dynamically-compiled bilieceae. Their capabilities are truly great. First, such a library is loaded into the physical address space only once for all processes. Secondly, you can expand the functionality of your program by loading the additional library, which will provide this functionality. And all this without restarting the program itself. And the problem of updates is being solved. For a dynamically composable library, you can define a standard interface and influence the functionality and quality of your main program simply by changing the library version. Such code reuse methods are even called "plug-in architecture". But the topic is not about that.

By the way, the impatient can all download and try right now.


')
Of course, rarely does a dynamically assembled library rely solely on itself in its implementation, that is, the processor’s computational capabilities and memory. Libraries use libraries. Or, at least, standard libraries. As, for example, programs on C \ C ++ use the standard C \ C ++ libraries. The latter, by the way, for convenience are also organized in a dynamically linked form (libc.so and libstdc ++. So). They themselves are stored in special format files. My research was conducted for Linux, in which the main format of dynamically composable libraries is ELF (Executable and Linkable Format).
Some time ago I was faced with the need to intercept function calls from one library to another. Just to process them in a special way. This is called call forwarding.

More about redirection


To begin with, we will formulate the problem on a concrete example. Suppose we have a program called “test” in C (file test.c) and two shared libraries (files libtest1.c and libtest2.c), with the same content, compiled in advance. These libraries provide one function at a time: libtest1 () and libtest2 (), respectively. In their implementation, each of them uses the puts () function from the standard C library.

The task is as follows:
  1. It is necessary to replace the call to the puts () function for both libraries with a call to the redirected_puts () function implemented in the main program (file test.c), which, in turn, can use the original puts ();
  2. Cancel the changes you have made, that is, make the repeated call to libtest1 () and libtest2 () lead to a call to the original puts ().

In this case, changing the code or recompiling the libraries themselves is not allowed, only the main program.

Why do you need it?


This example illustrates two very interesting features of this redirection:
  1. It is carried out exclusively for a specific dynamically compiled library, and not for the whole process, as when using the dynamic loader LD_PRELOAD environment variable, which allows other modules to safely use the original function;
  2. It occurs while the program is running and does not require its restart.
Where can this be applied? Well, for example, in your program with a bunch of plug-ins, you can intercept their calls to system resources or any other libraries without affecting other plug-ins and the application itself. Or even do the same thing from your own plug-in to some application.
There is no legal way to solve this problem. The only option is to deal with the ELF and make the necessary changes in the memory yourself.

Go!

ELF in brief


The best way to understand ELF is to be patient and read its specification carefully a couple of times, then write a simple program, compile it and examine it in detail using a hex editor comparing what you see with the specification. This method of research immediately prompts the idea to write some simple parser for ELF, as there will be a lot of routine work. But do not hurry. There are already several such utilities. For the study, we take the files from the previous section:

File test.c

#include <stdio.h> #include <dlfcn.h> #define LIBTEST1_PATH "libtest1.so" //position dependent code (for 32 bit only) #define LIBTEST2_PATH "libtest2.so" //position independent code void libtest1(); //from libtest1.so void libtest2(); //from libtest2.so int main() { void *handle1 = dlopen(LIBTEST1_PATH, RTLD_LAZY); void *handle2 = dlopen(LIBTEST2_PATH, RTLD_LAZY); if (NULL == handle1 || NULL == handle2) fprintf(stderr, "Failed to open \"%s\" or \"%s\"!\n", LIBTEST1_PATH, LIBTEST2_PATH); libtest1(); //calls puts() from libc.so twice libtest2(); //calls puts() from libc.so twice puts("-----------------------------"); dlclose(handle1); dlclose(handle2); return 0; } 

File libtest1.c

 int puts(char const *); void libtest1() { puts("libtest1: 1st call to the original puts()"); puts("libtest1: 2nd call to the original puts()"); } 

File libtest2.c

 int puts(char const *); void libtest2() { puts("libtest2: 1st call to the original puts()"); puts("libtest2: 2nd call to the original puts()"); } 

What are the parts of ELF?


To answer this question, you need to look inside this file. For this there are such utilities:
Relocation is a special term for that place in the ELF file that refers to a symbol from another module. The static (ld) or dynamic (ld-linux.so.2) linker \ loader directly modifies such places.

Any ELF file starts with a special header. Its structure, as well as the description of many other ELF elements, can be found in the /usr/include/linux/elf.h file. The header has a special field in which the offset from the beginning of the section header table file is recorded. Each element of this table describes a section in the ELF. A section is the smallest indivisible structural element in an ELF file. When loaded into memory, sections are combined into segments. Segments are the smallest indivisible parts of an ELF file that can be mapped into memory by the loader (ld-linux.so.2). Segments are described by a table of segments, the offset of which is also in the ELF file header.

The most important of them are:
To compile the above files, run the following commands:
 gcc -g3 -m32 -shared -o libtest1.so libtest1.c gcc -g3 -m32 -fPIC -shared -o libtest2.so libtest2.c gcc -g3 -m32 -L$PWD -o test test.c -ltest1 -ltest2 –ldl 

The first command creates the dynamically compiled library libtest1.so. The second is libtest2.so. Pay attention to the –fPIC key. It causes the compiler to generate a so-called Position Independent Code. Details in the next section. The third command creates an executable called test by compiling the test.c file and linking it with the libtest1.so and libtest2.so libraries already created. The latter are in the current directory, which is reflected in the use of the –L $ PWD key. The libdl.so layout is required to use the dlopen () and dlclose () functions.

To run the program, you must run the following commands:
 export LD_LIBRARY_PATH=$PWD:$LD_LIBRARY_PATH ./test 

That is, add the path to the current directory to the dynamic linker / loader as a path to search for libraries. The output of the program we get this:

libtest1: 1st call to the original puts()
libtest1: 2nd call to the original puts()
libtest2: 1st call to the original puts()
libtest2: 2nd call to the original puts()
-----------------------------

Now we will look at sections of the test module. To do this, run readelf with the –a key. The most interesting of them are listed below:

ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048580
Start of program headers: 52 (bytes into file)
Start of section headers: 21256 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 8
Size of section headers: 40 (bytes)
Number of section headers: 39
Section header string table index: 36

The standard header of the executable. The magic sequence is in the first 16 bytes. The type of module is indicated (in this case, executable, and there is also object (.o) and shared (.so)), architecture (i386), recommended entry point, offsets to segment and section headers and their sizes. At the very end is the offset in the string table for the section titles.

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048134 000134 000013 00 A 0 0 1
...
[ 5] .dynsym DYNSYM 08048200 000200 000110 10 A 6 1 4
[ 6] .dynstr STRTAB 08048310 000310 0000df 00 A 0 0 1
...
[ 9] .rel.dyn REL 08048464 000464 000010 08 A 5 0 4
[10] .rel.plt REL 08048474 000474 000040 08 A 5 12 4
[11] .init PROGBITS 080484b4 0004b4 000030 00 AX 0 0 4
[12] .plt PROGBITS 080484e4 0004e4 000090 04 AX 0 0 4
[13] .text PROGBITS 08048580 000580 0001fc 00 AX 0 0 16
[14] .fini PROGBITS 0804877c 00077c 00001c 00 AX 0 0 4
[15] .rodata PROGBITS 08048798 000798 00005c 00 A 0 0 4
...
[20] .dynamic DYNAMIC 08049f08 000f08 0000e8 08 WA 6 0 4
[21] .got PROGBITS 08049ff0 000ff0 000004 04 WA 0 0 4
[22] .got.plt PROGBITS 08049ff4 000ff4 00002c 04 WA 0 0 4
[23] .data PROGBITS 0804a020 001020 000008 00 WA 0 0 4
[24] .bss NOBITS 0804a028 001028 00000c 00 WA 0 0 4
...
[27] .debug_pubnames PROGBITS 00000000 0011b8 000040 00 0 0 1
[28] .debug_info PROGBITS 00000000 0011f8 0004d9 00 0 0 1
[29] .debug_abbrev PROGBITS 00000000 0016d1 000156 00 0 0 1
[30] .debug_line PROGBITS 00000000 001827 000309 00 0 0 1
[31] .debug_frame PROGBITS 00000000 001b30 00003c 00 0 0 4
[32] .debug_str PROGBITS 00000000 001b6c 00024e 01 MS 0 0 1
...
[36] .shstrtab STRTAB 00000000 0051a8 000160 00 0 0 1
[37] .symtab SYMTAB 00000000 005920 000530 10 38 57 4
[38] .strtab STRTAB 00000000 005e50 000268 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)

Here you can see a list of all sections of the experimental ELF file, their type and the mode of loading into memory (R, W, X and A).

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x00100 0x00100 RE 0x4
INTERP 0x000134 0x08048134 0x08048134 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x007f8 0x007f8 RE 0x1000
LOAD 0x000ef4 0x08049ef4 0x08049ef4 0x00134 0x00140 RW 0x1000
DYNAMIC 0x000f08 0x08049f08 0x08049f08 0x000e8 0x000e8 RW 0x4
NOTE 0x000148 0x08048148 0x08048148 0x00020 0x00020 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
GNU_RELRO 0x000ef4 0x08049ef4 0x08049ef4 0x0010c 0x0010c R 0x1

This is a list of segments - original containers for sections in memory. The path to the special module - the dynamic linker / loader is also indicated. That he has to arrange the contents of this ELF file in memory.

Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag
06
07 .ctors .dtors .jcr .dynamic .got

But how will the distribution of sections by segments at boot time.
But the most interesting section in which information about imported and exported dynamically linked functions is stored is called “.dynsym”:

Symbol table '.dynsym' contains 17 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FUNC GLOBAL DEFAULT UND libtest2
2: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
3: 00000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
4: 00000000 0 FUNC GLOBAL DEFAULT UND dlclose@GLIBC_2.0 (2)
5: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 (3)
6: 00000000 0 FUNC GLOBAL DEFAULT UND libtest1
7: 00000000 0 FUNC GLOBAL DEFAULT UND dlopen@GLIBC_2.1 (4)
8: 00000000 0 FUNC GLOBAL DEFAULT UND fprintf@GLIBC_2.0 (3)
9: 00000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.0 (3)
10: 0804a034 0 NOTYPE GLOBAL DEFAULT ABS _end
11: 0804a028 0 NOTYPE GLOBAL DEFAULT ABS _edata
12: 0804879c 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used
13: 0804a028 4 OBJECT GLOBAL DEFAULT 24 stderr@GLIBC_2.0 (3)
14: 0804a028 0 NOTYPE GLOBAL DEFAULT ABS __bss_start
15: 080484b4 0 FUNC GLOBAL DEFAULT 11 _init
16: 0804877c 0 FUNC GLOBAL DEFAULT 14 _fini

In addition to other functions necessary for the correct loading / unloading of the program, you can find familiar names: libtest1, libtest2, dlopen, fprintf, puts, dlclose. For all of them, the type is FUNC and the fact that they are not defined in this module - the section index is marked as UND.

The “.rel.dyn” and “.rel.plt” sections are redeployment tables for those .dynsym characters for which relocation is generally necessary when linking.

Relocation section '.rel.dyn' at offset 0x464 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
08049ff0 00000206 R_386_GLOB_DAT 00000000 __gmon_start__
0804a028 00000d05 R_386_COPY 0804a028 stderr

Relocation section '.rel.plt' at offset 0x474 contains 8 entries:
Offset Info Type Sym.Value Sym. Name
0804a000 00000107 R_386_JUMP_SLOT 00000000 libtest2
0804a004 00000207 R_386_JUMP_SLOT 00000000 __gmon_start__
0804a008 00000407 R_386_JUMP_SLOT 00000000 dlclose
0804a00c 00000507 R_386_JUMP_SLOT 00000000 __libc_start_main
0804a010 00000607 R_386_JUMP_SLOT 00000000 libtest1
0804a014 00000707 R_386_JUMP_SLOT 00000000 dlopen
0804a018 00000807 R_386_JUMP_SLOT 00000000 fprintf
0804a01c 00000907 R_386_JUMP_SLOT 00000000 puts

What is the difference between these tables in terms of dynamic function layout? This is the topic of the next section.

How are shared ELF libraries arranged?


The compilation of the libtest1.so and libtest2.so libraries was somewhat different. libtest2.so was compiled with the –fPIC key (generate a Position Independent Code). Let's see how it affected the tables of dynamic symbols for these two modules (use readelf):

Symbol table '.dynsym' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
2: 00000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
3: 00000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.0 (2)
4: 00000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.1.3 (3)
5: 00002014 0 NOTYPE GLOBAL DEFAULT ABS _end
6: 0000200c 0 NOTYPE GLOBAL DEFAULT ABS _edata
7: 0000043c 32 FUNC GLOBAL DEFAULT 11 libtest1
8: 0000200c 0 NOTYPE GLOBAL DEFAULT ABS __bss_start
9: 0000031c 0 FUNC GLOBAL DEFAULT 9 _init
10: 00000498 0 FUNC GLOBAL DEFAULT 12 _fini

Symbol table '.dynsym' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
2: 00000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
3: 00000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.0 (2)
4: 00000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.1.3 (3)
5: 00002018 0 NOTYPE GLOBAL DEFAULT ABS _end
6: 00002010 0 NOTYPE GLOBAL DEFAULT ABS _edata
7: 00002010 0 NOTYPE GLOBAL DEFAULT ABS __bss_start
8: 00000304 0 FUNC GLOBAL DEFAULT 9 _init
9: 0000043c 52 FUNC GLOBAL DEFAULT 11 libtest2
10: 000004a8 0 FUNC GLOBAL DEFAULT 12 _fini

So, the dynamic symbol tables for both libraries differ only in the order of the symbols themselves. It can be seen that both of them use the undefined function puts (), and provide libtest1 () or libtest2 (). How have relocations changed?

Relocation section '.rel.dyn' at offset 0x2cc contains 8 entries:
Offset Info Type Sym.Value Sym. Name
00000445 00000008 R_386_RELATIVE
00000451 00000008 R_386_RELATIVE
00002008 00000008 R_386_RELATIVE
0000044a 00000302 R_386_PC32 00000000 puts
00000456 00000302 R_386_PC32 00000000 puts
00001fe8 00000106 R_386_GLOB_DAT 00000000 __gmon_start__
00001fec 00000206 R_386_GLOB_DAT 00000000 _Jv_RegisterClasses
00001ff0 00000406 R_386_GLOB_DAT 00000000 __cxa_finalize

Relocation section '.rel.plt' at offset 0x30c contains 2 entries:
Offset Info Type Sym.Value Sym. Name
00002000 00000107 R_386_JUMP_SLOT 00000000 __gmon_start__
00002004 00000407 R_386_JUMP_SLOT 00000000 __cxa_finalize

For libtest1.so, the relocation for the puts () function occurs twice in the “.rel.dyn” section. Let's look at these places directly in the module using a disassembler. It is necessary to find the libtest1 () function in which the double call to puts () occurs. Use objdump –D:

 0000043c <libtest1>: 43c: 55 push %ebp 43d: 89 e5 mov %esp,%ebp 43f: 83 ec 08 sub $0x8,%esp 442: c7 04 24 b4 04 00 00 movl $0x4b4,(%esp) 449: e8 fc ff ff ff call 44a <libtest1+0xe> 44e: c7 04 24 e0 04 00 00 movl $0x4e0,(%esp) 455: e8 fc ff ff ff call 456 <libtest1+0x1a> 45a: c9 leave 45b: c3 ret 45c: 90 nop 45d: 90 nop 45e: 90 nop 45f: 90 nop 

We have two relative CALL instructions (code E8) with operands 0xFFFFFFFC. A relative CALL with such an operand is meaningless, since, in essence, it transfers control one byte ahead relative to the address of the CALL instruction. If you look at the relocation offset for puts () in the “.rel.dyn” section, you will find that they apply to the operand of the CALL instruction. Thus, in both cases of accessing puts (), the bootloader will simply overwrite 0xFFFFFFFC so that the CALL jumps to the correct address of the puts () function.
This is how relocation of type R_386_PC32 works.

Now pay attention to libtest2.so:

Relocation section '.rel.dyn' at offset 0x2cc contains 4 entries:
Offset Info Type Sym.Value Sym. Name
0000200c 00000008 R_386_RELATIVE
00001fe8 00000106 R_386_GLOB_DAT 00000000 __gmon_start__
00001fec 00000206 R_386_GLOB_DAT 00000000 _Jv_RegisterClasses
00001ff0 00000406 R_386_GLOB_DAT 00000000 __cxa_finalize

Relocation section '.rel.plt' at offset 0x2ec contains 3 entries:
Offset Info Type Sym.Value Sym. Name
00002000 00000107 R_386_JUMP_SLOT 00000000 __gmon_start__
00002004 00000307 R_386_JUMP_SLOT 00000000 puts
00002008 00000407 R_386_JUMP_SLOT 00000000 __cxa_finalize

The call to puts () is mentioned only once, and, moreover, in the “.rel.plt” section. Let's look at the assembler and debug it:

 0000043c <libtest2>: 43c: 55 push %ebp 43d: 89 e5 mov %esp,%ebp 43f: 53 push %ebx 440: 83 ec 04 sub $0x4,%esp 443: e8 ef ff ff ff call 437 <__i686.get_pc_thunk.bx> 448: 81 c3 ac 1b 00 00 add $0x1bac,%ebx 44e: 8d 83 d0 e4 ff ff lea -0x1b30(%ebx),%eax 454: 89 04 24 mov %eax,(%esp) 457: e8 f8 fe ff ff call 354 <puts@plt> 45c: 8d 83 fc e4 ff ff lea -0x1b04(%ebx),%eax 462: 89 04 24 mov %eax,(%esp) 465: e8 ea fe ff ff call 354 <puts@plt> 46a: 83 c4 04 add $0x4,%esp 46d: 5b pop %ebx 46e: 5d pop %ebp 46f: c3 ret 

Operands of CALL instructions are already different and meaningful, which means that they indicate something. This is no longer just padding. It is also useful to note that before calling most puts (), writing 0x1FF4 (0x1BAC + 0x448) to the EBX register takes place. The debugger helps to recognize the initial EBX value of 0x448. So it will be useful somewhere further. The address 0x354 leads us to a very interesting section “.plt”, which, like “.text”, is marked as executable. Here she is:

 Disassembly of section .plt: 00000334 <__gmon_start__@plt-0x10>: 334: ff b3 04 00 00 00 pushl 0x4(%ebx) 33a: ff a3 08 00 00 00 jmp *0x8(%ebx) 340: 00 00 add %al,(%eax) ... 00000344 <__gmon_start__@plt>: 344: ff a3 0c 00 00 00 jmp *0xc(%ebx) 34a: 68 00 00 00 00 push $0x0 34f: e9 e0 ff ff ff jmp 334 <_init+0x30> 00000354 <puts@plt>: 354: ff a3 10 00 00 00 jmp *0x10(%ebx) 35a: 68 08 00 00 00 push $0x8 35f: e9 d0 ff ff ff jmp 334 <_init+0x30> 00000364 <__cxa_finalize@plt>: 364: ff a3 14 00 00 00 jmp *0x14(%ebx) 36a: 68 10 00 00 00 push $0x10 36f: e9 c0 ff ff ff jmp 334 <_init+0x30> 

At the address of interest to us 0x354 we find three instructions. In the first of these, an unconditional jump occurs at the address pointed to by EBX (0x1FF4) plus 0x10. Having made simple calculations, we will receive value of the pointer 0x2004. These addresses fall into the “.got.plt” section.

 Disassembly of section .got.plt: 00001ff4 <.got.plt>: 1ff4: 20 1f and %bl,(%edi) ... 1ffe: 00 00 add %al,(%eax) 2000: 4a dec %edx 2001: 03 00 add (%eax),%eax 2003: 00 5a 03 add %bl,0x3(%edx) 2006: 00 00 add %al,(%eax) 2008: 6a 03 push $0x3 ... 

The most interesting thing is found when we deny this pointer and, finally, we get the unconditional jump address, equal to 0x35A. But, this is, in fact, the following instruction! Why was it necessary to perform such complex manipulations and refer to the “.got.plt” section in order to simply move on to the next instruction? What is PLT and GOT?

PLT (Procedure Linkage Table) is a procedure layout table. It is present in executable and shared modules. This is an array of stubs, one for each function imported.

 PLT[n+1]: jmp *GOT[n+3] push #n @push n as a signal to the resolver jmp PLT[0] 

Calling a function at the PLT [n + 1] address will result in an indirect control transition at the GOT address [n + 3]. On the first call, GOT [n + 3] points back to PLT [n + 1] + 6, which is the PUSH \ JMP sequence to PLT [0]. Passing through PLT [0], the linker uses the stored stack argument to determine 'n' and then resolves the 'n' character. Then the linker corrects the GOT [n + 3] value so that it points directly to the target subroutine, and eventually calls it. Each subsequent PLT call [n + 1] will be directed to the target subroutine without such permission of its address via the JMP instruction.

The first PLT element is special and is used to jump to the dynamic address resolution code.

 PLT[0]: push &GOT[1] jmp GOT[2] @points to resolver() 

Control is transferred to the linker code. The 'n' is already on the stack and the GOT [1] address is added to the same place. Thus, the linker (located in /lib/ld-linux.so.2) can determine which library requires its services.

GOT (Global Offset Table) is a global offset table. Its first three elements are reserved. At the first GOT initialization, all its elements that relate to address resolution in the PLT point back to PLT [0].

These special items are:
Each library and executable has its own PLT and GOT.

This is how relocation of type R_386_JUMP_SLOT, which was used in the library libtest2.so, works. The remaining types of relocations are static layout, so we will not be useful.

In the methods for resolving the call of imported functions, there is a difference between the code that depends on the position of loading into memory and is not dependent on it (PIC).

Important conclusions


We make some useful conclusions:
  1. All information on imported and exported functions can be obtained in the “.dynsym” section.
  2. If the module was compiled in PIC mode (-fPIC key), then calls to imported functions will be made through PLT and GOT, relocation will be made only once for each function and will be used for the first instruction of a specific element in PLT. Information on the relocation itself can be found in the “.rel.plt” section.
  3. If the –fPIC key was not used when compiling the library, then relocations will be performed on the operand of each relative CALL instruction as many times as there are calls to some imported function in the code. Information on the relocation itself can be found in the “.rel.dyn” section.

Note: the compilation key –fPIC is mandatory for the 64-bit architecture. That is, in 64-bit libraries, the resolution of calls to imported functions is always done through PLT \ GOT. Plus, on this architecture, the relocations are called “.rela.plt” and “.rela.dyn”.

The long-awaited decision


To redirect an imported function in a dynamically composable library, you need to know the following:
  1. File system path to this library
  2. The virtual address where it was loaded
  3. Name of the function being replaced
  4. Address of the substitute function
You also need to get the address of the original function in order to perform the reverse redirection and, thus, return everything to its place.

The prototype of the redirect function in C comes out as follows:

 void *elf_hook(char const *library_filename, void const *library_address, char const *function_name, void const *substitution_address); 


Redirection algorithm


Here is the algorithm for the redirection function:
  1. Open the library file.
  2. We remember the symbol index in the “.dynsym” section, whose name corresponds to the name of the function we are looking for.
  3. Look through the “.rel.plt” section and look for a relocation for the symbol with the specified index.
  4. If such a symbol is found, we retain the original address, then to return it from the function, and write the address of the substitute function to the place specified in the relocation. This location is calculated as the sum of the library load address in memory and the offset in relocation. Everything. The function address has been changed. Redirection will occur whenever the library calls this function. Exit the function and return the address of the original.
  5. If such a symbol in the “.rel.plt” section is not found, then look for it in the “rel.dyn” section by the same principle. But, it must be remembered that in the relocations section “rel.dyn” the symbol with the desired index may occur more than once. Therefore, it is impossible to complete the search cycle after the first redirect. But the address of the original can be remembered at the first coincidence of the index and no longer calculate - it still does not change.
  6. We return the address of the original function or NULL, if the function with the desired name was not found.

Below is the code for this function in C:

 void *elf_hook(char const *module_filename, void const *module_address, char const *name, void const *substitution) { static size_t pagesize; int descriptor; //file descriptor of shared module Elf_Shdr *dynsym = NULL, // ".dynsym" section header *rel_plt = NULL, // ".rel.plt" section header *rel_dyn = NULL; // ".rel.dyn" section header Elf_Sym *symbol = NULL; //symbol table entry for symbol named "name" Elf_Rel *rel_plt_table = NULL, //array with ".rel.plt" entries *rel_dyn_table = NULL; //array with ".rel.dyn" entries size_t i, name_index = 0, //index of symbol named "name" in ".dyn.sym" rel_plt_amount = 0, // amount of ".rel.plt" entries rel_dyn_amount = 0, // amount of ".rel.dyn" entries *name_address = NULL; //address of relocation for symbol named "name" void *original = NULL; //address of the symbol being substituted if (NULL == module_address || NULL == name || NULL == substitution) return original; if (!pagesize) pagesize = sysconf(_SC_PAGESIZE); descriptor = open(module_filename, O_RDONLY); if (descriptor < 0) return original; if ( section_by_type(descriptor, SHT_DYNSYM, &dynsym) || //get ".dynsym" section symbol_by_name(descriptor, dynsym, name, &symbol, &name_index) || //actually, we need only the index of symbol named "name" in the ".dynsym" table section_by_name(descriptor, REL_PLT, &rel_plt) || //get ".rel.plt" (for 32-bit) or ".rela.plt" (for 64-bit) section section_by_name(descriptor, REL_DYN, &rel_dyn) //get ".rel.dyn" (for 32-bit) or ".rela.dyn" (for 64-bit) section ) { //if something went wrong free(dynsym); free(rel_plt); free(rel_dyn); free(symbol); close(descriptor); return original; } //release the data used free(dynsym); free(symbol); rel_plt_table = (Elf_Rel *)(((size_t)module_address) + rel_plt->sh_addr); //init the ".rel.plt" array rel_plt_amount = rel_plt->sh_size / sizeof(Elf_Rel); //and get its size rel_dyn_table = (Elf_Rel *)(((size_t)module_address) + rel_dyn->sh_addr); //init the ".rel.dyn" array rel_dyn_amount = rel_dyn->sh_size / sizeof(Elf_Rel); //and get its size //release the data used free(rel_plt); free(rel_dyn); //and descriptor close(descriptor); //now we've got ".rel.plt" (needed for PIC) table and ".rel.dyn" (for non-PIC) table and the symbol's index for (i = 0; i < rel_plt_amount; ++i) //lookup the ".rel.plt" table if (ELF_R_SYM(rel_plt_table[i].r_info) == name_index) //if we found the symbol to substitute in ".rel.plt" { original = (void *)*(size_t *)(((size_t)module_address) + rel_plt_table[i].r_offset); //save the original function address *(size_t *)(((size_t)module_address) + rel_plt_table[i].r_offset) = (size_t)substitution; //and replace it with the substitutional break; //the target symbol appears in ".rel.plt" only once } if (original) return original; //we will get here only with 32-bit non-PIC module for (i = 0; i < rel_dyn_amount; ++i) //lookup the ".rel.dyn" table if (ELF_R_SYM(rel_dyn_table[i].r_info) == name_index) //if we found the symbol to substitute in ".rel.dyn" { name_address = (size_t *)(((size_t)module_address) + rel_dyn_table[i].r_offset); //get the relocation address (address of a relative CALL (0xE8) instruction's argument) if (!original) original = (void *)(*name_address + (size_t)name_address + sizeof(size_t)); //calculate an address of the original function by a relative CALL (0xE8) instruction's argument mprotect((void *)(((size_t)name_address) & (((size_t)-1) ^ (pagesize - 1))), pagesize, PROT_READ | PROT_WRITE); //mark a memory page that contains the relocation as writable if (errno) return NULL; *name_address = (size_t)substitution - (size_t)name_address - sizeof(size_t); //calculate a new relative CALL (0xE8) instruction's argument for the substitutional function and write it down mprotect((void *)(((size_t)name_address) & (((size_t)-1) ^ (pagesize - 1))), pagesize, PROT_READ | PROT_EXEC); //mark a memory page that contains the relocation back as executable if (errno) //if something went wrong { *name_address = (size_t)original - (size_t)name_address - sizeof(size_t); //then restore the original function address return NULL; } } return original; } 

Full implementation of this feature with test examples is available for download .

Rewrite our test program:

 #include <stdio.h> #include <dlfcn.h> #include "elf_hook.h" #define LIBTEST1_PATH "libtest1.so" //position dependent code (for 32 bit only) #define LIBTEST2_PATH "libtest2.so" //position independent code void libtest1(); //from libtest1.so void libtest2(); //from libtest2.so int hooked_puts(char const *s) { puts(s); //calls the original puts() from libc.so because our main executable module called "test" is intact by hook puts("is HOOKED!"); } int main() { void *handle1 = dlopen(LIBTEST1_PATH, RTLD_LAZY); void *handle2 = dlopen(LIBTEST2_PATH, RTLD_LAZY); void *original1, *original2; if (NULL == handle1 || NULL == handle2) fprintf(stderr, "Failed to open \"%s\" or \"%s\"!\n", LIBTEST1_PATH, LIBTEST2_PATH); libtest1(); //calls puts() from libc.so twice libtest2(); //calls puts() from libc.so twice puts("-----------------------------"); original1 = elf_hook(LIBTEST1_PATH, LIBRARY_ADDRESS_BY_HANDLE(handle1), "puts", hooked_puts); original2 = elf_hook(LIBTEST2_PATH, LIBRARY_ADDRESS_BY_HANDLE(handle2), "puts", hooked_puts); if (NULL == original1 || NULL == original2) fprintf(stderr, "Redirection failed!\n"); libtest1(); //calls hooked_puts() twice libtest2(); //calls hooked_puts() twice puts("-----------------------------"); original1 = elf_hook(LIBTEST1_PATH, LIBRARY_ADDRESS_BY_HANDLE(handle1), "puts", original1); original2 = elf_hook(LIBTEST2_PATH, LIBRARY_ADDRESS_BY_HANDLE(handle2), "puts", original2); if (NULL == original1 || original1 != original2) //both pointers should contain hooked_puts() address now fprintf(stderr, "Restoration failed!\n"); libtest1(); //again calls puts() from libc.so twice libtest2(); //again calls puts() from libc.so twice dlclose(handle1); dlclose(handle2); return 0; } 

Compile:

 gcc -g3 -m32 -shared -o libtest1.so libtest1.c gcc -g3 -m32 -fPIC -shared -o libtest2.so libtest2.c gcc -g3 -m32 -L$PWD -o test test.c elf_hook.c -ltest1 -ltest2 -ldl 

And run:

 export LD_LIBRARY_PATH=$PWD:$LD_LIBRARY_PATH ./test 


The output we get the following:

libtest1: 1st call to the original puts()
libtest1: 2nd call to the original puts()
libtest2: 1st call to the original puts()
libtest2: 2nd call to the original puts()
-----------------------------
libtest1: 1st call to the original puts()
is HOOKED!
libtest1: 2nd call to the original puts()
is HOOKED!
libtest2: 1st call to the original puts()
is HOOKED!
libtest2: 2nd call to the original puts()
is HOOKED!
-----------------------------
libtest1: 1st call to the original puts()
libtest1: 2nd call to the original puts()
libtest2: 1st call to the original puts()
libtest2: 2nd call to the original puts()

That indicates the complete implementation of the task set at the very beginning. Hooray!

How to learn the address on which the shared library was loaded?

This very interesting question arises upon careful consideration of the prototype function for redirection. After some research, I was able to find a way to determine the address of the library load by its handle, which is returned by the dlopen () function. This is done in such a macro:

#define LIBRARY_ADDRESS_BY_HANDLE(dlhandle) ((NULL == dlhandle) ? NULL : (void*)*(size_t const*)(dlhandle))


How to record and restore the address of the new function?

There is no problem with rewriting addresses that are indicated by relocations from the “.rel.plt” section. In essence, the operand of the JMP instruction of the corresponding element from the “.plt” section is rewritten. And the operands of such an instruction are simply addresses.

The situation is more interesting with the use of relocations to operands of relative CALL instructions (code E8). Jump addresses in them are calculated by the formula:

address_of_a_function = CALL_argument + address_of_the_next_instruction

So we can find out the address of the original function. , CALL, :

CALL_argument = address_of_a_function - address_of_the_next_instruction

“.rel.dyn” , “RE”, , . , . mprotect(). – , . . : ( ):

page_address = (size_t)relocation_address & ( ((size_t) -1) ^ (pagesize - 1) );

, 4096 (0x1000) 32- :

page_address = (size_t)relocation_address & (0xFFFFFFFF ^ 0xFFF) = (size_t)relocation_address & 0xFFFFF000;

, , sysconf(_SC_PAGESIZE).

Usage example

plug-in Firefox, , , Adobe Flash plug-in' (libflashplayer.so). , Adobe Flash Internet Firefox plug-in'.

Good luck!

Related Links

, .

Source: https://habr.com/ru/post/106107/


All Articles