Your organization’s SOC team intercepted suspicious network activity originating from a core infrastructure server at a major telecommunications provider in Southeast Asia. Despite enterprise-grade EDR solutions being deployed, no alerts were triggered. The malware appears to use advanced evasion techniques involving Berkeley Packet Filter (BPF) to achieve complete network stealth. Your task is to fully reverse engineer this sophisticated backdoor and extract all indicators of compromise.
The sample is a Linux ELF backdoor built around a passive BPF socket filter. It takes a lock file so only one instance runs, copies itself into memory-backed storage under a hidden name, and renames its process to one of several innocuous-looking names. The filter attaches to a raw socket and silently picks out UDP, ICMP and TCP packets carrying magic values, so no listening port ever appears. A trigger packet carries a password that selects either a reverse shell or a bind shell, and the bind shell uses iptables to redirect a port range to the implant. After a short plaintext handshake, shell traffic is encrypted. The malware also disables shell history and stamps a fixed timestamp on its files to hide its tracks.
unzip, using the password from DANGER.txtobjdump, never executedNote: Everything below comes from static disassembly.
mainsits at0x4d30in the objdump listing and the BPF filter at0x35ba. There was no decompiler in the workflow, so the stack strings are read by hand from themovimmediates.
Extract the archive with the password from DANGER.txt and hash the ELF before anything else.
sha256sum shadow_monarch
Disassemble the binary and read main. The path is built on the stack one byte at a time at 0x8280, then passed to access() and to open() with O_CREAT, which is the single-instance check. Reassemble the bytes in order to get the path.
objdump -d -M intel shadow_monarch > dis.txt
Gotcha: Rebuilding a path from byte-sized
movinstructions makes it easy to drop a character. Check the result against thesystemdnaming pattern the author is imitating.
The strings in .rodata include an env style argument that points the shell history file at /dev/null, so commands run through the implant leave no trace in the user’s history.
objdump -s -j .rodata shadow_monarch
Same .rodata dump. The copy target in /dev/shm is a dot-prefixed name styled like a systemd runtime file, so it hides from a plain ls.
objdump -s -j .rodata shadow_monarch
In main the implant calls rand() and takes the result modulo 10 to index a table of process-name pointers. Ten pointers in the table, modulo 10, gives ten possible disguises.
grep -n 'rand' dis.txt
The malware calls utimes() with a hardcoded epoch to timestomp its files. The immediate is 0x654d7280, which converts to midnight UTC on the answer date.
date -u -d @$((0x654d7280)) +%F
The classic BPF program is at 0x35ba. It branches on the IP protocol with jeq for UDP (0x11) and ICMP (1), then compares a field in the payload against the magic constant.
objdump -d -M intel shadow_monarch | sed -n '/35ba/,+80p'
Same filter, TCP branch. The jeq there compares against a different, shorter constant than the UDP/ICMP path.
objdump -d -M intel shadow_monarch | sed -n '/35ba/,+80p'
This is Traffic Signaling: Socket Filters in ATT&CK. The ID was reasoned from the behaviour in Q3 and Q4 (a filter on a raw socket that wakes the implant on magic values), not read from an artifact.
main builds two passwords on the stack and memcmps the received one against them. The branch where memcmp returns 0 is the reverse shell.
sed -n '/4d30/,+400p' dis.txt
The second stack string from Q5. When the comparison against it returns 1, execution takes the bind shell path.
sed -n '/4d30/,+400p' dis.txt
The bind routine loops over a port range, trying to bind each port. The loop counter starts at 0x7a69 and ends at 0x7e50; convert the start to decimal.
echo $((0x7a69))
The bind shell does not leave a visible listener on the attacker-facing port. It runs an iptables -t nat -A <chain> ... REDIRECT command assembled from .rodata strings, steering traffic to the chosen port.
objdump -s -j .rodata shadow_monarch
Once the shell connection is up, the code does a write(fd, rodata+0x6021, 4). Read the four bytes at that .rodata offset.
objdump -s -j .rodata shadow_monarch
The cipher routine keeps a 256-byte state array with two index bytes at offsets +0x100 and +0x101. That layout is the RC4 key schedule and keystream generator. This was reasoned from the structure of the code, not from a named string or library call in the binary.
| Phase | Action |
|---|---|
| Execution | Takes a lock file so only one instance runs |
| Defense Evasion | Copies itself into /dev/shm under a hidden name and renames its process to one of ten disguises |
| Defense Evasion | Sets HISTFILE=/dev/null and timestomps files to a fixed date |
| Command and Control | Opens a raw socket with a BPF filter that matches magic values in UDP, ICMP and TCP packets |
| Command and Control | Trigger password selects a reverse shell or a bind shell |
| Command and Control | Bind shell uses an iptables PREROUTING redirect over a port range |
| Command and Control | Short plaintext handshake, then RC4-encrypted shell traffic |
| Type | Value |
|---|---|
| SHA256 | 41de260cb5518df31ad437f4c0bf0fb2fea848e7648b1911658275da00f2fd16 |
| Lock file | /var/run/systemd-private.pid |
| Dropped copy name | .systemd-runtime (in /dev/shm) |
| BPF magic (UDP/ICMP) | 0x8337 |
| BPF magic (TCP) | 0x4141 |
| Reverse shell password | ph4nt0m_g4t3 |
| Bind shell password | r3d_m3nsh3n |
| Bind port range start | 31337 |
| Handshake string | 3458 |
| Technique | ID | Description |
|---|---|---|
| Traffic Signaling: Socket Filters | T1205.002 | BPF filter on a raw socket wakes the implant on magic values |
| Encrypted Channel: Symmetric Cryptography | T1573.001 | RC4 over the shell channel |
| Masquerading: Match Legitimate Name or Location | T1036.005 | Random choice of ten process names and a systemd-style file name |
| Impair Defenses: Impair Command History Logging | T1562.003 | HISTFILE=/dev/null |
| Indicator Removal: Timestomp | T1070.006 | utimes() with a hardcoded epoch |
A raw socket with a BPF filter is invisible to port-based monitoring. The implant never listens, so ss and port scans show nothing. Audit for processes holding AF_PACKET sockets and for SO_ATTACH_FILTER calls, via auditd on socket and setsockopt or an eBPF sensor.
Execution from /dev/shm with a dot-prefixed name is worth an alert. Memory-backed paths rarely hold legitimate binaries. Hidden files there, especially ones styled like systemd units, should be flagged.
Bind shells that rely on iptables -t nat PREROUTING leave a trail in the firewall. Alert on NAT rule changes outside of change windows and diff the nat table against a known-good baseline.
Process-name masquerading breaks name-based allowlists. Compare a process’s name with its executable path in /proc/<pid>/exe, and treat mismatches as suspicious.
The backdoor hid in plain sight by never opening a port: a BPF filter on a raw socket quietly waited for magic values, and only then did a password pick the shell type. Every static indicator (magic values, passwords, lock path, redirect chain) came out of the disassembly, which is exactly the detail EDR with no raw-socket visibility cannot give you.