Spot the Difference — SECPlayground Christmas CTF 2023 Writeup

Spot the Difference [Crypto / Stego, Medium 20 Pts., 1 Solve]

Nisaruj Rattanaaram
4 min readDec 26, 2023

Santa’s on a mission and needs your help! Two Christmas images are hiding a crucial message for this year’s festivities. Santa loves a good mystery and hasn’t spilled a single clue about where the hidden flag might be. Format: crypto{…}

Given two Santa images: bad_santa_original.png and bad_santa.png

Given two similar png images, we start by comparing them using Stegsolve. With Image Combiner, we should notice the differences between pixels in a diagonal line in SUB mode, like below.

We will work further on a Python script from now on because it’s more convenient. We load two images in matrix form and then compare them with an XOR function because it makes more sense than a subtraction.

Once XORed the given images, we will learn that in a diagonal manner, each pixel has a difference of just one bit — the Least Significant one. Therefore, it would make sense now that we cannot visually identify the differences with our eyes. The result can be multiplied by 255 to make it easier to spot the differences.

from PIL import Image
import numpy as np

img_ori = Image.open('bad_santa_original.png').convert('RGB')
arr_ori = np.array(img_ori)

img_new = Image.open('bad_santa.png').convert('RGB')
arr_new = np.array(img_new)

print(arr_ori.shape, arr_new.shape) # (1870, 1870, 3)

diff = (arr_new ^ arr_ori) * 255

diff_img = Image.fromarray(diff, 'RGB')
diff_img.save('diff.png')

We should now clearly see the differences after running the script.

The XOR Result (diff.png)

The next step is finding a way to extract the flag from our result. Following pixels in the diagonal line, we see that some of them indicate the difference (R, G, or B dots) while the others don’t (black dots). How about transforming these to 0s and 1s and forming them into a binary string?

FLAG_LEN = 21

# For each pixel in diagonal, return False if there is no difference
# return True otherwise.
result = np.any(diff.diagonal(), axis=0)

# Truncate to keep only useful pixels
result = result[:FLAG_LEN * 8]

# Transform array of booleans to binary string
result = ''.join(list(map(lambda p: str(int(p)), result)))

print(result)
# 100111001000110110000110100011111000101110010000100001001011110111011110110001110101111101000010001101000100010001011111001001000100000101001110010101000100000101111101

We finally have the binary string! It’s time to convert to an ASCII string! Unfortunately, we only got a valid half of the flag :(

Transform binary to ascii string (CyberChef)

What if we try to flip the bits in the first half before decoding it? To do so, we can XOR each bit with ‘1’ or ‘True’. Run the script again to get the real flag!

FLAG_LEN = 21

# For each pixel in diagonal, return False if there is no difference
# return True otherwise.
result = np.any(diff.diagonal(), axis=0)

# Truncate to keep only useful pixels
result = result[:FLAG_LEN * 8]

# Flip the bits in the first half
result[:FLAG_LEN // 2 * 8 - 7] ^= True

# Transform array of booleans to a binary string
result = ''.join(list(map(lambda p: str(int(p)), result)))

print(result)
# 011000110111001001111001011100000111010001101111011110110100001000100001010001110101111101000010001101000100010001011111001001000100000101001110010101000100000101111101
Transform binary to ascii string again (CyberChef)

crypto{B!G_B4D_$ANTA}

Solving Script

from PIL import Image
import numpy as np

img_ori = Image.open('bad_santa_original.png').convert('RGB')
arr_ori = np.array(img_ori)

img_new = Image.open('bad_santa.png').convert('RGB')
arr_new = np.array(img_new)

diff = (arr_new ^ arr_ori) * 255

diff_img = Image.fromarray(diff, 'RGB')
diff_img.save('diff.png')

FLAG_LEN = 21

# For each pixel in diagonal, return False if there is no difference
# return True otherwise.
result = np.any(diff.diagonal(), axis=0)

# Truncate to keep only useful pixels
result = result[:FLAG_LEN*8]

# Flip the bits in the first half
result[:FLAG_LEN // 2 * 8 - 7] ^= True

# Transform array of booleans to a binary string
result = ''.join(list(map(lambda p: str(int(p)), result)))

# Decode the flag
flag = ''
for i in range(0, len(result), 8):
flag += chr(int(result[i:i+8], 2))

print('The flag is', flag)
# The flag is crypto{B!G_B4D_$ANTA}

--

--

Nisaruj Rattanaaram
Nisaruj Rattanaaram

Written by Nisaruj Rattanaaram

Cybersecurity Engineer | HTB CPTS, Sec+, PenTest+, CEH | CTF Player

No responses yet