[Writeup] Huntress 2024 (Reverse Engineering): OceanLocust

⚠️⚠️⚠️ This is a solution to the challenge. This post will be full of spoilers.
Download the binaries here: https://github.com/mlesterdampios/huntress-2024-binary-challenges

We are given a png file and a binary with it. Upon initial triage, seems like the binary is a tool for Steganography. Our task is to retrieve the file from the png by reversing the binary and make a decryption tool.

Finding the entry point:

Now we reverse this gigantic function.

First, let’s understand the PNG file.

1. Understand the PNG File Structure

PNG files consist of an 8-byte signature followed by a series of chunks. Each chunk has the following format:

  • Length: 4 bytes (big-endian integer)
  • Chunk Type: 4 bytes (ASCII characters)
  • Chunk Data: Variable length
  • CRC: 4 bytes (Cyclic Redundancy Check)

Standard chunk types include IHDRPLTEIDAT, and IEND. However, PNG files can also contain custom ancillary chunks, which can be used to store additional data without affecting the image’s visual appearance.

2. Identify Custom Chunks

From the code snippet, it seems the application is adding custom chunks to the PNG file. Look for chunk types that are not standard. In the code, you can see references to functions that handle chunks, such as sub_140005D60, which appears to add a chunk with a given type.

sub_140005D60(&v70, &v50, "IHDR", 4i64);

But since IHDR is a standard chunk, look for other custom chunk types being used. Since the code is obfuscated, we might not see the actual chunk names directly. However, we can infer that custom chunks are being added to store the flag data.

So what I did was to put a breakpoint at `v19 = v69` as this variables would likely contain the information how the chunks are stored.

1st bp hit:

debug023:0000023C584F7EC0                 db  62h ; b
debug023:0000023C584F7EC1                 db  69h ; i
debug023:0000023C584F7EC2                 db  54h ; T
debug023:0000023C584F7EC3                 db  61h ; a
debug023:0000023C584F7EC4                 db  3Ch ; <
debug023:0000023C584F7EC5                 db    2

2nd bp hit:

debug023:0000023C584F7EC0                 db  62h ; b
debug023:0000023C584F7EC1                 db  69h ; i
debug023:0000023C584F7EC2                 db  54h ; T
debug023:0000023C584F7EC3                 db  62h ; b
debug023:0000023C584F7EC4                 db  3Ch ; <
debug023:0000023C584F7EC5                 db    2

3rd bp hit:

debug023:0000023C584F7EC0                 db  62h ; b
debug023:0000023C584F7EC1                 db  69h ; i
debug023:0000023C584F7EC2                 db  54h ; T
debug023:0000023C584F7EC3                 db  63h ; c
debug023:0000023C584F7EC4                 db  3Ch ; <
debug023:0000023C584F7EC5                 db    2

It just repeats, but only the 0000023C584F7EC3 changes alphabetically until reaching `i`.

Notable Patterns:

  • The bytes at offsets 0 to 2 are constant: 'b''i''T'.
  • The byte at offset 3 changes from 'a' to 'b' to 'c', incrementing alphabetically up to 'i'.
  • The rest of the bytes remain constant or contain padding.

Interpreting the Data

Given that the data starts with 'biT' followed by a changing letter, it’s likely that this forms a chunk type in the PNG file.

Chunk Type Formation:

Chunk Type: 4 ASCII characters.

The observed chunk types are:

  • 'biTa'
  • 'biTb'
  • 'biTc'
  • 'biTi'

Understanding the Application’s Behavior

From your decompiled code and observations, the application seems to:

  1. Create Custom PNG Chunks:
    • It generates multiple custom chunks with types 'biTa''biTb', …, 'biTi'.
    • These chunks are likely used to store encrypted portions of the flag.
  2. Encrypt Flag Data:
    • The flag is divided into segments.
    • Each segment is XORed with a key derived from the chunk type or chunk data.
    • The encrypted segments are stored in the corresponding custom chunks.
  3. Key Derivation:
    • The key used for XORing seems to be derived from the chunk data (v69) or possibly the chunk type.
    • Since v19 = v69, and v69 points to the data starting with 'biTa', it’s possible that the chunk data itself is used as the key.

Reversing the Process

To extract and decode the embedded flag, we’ll need to:

  1. Parse the PNG File and Extract Custom Chunks:
    • Read the PNG file and extract all chunks, including custom ones with types 'biTa''biTb', …, 'biTi'.
  2. Collect Encrypted Data and Keys:
    • For each custom chunk:
      • Extract the encrypted data (chunk data).
      • Derive the key from the chunk data or type.
  3. Decrypt the Data:
    • XOR the encrypted data with the derived key to recover the original flag segments.
    • Concatenate the decrypted segments to reconstruct the full flag.

1. Read the PNG File and Extract Chunks

import struct

def read_chunks(file_path):
    with open(file_path, 'rb') as f:
        # Read the PNG signature
        signature = f.read(8)
        if signature != b'\x89PNG\r\n\x1a\n':
            raise Exception('Not a valid PNG file')

        chunks = []
        while True:
            # Read the length (4 bytes)
            length_bytes = f.read(4)
            if len(length_bytes) < 4:
                break  # End of file
            length = struct.unpack('>I', length_bytes)[0]

            # Read the chunk type (4 bytes)
            chunk_type = f.read(4).decode('ascii')

            # Read the chunk data
            data = f.read(length)

            # Read the CRC (4 bytes)
            crc = f.read(4)

            chunks.append({
                'type': chunk_type,
                'data': data,
                'crc': crc
            })

        return chunks

2. Identify Custom Chunks

def extract_custom_chunks(chunks):
    standard_chunks = {
        'IHDR', 'PLTE', 'IDAT', 'IEND', 'tEXt', 'zTXt', 'iTXt',
        'bKGD', 'cHRM', 'gAMA', 'hIST', 'iCCP', 'pHYs', 'sBIT',
        'sPLT', 'sRGB', 'tIME', 'tRNS'
    }
    custom_chunks = []
    for chunk in chunks:
        if chunk['type'] not in standard_chunks:
            custom_chunks.append(chunk)
    return custom_chunks

3. Sort Chunks Based on Sequence

def sort_custom_chunks(chunks):
    # Sort chunks based on the fourth character of the chunk type
    return sorted(chunks, key=lambda c: c['type'][3])

4. Extract Encrypted Data and Keys

def derive_key_from_chunk_type(chunk_type):
    return chunk_type.encode('ascii')

5. Decrypt the Encrypted Data

def xor_decrypt(data, key):
    decrypted = bytearray()
    key_length = len(key)
    for i in range(len(data)):
        decrypted_byte = data[i] ^ key[i % key_length]
        decrypted.append(decrypted_byte)
    return bytes(decrypted)

6. Combine Decrypted Segments

def extract_flag_from_chunks(chunks):
    flag_parts = []
    for chunk in chunks:
        key = derive_key_from_chunk_type(chunk['type'])
        # Or use derive_key_from_chunk_data(chunk)
        encrypted_data = chunk['data']
        decrypted_data = xor_decrypt(encrypted_data, key)
        flag_parts.append(decrypted_data)
    flag = b''.join(flag_parts)
    return flag.decode()

7. Full Extraction Script

def extract_flag(file_path):
    chunks = read_chunks(file_path)
    custom_chunks = extract_custom_chunks(chunks)
    sorted_chunks = sort_custom_chunks(custom_chunks)
    flag = extract_flag_from_chunks(sorted_chunks)
    return flag

# Example usage
flag = extract_flag('embedded_flag.png')
print("Recovered Flag:", flag)

Full Code

import struct
import sys

def read_chunks(file_path):
    """
    Reads all chunks from a PNG file.

    :param file_path: Path to the PNG file.
    :return: List of chunks with their type, data, and CRC.
    """
    chunks = []
    with open(file_path, 'rb') as f:
        # Read the PNG signature (8 bytes)
        signature = f.read(8)
        if signature != b'\x89PNG\r\n\x1a\n':
            raise Exception('Not a valid PNG file')

        while True:
            # Read the length of the chunk data (4 bytes, big-endian)
            length_bytes = f.read(4)
            if len(length_bytes) < 4:
                break  # End of file reached
            length = struct.unpack('>I', length_bytes)[0]

            # Read the chunk type (4 bytes)
            chunk_type = f.read(4).decode('ascii')

            # Read the chunk data
            data = f.read(length)

            # Read the CRC (4 bytes)
            crc = f.read(4)

            chunks.append({
                'type': chunk_type,
                'data': data,
                'crc': crc
            })

    return chunks

def extract_custom_chunks(chunks):
    """
    Filters out standard PNG chunks to extract custom chunks.

    :param chunks: List of all chunks from the PNG file.
    :return: List of custom chunks.
    """
    standard_chunks = {
        'IHDR', 'PLTE', 'IDAT', 'IEND', 'tEXt', 'zTXt', 'iTXt',
        'bKGD', 'cHRM', 'gAMA', 'hIST', 'iCCP', 'pHYs', 'sBIT',
        'sPLT', 'sRGB', 'tIME', 'tRNS'
    }
    custom_chunks = []
    for chunk in chunks:
        if chunk['type'] not in standard_chunks:
            custom_chunks.append(chunk)
    return custom_chunks

def sort_custom_chunks(chunks):
    """
    Sorts custom chunks based on the fourth character of the chunk type.

    :param chunks: List of custom chunks.
    :return: Sorted list of custom chunks.
    """
    return sorted(chunks, key=lambda c: c['type'][3])

def derive_key_from_chunk_type(chunk_type):
    """
    Derives the key from the chunk type.

    :param chunk_type: Type of the chunk (string).
    :return: Key as bytes.
    """
    return chunk_type.encode('ascii')

def xor_decrypt(data, key):
    """
    Decrypts data by XORing it with the key.

    :param data: Encrypted data as bytes.
    :param key: Key as bytes.
    :return: Decrypted data as bytes.
    """
    decrypted = bytearray()
    key_length = len(key)
    for i in range(len(data)):
        decrypted_byte = data[i] ^ key[i % key_length]
        decrypted.append(decrypted_byte)
    return bytes(decrypted)

def extract_flag_from_chunks(chunks):
    """
    Extracts and decrypts the flag from custom chunks.

    :param chunks: List of sorted custom chunks.
    :return: Decrypted flag as a string.
    """
    flag_parts = []
    for chunk in chunks:
        key = derive_key_from_chunk_type(chunk['type'])
        encrypted_data = chunk['data']
        decrypted_data = xor_decrypt(encrypted_data, key)
        flag_parts.append(decrypted_data)
    flag = b''.join(flag_parts)
    # Remove padding if any (e.g., 0xAB bytes)
    flag = flag.rstrip(b'\xAB')
    return flag.decode('utf-8', errors='replace')

def extract_flag(file_path):
    """
    Main function to extract the flag from the PNG file.

    :param file_path: Path to the PNG file.
    :return: Decrypted flag as a string.
    """
    # Read all chunks from the PNG file
    chunks = read_chunks(file_path)
    # Extract custom chunks where the flag is hidden
    custom_chunks = extract_custom_chunks(chunks)
    # Sort the custom chunks based on their sequence
    sorted_chunks = sort_custom_chunks(custom_chunks)
    # Extract and decrypt the flag from the custom chunks
    flag = extract_flag_from_chunks(sorted_chunks)
    return flag

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print("Usage: python extract_flag.py <path_to_png_file>")
        sys.exit(1)
    png_file_path = sys.argv[1]
    try:
        recovered_flag = extract_flag(png_file_path)
        print("Recovered Flag:", recovered_flag)
    except Exception as e:
        print("An error occurred:", str(e))
        sys.exit(1)

Flag!

Leave a Reply

Your email address will not be published. Required fields are marked *