This challenge is a dynamic reversing challenge where we’re trying to determine the proper input, using only hot and cold responses to close in on the proper flag. We don’t even have to do any hardcore reversing on this one, the challenge can be solved dynamically. If we did go ahead and reverse the binary though, we would see that it’s more or less simply doing a bitwise distance calculation to determine if our flag is closer or farther than the prior guess.
Source Code (not provided with the challenge)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_FLAG_SIZE 256
#define DEBUG 0
// Function to count the number of matching bits between two strings
int count_matching_bits(const char *guess, const char *flag) {
int count = 0;
size_t guess_len = strlen(guess);
size_t flag_len = strlen(flag);
size_t max_len = guess_len > flag_len ? guess_len : flag_len;
for (size_t i = 0; i < max_len; i++) {
char guess_char = i < guess_len ? guess[i] : 0;
char flag_char = i < flag_len ? flag[i] : 0;
for (int bit = 0; bit < 8; bit++) {
int guess_bit = (guess_char & (1 << bit)) != 0;
int flag_bit = (flag_char & (1 << bit)) != 0;
if (guess_bit == flag_bit) {
count++;
}
// Debugging: Print each bit comparison
if (DEBUG) {
printf("Debug: Char %zu, Bit %d: Guess %d, Flag %d, Match: %s\n",
i, bit, guess_bit, flag_bit, guess_bit == flag_bit ? "Yes" : "No");
}
}
}
// Subtract the number of extra bits in the longer string
int extra_bits = (max_len - guess_len) * 8 + (max_len - flag_len) * 8;
count -= extra_bits;
return count;
}
int main() {
FILE *file = fopen("/tmp/flag.txt", "r");
if (!file) {
fprintf(stderr, "Error: flag.txt not found.\n");
return 1;
}
printf("Welcome to the MetaCTF hot coco machine! To brew the perfect drink, you need to provide the exact right flag!\n");
char flag[MAX_FLAG_SIZE];
if (!fgets(flag, MAX_FLAG_SIZE, file)) {
fprintf(stderr, "Error: Could not read flag from flag.txt.\n");
fclose(file);
return 1;
}
fclose(file);
// Remove newline character from flag if present
size_t flag_len = strlen(flag);
if (flag[flag_len - 1] == '\n') {
flag[flag_len - 1] = '\0';
flag_len--;
}
// Debugging: Print the correct flag
if (DEBUG) {
printf("Debug: The correct flag is: %s\n", flag);
}
char guess[MAX_FLAG_SIZE];
int previous_score = -1;
while (1) {
printf("Enter your guess: ");
fflush(stdout);
if (!fgets(guess, MAX_FLAG_SIZE, stdin)) {
fprintf(stderr, "Error reading input.\n");
continue;
}
// Remove newline character from guess if present
size_t guess_len = strlen(guess);
if (guess[guess_len - 1] == '\n') {
guess[guess_len - 1] = '\0';
guess_len--;
}
int current_score = count_matching_bits(guess, flag);
if (strcmp(guess, flag) == 0) {
printf("Congratulations! You've guessed the flag correctly.\n");
fflush(stdout);
break;
}
if (previous_score == -1) {
printf("Good initial guess, let's keep closing in...\n");
} else if (current_score > previous_score) {
printf("Hotter! The coco is getting closer to the right temperature!\n");
} else if (current_score < previous_score) {
printf("Colder! These settings are making the coco colder!\n");
} else {
printf("No change, this guess made no noticeable difference...\n");
}
fflush(stdout);
// Debugging: Show current and previous scores
if (DEBUG) {
printf("Debug: Current score: %d, Previous score: %d\n", current_score, previous_score);
}
previous_score = current_score;
}
return 0;
}
Solve Method
The first step to getting to a solve is to determine the proper length of the flag, since there is no limit to the number of guesses we can make, we can start by just guessing a single character, and keep guessing until we get a “Colder” response, as every “Hotter” response means that we are closer and closer to the correct length.
After we know the proper length, we need to determine which character is used in each position. We could use a smart algorithm for this, but we can also just try each possible character in a position, switch back to the best known character, and then continue. For example, we would start by trying “A”, then “B”, if B is colder than A, then we guess our best known character (A) again, then try C, updating it when we get a new best guess. Once we’ve gone through all the printable characters in a position, we move to the next. With this method, we leak the flag byte by byte.
Solution Script
from pwn import *
from time import sleep
# Start the process
p = remote('host5.metaproblems.com', 7520)
# Step 1: Determine the length of the flag
flag_length = 0
current_guess = ''
previous_score = -1
p.recv(timeout=1)
p.sendline('A')
p.recv(timeout=1)
# Try increasing lengths until we no longer "Hotter" responses, meaning our flag is too long.
for length in range(2, 257): <em># MAX_FLAG_SIZE is 256</em>
guess = b'A' * length
p.sendline(guess)
response = p.recvline().decode()
if "Hotter" in response or "Good initial guess" in response:
flag_length = length
print(flag_length)
else:
break
#sleep(1)
p.success(f"The flag is {flag_length} characters long.")
p.recv()
# Step 2: Determine the correct bits
flag = ['A'] * flag_length
for i in range(flag_length):
best_char = flag[i]
for char in range(32, 127): # Printable ASCII range
if chr(char) in ['\x00', '\n']:
continue
test_flag = flag[:]
test_flag[i] = best_char
test_guess = ''.join(test_flag).encode()
<em># Reset the baseline</em>
p.recv(timeout=1) <em>#clear the buffer</em>
p.sendline(test_guess)
p.recvline(timeout=1)
test_flag[i] = chr(char)
test_guess = ''.join(test_flag).encode()
p.sendline(test_guess)
response = p.recvline().decode()
if "Hotter" in response:
best_char = chr(char)
#p.info(f'New best character for postition {i}: "{best_char}" - Current flag: {''.join(test_flag)}')
elif "Congratulations" in response:
p.success(f"The flag is: {''.join(test_flag)}")
p.close()
exit()
flag[i] = best_char
p.success(f'Determined character for postition {i}: "{best_char}" - Current flag: {''.join(flag)}')
# Join the flag and print it
final_flag = ''.join(flag)
p.error(f"Something went wrong, best guess is: {final_flag}")
# Close the process
p.close()