Symatrix Writeup Google CTF 2023

Posted on Jul 9, 2023

I solved Symatrix, one of of the challenges on Google CTF 2023. This challenge was categorized in misc and it was a steganography challenge worth 114 points. So I’m writing my first CTF writeup.

The Challenge

The CIA has been tracking a group of hackers who communicate using PNG files embedded with a custom steganography algorithm.
An insider spy was able to obtain the encoder, but it is not the original code.
You have been tasked with reversing the encoder file and creating a decoder as soon as possible in order to read the most recent PNG file they have sent.

Attachments

encoder.c - Looks like it’s using Cython, “a superset of Python that allows you to write Python-like code with optional static typing, which can be compiled into highly optimized C/C++ extensions for improved performance”.

symatrix.png - Notice how the image is symmetric. picture of symatrix.png

Converting Cython to Python

The Cython file has 6810 lines so it took me a little time to figure out, but I noticed that from line 2620, it had a part of Python code commented like this.

  #if defined(__Pyx_Generator_USED) || defined(__Pyx_Coroutine_USED)
  if (__Pyx_patch_abc() < 0) __PYX_ERR(0, 1, __pyx_L1_error)
  #endif

  /* "encoder.py":1
 * from PIL import Image             # <<<<<<<<<<<<<<
 * from random import randint
 * import binascii
 */
  __pyx_t_1 = PyList_New(1); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 1, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_1);
  __Pyx_INCREF(__pyx_n_s_Image);

Then, I pieced all of them together to create one Python file which looks like this.

from PIL import Image
from random import randint
import binascii

def hexstr_to_binstr(hexstr):
    n = int(hexstr, 16)
    bstr = ''
    while n > 0:
        bstr = str(n % 2) + bstr
        n = n >> 1
    if len(bstr) % 8 != 0:
        bstr = '0' + bstr
    return bstr


def pixel_bit(b):
    return tuple((0, 1, b))


def embed(t1, t2):
    return tuple((t1[0] + t2[0], t1[1] + t2[1], t1[2] + t2[2]))


def full_pixel(pixel):
    return pixel[1] == 255 or pixel[2] == 255

print("Embedding file...")

bin_data = open("./flag.txt", 'rb').read()
data_to_hide = binascii.hexlify(bin_data).decode('utf-8')

base_image = Image.open("./original.png")

x_len, y_len = base_image.size
nx_len = x_len * 2

new_image = Image.new("RGB", (nx_len, y_len))

base_matrix = base_image.load()
new_matrix = new_image.load()

binary_string = hexstr_to_binstr(data_to_hide)
remaining_bits = len(binary_string)

nx_len = nx_len - 1
next_position = 0

for i in range(0, y_len):
    for j in range(0, x_len):

        pixel = new_matrix[j, i] = base_matrix[j, i]

        if remaining_bits > 0 and next_position <= 0 and not full_pixel(pixel):
            new_matrix[nx_len - j, i] = embed(pixel_bit(int(binary_string[0])),pixel)
            next_position = randint(1, 17)
            binary_string = binary_string[1:]
            remaining_bits -= 1
        else:
            new_matrix[nx_len - j, i] = pixel
            next_position -= 1


new_image.save("./symatrix.png")
new_image.close()
base_image.close()

print("Work done!")
exit(1)

Analyzing the Python File

I asked ChatGPT.

“Explain this program in 4 sentences”
The code hides binary data within an image by modifying the pixel values. It reads binary data from a file and converts it to a binary string. Then, it iterates through the pixels of the image, embedding the binary data by modifying specific pixels based on certain conditions. The resulting image contains the hidden binary data.

“What do you mean by certain conditions”
By “certain conditions,” I mean specific criteria or requirements that must be met in order to embed the binary data into the image. In the given code, the conditions are:

  1. The position in the image: The code checks if it’s the appropriate position to embed a bit. This is determined by the variable next_position, which ensures that the bits are embedded at regular intervals.

  2. Available space in the pixel: The code checks if the pixel has space available to embed the bit. This is determined by the function full_pixel, which checks if the pixel is fully white (255 in the green and blue channels). If the pixel is full, it means that no more data can be embedded in that pixel.

These conditions ensure that the binary data is embedded in a controlled manner, preserving the image quality while hiding the data within the pixel values.


Anyways, this program copies the original image to make the new image symmetric, then it goes through the newly copied image and edits the pixel to add part of the flag data. When it’s embedding the hidden data to a pixel, it marks the pixel using the function pixel_bit.

new_matrix[nx_len - j, i] = embed(pixel_bit(int(binary_string[0])),pixel)

When I looked into the function pixel_bit, it looks like it returns a certain value with the part of the flag data.

def pixel_bit(b):
    return tuple((0, 1, b))

So, I noticed that I can just look for pixels that looks like (0, 1, b) and add up b to get the full flag data.

Making the Decoder

I created the decoder with similar program to the encoder. Here’s the decoder that I made:

from PIL import Image

def binstr_to_hexstr(binstr):
    n = int(binstr, 2)
    return hex(n)[2:]

def full_pixel(pixel):
    return pixel[1] == 255 or pixel[2] == 255

def decode():
    encoded_image = Image.open("./symatrix.png")

    nx_len, y_len = encoded_image.size
    x_len = nx_len // 2

    encoded_matrix = encoded_image.load()

    binary_string = ""

    for i in range(0, y_len):
        for j in range(0, x_len):
            l = encoded_matrix[j, i]
            r = encoded_matrix[nx_len - j - 1, i]
            if l[0] == r[0] and l[1]+1 == r[1]:
                binary_string += str(r[2])

    hex_string = binstr_to_hexstr(binary_string)
    print(hex_string)
    decoded_data = bytes.fromhex(hex_string)

    with open('./decoded_file.txt', 'wb') as f:
        f.write(decoded_data)

    encoded_image.close()

    print("Decoding complete!")

decode()

By running the decoder, the flag was outputted in decoded_file.txt with the flag CTF{W4ke_Up_Ne0+Th1s_I5_Th3_Fl4g!}

Conclusion

It took me couple hours to solve this challenge, but I found it really fun solving it. Though I’m a little sad because I couldn’t get to other challenges because I didn’t have much time, I’m glad I was able to solve Symatrix. The files I used to solve Symatrix can be found here. Thanks for reading my first writeup!