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)
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.
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)
(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:
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