// HackTheBox  ·  writeup

ShadowMonarch

HackTheBox Hard objdumpsha256sumunzip
Malware Analysis

Scenario

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.

Executive Summary

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.

Artifacts

Note: Everything below comes from static disassembly. main sits at 0x4d30 in the objdump listing and the BPF filter at 0x35ba. There was no decompiler in the workflow, so the stack strings are read by hand from the mov immediates.

Execution

Q1 — What is the SHA256 hash of the malware sample?

Extract the archive with the password from DANGER.txt and hash the ELF before anything else.

sha256sum shadow_monarch
Click flag to reveal 41de260cb5518df31ad437f4c0bf0fb2fea848e7648b1911658275da00f2fd16
Q2 — What is the full path of the mutex/lock file used by the malware to prevent multiple instances?

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 mov instructions makes it easy to drop a character. Check the result against the systemd naming pattern the author is imitating.

Click to reveal answer /var/run/systemd-private.pid

Defense Evasion

Q12 — What environment variable is set to /dev/null to prevent bash history logging?

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
Click to reveal answer HISTFILE
Q13 — What is the filename used when the malware copies itself to /dev/shm?

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
Click flag to reveal .systemd-runtime
Q14 — How many different process names can the malware masquerade as?

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
Click to reveal answer 10
Q11 — What date does this timestamp represent?

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
Click flag to reveal 2023-11-10

Command and Control

Q3 — What magic value does the BPF filter check for in UDP/ICMP packets?

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'
Click flag to reveal 0x8337
Q4 — What magic value does the BPF filter check for in TCP packets?

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'
Click to reveal answer 0x4141
Q15 — What MITRE ATT&CK technique ID describes the use of BPF socket filters for traffic signaling?

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.

Click flag to reveal T1205.002
Q5 — What password initiates a reverse shell connection?

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
Click flag to reveal ph4nt0m_g4t3
Q6 — What password initiates a bind shell?

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
Click to reveal answer r3d_m3nsh3n
Q7 — What is the starting value in the port range used by the bind shell?

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))
Click flag to reveal 31337
Q8 — What iptables chain is used for port redirection?

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
Click to reveal answer PREROUTING
Q9 — What 4-character string is sent as a handshake before encrypted communication begins?

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
Click flag to reveal 3458
Q10 — What encryption algorithm is used for shell communication?

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.

Click to reveal answer RC4

Attack Summary

PhaseAction
ExecutionTakes a lock file so only one instance runs
Defense EvasionCopies itself into /dev/shm under a hidden name and renames its process to one of ten disguises
Defense EvasionSets HISTFILE=/dev/null and timestomps files to a fixed date
Command and ControlOpens a raw socket with a BPF filter that matches magic values in UDP, ICMP and TCP packets
Command and ControlTrigger password selects a reverse shell or a bind shell
Command and ControlBind shell uses an iptables PREROUTING redirect over a port range
Command and ControlShort plaintext handshake, then RC4-encrypted shell traffic

IOCs

TypeValue
SHA25641de260cb5518df31ad437f4c0bf0fb2fea848e7648b1911658275da00f2fd16
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 passwordph4nt0m_g4t3
Bind shell passwordr3d_m3nsh3n
Bind port range start31337
Handshake string3458

MITRE ATT&CK

TechniqueIDDescription
Traffic Signaling: Socket FiltersT1205.002BPF filter on a raw socket wakes the implant on magic values
Encrypted Channel: Symmetric CryptographyT1573.001RC4 over the shell channel
Masquerading: Match Legitimate Name or LocationT1036.005Random choice of ten process names and a systemd-style file name
Impair Defenses: Impair Command History LoggingT1562.003HISTFILE=/dev/null
Indicator Removal: TimestompT1070.006utimes() with a hardcoded epoch

Defender Takeaways

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.

Conclusion

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.