Post

HTB Sherlock: PhantomRing

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

PropertiesValue 
MD56d9a243b881984aa25fc0e770dc0634c 
SHA-16580406b3487c7bc32ae8024add3715131f53d24 
SHA-2562d7b1b2178f76c26893b2a56cbf9b36700235259e76b893d53817d5b66b634a5 
SSDEEP768:MXVXtldVNF91tldVNF91tldVNF91tldVNoAYwIg4QoAa0dRgmAzw1MI:Mt5Mw1M 
TLSHT1FEE2A31BB291DF38E4D4F2301BDBD6E0A62078F06736315F275546B72AB33984B78A46 
File TypeELF 
MagicELF 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 
Telfhasht123d0a750dd2f928911a13570c72b4f8011486a43737311128530c5d4817431cc085e4e 
TrIDELF Executable and Linkable format (Linux) (50.1%)   ELF Executable and Linkable format (generic) (49.8%) 
DetectItEasyELF64   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] 
MagikaELF 
File size30.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

This post is licensed under CC BY 4.0 by the author.