← Back to home

iCTF 2023 — IslandParty

You open your mailbox and find a strange postcard (invite.bmp). Flipping it around, you squint your eyes and try to decipher the wobbly handwriting:

...

MYSTERIOUS INVITE: 'Generative AI is all the rage this days, so I couldn't pass up the opportunity to use it for this year's invite. Have you heard models like Google's Imagen will include a hidden watermark on AI generated images? I might not have algorithms quite as fancy as Google's, but I've also encoded a little something into this invite--the address! Decode it, and you'll be more than welcome to attend.'

...

MYSTERIOUS INVITE: 'One last piece of advice. All great thing come in three. Three sides to a triangle, three wise monkeys, three lights in a stoplight. Let the number three guide you, and you shall find my island.'

We're given the following image, within which is encoded data about the flag:

<p align="center"> <img src="https://gist.github.com/assets/60120929/498dea08-696d-4f31-ad8c-387738aec7f4" width="600px"> </p>
(the above image has been converted from a .bmp to a .jpg for GitHub, and likely won't work with the rest of this writeup)

Examining the histograms for this image yield some interesting results:

Code (py):

1import cv2
2import numpy as np
3
4import matplotlib
5import matplotlib.pyplot as plt
6
7matplotlib.use('tkagg')
8
9use_cv2_hist = False
10
11
12def make_histogram(img: np.ndarray, title: str, n: int, lim: int = 256):
13    dims = len(img.shape)
14    channel = n - 1 if dims == 3 else 0
15
16    if n != -1:
17        plt.subplot(3, 1, n)
18
19    if use_cv2_hist:
20        plt.plot(cv2.calcHist([img], [channel], None, [lim], [0, lim]))
21    else:
22        data = img[:, :, channel].ravel() if dims == 3 else img.ravel()
23        plt.hist(data, bins=256, range=(0, 255))
24
25    plt.ylabel('Occurrences')
26    plt.xlim([0, lim])
27    plt.title(title)
28
29
30def show_img(img: np.ndarray):
31    make_histogram(img, "Reds", 1)
32    make_histogram(img, "Greens", 2)
33    make_histogram(img, "Blues", 3)
34    plt.show()
35
36    img2 = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)
37
38    make_histogram(img2, "Hues", 1, lim=180)
39    make_histogram(img2, "Saturations", 2)
40    make_histogram(img2, "Values", 3)
41    plt.show()
42
43    img3 = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
44
45    make_histogram(img3, "Intensities", -1)
46    plt.show()
47
48
49img = cv2.imread('invite.bmp')
50img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
51show_img(img)

Figure_d3 Figure_d2

A clue is that the invite image features the DALL-E 2 signature, telling us what it was generated with. Going to the DALL-E 2 website (you'll need to sign in) and scrolling down the list of examples, we find the original "otter with pearl earring" image:

<img width="1248" alt="image (11)" src="https://gist.github.com/assets/60120929/43a907d7-a646-41df-8261-37797dd67963"> <p align="center"> <img src="https://gist.github.com/assets/60120929/3476550b-1706-404f-b3f4-a22c015c7ae4" width="600px"> </p>
(the above image has been converted from a .webp to a .jpg for GitHub, and might not work with the rest of this writeup)

Now that we have the original, we can compare its histograms to those of our invite; notice how the semi-periodic dips in RGB are gone.

Figure_g

This hints that we should taking the diff between the two images. Doing so yields this vibrant mess:

Code (py):

1img = cv2.imread('original.webp')
2# img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
3# show_img(img)
4
5img2 = cv2.imread('invite.bmp')
6# img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB)
7# show_img(img2)
8
9diff = img2 - img
10cv2.imwrite('invite_diff.jpg', diff)
11# show_img(diff)

invite_diff

(can you make out some digits already? :eyes:)

The challenge description mentioned groups of 3, which might just be related to the 3 channels of a color image. Splitting this diff into its R, G, and B channels gives

<p align="center"> <img src="https://gist.github.com/assets/60120929/49b0a335-2e33-47ac-aad2-b7a529afa24d" width="290px"> <img src="https://gist.github.com/assets/60120929/8d0bbb81-fdb4-4863-9e90-7e7d52603301" width="290px"> <img src="https://gist.github.com/assets/60120929/1520c457-59a6-4215-95de-5965778692a0" width="290px"> </p>

and summing these channels with overflow results in the following:

invite_diff_combined

From here, the encoded pattern becomes slightly more discernable. You can make use of the fact that the pattern seems to repeat every 2 rows and columns to disambiguate noisy characters. Inspecting this pattern carefully gives the coordinates of the island:

Code:

122.441N
274.220W
<img width="1083" alt="image" src="https://gist.github.com/assets/60120929/2104ef6f-6b84-468a-8f47-7c87a0cdba2b">