My user at this CTF: https://play.fe-ctf.dk/users/21

Task Link to heading

[crypto], [local]
We've heard good things about Argon2, so this must be secure!
Note: Don't be so sad, have a pwnie[link]

Solution Link to heading

Warning
I was too eager to get started and completely missed the “pwnie” image from the task description note. No wonder I had issues… Remember to read the task thoroughly kids!

In this challenge we are given two images and some code. In the challenge there’s a link to another image, which is a picture of a pony. Here are the images:

flag.png
flag.png

what.png
what.png

unipwnie-10.png
unipwnie-10.png

And here’s the provided code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/usr/bin/env python3
import os
import sys
import itertools
from PIL import Image, ImageChops
from argon2.low_level import hash_secret_raw, Type

def expand_key(key, size):
    return hash_secret_raw(
        key,
        hash_len=size,
        salt=b'saltysalt',
        time_cost=1,
        memory_cost=1024,
        parallelism=1,
        type=Type.I,
    )

def imgcrypt(img, key):
    keyimg = Image.new(img.mode, img.size)
    keyimg.frombytes(expand_key(key, len(img.tobytes())))
    return ImageChops.add_modulo(img, keyimg)

if __name__ == "__main__":
    if len(sys.argv) != 4:
        print(f'usage: {sys.argv[0]} <keyfile> <inimg> <outimg>')
        exit(1)

    inimg = Image.open(sys.argv[2])

    keyfile = sys.argv[1]
    if os.path.exists(keyfile):
        with open(keyfile, 'rb') as f:
            key = f.read()
    else:
        print(f'Generating key; saving to {keyfile}')
        key = os.urandom(32)
        with open(keyfile, 'wb') as f:
            f.write(key)

    outimg = imgcrypt(inimg, key)
    outimg.save(sys.argv[3])

Funny story - I completely missed the “Argon2” hint because I rushed to solve this challenge… Argon2 is a key derivation function (KDF) and the intended way to solve this, might have been to guess the key used for the encryption (Which seems pretty gnarly to be honest…)

If you’ve ever tried to encrypt images using AES, you might be familiair with ECB mode and why that sucks. I recall hearing about about encrypting several plaintexts (Or images) using the same key. This kind of encryption is vulnerable to “known-plaintext attacks” and it inspired me to try to XOR each pixel from the what.png and flag.png images.

XOR’ing two ciphertexts (Or images) that was made with the same key, is known as a known-plaintext attack and I had the idea to try this for the images. In short:

1
2
3
4
5
6
7
8
9
C_flag = Enc(flag.png, K) 
C_what = Enc(what.png, K) 

# If we XOR
C_flag ⊕ C_what = Enc(flag.png, K) ⊕ Enc(what.png, K)

# The K (Representing the key, that we don't know in this case) cancels out, 
# and we are left with the XOR of the original plaintexts.  
C_flag ⊕ C_what = flag.png ⊕ what.png

So, the idea is, to take each pixel from both what.png and flag.png and XOR them. While that did produce results, I was having a hard time with my script because of the alpha channels. After a couple of hours I finally had a script that produced a workable result. I’ll post the script here and explain the idea below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from PIL import Image

def xor_images(image1_path, image2_path, output_path):
    # Open the two images
    image1 = Image.open(image1_path)
    image2 = Image.open(image2_path)

    # Ensure that the two images have the same size
    if image1.size != image2.size:
        print("Images must have the same dimensions for XOR operation.")
        return

    # Ensure that both images are in the same mode (RGBA)
    if image1.mode != "RGBA":
        image1 = image1.convert("RGBA")

    if image2.mode != "RGBA":
        image2 = image2.convert("RGBA")

    # Create an empty output image with the same dimensions
    result = Image.new("RGBA", image1.size)

    # Iterate over the pixels and perform the XOR operation
    for x in range(image1.width):
        for y in range(image1.height):
            pixel1 = image1.getpixel((x, y))
            pixel2 = image2.getpixel((x, y))

            # XOR the pixel values
            xor_result = tuple(p1 ^ p2 for p1, p2 in zip(pixel1, pixel2))

            # Set the XOR result as the pixel value in the output image
            result.putpixel((x, y), xor_result)

    # Save the result to the output path
    result.save(output_path)

if __name__ == "__main__":
    image1_path = "what.png"  # Replace with the path to your first image
    image2_path = "flag.png"  # Replace with the path to your second image
    output_path = "result.png"  # Path to save the XOR result

    xor_images(image1_path, image2_path, output_path)

Description:
This Python script performs an XOR operation on two RGBA images, creating an output image. The XOR operation combines the two input images to reveal differences between them.

How the Script Works:

  1. The script uses the Python Imaging Library (PIL) to work with the images. It opens two input images: image1 and image2.

  2. To ensure consistency, the script checks that both input images have the same dimensions (width and height). Images with different dimensions cannot be XORed together.

  3. The script also ensures that both images are in the RGBA mode (Red, Green, Blue, Alpha). If they are not, it converts them to RGBA mode. RGBA mode allows us to work with color and transparency information.

  4. An output image, result, is created with the same dimensions as the input images. This image will store the result of the XOR operation.

  5. The script iterates over each pixel of the input images using nested loops. For each pixel, it retrieves the RGBA values from image1 and image2.

  6. The script then performs an XOR operation on the RGBA values of the two pixels. XORing two values returns 1 (True) if they are different and 0 (False) if they are the same. This operation reveals differences in color and transparency between the input images.

  7. The resulting XOR value is a tuple containing the RGBA values of the new pixel in the output image.

  8. The script sets this XOR result as the pixel value in the result image at the same coordinates (x, y) as the original pixels.

  9. The loop continues until all pixels in the input images have been processed.

  10. Once all XOR operations are complete, the resulting result image is saved to the specified output file path.

Here’s the result I got (You can switch the color theme of this site to see better):

result.png
result.png

As mentioned, probably not the intended solution, but by fiddling with the contrast and colors I was able to read the flag. Thanks for the challenge 🍻