Tricephalic Hellkeeper: a tale of a passive backdoor

We recently found a new passive backdoor targeting Linux and Solaris servers, wich can use TCP, UDP or ICMP packets as it’s triggers.

In this article we will dive into BPF in order to assess it’s capabilities :D

Introduction to Passive backdoors

Passive backdoors are implants designed to be more stealthy than common backdoors, especially by avoiding listening on ports or pinging back to a Command and Control server.

These backdoors are not novel, one of the first sample being cd00r from phenoelit, a passive backdoor waiting for a TCP port knocking sequence, dating back to 2000 [1].

Passive backdoors are also used by some of the most advanced attackers, such as Duqu2 with their portserv.sys driver, the famous Equation Group with their Bvp47 or DewDrop implant [2, 3], or the Turla threat group with their Uroburos rootkit that we previously analyzed [11].

This kind of backdoor have several advantages:

While not as complex as Bvp47, this backdoor provided the attackers with simple yet powerful capabilities, such as a remote access to the infected systems.


This backdoor family uses a BPF filter in order to await a trigger packet, and depending on the received command will either send a ping back, launch a bind shell, or connect a remote shell to the attacker provided IP address.

The implant expects to be launched as the privileged root user, and uses a lock in order to avoid being launched several times. The attacker took special attention to make the lock’s filename appears legitimate, and we have seen several of these filenames used in the samples. They can be found in the IOCs Annex of this article.


The binary does not implement any kind of persistance, so we assume that the attacker will provide this persistance by modifying configuration files on the targeted system during installation.

Protection techniques

The malware uses several technique in order to avoid detection, these techniques varying between samples. Some of them relaunched from memory, in order to avoid traces on the filesystem, while others modified their access and modification timestamp.

In memory launch

Some of the samples copied themself in the /dev/shm folder with a custom filename, before relaunching the copied sample. This technique avoid leaving traces on the target filesystem, and ensures the binary is completely removed on reboot.

Process renaming

The malware will rename itself using the prctl function with the argument PR_SET_NAME, and a random legitimate looking name.

These names are hardcoded in the binary, and vary between the samples.

A partial list of used process names can be found in annex of this article.


In some of the samples, a function dubbed set_time was called to alter the access and modification timestamp of the binary using the utimes function. The timestamp used was always 0x490a083c (2008-10-30T20:17:16).

Command and Control

The command and control mechanism of this backdoor is relatively simple, it waits for a trigger packet, then depending on the command, will establish a bind shell, a reverse shell, or send a reply to the attacker.

BPF filter analysis

Berkeley Packet Filter, or BPF, is a technology dating back to the early 90s which was initially designed to filter network packets.

This filtering subsystem is documented in the Linux kernel [5, 6], and is used under the hood by tools such as tcpdump.

This technology was also used by implants such as dewdrop from the Equation toolset [3].

BPF packet filters are implemented using a custom bytecode consisting on about two dozens opcodes, all following the same pattern: {opcode, jt, jf, k}.

Once compiled, such a filter can be instanciated using either libpcap (pcap_setfilter), or the standard library function setsockopt using the SO_ATTACH_FILTER option.

In the case of this backdoor, the latter was chosen, and the BPF bytecode is included in binary form in the sample.

The pseudo code of the filter installation and packet parsing loop looks like this:

  sock_fprog fprog;
  // Simplified copy loop
  memcpy(&stored_data, &filter_bytecode, 0x1e * 8);
  fprog.len = 0x1e;
  fprog.filter = &stored_data;

  // Create a raw socket
  hSocket = socket(AF_PACKET,SOCK_RAW,(uint)uVar1);
  if ((hSocket < 1) || (iVar2 = setsockopt(hSocket, 1, SO_ATTACH_FILTER, &fprog, sizeof(sock_fprog)), iVar2 == -1)) {
  while( true ) {
    do {
      do {
        recvfrom(dwCMD,received_buffer,0x200,0,(sockaddr *)0x0,(socklen_t *)0x0);

        // Now, parse the packet

We chose to dump it from the sample, and developed a BPF processor for Ghidra [7] to disassemble it. This processor will be released shortly after some more testing :)

The disassembled graph is the following:

BPF Bytecode disassembly

The BPF bytecode processes the packet in several steps:

  1. Filter for IPv4 traffic
  2. Triage between UDP / TCP or ICMP traffic
  3. Get the first 2 bytes of packet data
  4. Check if these 2 bytes looks like a trigger packet (0x5293 for TCP, 0x7255 for UDP and ICMP)
  5. return the packet if true

While searching for similar samples, we also found a Solaris variant of this malware. This variant did not use the setsockopt function, but instead relied on libpcap to install it’s filter. This time the filter could be found in text form, which confirmed our analysis of the BPF byte code:

(udp[8:2]=0x7255) or (icmp[8:2]=0x7255) or (tcp[((tcp[12]&0xf0)>>2):2]=0x5293)

We can deduce from this that the backdoor is triggerable by three different means:

  1. An UDP packet starting with the bytes 0x7255
  2. An ICMP packet also starting with the bytes 0x7255
  3. A TCP packet starting with the bytes 0x5293.

Passwords / commands:

If a packet is matched by the filter, it is returned to the malware process in order to be parsed. The expected format is the following:

| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |      ␡ ␡
| PASSWORD (continued)          |

The processing function will extract the command string from the packet, and compare it to a set of hardcoded names, such as:

By default, the implant will send the character “1” (encoded as 0x31) to the IP address provided in the packet.

Both the bind shell and the reverse shell processes are renamed (often as /usr/libexec/postfix/master). The attacker is also provided with a clean environment, the following environment variables being set:

It should be noted that some of the latest variants of the malware are no longer using keywords such as socket for triggering it’s capabilities, but are using MD5 hashes. It may imply that the attacker has improved it’s implant in order to be more resilient or secure.

The whole behaviour of the implant can be summarized in the following schema:

Implant Behavior


If the malware lauches the bind shell or the reverse shell, it will encrypts it’s traffic using the RC4 algorithm. The key used is simply the received command as seen in the previous part (such as socket or justforfun).

Aside note: a backdoor’s bug

There is a slight bug in the malware developper code, which will trigger only if the backdoor is launched when server is receiving a lot of traffic.

This bug was documented in [8], and is due to the fact that the socket can receive traffic before the BPF filter is applied.


The idea behind this backdoor was neither new nor complicated, however it provided the attacker with critical capabilities against the targeted networks. It also allowed them to remain stealthy for a long time.

The attacker was updating regularly its toolset with new keywords, improving a little bit its implant with each release by changing their command names, their processes names, or their filenames in order to avoid trivial detection.

Other publications

While finishing this article and a set of slides presented at an event in early May, we noticed an unusual number of rescans of this malware family. This was due to the fact that security researcher @GossiTheDog tweeted a corresponding hash [9].

This malware family seems to be tracked by PwC as BPFDoor used by the Red Menshen threat actor, and we look forward to their presentation at Troopers 2022 [10]!


IOCs - filenames

These filenames can be used as IOCs because, while looking legitimate, they are used only by this malware family.

IOCs - Hashes

IOCs - Yara rule

rule Linux_TricephalicImplant {
        author = "Exatrack"
        description = "Detect Linux passive backdoors"
        tlp =  "WHITE"
        source =  "Exatrack"

        $str_message_01 = "hald-addon-acpi: listening on acpi kernel interface /proc/acpi/event"
        $str_message__02 = "/var/run/"
        $str_message_03 = "/bin/rm -f /dev/shm/%s;/bin/cp %s /dev/shm/%s && /bin/chmod 755 /dev/shm/%s && /dev/shm/%s --init && /bin/rm -f /dev/shm/%s" // in the stack
        $str_message_04 = "Cant fork pty"

        $str_hald_05 = "/sbin/iptables -t nat -D PREROUTING -p tcp -s %s --dport %d -j REDIRECT --to-ports %d"

        $str_command_01 = "/sbin/iptables -t nat -A PREROUTING -p tcp -s %s --dport %d -j REDIRECT --to-ports %d"
        $str_command_02 = "/sbin/iptables -I INPUT -p tcp -s %s -j ACCEPT"
        $str_command_03 = "/bin/rm -f /dev/shm/%s"
        $str_command_04 = "/bin/cp %s /dev/shm/%s"
        $str_command_05 = "/bin/chmod 755 /dev/shm/%s"
        $str_command_06 = "/dev/shm/%s --init"

        $str_server_01 = "[+] Spawn shell ok."
        $str_server_02 = "[+] Monitor packet send."
        $str_server_03 = "[-] Spawn shell failed."
        $str_server_04 = "[-] Can't write auth challenge"
        $str_server_05 = "[+] Packet Successfuly Sending %d Size."
        $str_server_06 = "[+] Challenging %s."
        $str_server_07 = "[+] Auth send ok."
        $str_server_08 = "[+] possible windows"

        $str_filter_01 = "(udp[8:2]=0x7255)"
        $str_filter_02 = "(icmp[8:2]=0x7255)"
        $str_filter_03 = "(tcp[((tcp[12]&0xf0)>>2):2]=0x5293)"
        $str_filter_04 = {15 00 ?? ?? 55 72 00 00}
        $str_filter_05 = {15 00 ?? ?? 93 52 00 00}

        $error_01 = "[-] socket"
        $error_02 = "[-] listen"
        $error_03 = "[-] bind"
        $error_04 = "[-] accept"
        $error_05 = "[-] Mode error."
        $error_06 = "[-] bind port failed."
        $error_07 = "[-] setsockopt"
        $error_08 = "[-] missing -s"
        $error_09 = "[-] sendto"
        any of ($str*) or 3 of ($error*)

Disassembled BPF bytecode

  // Filter for IPv4 traffic
  0  28 00 00 00 0c 00 00 00  ldh	[0xc]
  8  15 00 00 1b 00 08 00 00  jeq	0x800, +0x0, +0x1b
  10  30 00 00 00 17 00 00 00  ldb	[0x17]

  // Check for UDP
  18  15 00 00 05 11 00 00 00  jeq	0x11, +0x0, +0x5
  20  28 00 00 00 14 00 00 00  ldh	[0x14]
  28  45 00 17 00 ff 1f 00 00  jset	0x1fff, +0x17, +0x0

  // Get packet first 2 bytes of data
  30  b1 00 00 00 0e 00 00 00  ldxb	4*([0xe]&0xf)
  38  48 00 00 00 16 00 00 00  ldh	[x+0x16]

  // Check for trigger marker 0x7255
  40  15 00 13 14 55 72 00 00  jeq	0x7255, +0x13, +0x14

  // Check for TCP protocol
  48  15 00 00 07 01 00 00 00  jeq	0x1, +0x0, +0x7

  // Get packet first two bytes
  50  28 00 00 00 14 00 00 00  ldh	[0x14]
  58  45 00 11 00 ff 1f 00 00  jset	0x1fff, +0x11, +0x0
  60  b1 00 00 00 0e 00 00 00  ldxb	4*([0xe]&0xf
  68  48 00 00 00 16 00 00 00  ldh	[x+0x16]

  // Check for trigger marker
  70  15 00 00 0e 55 72 00 00  jeq	0x7255, +0x0, +0xe

  // Check for TCP packet format
  78  50 00 00 00 0e 00 00 00  ldb	[x+0xe]
  80  15 00 0b 0c 08 00 00 00  jeq	0x8, +0xb, +0xc
  88  15 00 00 0b 06 00 00 00  jeq	0x6, +0x0, +0xb
  90  28 00 00 00 14 00 00 00  ldh	[0x14]
  98  45 00 09 00 ff 1f 00 00  jset	0x1fff, +0x9, +0x0

  // Get packet data
  a0  b1 00 00 00 0e 00 00 00  ldxb	4*([0xe]&0xf)
  a8  50 00 00 00 1a 00 00 00  ldb	[x+0x1a]
  b0  54 00 00 00 f0 00 00 00  and	0xf0
  b8  74 00 00 00 02 00 00 00  rsh	0x2
  c0  0c 00 00 00 00 00 00 00  add	x
  c8  07 00 00 00 00 00 00 00  tax
  d0  48 00 00 00 0e 00 00 00  ldh	[x+0xe]

  // Filter for trigger packet (this time 0x5293)
  d8  15 00 00 01 93 52 00 00  jeq	0x5293, +0x0, +0x1
  e0  06 00 00 00 ff ff 00 00  ret	0xffff
  e8  06 00 00 00 00 00 00 00  ret	0x0

List of process names

This is a partial list of process names which can be used by the malware to masquerade himself.