
There are two files provided to us: chall.jpg and font-reference.png. The font reference file is self-explanatory and there’s no hidden data in it. chall.jpg, on the other hand, appears to be a QR code with some text at the bottom right:


The text at the bottom right of the QR code is “{not_”. Along with this, three hints are presented to us:
- QR codes don’t have to contain just text…
- Make sure the flag format is followed.
- Letters tend to be towards the lower right of the images.
Reading the chall.jpg QR image
zbarimg
is a command line tool that will allow us to scan QR codes with ease. However, when we try scanning this QR code with it, we get raw binary data rather than text.

Because of this, we will pipe the output to a file, and specify some special parameters to zbar in order to make sure it doesn’t mangle the binary data by thinking of it as text data. When running the “file” command, it tells us that the scanned file is a ZIP file.

When we unzip the ZIP file, we are presented with a single file “a”. It is a bcapng file (from the bcapng challenge), which is why the description made sure to let us know to do that challenge first. So, not only will this be a QRussian_Dolls writeup, it’ll also be a bcapng writeup 😀

from PIL import Image
#taken from the bcapng challenge
#the beginning numbers are likely the width and height
with open("a", "r") as f:
#construct the image
data = f.read()[1:-1].split("_")
width = int(data[0])
height = int(data[1])
with Image.new("RGB", (width, height), 0) as img:
#generate the pixels
x = 0
y = 0
for pixel in data[2]:
#define the current pixel color
color = (255,255,255)
if pixel == "0":
color = (0,0,0)
#set the current pixel
try:
img.putpixel((x,y), color)
except Exception as e:
print(f"Err at {(x,y)}: {e}")
print((width,height))
exit()
#increment the x
x += 1
if x >= width:
x = 0
y += 1
#scale up the image so zbarimg can read it
multiplier = 3
with Image.new("RGB", (width*multiplier, height*multiplier), 0) as scaled:
#we will iterate through each pixel row by row
current_x = 0
current_y = 0
for y in range(height):
#set the current Y
current_y = y*multiplier
#iterate X
for x in range(width):
#set the current X
current_x = x*multiplier
#each pixel now occupies a pseudo-pixel of size MULTIPLIER
for x_mult in range(multiplier):
for y_mult in range(multiplier):
scaled.putpixel((current_x+x_mult, current_y+y_mult), img.getpixel((x,y)))
#save the scaled image
scaled.save("out2.jpg")

The text in the second QR code is “babu5hk” (remembering that in l33tspeak, “s”=5). This means that, combined with the first QR code, we have bcactf{n0t_babu5hk
Analyzing the second QR code
When performing the same procedure as the previous QR code, we get something different. It’s not a bcapng file; it’s something…different.

Or, well, not exactly. You see, it’s still a bcapng image, except we don’t have the width and height explicitly stated, and the 0’s are spaces, 1’s are hashtags. For figuring out the width and height, I basically just did trial and error, until I discovered that the QR code fit perfectly with width=90, and height=45 allowing for all of the QR code to fit the entire image and for zbarimg to recognize it.
The dump2.py script is basically the same to the bcapng.py script, except width and height are set to those static numbers, and the new 0’s=space and 1’s=hashtag rule is in play.

So, now we have the text {[email protected]_d0. It looks close enough to be the full flag, maybe close enough for you to guess the rest, but it’s time to do one final QR scan.
The final scan
Hmmm…weird. This time it isn’t a ZIP; it’s just a straight up GIF file.

Since running it in “display” will make the program automatically exit after a certain period of time, we’ll open the file in Firefox (or anything to view pictures, really).

It’s pretty obvious what it says, and it even has the closing tag, meaning we’re done! So, our full flag, with the added prefix, is bcactf{[email protected]_d0115}.