HTB Sherlock: PhantomRing

PhantomRing

HackTheBox Sherlock, rated Easy. A Linux ELF agent sample to reverse: C2 config, a small command protocol, and some anti-EDR tricks.

Task 1: What is the SHA256 hash of the malicious binary?

Loaded the sample in FLARE-VM and ran it through HashMyFiles for the hash.

ev_1

Answer: 2d7b1b2178f76c26893b2a56cbf9b36700235259e76b893d53817d5b66b634a5

Task 2: What is the IP address hardcoded in the binary for C2 communication?

Ran strings on the binary. Only one IPv4 came out of the output.

Answer: 192.168.56.1

Task 3: What port does the agent connect to on the C2 server?

Disassembled in Ghidra and traced the IP string reference. Got the connect log line, port wr itten in as a hex:

printf("[+] Connected to %s:%d\n","192.168.56.1",0x115d);

0x115d = 4445.

Answer: 4445

Task 4: How many seconds does the agent wait before attempting to reconnect after a failed connection?

Same main function, right after the failure path:

fwrite("connect() failed: trying to reconnect\n",1,0x26,stderr);
...
sleep(0x78);

0x78 = 120.

Answer: 120

Task 5: How many different commands does the agent support? (excluding invalid commands)

process_cmd function is a straight chain of strncmp/strcmp checks, one per command, falling through to a “404 Command not found” if nothing matches:

strncmp(param_3,"get ",4)      -> cmd_get
strncmp(param_3,"recv ",5)     -> cmd_recv
strncmp(param_3,"users",5)     -> cmd_users
strncmp(param_3,"ss",2)        -> cmd_ss
strcmp(param_3,"netstat")      -> cmd_ss
strncmp(param_3,"ps",2)        -> cmd_ps
strncmp(param_3,"me",2)        -> cmd_me
strncmp(param_3,"kick",4)      -> cmd_kick
strncmp(param_3,"privesc",7)   -> cmd_privesc
strcmp(param_3,"sdestruct")    -> cmd_selfdestruct
strncmp(param_3,"killbpf",7)   -> cmd_killbpf
strncmp(param_3,"exit",4)      -> cmd_exit

That’s 12 checks, but ss and netstat both route to cmd_ss, so it’s 11 distinct commands.

Answer: 11

Task 6: What Linux kernel interface does this malware abuse to evade EDR syscall monitoring?

Every command handler goes through the same pattern instead of a plain syscall. Deleting a file, for instance:

local_2a0 = io_uring_get_sqe(param_1);
io_uring_prep_unlinkat(local_2a0,0xffffff9c,local_218,0);
io_uring_submit(param_1);
io_uring_wait_cqe(param_1,&local_2b8);

File reads, unlinks, everything routed through io_uring submission and completion queues instead of open/read/unlink. EDR hooking on classic syscalls won’t be able to see those actions being performed.

Answer: io_uring

Task 7: What file does the agent read to enumerate logged-in users?

cmd_users reads it from disk with a uring read:

iVar2 = read_file_uring(param_1,"/var/run/utmp",local_4018,0x2000);

Answer: /var/run/utmp

Task 8: What directory does the agent scan when searching for SUID binaries for privilege escalation?

cmd_privesc opens the directory and builds a “Potential SUID binaries” report from what it goes through:

local_4330 = opendir("/usr/bin");
...
iVar2 = snprintf(local_4018,0x4000,"Potential SUID binaries:\n");

Answer: /usr/bin

Task 9: What string does the agent search for in /proc/[pid]/maps to identify security tools using eBPF?

cmd_killbpf walks /proc/[pid]/maps looking for a loaded BPF map, the signature of an eBPF-based monitor attached to the process:

pcVar4 = strstr(local_4018,"anon_inode:bpf-map");

Answer: anon_inode:bpf-map

Task 10: What is the full path of the first tracing file the agent attempts to disable?

Same cmd_killbpf, before the eBPF map scan it goes after ftrace directly. Array of three paths at the function start:

local_6138[0] = "/sys/kernel/debug/tracing/tracing_on";
local_6138[1] = "/sys/kernel/debug/tracing/set_event";
local_6138[2] = "/sys/kernel/debug/tracing/current_tracer";

Answer: /sys/kernel/debug/tracing/tracing_on

Task 11: What procfs path does the agent read to find its own executable location before self-destruction?

cmd_selfdestruct resolves its own binary path via the symlink every process has to itself:

sVar1 = strlen("Agent will self-destruct\n");
send_all(param_1,param_2,local_2b0,sVar1);
local_2a8 = readlink("/proc/self/exe",local_218,0x1ff);

Answer: /proc/self/exe

Task 12: What command string is compared by the agent to trigger deletion of its own binary?

Back in process_cmd, the branch that routes to cmd_selfdestruct:

iVar1 = strcmp(param_3,"sdestruct");
if (iVar1 == 0) {
  cmd_selfdestruct(param_1,param_2);

Answer: sdestruct

Command summary

process_cmd (Task 5) in full:

CommandHandlerDoes
get <file>cmd_getExfiltrate a file to the operator
recv <file>cmd_recvReceive a file from the operator
userscmd_usersList logged-in users (Task 7)
ss / netstatcmd_ssList network connections
pscmd_psList processes
mecmd_meReport agent’s own identity/context
kick <target>cmd_kickDisconnect a client/session
privesccmd_privescScan /usr/bin for SUID binaries (Task 8)
sdestructcmd_selfdestructDelete own binary via /proc/self/exe (Tasks 11, 12)
killbpfcmd_killbpfDisable ftrace and hunt eBPF-based monitors (Tasks 9, 10)
exitcmd_exitClose the session

Detection notes

  • Static hash match: 2d7b1b2178f76c26893b2a56cbf9b36700235259e76b893d53817d5b66b634a5.
  • Outbound to 192.168.56.1:4445, reconnect loop on failure every 120s.
  • Heavy io_uring usage for file and process I/O instead of standard syscalls is itself a signal.