THM Hackfinity Reversing Challenge Writeup: Compute Some Magic
Challenge Overview
The "Compute Some Magic" challenge was a reverse-engineering task involving a 64-bit ELF binary running as a network service on 10.10.184.40:9003
. The goal was to interact with the server, provide the correct input (a "magic spell"), and retrieve the flag. The binary was provided (or its disassembly was analyzed), and the challenge required understanding the program’s logic to determine the correct input that would trigger the flag to be sent.
The flag format was expected to be THM{...}
, as this was a TryHackMe challenge. The final flag retrieved was THM{s0m3_mag1c_that.can_b3_computed}
.
Initial Analysis
Step 1: Connect to the Server
I started by connecting to the server using netcat
to understand its behavior:
nc 10.10.184.40 9003
The server responded with the prompt "Compute some magic!", indicating that I needed to provide a "magic spell" as input. I initially tried the input gandalf
, but the server didn’t respond with the flag, suggesting the input was incorrect.
Step 2: Analyze the Binary
The binary’s disassembly and decompiled code were provided, revealing it to be a 64-bit ELF executable for Linux. Key sections and functions were identified:
ELF Structure:
- The binary had standard ELF sections like
.text
,.plt
,.data
, and.bss
. - Imported functions included
socket
,bind
,listen
,accept
, andsend
(indicating a network server), as well asfopen
,fread
, andfclose
(suggesting file I/O, likely for reading the flag).
Key Functions:
main
: Sets up a TCP server on port 9003, accepts client connections, and processes input.sendBanner
: Sends the "Compute some magic!" prompt to the client.checkSpell
: Validates the user’s input and determines whether to proceed.read_flag
: Reads the flag from a file (flag.txt
) and sends it to the client.
Main Function
The main
function (decompiled) revealed the program’s flow:
void main(void) {
int local_4c; // Server socket
int local_48; // Client socket
int local_44; // Result of checkSpell
size_t local_40; // Length of input
struct sockaddr local_38; // Socket address structure
char local_28[24]; // Input buffer
socklen_t local_50; // Placeholder for accept length argument
while (true) {
local_4c = socket(AF_INET, SOCK_STREAM, 0); // Create TCP socket (using constants for clarity)
// Set socket options, bind to port 9003, and listen
local_38.sa_family = AF_INET; // AF_INET (usually 2)
// Assuming local_38 is sockaddr_in structure
((struct sockaddr_in *)&local_38)->sin_port = htons(9003); // Port 9003 (0x232b)
((struct sockaddr_in *)&local_38)->sin_addr.s_addr = INADDR_ANY; // Bind to any interface
bind(local_4c, &local_38, sizeof(local_38)); // Use sizeof structure
listen(local_4c, 3);
local_50 = sizeof(local_38); // Initialize length for accept
local_48 = accept(local_4c, &local_38, &local_50); // Accept client
sendBanner(local_48); // Send "Compute some magic!"
read(local_48, local_28, 0x10); // Read up to 16 bytes of input
// Null-terminate the buffer safely after read
local_28[15] = '\0'; // Ensure null termination if 16 bytes read
// Find potential newline and remove it
char *newline = strchr(local_28, '\n');
if (newline) {
*newline = '\0';
}
local_40 = strlen(local_28); // Calculate length after potential newline removal
local_44 = checkSpell(local_28, local_48); // Validate input
if (local_44 == 1) {
puts("\nSpell check successful.");
} else {
puts("\nSpell check failed.");
}
close(local_48);
close(local_4c);
}
}
- The server listens on port 9003, accepts a connection, sends a banner, reads up to 16 bytes of input into
local_28
, and callscheckSpell
to validate the input. - If
checkSpell
returns1
, it prints "Spell check successful."; otherwise, it prints "Spell check failed.". - Notably,
main
doesn’t directly callread_flag
, suggesting thatcheckSpell
(or a function it calls) is responsible for retrieving the flag.
read_flag Function
The read_flag
function’s disassembly showed it opens flag.txt
, reads its contents, and sends them to the client:
// Pseudocode (I forgot to save the code)
void read_flag(int client_socket) {
char flag_buffer[128]; // Assumming buffer size
FILE *file = fopen("flag.txt", "r");
if (file != NULL) {
if (fgets(flag_buffer, sizeof(flag_buffer), file) != NULL) {
send(client_socket, flag_buffer, strlen(flag_buffer), 0);
}
fclose(file);
}
}
This confirmed that the flag was stored in flag.txt
, and read_flag
would send it over the socket if called.
checkSpell Function
The checkSpell
function was the key to solving the challenge:
int checkSpell(char *input_spell, int client_socket) {
size_t spell_len;
int result;
spell_len = strlen(input_spell);
if (spell_len < 1) {
puts("Empty string.");
result = 0;
} else {
switch (input_spell[0]) { // Check the first character
case 'A': func_1(input_spell, client_socket); break;
case 'B': func_2(input_spell, client_socket); break;
case 'C': func_3(input_spell, client_socket); break;
case 'D': func_4(input_spell, client_socket); break;
case 'E': func_5(input_spell, client_socket); break;
case 'F': func_6(); break;
case 'G': func_7(input_spell, client_socket); break;
case 'H': func_8(input_spell, client_socket); break;
case 'I': func_9(); break;
case 'J': func_11(); break;
case 'K': func_12(); break;
case 'L': func_13(input_spell, client_socket); break;
case 'M': func_14(); break;
case 'N': func_15(); break;
case 'O': func_16(); break;
case 'P': func_17(input_spell, client_socket); break;
case 'Q': func_17(input_spell, client_socket); break; // Note: P and Q call the same function
// R is missing
case 'S': func_19(); break;
case 'T': func_20(); break;
case 'U': func_21(); break;
case 'V': func_22(); break;
case 'W': func_23(); break;
case 'X': func_24(input_spell, client_socket); break; // This likely calls read_flag
case 'Y': func_25(input_spell, client_socket); break;
case 'Z': func_26(input_spell, client_socket); break;
default:
always(); // Called for non-uppercase letters or 'R'
return 0; // Spell check fails
}
// If a case matches (and doesn't return early), spell check succeeds
result = 1;
}
return result;
}
checkSpell
checks the first character of the input (input_spell
).- If the input is empty, it returns
0
(failing the check). - Otherwise, it uses a
switch
statement to handle uppercase lettersA
toZ
(exceptR
). - For each matching letter, it calls a corresponding function (
func_1
tofunc_26
). - If the first character isn’t
A
toZ
(or isR
), it callsalways()
and returns0
. - If a function is called (and the
switch
case completes),checkSpell
returns1
, leading to "Spell check successful." inmain
.
Solving the Challenge
Step 3: Test Initial Inputs
My first input, gandalf
, failed because it started with a lowercase g
. The switch
statement in checkSpell
only handles uppercase A
to Z
, so gandalf
triggered the default
case, called always()
, and returned 0
, resulting in "Spell check failed.".
I then tried Gandalf
(capitalizing the first letter):
nc 10.10.184.40 9003
(Server responds: Compute some magic!)
(Input: Gandalf)
Server Output:
Spell check successful.
- This triggered
func_7
(forG
) and returned1
, printing "Spell check successful.". - However, no flag was sent, indicating that
func_7
didn’t callread_flag
.
Step 4: Hypothesize the Path to the Flag
Since checkSpell
returned 1
for any input starting with A
to Z
(except R
), but Gandalf
didn’t yield the flag, I hypothesized that:
- One of the
func_X
functions (called bycheckSpell
) must callread_flag
. - I needed to test inputs starting with each uppercase letter (
A
toZ
, excludingR
) to find whichfunc_X
function sends the flag.
Step 5: Automate Testing with a Script
To systematically test inputs, I wrote a Python script to try a magic-related word starting with each applicable uppercase letter:
import socket
import sys # For better error output
host = "10.10.184.40"
port = 9003
spells = {
'A': "Abracadabra", 'B': "Bazooka", 'C': "Charm",
'D': "Dumbledore", 'E': "Expelliarmus", 'F': "Fireball",
'G': "Gandalf", 'H': "Hocuspocus", 'I': "Incendio",
'J': "Jinx", 'K': "Kappa", 'L': "Lumos", 'M': "Magic",
'N': "Nox", 'O': "Obliviate", 'P': "Protego",
'Q': "Quidditch", 'S': "Stupefy", 'T': "Transfigure", # Skipping R
'U': "Unicorn", 'V': "Voldemort", 'W': "Wingardium",
'X': "Xeno", 'Y': "Yeti", 'Z': "Zephyr"
}
flag_found = False
for letter, spell in spells.items():
if flag_found:
break
print(f"--- Testing spell: {spell} ---")
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(5) # Set a general timeout
s.connect((host, port))
banner = s.recv(1024).decode()
print(f"Received Banner: {banner.strip()}")
print(f"Sending Spell: {spell}")
s.sendall((spell + "\n").encode()) # Use sendall for reliability
# Receive response (could be success message or flag)
response = ""
try:
# Loop to receive potentially fragmented data
while True:
chunk = s.recv(1024)
if not chunk:
break # Connection closed
response += chunk.decode()
# Check if flag format is in the current response
if "THM{" in response:
print(f"FLAG FOUND with spell '{spell}': {response.strip()}")
flag_found = True
break # Exit inner loop
# Check for common end-of-message markers if needed
if "successful." in response or "failed." in response:
break # Stop receiving if known message received
except socket.timeout:
print("Socket timed out waiting for response.")
except Exception as e:
print(f"Error during receive for spell '{spell}': {e}", file=sys.stderr)
print(f"Received Response: {response.strip()}")
# Double check if flag was found in the loop above
if flag_found:
break # Exit outer loop
if "successful" in response.lower() and not flag_found:
print(f"Spell '{spell}' was successful, but no flag detected in initial response.")
# Optional: Try one more receive just in case? Usually not needed if flag replaces success msg.
# try:
# s.settimeout(1)
# extra_data = s.recv(1024).decode()
# if extra_data and "THM{" in extra_data:
# print(f"FLAG FOUND in subsequent receive for spell '{spell}': {extra_data.strip()}")
# flag_found = True
# except socket.timeout:
# pass # No extra data
except socket.timeout:
print(f"Connection timed out for spell '{spell}'", file=sys.stderr)
except socket.error as e:
print(f"Socket error for spell '{spell}': {e}", file=sys.stderr)
except Exception as e:
print(f"An unexpected error occurred for spell '{spell}': {e}", file=sys.stderr)
if not flag_found:
print("\nFlag not found after testing all spells.")
Running the script produced output similar to this, eventually finding the flag:
--- Testing spell: Wingardium ---
Received Banner: Compute some magic!
Sending Spell: Wingardium
Received Response:
Spell check successful.
Spell 'Wingardium' was successful, but no flag detected in initial response.
--- Testing spell: Xeno ---
Received Banner: Compute some magic!
Sending Spell: Xeno
FLAG FOUND with spell 'Xeno': THM{s0m3_mag1c_that.can_b3_computed}
Step 6: Retrieve the Flag
The input Xeno
(starting with X
) triggered func_24
in checkSpell
. Unlike the other functions tested, func_24
presumably called read_flag
, which sent the flag directly over the socket instead of the "Spell check successful." message. The flag was:
THM{s0m3_mag1c_that.can_b3_computed}
Final Answer
The flag for the "Compute Some Magic" challenge is:
THM{s0m3_mag1c_that.can_b3_computed}
Alternative Approaches
While the script worked, there were other ways to solve the challenge more efficiently:
- Static Analysis:
- Use a decompiler (e.g., Ghidra or IDA) to analyze the binary.
- Find the address or symbol for
read_flag
and look for cross-references (XREFs) to see which function calls it. - Following the call graph back would lead from
read_flag
tofunc_24
, and then to thecase 'X'
block withincheckSpell
.
- Dynamic Analysis:
- Run the binary in a debugger (e.g.,
gdb
):gdb ./compute_magic_binary # Replace with actual binary name
- Set a breakpoint at the
read_flag
function and run the program:(gdb) break read_flag (gdb) run
- In a separate terminal, connect with
nc localhost 9003
(assuming you're running the binary locally) and test inputs (A
,B
,C
...,X
). When you sendXeno
(or any string starting withX
), the breakpoint ingdb
should be hit, confirming thatX
triggers the flag read.
- Run the binary in a debugger (e.g.,
- Manual Testing:
- Manually connect using
nc
and test inputs starting with each letter (A
throughZ
, skippingR
). While slower, this would eventually lead to sendingX...
and receiving the flag.
- Manually connect using
Lessons Learned
- Understand the Program Flow:
- Analyzing the
main
andcheckSpell
functions revealed the core logic: a server validating input based on the first character and dispatching to different functions. - Recognizing that
read_flag
wasn't called directly inmain
pointed towards the functions called bycheckSpell
as the path to the flag.
- Analyzing the
- Leverage Automation:
- The large
switch
statement incheckSpell
strongly suggested a brute-force or systematic testing approach. Scripting the connection and input testing saved significant time and reduced potential errors compared to manual testing.
- The large
- Pay Attention to Details:
- The initial failure with
gandalf
(lowercaseg
) highlighted the case-sensitivity of theswitch
statement (A
toZ
only). - The theme ("Compute some magic!") guided the choice of test inputs, although the specific word (
Xeno
) was less important than the starting letter (X
), found through systematic testing.
- The initial failure with
- Use Appropriate Tools:
- For future, similar challenges, using a decompiler (static analysis) or debugger (dynamic analysis) to directly identify the function calling
read_flag
would likely be much faster than iterating through all possibilities with a script.
- For future, similar challenges, using a decompiler (static analysis) or debugger (dynamic analysis) to directly identify the function calling
Conclusion
The "Compute Some Magic" challenge was a good exercise in basic reverse-engineering and network service interaction. By combining static analysis (understanding the C code structure and function roles like checkSpell
and read_flag
) with systematic testing (either manually or via script), the condition required to trigger the flag (X...
input) could be found, leading to the flag: THM{s0m3_mag1c_that_can_b3_computed}
. This challenge demonstrated how understanding program control flow is crucial in CTF reversing tasks.