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
- Stealth by Design: Butoflex hijacks legitimate executable (e.g., httpd) via library injection, intercepting network traffic while masquerading as normal activity. Its passive trigger mechanism waiting for a specific HTTP request minimizing detection risks.
- Forensic Evasion: The malware wipes command-line arguments mid-execution and tampers with timestamps to sabotage incident response investigations.
- Persistence Through Updates: It monitors file changes (via inode tracking) to reinfect updated binaries, ensuring survival even after patches or reboots - without dropping new files.
- Decade-Long Dormancy: The backdoor’s 10+ year lifespan underscores the threat of ‘sleeping’ compromises in critical infrastructure, where attackers bide their time for high-value targets.
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.

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:
- __libc_csu_fini
- __libc_csu_init
- _DT_FINI
- _DT_INIT
- _edata
- _end
- accept
- accept4
- main
- seteuid
- setresuid
- setreuid
- setuid
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:

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.

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):

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).

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
- T1036.011 : Masquerading: Overwrite Process Arguments
- T1070.006 : Indicator Removal: Timestomp
- T1574.??? : Hijack Execution Flow: Modify executable
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.