Static and dynamic analysis of Auto-Color’s execution flow, C2 communication, and LD_PRELOAD rootkit.
Auto-Color in a Nutshell
Auto-Color is an emerging Linux backdoor first documented by Palo Alto Networks Unit 42 in February 2025. The earliest known samples were collected between November and December 2024, with universities and government offices in North America and Asia being the main observed targets. The family was named after the filename used by the payload after installation. Its initial delivery method is still unknown, but once executed it can provide an attacker with remote access while using several techniques to hide its activity.
Technical Summary
The sample analyzed in this report has two noticeably different execution paths. Without root privileges, it moves into the background, stores its state under /tmp/cross, and repeatedly tries to contact 146.70.41.178:443. When it runs as root, it goes a step further by installing itself under /var/log/cross and dropping a shared library named libcext.so.2.
This library is the most interesting part of the sample. Auto-Color writes it to /lib/x86_64-linux-gnu/libcext.so.2 and adds its path to /etc/ld.so.preload. This causes the library to load inside other dynamically linked programs, where it can hide the malware's files and filter entries from /proc/net/tcp. Auto-Color therefore combines its backdoor functionality with a user-space rootkit designed to conceal its activity.
- The sample chooses /var/log/cross when running as root and /tmp/cross otherwise.
- It uses a double fork to continue running in the background.
- It repeatedly attempts to reach 146.70.41.178:443 and also connects to local ports 9002 and 9003.
- In a root context, it installs an embedded 35,160-byte library and registers it through /etc/ld.so.preload.
- The library hooks common libc functions, attempts to disable SELinux enforcement, hides implant-related files, and filters /proc/net/tcp.
Sample Information
- Format 64-bit LSB PIE ELF executable
- Architecture x86–64 Linkage
- Dynamically linked
- Symbols Stripped
- Size 229,160 bytes
- Build ID 043cd4c5b9346b072d4c6dff0a87092556031018
- SHA-256 270fc72074c697ba5921f7b61a6128b968ca6ccbf8906645e796cfc3072d4c43
- VirusTotal at capture time 36 of 63 engines detected the file
Figure 1 — Initial VirusTotal triage.Technical Analysis
I kept the original sample unchanged and used a hash-matched working copy for every run. The lab only had the loopback interface enabled, and both /etc/ld.so.preload and /var/log/cross were absent before execution. I traced the sample with strace for 75 seconds, followed its child processes, and then used IDA to explain the behavior seen in the trace.
Choosing the Working Directory
One of the first useful branches in IDA is a direct call to geteuid(). If the effective UID is zero, Auto-Color uses /var/log/cross. Otherwise, it falls back to /tmp/cross. After choosing the directory, both branches continue into the same code.
Figure 2 — geteuid() selects the root or non-root working directory.The non-root trace follows the second path exactly:
mkdir("/var/log/cross", 0777) = -1 EACCES
mkdir("/tmp/cross", 0777) = 0The requested modes shown in the trace are applied before umask, so they do not necessarily represent the final permissions on disk.
Moving into the Background
After selecting its working directory, the sample forks twice:
PID 4915: execve(sample) = 0
PID 4915: clone(...) = 4916
PID 4915: exit_group(0)
PID 4916: clone(...) = 4917
PID 4916: exit_group(0)
PID 4917: chdir("/") = 0
PID 4917: clone3(CLONE_THREAD ...) = 4921
The original process and the first child both exit, while PID 4917 continues, changes its working directory to /, and creates a new thread. This is the classic part of a double-fork pattern used to keep a process running in the background. I did not see a successful setsid() call, so I would not describe it as a complete textbook daemonization sequence.
During this run, the sample printed #green mode... twice, which appears to be its non-root execution mode.
Local Files and State
Once the final child is running, it creates several files under /tmp/cross:
openat(..., "config-err-A7F5EF0D", O_RDWR|O_CREAT, 0666) = 3
flock(3, LOCK_EX|LOCK_NB) = 0
openat(..., "config-err-20FF3326", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 4
write(4, <8-byte binary value>, 8) = 8
The first file, config-err-A7F5EF0D, is most likely used as a single-instance lock because the sample immediately requests an exclusive, non-blocking lock on it. The second file, config-err-20FF3326, contains an eight-byte binary value that the malware reads again roughly every 4.16 seconds. It also keeps looking for config-err-03EB4BCE, although that file was never created during the run.
Figure 3 — Artifacts created under /tmp/cross during non-root execution.I was not able to decode the eight-byte value, so there is not enough evidence to call it a key or an encrypted configuration.
Running as Root
An earlier root run left the following files under /var/log/cross:
/var/log/cross/auto-color
/var/log/cross/config-err-20EE3326
/var/log/cross/config-err-A7E5EF0D
Figure 4 — Files observed under /var/log/cross after root-context execution.That first run showed the files on disk, but it did not capture how they were created. I repeated the test under strace, which exposed the full installation sequence:
#install ok
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libcext.so.2",
O_WRONLY|O_CREAT|O_TRUNC, 0755) = 3
write(3, <embedded ELF>, 35160) = 35160
openat(AT_FDCWD, "/etc/ld.so.preload",
O_WRONLY|O_CREAT|O_TRUNC, 0644) = 3
write(3, "/lib/x86_64-linux-gnu/libcext.so.2", 34) = 34
The same trace captured the creation of /var/log/cross/auto-color. The sample later removed the working copy that had been launched. This confirms what the #install ok message refers to: Auto-Color drops the embedded library and writes its path to /etc/ld.so.preload. On this REMnux system, /lib/x86_64-linux-gnu resolves to the matching path under /usr/lib, which is why some dependency tools displayed a different location for the same file.
Figure 5 — Root-context execution emitted #install ok; the accompanying syscall trace established the writes behind the message.After execution, the installed auto-color file was still present under /var/log/cross:
Figure 6 — auto-color observed under /var/log/cross after root-context execution.The screenshot shows the final filesystem state, while the per-PID trace links these writes directly to the sample.
Network Activity
While running, Auto-Color repeatedly tries to connect to 146.70.41.178 over TCP port 443:
connect(..., {AF_INET, 146.70.41.178:443}, 16) = -1 ENETUNREACH
sendto(..., <16-byte value>, 16, MSG_NOSIGNAL, ...) = -1 EPIPEThe lab had no external route, so every connection failed with ENETUNREACH, followed by an EPIPE when the sample tried to send data. Each attempt included a different 16-byte value and repeated every four to five seconds. Because the connection never completed, I could not determine whether this value is a victim identifier, handshake data, or something else.
Figure 7 — Repeated connection attempts to the observed external endpoint.The sample also starts non-blocking connections to two local ports:
127.0.0.1:9002
127.0.0.1:9003
Both calls returned EINPROGRESS. This confirms the connection attempts, but not a completed session, and the purpose of these local ports remains unclear.
Analyzing the Dropped Rootkit
During the first root run, libcext.so.2 appeared in the process dependency output even though it was not a normal dependency of the main executable:
Figure 8 — libcext.so.2 observed in dependency resolution.The root trace later showed that Auto-Color creates this file itself and registers it through /etc/ld.so.preload. I then extracted the embedded object from the main sample and loaded it separately in IDA.
Extracting the Embedded Library
The installer passes a buffer at 0x250A0, a size of 35,160 bytes, and mode 0755 to a file-writing helper. It then uses the same helper to write the 34-byte library path to /etc/ld.so.preload with mode 0644:
// Simplified from main and sub_868C.
write_file(lib_path, &unk_250A0, 35160, 0755);
write_file("/etc/ld.so.preload", lib_path, 34, 0644);
int write_file(const char *path, const void *data, size_t size, mode_t mode) {
open_fn real_open = dlsym(loader_handle(), "open");
int fd = real_open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);
return write(fd, data, size);
}I exported this range directly from IDA and verified that it was a complete ELF shared object before continuing with the analysis.
- Source range in main sample 0x250A0 through 0x2D9F7
- Size 35,160 bytes (0x8958)
- Format ELF64, little-endian, x86-64, ET_DYN shared object
- SHA-256 bf503b5eb456f74187a17bb8c08bccc9b3d91a7f0f6fd50110540b051510d1ca
- Installed path /lib/x86_64-linux-gnu/libcext.so.2
- Load mechanism Absolute path written to /etc/ld.so.preload
- Direct dependencies libdl.so.2, libc.so.6
Figure 9 — The recovered 35,160-byte ELF loaded independently in IDA Pro.The code matches the root trace exactly. The library is created with O_WRONLY|O_CREAT|O_TRUNC and mode 0755, and the preload file is written with mode 0644. This explains why the library appeared in dependency output: it is injected globally through /etc/ld.so.preload, not declared by the main executable.
How the Library Starts
The shared object has a constructor at 0x2590. Since the library is loaded through /etc/ld.so.preload, this constructor runs before the target program reaches main.
If the process is running as root, the constructor loads libselinux.so.1, resolves security_setenforce, and calls it with 0. This is an attempt to switch SELinux out of enforcing mode.
Figure 10 — Constructor sub_2590 resolves security_setenforce and invokes it with zero in root context.Next, it creates or attaches to a System V shared-memory segment using the key 0x36D94F19. The library uses this segment to share state between its hooks, including the paths /var/log/cross and /etc/ld.so.preload.
Figure 11 — Shared-memory initialization and implant/preload paths in the constructor.Before calling start(), the constructor checks the current executable path. It skips activation inside auditd, cron, crond, acpid, and atd under /sbin or /usr/sbin. This likely helps the rootkit avoid breaking important services or exposing itself inside monitoring-related processes.
Figure 12 — Process exclusions before the constructor calls start().Hooked Functions
The library exports replacements for a wide range of libc functions:
- File openingopen, open64, openat, openat64, fopen, fopen64
- Directory enumerationreaddir, readdir64
- Metadata and permissions__xstat, __xstat64, __lxstat, __lxstat64, statx, newfstatat, chmod, fchmod, fchmodat
- Deletionunlink, unlinkat, remove
- Network and capturerecvmsg, exported pcap_loop and pcap_dispatch wrappers
- Memory handling__wrap_memcpy
By intercepting these functions, the library can control what other programs see when they open files, list directories, query metadata, delete files, or inspect network-related data.
Calling the Real libc Functions
Each hook still needs access to the original libc function. To avoid calling itself again, the wrapper resolves the next implementation with dlsym(RTLD_NEXT, ...). The open hook performs the equivalent of:
real_open = dlsym((void *)-1, "open"); // RTLD_NEXT
fd = real_open(selected_path, flags, mode);
The same pattern appears throughout the file, directory, metadata, permission, deletion, and packet-capture wrappers. This is the core of the library’s interposition mechanism.
Hiding Network Connections
The file-opening hooks pay special attention to paths ending in /net/tcp. When one of these paths is requested, the library builds a filtered copy and returns that file instead of the original procfs entry.
// Behavior reconstructed from the wrappers and sub_2B70.
if (ends_with(requested_path, "/net/tcp")) {
filtered_path = build_filtered_tcp_view("/proc/net/tcp", shared_state);
if (filtered_path)
requested_path = filtered_path;
}
return real_open(requested_path, flags, mode);
The helper reads the real /proc/net/tcp, compares its values with entries stored in shared memory, removes matching lines, and writes the remaining content to a temporary file. A tool such as cat, ss, or a monitoring agent can therefore receive a sanitized socket table while the actual connection still exists in the kernel.
Hiding Its Own Files
The rootkit also needs to hide the files that keep it active. It uses dladdr and realpath to find its own location, then checks file operations against that path and /etc/ld.so.preload. The hooks use alternate .xxx and .real names during redirection, while the readdir hooks remove the relevant entries from directory listings.
At this point, the role of libcext.so.2 is clear. It is a user-space preload rootkit that hides Auto-Color's files and selected network entries. I did not find any evidence of a kernel module or a kernel-level hook.
Indicators of Compromise
Host Indicators
- SHA-256: 270fc72074c697ba5921f7b61a6128b968ca6ccbf8906645e796cfc3072d4c43 — Confirmed sample.
- /tmp/cross — Confirmed in non-root trace.
- /tmp/cross/config-err-A7F5EF0D — Creation and file lock confirmed.
- /tmp/cross/config-err-20FF3326 — Eight-byte write and repeated reads confirmed.
- /tmp/cross/config-err-03EB4BCE — Repeatedly probed but not observed on disk.
- /var/log/cross/auto-color — Observed after root-context execution.
- /var/log/cross/config-err-* — Observed after root-context execution.
- /lib/x86_64-linux-gnu/libcext.so.2 — Installation confirmed; SHA-256: bf503b5eb456f74187a17bb8c08bccc9b3d91a7f0f6fd50110540b051510d1ca.
- /etc/ld.so.preload — Creation and preload-path write confirmed.
- /etc/ld.so.preload.xxx and /etc/ld.so.preload.real — Strings and concealment/redirection logic confirmed through static analysis.
Indicator Status 146.70.41.178:443/TCP Confirmed connection target 127.0.0.1:9002/TCP Confirmed attempt; purpose unknown 127.0.0.1:9003/TCP Confirmed attempt; purpose unknown
Conclusion
The non-root run initially made Auto-Color look like a fairly small backdoor: it moved into the background, maintained a few local files, and kept trying to reach its C2 server. The root run revealed the more important part of the infection. With enough privileges, the sample installs itself under /var/log/cross and registers libcext.so.2 through /etc/ld.so.preload.
After extracting the library, its purpose became clear. It is a user-space rootkit that runs inside other dynamically linked processes, attempts to disable SELinux enforcement, hides its own files, and filters the network information returned from /proc/net/tcp. A few values, including the eight-byte state file and the changing 16-byte network value, are still undecoded, but the main execution and concealment behavior is now understood.
Appendix A: IDA payload-recovery script
import ida_bytes
import os
start = 0x250A0
size = 0x8958
output_dir = r"C:\Users\zyad\Desktop\270fc72074c697ba5921f7b61a6128b968ca6ccbf8906645e796cfc3072d4c43"
output = os.path.join(output_dir, "extracted_payload.elf")
data = ida_bytes.get_bytes(start, size)
assert data is not None
assert len(data) == size
assert data[:4] == b"\x7fELF"
with open(output, "wb") as f:
f.write(data)
print("Start:", hex(start))
print("End:", hex(start + size))
print("Size:", len(data))
print("Saved:", output)The assertions make sure the exported range is complete and starts with the expected ELF magic. After extraction, I used the following commands for a quick verification:
sha256sum extracted_payload.elf
file extracted_payload.elf
readelf -hW -dW -rW -Ws extracted_payload.elf
readelf -x .init_array extracted_payload.elf
nm -D extracted_payload.elf
objdump -d -M intel extracted_payload.elf
strings -a -tx extracted_payload.elf
References
- Unit 42: Linux Malware Auto-Color Targets Universities and Government Organizations
- Detecting Auto-color malware with Wazuh
- Analysis of an AutoColor Backdoor Variant
- Auto-Color malware analysis by zw01f
Introduction to Malware Binary Triage (IMBT) Course
Looking to level up your skills? Get 10% off using coupon code: MWNEWS10 for any flavor.
Enroll Now and Save 10%: Coupon Code MWNEWS10
Note: Affiliate link – your enrollment helps support this platform at no extra cost to you.
Article Link: https://medium.com/@0xzyadelzyat/reverse-engineering-the-auto-color-linux-backdoor-db2aecbd2887?source=rss-b460882b1cf8------2