Quick Recovery 1337UP Live 2024 Walkthrough

CyanCharley
6 min readNov 17, 2024

--

The challenge

Quick Recovery is one of the CTF Challenges in the misc category of 1337UP Live 2024 CTF challenge worth 100 points.

What we know

Hey, check this QR code ASAP! It’s highly sensitive so I scrambled it, but you shouldn’t have a hard time reconstructing — just make sure to update the a_order to our shared PIN. The b_order is the reverse of that 😉

qrecovery.zip

In the zip file we get The following

  • Obscured.png

The obscured.png has an image of a scrambled qr code that we have to solve for the flag.

  • gen.py

The gen.py is a python script that was used in scrambling the qr code.

from PIL import Image, ImageDraw
from itertools import permutations
import subprocess

qr_code_image = Image.open("qr_code.png")
width, height = qr_code_image.size
half_width, half_height = width // 2, height // 2

squares = {
"1": (0, 0, half_width, half_height),
"2": (half_width, 0, width, half_height),
"3": (0, half_height, half_width, height),
"4": (half_width, half_height, width, height)
}


def split_square_into_triangles(img, box):
x0, y0, x1, y1 = box
a_triangle_points = [(x0, y0), (x1, y0), (x0, y1)]
b_triangle_points = [(x1, y1), (x1, y0), (x0, y1)]

def crop_triangle(points):
mask = Image.new("L", img.size, 0)
draw = ImageDraw.Draw(mask)
draw.polygon(points, fill=255)
triangle_img = Image.new("RGBA", img.size)
triangle_img.paste(img, (0, 0), mask)
return triangle_img.crop((x0, y0, x1, y1))

return crop_triangle(a_triangle_points), crop_triangle(b_triangle_points)


triangle_images = {}
for key, box in squares.items():
triangle_images[f"{key}a"], triangle_images[f"{key}b"] = split_square_into_triangles(
qr_code_image, box)

a_order = ["1", "2", "3", "4"] # UPDATE ME
b_order = ["1", "2", "3", "4"] # UPDATE ME

final_positions = [
(0, 0),
(half_width, 0),
(0, half_height),
(half_width, half_height)
]

reconstructed_image = Image.new("RGBA", qr_code_image.size)

for i in range(4):
a_triangle = triangle_images[f"{a_order[i]}a"]
b_triangle = triangle_images[f"{b_order[i]}b"]
combined_square = Image.new("RGBA", (half_width, half_height))
combined_square.paste(a_triangle, (0, 0))
combined_square.paste(b_triangle, (0, 0), b_triangle)
reconstructed_image.paste(combined_square, final_positions[i])

reconstructed_image.save("obscured.png")
print("Reconstructed QR code saved as 'obscured.png'")

Looking at the working mechanism

The python code has two parameters (a_order & b_order) that are to be edited. Lets use a dummy image to test the code.

What we found

  • When the elements in the List a_order and b_order are higher than 4 or less than 1 or isnt an intiger an error occurs. Therefore, both a_order and b_order only contain integers between 1 and 4. i.e. [1, 2, 3, 4]
  • When any element is repeated in either a_order or b_order the resulting image contains repeated sections thus causing loss of data. Therefore, all elements in a_order and b_order are unique within their own list. For example:

Here we can see,

when a_order = [“1”, “1”, “4”, “4”] and b_order = [“2”, “2”, “3”, “3”] Sections labeled as “2”, “3” ,“A” and “D” are lost

but when a_order = [“1”, “2”, “3”, “4”] and b_order = [“4”, “3”, “2”, “1”] no Sections are lost but are just scrambled within the image.

  • Also we now know how the above script functions

First lets take our original image and divide it into 4 sections represented by the four colors. Then divide each section into halves through a diagonal and label each section by either a number or letter.

Now lets see how a_order and b_order works

The elements in a_order determines the sections to be placed in positions 1, 2, 3, and 4 and the elements in b_order determines the sections to be placed in positions A, B, C, and D. For example

a_order = [“3”, “2”, “4”, “1”]
b_order = [“2”, “3”, “4”, “1”] means

section 3 in position 1, section 2 in position 2, section 4 in position 3 and section 1 in position 4

section B in position A, section C in position B, Section D in position C and section A in position D

Solving the challenge

We know

  • The elements of a_order and b_order are unique integers between 1 and 4
  • a_order is in reverse order of b_order

By permutation we know there are 24 possible lists for either a_order or b_order.

[“1”, “2”, “3”, “4”] [“1”, “2”, “4”, “3”] [“1”, “3”, “2”, “4”] [“1”, “3”, “4”, “2”] [“1”, “4”, “2”, “3”] [“1”, “4”, “3”, “2”]
[“2”, “1”, “3”, “4”] [“2”, “1”, “4”, “3”] [“2”, “3”, “1”, “4”] [“2”, “3”, “4”, “1”] [“2”, “4”, “1”, “3”] [“2”, “4”, “3”, “1”]
[“3”, “1”, “2”, “4”] [“3”, “1”, “4”, “2”] [“3”, “2”, “1”, “4”] [“3”, “2”, “4”, “1”] [“3”, “4”, “1”, “2”] [“3”, “4”, “2”, “1”]
[“4”, “1”, “2”, “3”] [“4”, “1”, “3”, “2”] [“4”, “2”, “1”, “3”] [“4”, “2”, “3”, “1”] [“4”, “3”, “1”, “2”] [“4”, “3”, “2”, “1”]

if we take the above lists as a_order we can write b_order in the reverse order and use this to brute force. I modified the provided python script to run through all possible lists and output an image file for each.

from PIL import Image, ImageDraw
from itertools import permutations

# Load the QR code image
qr_code_image = Image.open("qr.png")
width, height = qr_code_image.size
half_width, half_height = width // 2, height // 2

# Define the squares
squares = {
"1": (0, 0, half_width, half_height),
"2": (half_width, 0, width, half_height),
"3": (0, half_height, half_width, height),
"4": (half_width, half_height, width, height)
}

# Function to split squares into triangles
def split_square_into_triangles(img, box):
x0, y0, x1, y1 = box
a_triangle_points = [(x0, y0), (x1, y0), (x0, y1)]
b_triangle_points = [(x1, y1), (x1, y0), (x0, y1)]

def crop_triangle(points):
mask = Image.new("L", img.size, 0)
draw = ImageDraw.Draw(mask)
draw.polygon(points, fill=255)
triangle_img = Image.new("RGBA", img.size)
triangle_img.paste(img, (0, 0), mask)
return triangle_img.crop((x0, y0, x1, y1))

return crop_triangle(a_triangle_points), crop_triangle(b_triangle_points)

# Prepare triangle images
triangle_images = {}
for key, box in squares.items():
triangle_images[f"{key}a"], triangle_images[f"{key}b"] = split_square_into_triangles(qr_code_image, box)

# Get all permutations of orders
orders = list(permutations(["1", "2", "3", "4"]))

# Iterate through all permutations
for a_order_tuple in orders:
a_order = list(a_order_tuple)
b_order = list(reversed(a_order))

# Create a new reconstructed image
reconstructed_image = Image.new("RGBA", qr_code_image.size)

# Define positions for the final assembly
final_positions = [
(0, 0),
(half_width, 0),
(0, half_height),
(half_width, half_height)
]

# Assemble the new image
for i in range(4):
a_triangle = triangle_images[f"{a_order[i]}a"]
b_triangle = triangle_images[f"{b_order[i]}b"]
combined_square = Image.new("RGBA", (half_width, half_height))
combined_square.paste(a_triangle, (0, 0))
combined_square.paste(b_triangle, (0, 0), b_triangle)
reconstructed_image.paste(combined_square, final_positions[i])

# Save the image with the appropriate filename
output_filename = f"{''.join(a_order)}_{''.join(b_order)}.png"
reconstructed_image.save(output_filename)
print(f"Saved: {output_filename}")

Solution

The script ran through all possible lists and the output file when
a_order = [“2”, “4”, “1”, “3”] and b_order = [“3”, “1”, “4”, “2”] gives a valid qr code

which upon scanning gives the flag as

INTIGRITI{7h475_h0w_y0u_r3c0n57ruc7_qr_c0d3}

Alternative Method

Since the obscured qr code is not scrambled too much an alternative method can be used for solving it. I named this method the JIGSAW_PUZZLE_METHOD (patent pending). And it works exactly the way it sounds. Lets go through the steps

  • Step1: Take the obscured.png file in a form where it can be chopped into pieces. You can print it out and use scissors or in my case i use photoshop.
  • Step2: Solve it as if its a jigsaw puzzle.
  • Step3: Scan the solved qr code.
  • Step4: Submit the flag and feel good.

Github link : https://github.com/RonakBajracharya/Quick_Recovery_1337UP_Live_2024

--

--

No responses yet