Phantom Resolver
The challenge provides us with 2 binary files:
- server_daemon
- libmonitor.so
Looking at the server daemon decompilation:
int __fastcall main(int argc, const char **argv, const char **envp)
{
const char **v3; // rbx
print_banner(argc, argv, envp);
printf("\n[*] Starting daemon in ");
if ( argc <= 1 )
{
LABEL_7:
puts("INTERACTIVE mode");
puts("[!] Warning: daemon mode not enabled");
puts("[!] Use --daemon flag for production deployment");
}
else
{
v3 = argv + 1;
while ( strcmp(*v3, "--daemon") )
{
if ( ++v3 == &argv[(unsigned int)(argc - 2) + 2] )
goto LABEL_7;
}
puts("DAEMON mode");
}
putchar(10);
initialize_subsystems();
puts("\n[*] Running system integrity check...");
system_check();
puts("\n[*] Daemon initialization complete");
return 0;
}
We can see that it prints some lines and then calls system_check(), looking at system check:
// attributes: thunk
__int64 system_check(void)
{
return system_check();
}
the disassembly says:
jmp cs:off_5018
So it jumps to some offset thats not part of this binary, this is where the libmonitor.so comes into play. Let’s decompile the so file.
Looking at the functions, we see a system_check function, We don’t need to understand the entire function, but here’s the important parts
if ( v2 >= 0
&& (close(v2), v3 = open("/proc/self/exe", 0), v4 = v3, v3 >= 0)
&& (lseek(v3, 12288, 0), v5 = read(v4, &buf, 6u), close(v4), v5 > 5)
&& buf == 1330792515
&& v8 == 16717
&& (unsigned int)sub_1300(v4) )
{
return (int (*)())backdoor_function;
}
else
{
return normal_function;
}
Some specific condition triggers the backdoor_function, otherwise it just runs the normal_function. Convering these integer values into ASCII:
buf == 1330792515 -> CHRO
&& v8 == 16717 -> MA
We get “CHROMA”, This will come in handy later.
The normal_function doesn’t really do much
int normal_function()
{
puts("[*] Monitoring system: nominal");
return puts("[*] All security checks passed");
}
Let’s move on to the backdoor function. We can see it using our hardoded key
v3 = _mm_cvtsi32_si128(buf);-> Just the integer buf_intv4 = _mm_xor_si128(_mm_srli_epi32(v3, 0x10u), v3);v5 = _mm_cvtsi128_si32(_mm_xor_si128(v4, _mm_srli_epi32(v4, 8u))) ^ v10 ^ v11;-> XOR the integer with itself right-shifted by 16 bits
This seems to calculate the key from “CHROMA”.
- Take 0x4F524843 (“CHRO”) and XOR it with itself right-shifted by 16 bits → 0x4F520711
- Take that result and XOR it with itself right-shifted by 8 bits → 0x4F1D5516
- Take the lowest byte (0x16) and XOR it with ‘M’ (0x4D) and ‘A’ (0x41)
- 0x16 ^ 0x4D ^ 0x41 = 0x1A (Decimal 26)
So the calculated key is 0x1A
Now we apply this key (0x1A) to the hex strings found in backdoor_function.
si128 = _mm_load_si128((const __m128i *)&xmmword_2120);
v13[0] = _mm_load_si128((const __m128i *)&xmmword_2130);
*(__m128i *)((char *)v13 + 9) = _mm_load_si128((const __m128i *)&xmmword_2140);
The logic is a “rolling XOR” where the key changes slightly using the previous byte
v7 = 86;
do
{
p_si128 = (__m128i *)((char *)p_si128 + 1);
putchar((unsigned __int8)v5 ^ (unsigned __int8)v7);
v7 = p_si128->m128i_i8[0];
}
while ( p_si128->m128i_i8[0] );
We can script the decryption logic in python, and retrieve the hardcoded bytes from the binary (xmmword_21…).
import struct
def solve():
print("[-] Configuring Master Key...")
# 1. The hardcoded key derived from "CHROMA"
key = 0x1A # Calculated from step 1
# 2. Reconstruct the encrypted buffer (Little Endian)
# xmmword_2120
chunk1 = bytes.fromhex("772A6E742E726A615C4E59742A772956")[::-1]
# xmmword_2130 (We only need the first 9 bytes before the overwrite hits)
chunk2_full = bytes.fromhex("4579746F7C2B4568296C762A69296845")[::-1]
chunk2_part = chunk2_full[:9]
# xmmword_2140 (This overwrites the buffer starting at offset 25)
# Note: The snippet for 2140 was 30 chars long (15 bytes)
chunk3 = bytes.fromhex("676368296E692E774579746F7C2B45")[::-1]
# Combine them: Chunk1 + Chunk2_Part + Chunk3
# (The overwrite logic in C effectively stitches these together)
ciphertext = chunk1 + chunk2_part + chunk3
print("[-] Decrypting...")
flag = ""
v7 = 86 # Initial seed (0x56)
for byte in ciphertext:
# Decrypt char: Key ^ Previous_Byte
decrypted_char = key ^ v7
flag += chr(decrypted_char)
# Update Previous_Byte to current ciphertext byte
v7 = byte
print(f"\n[+] FLAG FOUND: {flag}")
if __name__ == "__main__":
solve()
Running this script gives us the flag:
L3m0nCTF{ph4nt0m_r3s0lv3r_1func_m4st3ry}