NiteCTF 2023 : Phantas
Context
We are given this gif and we have to find the hidden message within it.
Writeup
By looking at the gif's individual frames, we recognize that there are two QR codes. One out of two frame is in black and white and the rest is colored and looks corrupted except for the QR Code's position and alignement sections.
The fact that there are blue, green and red pixels confirms that there is in fact 3 QR code in the colored frames because there are occasionally black pixels and purple (blue + red), yellow (green + red), etc ... Meaning that we need to separate channels.
Here's a script to do that :
from PIL import Image
import numpy as np
im = Image.open("chal.gif")
parts_1 = list()
parts_2 = list()
parts_3 = list()
# To iterate through the entire gif
try:
i = 0
while 1:
im.seek(im.tell()+1)
if i % 2 == 0 :
px = im.load()
p = Image.new("RGB", (127,127)).load()
for x in range(127):
for y in range(127):
p[x,y] = px[x,y]
parts_1.append(p)
if i == 0 :
parts_2.append(p)
else :
px = im.load()
p = Image.new("RGB", (127,127)).load()
for x in range(127):
for y in range(127):
p[x,y] = px[x,y]
parts_2.append(p)
i += 1
offset_x = 0
offset_y = 0
except EOFError:
pass # end of sequence
print(i)
print(len(parts_1), len(parts_2))
res_1 = Image.new("RGB", (127*10,127*10))
res_2 = Image.new("RGB", (127*10,127*10))
res_3 = Image.new("RGB", (127*10,127*10))
res_4 = Image.new("RGB", (127*10,127*10))
img1 = res_1.load()
img2 = res_2.load()
img3 = res_3.load()
img4 = res_4.load()
for i in range(len(parts_1)):
offset_x = (i % 10) * 127
offset_y = (i // 10) * 127
for x in range(offset_x, offset_x + 127):
for y in range(offset_y, offset_y + 127):
img1[x,y] = parts_2[i][x - offset_x,y - offset_y]
img2[x,y] = (parts_1[i][x - offset_x,y - offset_y][0])*3
img3[x,y] = (parts_1[i][x - offset_x,y - offset_y][1])*3
img4[x,y] = (parts_1[i][x - offset_x,y - offset_y][2])*3
res_1.save("qr1.jpeg")
res_2.save("qr2.jpeg")
res_3.save("qr3.jpeg")
res_4.save("qr4.jpeg")
Upon scanning, we get rick rolled by qr1.jpeg but qr2.jpeg to qr4.jpeg gives us very large numbers $n$, $e$, $c$. Note that this is how variables used in RSA encryption are commonly called : $n$ for modulus, $e$ for the exponent and $c$ for ciphertext. However, $e$ is an awfully large exponent and makes it obviously vulnerable to Weiner's attack.
Here we store those variables in rsa_params.py and use this script to decipher $c$ :
from Crypto.Util.number import long_to_bytes
from rsa_params import *
import owiener
d = owiener.attack(e, n)
if d is None:
print("Failed")
exit(1)
m = pow(c,d,n)
print(long_to_bytes(m).decode())
Let's go ! We solved it :
$ python solve.py
nite{71m3_70_fr4m3_7h15_c0l0urful_e_n_c_0d3d_qr}