PicoCTF 2025 Cryptography—Guess My Cheese Part 2 Writeup

Glad to have y’all back to my WriteUp! In today’s post, I’ll walk you through a detailed, step-by-step breakdown of how I successfully tackled the Guess My Cheese Part2 challenge from picoCTF 2025. Get ready for an in-depth look at my approach, techniques, and insights into solving this exciting puzzle. Let’s dive in!

Analyzing the hints provided:

Hint 1: I heard that SHA-256 is the best hash function out there!

Hint 2: Remember Squeexy, we enjoy our cheese with exactly 2 nibbles of hexadecimal-character salt!

Hint 3: Ever heard of rainbow tables?

Here’s the challenge

As you can see it provides us some SHA-256 Hash and it’s clear that it’s salted.

Now what is SHA-256 first?

SHA-256 is a type of cryptographic hash function, meaning it takes any input (like text or data) and turns it into a fixed-size string of characters, typically 64 characters long when written in hexadecimal. It’s part of the SHA-2 family, which is a set of algorithms created by the NSA to provide secure ways of verifying data.

Now, let’s dive back into the challenge. If you take a closer look at the Cheese List, you’ll notice that the cheese names aren’t consistent. Some names mix uppercase and lowercase letters, and even include symbols like parentheses. This inconsistency means that different text encodings can represent these names as bytes in different ways. Some common encodings to consider are UTF-8, UTF-16 (both little-endian and big-endian), and Latin-1.

The goal here is to pair each cheese name with salt values, but with so many cheese names in the list, how do we efficiently handle this? For every cheese name (and its variations), we combine it with salt values that range from 0 to 255. The salt can be appended to the end, prepended to the beginning, or inserted at various points within the cheese name itself. This creates a wide range of potential combinations. The next step is to hash each combination. For every possible combination of cheese name, salt value, case variation, and encoding, we will calculate the SHA-256 hash.

Next, as the hash-cracking tools run, we will compare the computed hash with the target hash. If a match is found, we’ve successfully identified the correct combination.

Here’s my custom Python tool that automates the entire process:

import hashlib
import sys
import time

# Target SHA-256 hash to match
target_sha256_hash = "GIVEN_HASH"

# Supported encodings for testing
encoding_formats = ["utf-8", "utf-16-le", "utf-16-be", "latin-1"]

# Case transformations to apply
def case_original(text):
    return text

def case_lower(text):
    return text.lower()

def case_upper(text):
    return text.upper()

case_transformations = {
        "original": case_original,
        "lower": case_lower,
        "upper": case_upper,
}

# We will load the cheese from the cheese list
with open("cheese_list.txt", "r") as file:
    cheese_names = [line.strip() for line in file if line.strip()]

match_found = False

def check_hash(candidate_bytes, method, extra_info, cheese, case_type, encoding, salt):
    """
    Compute SHA-256 hash for a given byte sequence and compare it to the target hash.
    If a match is found, display relevant details and return True.
    """
    global match_found
    computed_hash = hashlib.sha256(candidate_bytes).hexdigest()

    if computed_hash == target_sha256_hash:
        print("\n[!!] VALID MATCH FOUND!")
        print("=" * 40)
        print(f"[+] Cheese Name  : {cheese}")
        print(f"[+] Case Variant : {case_type}")
        print(f"[+] Encoding     : {encoding}")
        print(f"[+] Salt Value   : (0x{salt:02x})")
        print(f"[+] Method Used  : {method}")
        print(f"[+] Extra Info   : {extra_info}")
        print(f"[+] SHA-256 Hash : {computed_hash}")
        try:
            decoded_candidate = candidate_bytes.decode(encoding)
        except Exception:
            decoded_candidate = repr(candidate_bytes)
        print(f"[+] Candidate String ({encoding}): {decoded_candidate}")
        print("=" * 40)
        match_found = True
        return True
    return False

# Start brute-force testing
start_time = time.time()
print("[*] Starting cheese cracking operation....")

for cheese in cheese_names:
    for case_type, transform_func in case_transformations.items():
        modified_cheese = transform_func(cheese)

        for encoding in encoding_formats:
            try:
                cheese_bytes = modified_cheese.encode(encoding)
            except Exception:
                continue

            for salt_value in range(256):
                salt_byte = bytes([salt_value])
                salt_hex_str = format(salt_value, "02x")

                try:
                    salt_hex_bytes = salt_hex_str.encode(encoding)
                except Exception:
                    salt_hex_bytes = salt_hex_str.encode("utf-8")  # Fallback encoding

                # Test different variations of salted hashes
                tests = [
                    (cheese_bytes + salt_byte, "append_raw", "raw byte appended"),
                    (salt_byte + cheese_bytes, "prepend_raw", "raw byte prepended"),
                    (cheese_bytes + salt_hex_bytes, "append_hex", "hex string appended"),
                    (salt_hex_bytes + cheese_bytes, "prepend_hex", "hex string prepended"),
                ]

                for candidate, method, extra_info in tests:
                    if check_hash(candidate, method, extra_info, cheese, case_type, encoding, salt_value):
                        break
                if match_found:
                    break

                # Insert salt byte at every possible index
                for i in range(len(cheese_bytes) + 1):
                    candidate = cheese_bytes[:i] + salt_byte + cheese_bytes[i:]
                    if check_hash(candidate, "insert_raw", f"at index {i}", cheese, case_type, encoding, salt_value):
                        break
                if match_found:
                    break

                # Insert hex-encoded salt at every index
                for i in range(len(cheese_bytes) + 1):
                    candidate = cheese_bytes[:i] + salt_hex_bytes + cheese_bytes[i:]
                    if check_hash(candidate, "insert_hex", f"at index {i}", cheese, case_type, encoding, salt_value):
                        break
                if match_found:
                    break
            if match_found:
                break
        if match_found:
            break
    if match_found:
        break

end_time = time.time()

if not match_found:
    print("[!] No matching cheese and salt combination was found.")
else:
    print(f"\n[+] Execution completed in {end_time - start_time:.2f} seconds.")

After execution, this is the output (Keep in mind that the cracking process might take some time):

We’ve finally obtained the cheese name and its corresponding salt in hexadecimal format, now let’s verify it!

As shown here, the tool prompts for the cheese name and the result. When we enter the plaintext version of the hash with the salt in it, it reveals the flag!

So fun!!!!

Last updated