Detection evasion on Linux
Let’s talk detection evasion on Linux, shall we?
I’ve seen a LOT of blog posts lately popping up about detection evasion on Linux to leave as little artifacts as possible in the /proc filesystem to evade detection from forensics.
Detection evasion to minimize stuff in the /proc filesystem that allows for detection, really only boils down to doing 5 things
- Changing the process name
- if root, changing /proc/self/exe link
- Use file-backed mappings of the process you want to mimic
- Make sure addresses look sane
- Running the ELF manually from memory without execve
All of these are extremely doable. So let’s look at how we do all 5 at once.
1 - Changing the process name
Changing the process name is a relatively simple process.
First, we need to get the original MM_MAP values for prctl. We do this by parsing /proc/self/stat:
struct prctl_mm_map *get_mm_map() {
FILE *f = NULL;
int i, fd;
char *buf_ptr;
char buf[4096];
int ret = 0;
ssize_t bytes_read = 0;
/*
* We don't really need to know all of this stuff, but unfortunately
* PR_SET_MM_MAP requires us to set it all at once, so we have to
* figure it out anyway.
*/
unsigned long start_data, end_data, start_brk, start_code, end_code,
start_stack, arg_start, arg_end, env_start, env_end, brk_val;
struct prctl_mm_map *prctl_map;
f = fopen("/proc/self/stat", "r");
if (!f) {
abort();
}
fd = fileno(f);
if (fd < 0) {
fclose(f);
abort();
}
bytes_read = read(fd, buf, sizeof(buf) - 1);
if (bytes_read <= 0) {
fclose(f);
abort();
}
buf[bytes_read] = '\0';
/* Skip the first 25 fields, column 26-28 are start_code, end_code,
* and start_stack */
buf_ptr = strchr(buf, ' ');
for (i = 0; i < 24; i++) {
if (!buf_ptr) {
fclose(f);
abort();
}
buf_ptr = strchr(buf_ptr + 1, ' ');
}
if (!buf_ptr) {
fclose(f);
abort();
}
i = sscanf(buf_ptr, "%lu %lu %lu", &start_code, &end_code, &start_stack);
if (i != 3) {
fclose(f);
abort();
}
/* Skip the next 19 fields, column 45-51 are start_data to arg_end */
for (i = 0; i < 19; i++) {
if (!buf_ptr) {
fclose(f);
abort();
}
buf_ptr = strchr(buf_ptr + 1, ' ');
}
if (!buf_ptr) {
fclose(f);
abort();
}
i = sscanf(buf_ptr, "%lu %lu %lu %lu %lu %lu %lu",
&start_data, &end_data, &start_brk, &arg_start, &arg_end, &env_start, &env_end);
if (i != 7) {
fclose(f);
abort();
}
/* Include the null byte here, because in the calculations below we
* want to have room for it. */
brk_val = syscall(12, 0);
prctl_map = malloc(sizeof(struct prctl_mm_map) + 1);
if (prctl_map == NULL) {
return NULL;
}
prctl_map->start_code = start_code;
prctl_map->end_code = end_code;
prctl_map->start_stack = start_stack;
prctl_map->start_data = start_data;
prctl_map->end_data = end_data;
prctl_map->start_brk = start_brk;
prctl_map->brk = brk_val;
prctl_map->arg_start = arg_start;
prctl_map->arg_end = arg_end;
prctl_map->env_start = env_start;
prctl_map->env_end = env_end;
prctl_map->auxv = NULL;
prctl_map->auxv_size = 0;
prctl_map->exe_fd = -1;
ret = prctl(PR_SET_MM, PR_SET_MM_MAP, prctl_map, sizeof(struct prctl_mm_map), 0);
if (ret != 0) {
fclose(f);
return NULL;
}
fclose(f);
return prctl_map;
}
Next, we set the proctitle using this information as a base:
struct prctl_mm_map *setproctitle(struct prctl_mm_map *prctl_map, char *title) {
static char *proctitle = NULL;
char *tmp_proctitle;
unsigned long arg_start, arg_end;
int len = strlen(title) + 1;
tmp_proctitle = realloc(proctitle, len);
if (!tmp_proctitle) {
abort();
}
proctitle = tmp_proctitle;
arg_start = (unsigned long)proctitle;
arg_end = arg_start + len;
prctl_map->arg_start = arg_start;
prctl_map->arg_end = arg_end;
int ret = prctl(PR_SET_MM, PR_SET_MM_MAP, prctl_map, sizeof(struct prctl_mm_map), 0);
if (ret == 0) {
strncpy((char *)arg_start, title, len);
} else {
abort();
}
return prctl_map;
}
And lastly, we need to change the name of the process using prctl:
static int change_name(const char *name) {
int ret = prctl(PR_SET_NAME, name);
if (ret < 0) {
abort();
}
return 0;
}
This allows you to change how the process looks inside of eg. ps aux, by first calling setproctitle, then secondly calling change_name.
An example program using this:
int main(void){
struct prctl_mm_map* prctl_map = get_mm_map();
setproctitle(prctl_map, "JOE_MAMA");
change_name("JOE_MAMA");
while(1){
sleep(5);
}
}
And this is how it shows up when you use eg. ps aux
zopazz 14415 0.0 0.0 2684 1804 pts/1 S+ 19:31 0:00 JOE_MAMA
This hides it from normal process listings and from /proc/self/stat. Let’s now look at how to change the name of /proc/self/exe in case you are root
2 - Changing /proc/self/exe (if root)
This is slightly more complicated, and also done via. prctl. If we take a look at how to supersede the link from the prctl man page:
PR_SET_MM_EXE_FILE
Supersede the /proc/pid/exe symbolic link with a new one pointing to a new executable file identified by the file descriptor provided in arg3 argument. The file descriptor should be obtained with a regular open(2) call.
To change the symbolic link, one needs to unmap all existing executable memory areas, including those created by the kernel itself (for example the kernel usually creates at least one executable memory area for the ELF .text section).
In Linux 4.9 and earlier, the PR_SET_MM_EXE_FILE operation can be performed only once in a process's lifetime; attempting to perform the operation a second time results in the error EPERM. This restriction was enforced for security reasons that were
subsequently deemed specious, and the restriction was removed in Linux 4.10 because some user-space applications needed to perform this operation more than once.
So we need to unmap the original binary R-X mappings first, then change the link and remap the mapping after. If you are already loading your own ELF into memory and running it without execve, you can do this step at the same time as you’re loading it in, as you’ll already be unmapping the ELF file anyways.
I actually demonstrated both of these techniques several years ago in my proc_mimicry project. I just didnt make too much of a fuzz about it.
The easiest way to do this, is to first parse the original executable section, jump somewhere else, unmap it, change the /proc/self/exe link then remapping yourself again. Alternatively it’s possible to temporarily load a shared object in-memory which takes care of this functionality and you won’t have to bother with this part.
Once you are unmapped the process is very simple for changing the mapping itself, just open the file and pass it to prctl:
static int change_proc_exe(const char *filename) {
int fd = open(filename, O_RDONLY);
if (fd < 0) {
abort();
}
int success = prctl(PR_SET_MM, PR_SET_MM_EXE_FILE, fd, 0, 0);
if (success != 0) {
abort();
}
return fd;
}
Since how it is smartest to unmap the original .text section highly depends on the implementation you want to use, I won’t show the code I personally used for this in the blog post (as it’s quite long). But you can check out the proc_mimicry code here if you’re interested in seeing how one implementation might look.
Remember, superseding /proc/self/exe IS ONLY POSSIBLE, if you are root. If you are not root, you are not allowed to supersede the link, no matter what you do.
3 - Using file backed mappings of the process you want to mimic
Part 3-5 here are kind of in the same vein. If you want your process to look real, it needs to look normal inside of /proc/self/maps if the /proc/self/exe for example points to /bin/systemd then we also need the binary mappings we have to point at /bin/systemd.
This problem seems complicated, as you can rename anonymous mappings, however they get a little anon tag in front of it, like this:
7c16f4d11000-7c16f4d12000 rwxp 00000000 00:00 0 [anon:/bin/bash]
In contrast a real binary mapping looks like this:
622596573000-622596575000 r--p 00000000 08:02 4368922 /usr/bin/cat
So how do we get around this? It’s actually super simple. We use real file-backed mappings marked with MAP_PRIVATE. When MAP_PRIVATE is used, modifications to the mappings are not reflected on the file beneath it. Here’s an example of doing so:
#include <linux/prctl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
/** Shellcode is just
loop:
mov rax, 1
lea rsi, [rip+hello]
mov rdi, 1
mov rdx, 0x10
syscall
jmp loop
hello:
.asciz "Hello World!!!!!"
**/
#define SHELLCODE "H\xc7\xc0\x01\x00\x00\x00H\x8d\x35\x12\x00\x00\x00H\xc7\xc7\x01\x00\x00\x00H\xc7\xc2\x10\x00\x00\x00\x0f\x05\xeb\xe0Hello World\x21\x21\x21\x21\x21\x21\x00"
#define SC_SZ 49
int main(void){
int fd = open("/bin/bash", O_RDONLY);
if(fd < 0){
perror("open");
exit(1);
}
void* mapping = mmap(0, 0x1000, 0x7, MAP_PRIVATE, fd, 0);
prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, mapping, 0x1000, "/bin/bash");
memset(mapping, '\0', 0x1000);
memcpy(mapping, SHELLCODE, SC_SZ);
// Prints hello world a lot :P
((void(*)())mapping)();
}
This mapping now looks like this in the program:
7f7202673000-7f7202674000 rwxp 00000000 08:02 4325798 /usr/bin/bash
as we opened /bin/bash and used it as our file-backed mapping. However since we replaced the content with out payload it’s just printing “Hello World!” a lot:

We can do this with any file, effectively mimicking it as long as we have read permissions to said file. This is very useful as it allows us to mimic any binary on the system and populate it with different data when we’re mapping our ELF file to execute it from memory.
4 - Using sane offset
There are 2 ways of getting sane offsets for your program
- You use the original binary mappings and overlay your ELF on top of it, this can be done by parsing
/proc/self/mapsand finding the relevant sections - You calculate sane offsets for your binary, mimicking how the kernel does it, and use those offsets
In both cases you still need to unmap the original ELF file and any shared objects it may have been using. This is most easily accomplished by using some kind of trampoline to do so.
I’d like to refer to the research made by Ivan who has covered how you can do this in practice.
Ivan opted to use the existing kernel-provided mappings, which is the first way of doing so. If you are not interested in using those, but calculating your own, you can do this relatively easily by mimicking how the kernel does it:
unsigned long get_random_u32(){
unsigned int randnum = 0;
getrandom(&randnum, sizeof(randnum), 0);
return (unsigned long)randnum;
}
#define TASK_SIZE (1UL << 47UL)
#define LOAD_DYN_BASE ((TASK_SIZE * 2UL)/ 3UL)
#define LOAD_PAGE_START ((LOAD_DYN_BASE + get_random_u32()) & 0xfffffffffffff000UL)
Here, LOAD_PAGE_START will be resolved to a valid PIE address that the kernel might make itself (which would be your PIE base address).
However, the problem with this method is that it has to be adapted dependent on architecture, this specific example is for how it’s done on x86_64.
LOAD_DYN_BASE is the one you would have to change and is the same as the ELF_ET_DYN_BASE in the kernel source code. How this one is calculated is what varies per architecture and can be found in the /arch/<yourarch>/asm/elf.h file. In the case of x86_64 it’s found here
5 - Loading the ELF in memory
Lastly you have to load the ELF into memory and mimic what the linker essentially normally does for you when spawning an ELF file. The main thing to remember is that your mappings need to be made fd backed for your dummy file and the start offset should be either calculated or grabbed from /proc/self/maps
If you want a loader to start creating your work from, you can use mine here which currently does not use file-backed mappings, however there are only about 4 mmap calls which would have to be replaced to support this (use the technique from step number 3).
If you stay away from using constants and globals, you can also just grab the .text section from this directly and use it just like shellcode.
Honorable mentions
I debated talking about memfd_secret and mentioning how to use it to make memory pages only readable by the process itself (not even readable by the kernel without manually walking the page table).
I decided against it as this post was about hiding from detection in the display of the /proc filesystem.
I might make a full blog post about memfd_secret and how to use it in the future for anti-forensics. But in case you are interested I made a modification to musl which makes the heap fully memfd_secret backed, making the heap unreadable by everyone except the process itself, you can find it here. I’m debating whether or not I’d like to continue work on it, to hide the stack and all ro/rw areas as well.
Lastly, if there’s any interest in it I will sit down and make a loader which employs all of the above at once send me a DM on discord, otherwise, do what you will with this information. Buh-bweh.