Categories
CTF Writeups

[CTF] interIUT 2k21 – Programming series : Redemption

Hey everyone ! I participated to the interIUT CTF 2021 edition with my team Arn'Hack last week end, and I wanted to share with you the write up of the Redemption serie. This 3 challenges were quite nice, and two of them were about image processing.

The first programming challenge I will not review here was about sorting souls at the gates of hell. We basically received a list of names with adjectives, and depending on the adjective, we had to specify if this person was going to heaven, or hell. It was specified to handle Leo's case (the challenge's creator) in a special way. So even if he was a "destructor" he was straight sent to heaven.

Redemption 1

Challenge's statement

Redemption 1

The maker noticed your scheming with the ARCHlinuxangel. To make it up to the father of all things, you'll need to pass 2 challenges. The first one consists of mowing a lawn in an artistic way. The answer is to be sent matching the following pattern : "coord_x1:coord_y1,coord_x2:etc".

Recon

The concept is actually really simple to understand. We receive a grid, with empty squares and the symbol # in it. We just have to send back the coordinates of all crosses.

There is an anomaly though, we have to send back Y first.

y\x  0   1   2
0  | _ | X | _ |
1  | _ | X | X |
2  | X | _ | _ |

For this grid, we should send back 0:1,1:1,1:2;2:0.

Solution

from pwn import *

context.log_level = 'debug'

r = remote("10.3.240.168", 1337)

pelouse = r.recvuntil("Eh") # Getting the grid
coords = []
lines = pelouse.decode().strip().split("\r\n") # Splitting it in seperate lines

for y, line in enumerate(lines):
    cases = line.split("|")
    cases = [w for w in cases if w=="#" or w=="_"] #removing parasites
    for x, case in enumerate(cases):
        if (case == "#"):
            coords.append(f"{y}:{x}")
 
r.sendline(",".join(coords).encode())

r.recvall()
r.close()

You'll notice I never print what I receive at the very end. For some reason, printing it was freezing my terminal, and since the goal was to solve it as fast as possible, I just used the context.log_level = 'debug' line to print all the traffic that goes through pwntools.

CTFIUT{hErbe_TondU_reDEmPti0n_Est_t0n_dU}

Redemption 2

Challenge's statement

Redemption 2

Well done, you've passed your first challenge to redemption. Let's get to the second. A hord of old ladies just arrived at a crossing, help them get over the street. To do so, you'll just have to say yes or no.

Recon

The first thing I did, is as always starting up the challenge by hand a few times, and trying to get as far as I can, to figure out what is randomly generated, what remains the same every time, what is the input awaited, etc.

As we connect to the socket, we receive a fairly long string, base64 encoded. It starts with "IVBOR". It's actually the base64 equivalent of the first bytes of a PNG image. I started writing a few python lines, allowing me to visualize the images that were sent to me, that didn't seem to be always the same.

from pwn import *
from PIL import Image
import io

p = remote("10.3.250.224", 1337)

data = p.recvuntil("\r\n").decode().strip().encode()
img = Image.open(io.BytesIO(base64.b64decode(data)))
img.show()

p.close()

First I use pwntools to connect to the socket by passing the IP and the port. Next, I collect the data the socket sends me until I reach a \r\n ( end of line). Finally, the data is decoded from base64, loaded as an image using PIL, and displayed.

The images we get are red or green pedestrian lights. We're supposed to answer "yes" when the light is green, and "no" when it's red. I tried, but it failed : we are supposed to answer fast.

After running the script a few times, I noticed that the set images seemed to be quite small since I often received pictures I'd already seen, even if the dimensions slightly changed. So I employed myself to download them all and sort them. It can easily be done by adding the following code to my program.

img = img.resize((200, 200))
img.save("./prog/tmp.png")

I resized all the images to 200x200 to make the later processing of the images easier for me.

It didn't take long until I had 5 green lights named from v1.png to v5.png (v for "vert") and 5 red lights named from r1.png to r5.png.

Image database

On top of the random resizing, the images were also slightly blurred, which made a direct byte per byte comparison impossible.

Solving by using similarity checks

I used a library called "image_similarity_measures" to perform a similarity check from our current image against all 10 images of our database. The one with the highest similarity score was considered the one, and the file name would tell us if the light was red or green.

I used two indices, SSIM (Structural similarity) and RMSE (Root-mean-square deviation) which will respectively measure the similarity and the differences between our images.

I usually use PIL for Image processing, but the documentation for this library used CV2, and since I was on a clock, I did the same to save some time converting the documentation code snippets.

from image_similarity_measures.quality_metrics import rmse, ssim
import cv2

ssim_measures = {}
rmse_measures = {}

img = cv2.imread("./tmp.png") # image sent by the socket
dim = (img.shape[1], img.shape[0]) # size is 200x200

data_dir = "./prog3/"
for file in os.listdir(data_dir): # cycling through the database
    img_path = os.path.join(data_dir, file)
    cmp_img = cv2.imread(img_path)
    ssim_measures[img_path]= ssim(img, cmp_img)
    rmse_measures[img_path]= rmse(img, cmp_img)

ssim_v = max(ssim_measures.values()) # get the name of the file with highest similarity
rmse_v = min(rmse_measures.values()) # get the name of the file with lowest difference

if ssim_v == rmse_v:
    if ssim_v.split("/")[2][0] == "r":
        ret = "non"
    elif ssim_v.split("/")[2][0] == "v":
        ret = "oui"
else:
    print("ERROR")
    exit()

So first a create dictionaries that will associate an image of the database to a similarity (or difference) value. I then cycle through the database, testing each image against the challenging image, filling the dictionaries. Finally, I recovered the file with the maximum similarity and the lowest rate of the difference. If the two matched I sent "oui" (yes) or "non" (no) based on the file's name.

Of course, this has to be done multiple times, for each old lady. Like for the first challenge, I used the debug output to visualize the flag.

Here is the final script :

from pwn import *
from PIL import Image
from image_similarity_measures.quality_metrics import rmse, ssim
import io, os, cv2

p = remote("10.3.250.224", 1337)

context.log_level = 'debug'

p.recvuntil("\r\n")
while True:
    texte = p.recvuntil("\r\n").decode().strip().encode()

    ret = ""
    img = Image.open(io.BytesIO(base64.b64decode(texte)))
    img = img.resize((200, 200))
    img.save("./tmp.png")
    img = cv2.imread("./tmp.png")

    ssim_measures = {}
    rmse_measures = {}

    dim = (img.shape[1], img.shape[0])

    data_dir = "./prog3/"
    for file in os.listdir(data_dir):
        img_path = os.path.join(data_dir, file)
        cmp_img = cv2.imread(img_path)
        ssim_measures[img_path]= ssim(img, cmp_img)
        rmse_measures[img_path]= rmse(img, cmp_img)

    ssim_v = max(ssim_measures.values())
    rmse_v = min(rmse_measures.values())

    if ssim_v == rmse_v:
        if ssim_v.split("/")[2][0] == "r":
            ret = "non"
        elif ssim_v.split("/")[2][0] == "v":
            ret = "oui"
    else:
        print("ERROR")
        exit()


    p.sendline(ret)
    reponse = p.recvuntil("\r\n").decode().strip().encode()

CTF_IUT{[email protected]€_les_mamIes_BravO}

Redemption 3

Redemption 3

The maker ultimately decided to put you through a final challenge, to make sure of your good faith. For this last step, you'll have to help firefighters get people out of a burning building. Your goal will be to provide the firefighters with a list of the window column order in which to save the people. The awaited response matches the following pattern : "col1,col2,col3,etc"

This challenge was actually concocted by the admins during the CTF, meeting the players' demands to get one more programming challenge.

Recon

The process was quite like the last. We receive a base64 encoded PNG, we have to process it right, and send back the result.

Following the same method as for the second part, I visualized one image to understand the assignment.

Burning Building

The idea is to read the numbers and send back a column order. For the previous example, the expected output is : 4,6,6,2,10,3,1,7,2,7,9,3,11,9,4.

Since all the buildings have the same layout, but there are spaces between them, my idea was to create a function that would process the data for one building, given the start positions (x, y) in pixels.

I used GIMP for that step and wrote down the coordinates of the most up-left corner of the most up-left window of each building

building 1 : (30, 164)
building 2 : (409, 168)
building 3 : (787, 169)

I also measured the radius of each window (they were perfect squares), and the distance between two windows horizontally and vertically.

Step 1 : Cycling through the windows

The first thing I did was cycle through the windows, and place a rectangle on each one, to tweak a little bit the value I had, to get the best fit.

from PIL import Image, ImageDraw

img = Image.open("building_on_fire.png")

base_x, base_y, radius, space_x, space_y, pad = 30, 164, 63, 24, 15, 6
for y in range(8):
    current_y = base_y + (radius+space_y) * y
    for x in range(4):
        current_x = base_x + (radius+space_x) * x
        zone = img.crop((current_x+pad, current_y+pad, current_x+radius-pad, current_y+radius-pad))
        img1 = ImageDraw.Draw(img)  
        img1.rectangle((current_x+pad, current_y+pad, current_x+radius-pad, current_y+radius-pad), fill ="red", outline ="red")

img.show()
Burning Building with identified zones

Step 2 : Using Tesseract

Tesseract is a python library allowing you to read text from an image (exactly what we want).

In order to make it work, I each time created a "sub-image" which content was a single window and passed to Tesseract for it to read. Since this module is very sensitive, I added some padding, to avoid having any kind of windows border or flame on the image. I also passed some arguments to Tesseract, to allow only a limited char-set (to avoid having it recognize letters).

def getBuildingLayout(base_x, base_y, radius, space_x, space_y, pad):
    """
    base_x : x coordinate top left corner (start pixel)
    base_y : y coordinate top left corner (start pixel)
    radius : amount of pixels between the left side and the right side of a window
    space_x : amount of pixels between two windows next to each other
    space_y : amount of pixels between two windows on top of each other
    pad : padding value to remove borders
    """
    building = []
    for y in range(8):
        current_y = base_y + (radius+space_y) * y
        building.append([])
        for x in range(4):
            current_x = base_x + (radius+space_x) * x
            zone = img.crop((current_x+pad, current_y+pad, current_x+radius-pad, current_y+radius-pad))
            texte = pytesseract.image_to_string(zone, config='--psm 7 -c tessedit_char_whitelist=0123456789').replace("\x0c", "").strip()
            if texte == "":
                building[y].append(0)
            else:
                if texte == "30": texte = "3" # Correct errors
                building[y].append(int(texte))
    return building

Step 3 : Final implementation

The final step is to merge the three buildings into one and to send back the column numbers in the right order (from 1 to 12). I won't explain this part since it's not the heart of the challenge, and it's fairly easy to understand.

Here is the final code I used. Beware that the Tesseract module has its limits, and I had to run the code about 7-8 times to get the flag.

from pwn import *
from PIL import Image, ImageDraw
import io, pytesseract, string

context.log_level = 'debug'

p = remote("10.3.253.171", 1337)

# Read and decode image
data = p.recvuntil("\r\n")
img = Image.open(io.BytesIO(base64.b64decode(data)))


def getBuildingLayout(base_x, base_y, radius, space_x, space_y, pad):
    """
    base_x : x coordinate top left corner (start pixel)
    base_y : y coordinate top left corner (start pixel)
    radius : amount of pixels between the left side and the right side of a window
    space_x : amount of pixels between two windows next to each other
    space_y : amount of pixels between two windows on top of each other
    pad : padding value to remove borders
    """
    building = []
    for y in range(8):
        current_y = base_y + (radius+space_y) * y
        building.append([])
        for x in range(4):
            current_x = base_x + (radius+space_x) * x
            zone = img.crop((current_x+pad, current_y+pad, current_x+radius-pad, current_y+radius-pad))
            texte = pytesseract.image_to_string(zone, config='--psm 7 -c tessedit_char_whitelist=0123456789').replace("\x0c", "").strip()
            if texte == "":
                building[y].append(0)
            else:
                if texte == "30": texte = "3" # Correct errors
                building[y].append(int(texte))
    return building


# Recovering the numbers for each building
pad = 6
building1 = getBuildingLayout(30, 164, 63, 24, 15, pad)
building2 = getBuildingLayout(409, 168, 63, 24, 15, pad)
building3 = getBuildingLayout(787, 169, 63, 24, 15, pad)

# Merging every thing in one big array
layout = []
for i in range(len(building1)):
    layout.append(building1[i]+building2[i]+building3[i])

# Extracting the firefighters itervention order
i, y, ret = 1, 0, []
while y 

FLAG{FIRE_iNTh€[email protected]}

Conclusion

Thanks for reading ! Overall, these three challenges were very nice, and progressive. However, they remained accessible to coders with little experience, even if it would take some time to get the hang of it. I'm personally not a fan of image processing, but it's refreshing to see something new in the programming section 🙂


Links

Here are the links to other write-ups my team made for this CTF :