

Something that always fascinated me was running code directly from memory. From Process Hollowing (aka RunPE) to PTRACE injection. I had some success playing around with it in C in the past, without using any of the previous mentioned methods, but unfortunately the code is lost somewhere in the forums of VXHeavens (sadly no longer online) but the code was buggy and worked only with Linux 32bit systems (I wish I knew about shm_open back then, which is sort of an alternative for the syscall we are using in this post, mainly targeting older systems where memfd_create is not available).

Overview and code

Recently, I have been trying to code in assembly a bit, I find it very interesting and I believe every developer should understand at least the basics of it. I chose FASM as my assembler because I think it is very simple, powerful and I like its concepts (like same source, same output). More information about its design can be found here. Anyway, I have written a small tool, memrun , that allows you to run ELF files from memory using the memfd_create syscall, which is available in Linux where kernel version is >= 3.17 .

What happens with memfd_create is that it acts like malloc syscall but will return a file descriptor that references an anonymous file (which does not exists in the disk) and we can pass it to execve and execute it from memory. There are a couple in-depth articles about it around the internet already so I will not get too deep into it. A nice one by magisterquis can be found at his page

The assembly code might look too big but there are some things we need to take care in this case that we don’t need to when writing in a HLL like Go (as you can see in its example below). Also it’s nice if you want to use the code for an exploit, you can just adjust the assembly instructions to your needs. Both examples are for x86_64 only:

format ELF64 executable 3 include "struct.inc" include "utils.inc" segment readable executable entry start start: ;----------------------------------------------------------------------------- ; parsing command line arguments ;----------------------------------------------------------------------------- pop rcx ; arg count cmp rcx , 3 ; needs to be at least two for the self program arg0 and target arg1 jne usage ; exit 1 if not add rsp , 8 ; skips arg0 pop rsi ; gets arg1 mov rdi , sourcePath push rsi ; save rsi push rdi call strToVar pop rsi ; restore rsi pop rdi mov rdi , targetProcessName pop rsi ; gets arg2 push rdi call strToVar ;----------------------------------------------------------------------------- ; opening source file for reading ;----------------------------------------------------------------------------- mov rdi , sourcePath ; loads sourcePath to rdi xor rsi , rsi ; cleans rsi so open syscall doesnt try to use it as argument mov rdx , O_RDONLY ; O_RDONLY mov rax , SYS_OPEN ; open syscall ; rax contains source fd (3) push rax ; saving rax with source fd ;----------------------------------------------------------------------------- ; getting source file information to fstat struct ;----------------------------------------------------------------------------- mov rdi , rax ; load rax (source fd = 3) to rdi lea rsi , [ fs tat ] ; load fstat struct to rsi mov rax , SYS_FSTAT ; sys_fstat syscall ; fstat struct conntains file information mov r12 , qword [ rsi + 48 ] ; r12 contains file size in bytes (fstat.st_size) ;----------------------------------------------------------------------------- ; creating memory map for source file ;----------------------------------------------------------------------------- pop rax ; restore rax containing source fd mov r8 , rax ; load r8 with source fd from rax mov rax , SYS_MMAP ; mmap number mov rdi , 0 ; operating system will choose mapping destination mov rsi , r12 ; load rsi with page size from fstat.st_size in r12 mov rdx , 0x1 ; new memory region will be marked read only mov r10 , 0x2 ; pages will not be shared mov r9 , 0 ; offset inside test.txt syscall ; now rax will point to mapped location push rax ; saving rax with mmap address ;----------------------------------------------------------------------------- ; close source file ;----------------------------------------------------------------------------- mov rdi , r8 ; load rdi with source fd from r8 mov rax , SYS_CLOSE ; close source fd syscall ;----------------------------------------------------------------------------- ; creating memory fd with empty name ("") ;----------------------------------------------------------------------------- lea rdi , [ bogusName ] ; empty string mov rsi , MFD_CLOEXEC ; memfd mode mov rax , SYS_MEMFD_CREATE syscall ; memfd_create mov rbx , rax ; memfd fd from rax to rbx ;----------------------------------------------------------------------------- ; writing memory map (source file) content to memory fd ;----------------------------------------------------------------------------- pop rax ; restoring rax with mmap address mov rdx , r12 ; rdx contains fstat.st_size from r12 mov rsi , rax ; load rsi with mmap address mov rdi , rbx ; load memfd fd from rbx into rdi mov rax , SYS_WRITE ; write buf to memfd fd syscall ;----------------------------------------------------------------------------- ; executing memory fd with targetProcessName ;----------------------------------------------------------------------------- xor rdx , rdx lea rsi , [ argv ] lea rdi , [ fdPath ] mov rax , SYS_EXECVE ; execve the memfd fd in memory syscall ;----------------------------------------------------------------------------- ; exit normally if everything works as expected ;----------------------------------------------------------------------------- jmp normal_exit ;----------------------------------------------------------------------------- ; initialized data ;----------------------------------------------------------------------------- segment readable writable fstat STAT usageMsg db "Usage: memrun <path_to_elf_file> <process_name>" , 0xA , 0 sourcePath db 256 dup 0 targetProcessName db 256 dup 0 bogusName db "" , 0 fdPath db "/proc/self/fd/3" , 0 argv dd targetProcessName

package main import ( "fmt" "io/ioutil" "os" "syscall" "unsafe" ) // the constant values below are valid for x86_64 const ( mfdCloexec = 0x0001 memfdCreate = 319 ) func runFromMemory ( displayName string , filePath string ) { fdName := "" // *string cannot be initialized fd , _ , _ := syscall . Syscall ( memfdCreate , uintptr ( unsafe . Pointer ( & fdName )), uintptr ( mfdCloexec ), 0 ) buffer , _ := ioutil . ReadFile ( filePath ) _ , _ = syscall . Write ( int ( fd ), buffer ) fdPath := fmt . Sprintf ( "/proc/self/fd/%d" , fd ) _ = syscall . Exec ( fdPath , [] string { displayName }, nil ) } func main () { lenArgs := len ( os . Args ) if lenArgs < 3 || lenArgs > 3 { fmt . Println ( "Usage: memrun process_name elf_binary" ) os . Exit ( 1 ) } runFromMemory ( os . Args [ 1 ], os . Args [ 2 ]) }

The full code for both versions can be found in this repo: https://github.com/guitmz/memrun

See it in action

Allow me to show it in action. Let’s start by creating a simple target file in C , named target.c . The file will try to open itself for reading and if it can’t, it will print a message forever every 5 seconds. We will execute it from memory:

#include <stdio.h> #include <unistd.h> int main ( int argc , char ** argv ) { printf ( "My process ID : %d

" , getpid ()); FILE * myself = fopen ( argv [ 0 ], "r" ); if ( myself == NULL ) { while ( 1 ) { printf ( "I can't find myself, I must be running from memory!

" ); sleep ( 5 ); } } else { printf ( "I am just a regular boring file being executed from the disk...

" ); } return 0 ; }

Now we build target.c :

$ gcc target.c -o target

We should also build our FASM or Go tool, I will use the assembly one here:

$ fasm memrun.asm flat assembler version 1.73.04 (16384 kilobytes memory, x64) 4 passes, 1221 bytes.

Running the file normally gives us this:

$ ./target My process ID : 4944 I am just a regular boring file being executed from the disk...

But using memrun to run it will be totally different:

$ ./memrun target MASTER_HACKER_PROCESS_NAME_1337 My process ID : 4945 I can't find myself, I must be running from memory! I can't find myself, I must be running from memory!

Furthermore, if you look for its pid with ps utility, this is what you get:

$ ps -f 4945 UID PID PPID C STIME TTY STAT TIME CMD guitmz 4945 4842 0 15:31 pts/0 S+ 0:00 MASTER_HACKER_PROCESS_NAME_1337

Finally, let’s check the process directory:

$ ls -l /proc/4945/{cwd,exe} lrwxrwxrwx 1 guitmz guitmz 0 Mar 27 15:38 /proc/4945/cwd -> /home/guitmz/memrun/assembly lrwxrwxrwx 1 guitmz guitmz 0 Mar 27 15:38 /proc/4945/exe -> /memfd: (deleted)

Note the /memfd: (deleted) part, no actual file in disk for this process :)

For those who know, this can be an interesting technique to run stealthy binaries in Linux, you can go even further by giving it a proper name (like a real Linux process) and detach it from the tty and change its cwd with some simple approches. Tip: fork is your friend :)

TMZ