⚠️⚠️⚠️ 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 IHDR
, PLTE
, IDAT
, 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
to2
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:
- 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.
- It generates multiple custom chunks with types
- 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.
- Key Derivation:
- The key used for XORing seems to be derived from the chunk data (
v69
) or possibly the chunk type. - Since
v19 = v69
, andv69
points to the data starting with'biTa'
, it’s possible that the chunk data itself is used as the key.
- The key used for XORing seems to be derived from the chunk data (
Reversing the Process
To extract and decode the embedded flag, we’ll need to:
- Parse the PNG File and Extract Custom Chunks:
- Read the PNG file and extract all chunks, including custom ones with types
'biTa'
,'biTb'
, …,'biTi'
.
- Read the PNG file and extract all chunks, including custom ones with types
- Collect Encrypted Data and Keys:
- For each custom chunk:
- Extract the encrypted data (chunk data).
- Derive the key from the chunk data or type.
- For each custom chunk:
- 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)