Threat Intelligence & APTs • Linux

BPFDoor: In-Depth Reverse Engineering of a Stealthy Linux Backdoor Using Raw Sockets

Reverse engineer the BPFDoor Linux backdoor to analyze how raw sockets and Berkeley Packet Filters bypass firewalls for covert remote access.

Executive Summary

Sample Metadata

Ghidra Analysis

When ghidra prompt for auto analyzing select extra parameter: Decompiler parameter ID . Then click analyze.

Screenshot 2026-06-02 at 11.07.09 AM.png
Figure: Screenshot 2026-06-02 at 11.07.09 AM.png Click to zoom ↗

Entry point

decompiled entry point.

PYTHON

void processEntry entry(undefined8 param_1,undefined8 param_2)

{
  undefined1 auStack_8 [8];
  
  __libc_start_main(FUN_00403ff9,param_2,&stack0x00000008,FUN_00404290,FUN_00404280,param_1,
                    auStack_8);
  do {
                    /* WARNING: Do nothing block with infinite loop */
  } while( true );
}

Decompiled Entry Point Summary

This is the standard _start boilerplate of a compiled Linux ELF binary, likely generated in Ghidra.

▪FUN_00403ff9: This is the actual main function where the malware's execution logic begins.
▪__libc_start_main: A standard library function that sets up the environment and calls main.
▪The Infinite Loop: A safety net. __libc_start_main never returns because it ends the process, but if it fails, the loop keeps the program from crashing randomly.
Screenshot 2026-06-02 at 11.38.46 AM.png
Figure: Screenshot 2026-06-02 at 11.38.46 AM.png Click to zoom ↗

Next step: FUN_00403ff9 to see what the malware actually does.

FUN00403ff9

image.png
Figure: image.png Click to zoom ↗

This Function appears to be main.

C
undefined8 FUN_00403ff9(int param_1, long *param_2)
▪param_1 maps directly to int argc (argument count).
▪param_2 maps directly to char argv (argument vector/values).

So just rename FUN_00403ff9 to mw_main

Decompiled code:

C
undefined8 mw_main(int argc,long *argv) // argument counter & argument vector.
{
  int iVar1;
  __uid_t _Var2;
  time_t tVar3;
  char *local_78 [4];
  char *local_58;
  char *local_50;
  char *local_48;
  char *local_40;
  char *local_38;
  char *local_30;
  char local_28 [16];
  char local_18 [16];
  
  // 1. Initialize fake strings and service array for process masquerading
  builtin_strncpy(local_18, "justtryit", 10);
  builtin_strncpy(local_28, "sockettcp", 10);
  local_78[0] = "/sbin/udevd -d";
  local_78[1] = "/sbin/mingetty /dev/tty6";
  local_78[2] = "/usr/sbin/console-kit-daemon --no-daemon";
  local_78[3] = "hald-addon-acpi: listening on acpi kernel interface /proc/acpi/event";
  local_58    = "dbus-daemon --system";
  local_50    = "hald-runner";
  local_48    = "pickup -l -t fifo -u";
  local_40    = "avahi-daemon: chroot helper";
  local_38    = "/sbin/auditd -n";
  local_30    = "/usr/lib/systemd/systemd-journald";

  // 2. Construct the mutex path string in memory: "/var/run/xinetd.lock"
  DAT_00605400 = 0x2f; // '/'
  DAT_00605401 = 0x76; // 'v'
  DAT_00605402 = 0x61; // 'a'
  DAT_00605403 = 0x72; // 'r'
  DAT_00605404 = 0x2f; // '/'
  DAT_00605405 = 0x72; // 'r'
  DAT_00605406 = 0x75; // 'u'
  DAT_00605407 = 0x6e; // 'n'
  DAT_00605408 = 0x2f; // '/'
  DAT_00605409 = 0x78; // 'x'
  DAT_0060540a = 0x69; // 'i'
  DAT_0060540b = 0x6e; // 'n'
  DAT_0060540c = 0x6e; // 'e'
  DAT_0060540d = 0x74; // 't'
  DAT_0060540e = 100;  // 'd'
  DAT_0060540f = 0x2e; // '.'
  DAT_00605410 = 0x6l; // 'l'
  DAT_00605411 = 0x6f; // 'o'
  DAT_00605412 = 99;   // 'c'
  DAT_00605413 = 0x6k; // 'k'
  DAT_00605414 = 0;    // Null terminator

  // 3. Mutex Check: If the lock file already exists, exit immediately
  iVar1 = access(&DAT_00605400, 4);
  if (iVar1 != 0) {
    
    // 4. Privilege Check: Ensure running as root (UID 0)
    _Var2 = getuid();
    if (_Var2 == 0) {
      bzero(&DAT_00605560, 0x224);
      
      // 5. Select a random fake process name from the array
      tVar3 = time((time_t *)0x0);
      srand((uint)tVar3);
      iVar1 = rand();
      strcpy(&DAT_00605568, local_78[iVar1 % 10]);
      strcpy(&DAT_00605768, local_18);
      strcpy(&DAT_00605776, local_28);
      
      // 6. Masquerade: Rewrite argv[0] to disguise process as the chosen service
      FUN_00401d17(argc,argv,&DAT_00605568);
      
      // 7. Evasion: Detach from terminal, fork, and run as a silent background daemon
      daemon(0, 0);
      chdir("/");
      
      FUN_004017dd();
      signal(0x11, FUN_004017fd); // Setup signal handlers (SIGCHLD)
      
      // 8. Locking: Write the PID and create the "/var/run/xinetd.lock" file
      DAT_006053e4 = getpid();
      iVar1 = open(&DAT_00605400, 0x41, 0x1a4); // 0x41 = O_CREAT | O_WRONLY
      close(iVar1);
      
      signal(0x11, (__sighandler_t)0x1); // Ignore SIGCHLD
      
      // 9. Payload Execution: Call the main malicious execution routine
      FUN_0040239a();
    }
    return 0;
  }
  
  // Exit point if lock file existed
  exit(0);
}

Main Analysis:

Fixing Strings.

So we have:

C
  iVar1 = access(&DAT_00605400, 4);

Here is the man page of the access function.

Screenshot 2026-06-02 at 12.07.54 PM.png
Figure: Screenshot 2026-06-02 at 12.07.54 PM.png Click to zoom ↗

It takes two arguments path name and mode which is a integer value.

let’s just rename DAT_00605400 to malware mw_pathname. and iVar1 to var_accessReturn

So basically if the function doesn’t find this path (mw_pathname) it will exit the program completely.

Checking what is the path.

It’s set a little bit up.

C
  mw_pathname = 0x2f;
  DAT_00605401 = 0x76;
  DAT_00605402 = 0x61;
  DAT_00605403 = 0x72;
  DAT_00605404 = 0x2f;
  DAT_00605405 = 0x72;
  DAT_00605406 = 0x75;
  DAT_00605407 = 0x6e;
  DAT_00605408 = 0x2f;
  DAT_00605409 = 0x78;
  DAT_0060540a = 0x69;
  DAT_0060540b = 0x6e;
  DAT_0060540c = 0x65;
  DAT_0060540d = 0x74;
  DAT_0060540e = 100;
  DAT_0060540f = 0x2e;
  DAT_00605410 = 0x6c;
  DAT_00605411 = 0x6f;
  DAT_00605412 = 99;
  DAT_00605413 = 0x6b;
  DAT_00605414 = 0;

This is 21 character’s long.

Let’s change that to 21 character long array of char.

Do right click —> Retype Global then char [21] .

Screenshot 2026-06-02 at 1.24.32 PM.png
Figure: Screenshot 2026-06-02 at 1.24.32 PM.png Click to zoom ↗

now it will change it to this.

C
builtin_strncpy(mw_pathname,"/var/run/xinetd.lock",0x15);

This looks good.

Screenshot 2026-06-02 at 1.24.59 PM.png
Figure: Screenshot 2026-06-02 at 1.24.59 PM.png Click to zoom ↗

So it’s checking whether it can access /var/run/xinetd.lock . and if can’t then it’s actually not running the program.

Checking Process UID

Let’s pretend that we have the access of the file /var/run/xinetd.lock . Then it’s getting the uid **** _Var2 = getuid();

What does it do

▪getuid() returns the real user ID of the calling process.

renaming _Var2 to var_processUID

Again if the process uid is not zero it will exit

C
		_Var2 = getuid();
    if (_Var2 == 0) {

So the process uid in Linux for 0 means you are running as root.

The we are getting time as the source for the random number generator.

C
      tVar3 = time((time_t *)0x0);
      srand((uint)tVar3);

Some data is copying into this particular variable DAT_00605568 which going to be passed to this function FUN_00401d17(argc,argv,&DAT_00605568); .

C
      strcpy(&DAT_00605568,local_78[var_tempReturn % 10]);
      strcpy(&DAT_00605768,local_18);
      strcpy(&DAT_00605776,local_28);
      FUN_00401d17(argc,argv,&DAT_00605568);

checking local_78 we have 4 results.

C
  local_78[0] = "/sbin/udevd -d";
  local_78[1] = "/sbin/mingetty /dev/tty6";
  local_78[2] = "/usr/sbin/console-kit-daemon --no-daemon";
  local_78[3] = "hald-addon-acpi: listening on acpi kernel interface /proc/acpi/event";

I’m renaming local_78 to mw_folderCommand and DAT_00605568 to mw_folderDestination for convenience.

next is strcpy(&DAT_00605768,local_18);

checking local_18 which is “justtryit”.

C
builtin_strncpy(local_18,"justtryit",10);

Summary:

▪Process Masquerading: The malware loads an array of legitimate Linux daemon names (e.g., /sbin/udevd -d, dbus-daemon, /usr/lib/systemd/systemd-journald). It randomly selects one and calls FUN_00401d17 to overwrite argv[0], disguising its process name from tools like ps and top.
▪Host-Based Mutex: It decodes the hex values at mw_pathname (**DAT_00605400) to construct the file path /var/run/xinetd.lock**. It checks for this file using access(); if it exists, the malware exits immediately to prevent duplicate instances.
▪Privilege Check: It calls getuid() to verify it is running with root privileges (UID 0). If it is not root, the function exits without executing the payload.
▪Daemonization: It invokes daemon(0,0), which forks the process, terminates the parent, detaches from the terminal to run silently in the background, and changes the working directory to /.
▪Payload Execution: After creating the lock file to secure its execution slot and configuring signal handling (SIGCHLD), it calls FUN_0040239a() to launch the primary malicious payload.

Then we have the function:

C
      FUN_00401d17(argc,argv,&mw_folderDestination);

FUN00401d17

C

undefined8 FUN_00401d17(int argc,long *argv,char *mw_folderDestination)

{
  undefined8 uVar1;
  size_t sVar2;
  size_t local_28;
  int local_1c;
  void *local_18;
  long local_10;
  
  local_28 = 0;
  mw_argv = (char *)*argv;
  local_1c = 0;
  while (*(long *)(environ + (long)local_1c * 8) != 0) {
    sVar2 = strlen(*(char **)(environ + (long)local_1c * 8));
    local_28 = sVar2 + local_28 + 1;
    local_1c = local_1c + 1;
  }
  local_18 = malloc(local_28);
  if (local_18 == (void *)0x0) {
    uVar1 = 0xffffffff;
  }
  else {
    local_1c = 0;
    while (*(long *)(environ + (long)local_1c * 8) != 0) {
      sVar2 = strlen(*(char **)(environ + (long)local_1c * 8));
      memcpy(local_18,*(void **)(environ + (long)local_1c * 8),sVar2 + 1);
      *(void **)(environ + (long)local_1c * 8) = local_18;
      sVar2 = strlen(*(char **)(environ + (long)local_1c * 8));
      local_18 = (void *)((long)local_18 + sVar2 + 1);
      local_1c = local_1c + 1;
    }
    local_10 = *argv;
    for (local_1c = 0; local_1c < argc; local_1c = local_1c + 1) {
      sVar2 = strlen((char *)argv[local_1c]);
      local_10 = local_10 + sVar2 + 1;
    }
    local_1c = 0;
    while (*(long *)(environ + (long)local_1c * 8) != 0) {
      sVar2 = strlen(*(char **)(environ + (long)local_1c * 8));
      local_10 = local_10 + sVar2 + 1;
      local_1c = local_1c + 1;
    }
    memset(mw_argv,0,local_10 - (long)mw_argv);
    strncpy(mw_argv,mw_folderDestination,local_10 - (long)mw_argv);
    prctl(0xf,mw_folderDestination);
    uVar1 = 0;
  }
  return uVar1;
}

Summary

TechniqueAPI/Primitives UsedTarget Monitoring Tool Fooled
Argument Overwritingmemset, strncpy on argv[0]ps, cat /proc/[PID]/cmdline
Thread Renamingprctl(PR_SET_NAME)top, htop, cat /proc/[PID]/status
Stability EnforcementHeap relocation of environPrevents segmentation faults due to stack overflow.

FUN0040239a

Copied