Some Backstory
In 2022, my friends and I were a sizable portion of the TUCTF team, which had lost most of its members and inertia post-COVID. As a result, the team had not hosted its annual CTF since 2019, but we wanted to bring it back. None of the active members had any idea about how to run a competition, but we loved solving challenges, and figured it couldn’t be too hard, right? My roommate/frequent pair programmer and I decided to team up to make a few challenges, and wanted to make a challenge that involved some creative thinking.
As a challenge author, I think image forensics is an area with so much potential. You can encode information in what the human eye can see and interpret, at the same time encoding information in what the computer can see and interpret. It is very common and very possible to steganographically hide information by having RGB tuples or lightness values map to bytes, but I think having the “normal” image contents reveal hints about the steganography is a fun twist.
The problem with image challenges (and forensics challenges in general), however, is that they can be too wide open. Even if you know for a fact that the bytes of a flag are encoded within an image, there are so many possible ways it is unfair to ask participants to guess. Just as an example, a flag could be:
- ASCII encoded in greyscale pixels where 0-255 map to bytes
- ASCII encoded in the R channel (or G or B) of pixels
- Packed ASCII encoded, where R, G, and B channels of a pixel store 3 bytes
- Dispersed across the least significant bits of subsequent pixels
- Any combination of the previous encodings, but using pixels randomly dispersed throughout the image with a map telling you where to look
The list is truly endless depending on how tricky of a challenge you want to make. The most difficult part of writing challenges, to me, is threading that needle of not giving away any solutions, but making the challenge solvable, and more importantly, not feel too ‘guessy’. Having been on the solvers’ side of the table, it was an interesting change of perspective, and made me a little more sympathetic towards the authors of some of my least favorite challenges in the past.
The Challenge
Participants were given the following image and description:
Tony Stark: "I'm trying to discover the secrets of the universe, but I spilled colored fruit all over my flag! Can you help?"
Admins: "What can we help with?"
Tony Stark: "201 052 243 241 302 302 303 312 052 311 303 300 314 245 052 312 252 253 311 052 301 241 312 252 052 304 310 303 242 300 245 301 :("
Admin: "I'm stuff"
The Solution
Looking at the image, the clues, and the challenge name, a few things should be clear:
- There is some sort of math problem or system of equations involving the fruits
- The answer, or at least part of it, revolves around base 6 numbers
Part 1
The first part of solving the challenge involves finding the solution to the given system of equations. The complicated equation at the bottom isn’t important (it simply evaluates to an undefined 0/0), but the top six equations form a system of six equations for six unknown variables. Note that all of the numbers on the right of the equation are in base 6, as indicated by the subscript, and overlapping fruits are simply counted together (i.e. 2 overlapping apples is 2*apple). Each fruit represents a variable, and solving the system of equations will result in the following values:
Apple (Red) = 3
Carrot (Orange) = 0
Banana (Yellow) = 4
Lime (Green) = 1
Blueberry (Blue) = 5
Eggplant (Purple) = 2
This system could be solved by hand or plugged into a program like Wolfram Alpha.
The large complex equation was meant as a means to check your work - if you found the correct values for each of the fruits, it should evaluate cleanly to 0/0. After feedback from teams, this equation was explicitly set to equal 0/0 in the image.
Part 2
The second part of this problem involved the picture of a flag at the bottom of the image. Analyzing the colors in the flag reveals that the colors of each fruit (all fruits are a single color with black outlines) appear quite frequently in the flag.
While there were other, similar colors in the flag (there were 3 slightly different shades of each color), we are only interested in pixels that match the exact RGB color code of the fruits. Iterating over the pixels and noting which ones match the fruits yields a repeating pattern: PURPLE PURPLE ORANGE PURPLE PURPLE GREEN GREEN BLUE GREEN PURPLE PURPLE ORANGE GREEN BLUE YELLOW RED PURPLE RED GREEN BLUE YELLOW RED GREEN ORANGE RED GREEN RED GREEN PURPLE GREEN RED GREEN PURPLE PURPLE RED BLUE RED GREEN GREEN RED GREEN PURPLE GREEN PURPLE YELLOW RED GREEN PURPLE GREEN PURPLE GREEN PURPLE YELLOW RED PURPLE RED BLUE RED PURPLE GREEN PURPLE PURPLE GREEN PURPLE ORANGE BLUE RED ORANGE GREEN RED PURPLE GREEN PURPLE RED BLUE PURPLE PURPLE BLUE RED GREEN RED RED ORANGE GREEN PURPLE ORANGE BLUE RED PURPLE GREEN RED PURPLE BLUE
.
Mapping those colors to the values of the fruits results in the repeating number 220221151220154323154310313121312235311312124312121243235321221205301321235225313301205321325
.
Looking closely at a selection of the flag image with all other colors removed, we can clearly see the first two characters, 2 2 0 2 2 1
represented in their pixel form.
As implied by the answers to the system of equations and the challenge description, this is in fact a series of 3-digit base-6 numbers. Splitting every 3 characters and converting to base-10 results in the repeating sequence 84, 85, 67, 84, 70, 123, 70, 114, 117, 49, 116, 95, 115, 116, 52, 116, 49, 99, 95, 121, 85, 77, 109, 121, 95, 89, 117, 109, 77, 121, 125
.
Converting these values to ASCII characters reveals the flag, TUCTF{Fru1t_st4t1c_yUMmy_YumMy}
, repeated over and over again.
Reflection
This challenge caught a bit of flak for a few different reasons, some more justified than others in my opinion. My favorite was a competitor who was convinced that the system of equations was incorrect, and despite being told that he had entered in the wrong values for the fruits, demanded to see my math degree because I clearly didn’t know what I was doing.
Skill issues aside, I think there are some legitimate complaints with the challenge. The biggest issue, in my opinion, was the step of pulling out the fruit pixels from the flag PNG to form the flag text. We tried to allude to this by making the fruits all one solid color, and putting the FLAG = ?
text in the image, but many teams still struggled with this logical jump. In hindsight, we should have made this step more explicit. I am loath to put “HINT: LOOK AT THE COLORS” in the description, but we could have perhaps had iconography of a magnifying glass looking at individual pixels, or a fruit with a magnifying glass, or something to that effect.
Another piece of feedback we got in our internal testing was the difficulty of converting base-6 to ASCII. To remedy this, we added the subscript 6 to each of the systems of the equations, as well as adding the sample base-6 to the challenge description to give a hint that triplets of base-6 can be used in the context of text. For the curious, it decodes to this.
Overall, I think this challenge was a good learning experience. Our post-CTF survey had one team put it as their favorite challenge, and another as their least favorite, with a few comments about the forensics category as a whole needing some easier challenges. Our main takeaway was that as challenge authors, what may appear clear to us might not be clear to the solvers.
Solve Script
There are many ways to solve this challenge, but here is mine, that takes in a cropped image of the flag from the challenge image and returns the flag, TUCTF{Fru1t_st4t1c_yUMmy_YumMy}
. I like using Pillow for my image scripts, but I am sure there are other valid ways to do this.
from PIL import Image
flagPath = "based_on_fruit_flag.png"
flag = Image.open(flagPath)
colors = {
(255, 127, 39, 255): 0, # carrot
(34, 177, 76, 255): 1, # lime
(163, 73, 164, 255): 2, # eggplant
(237, 28, 36, 255): 3, # apple
(255, 201, 14, 255): 4, # banana
(63, 72, 204, 255): 5 # blueberry
}
## Depending on how you isolate the flag, you may have to use RGBA vs RGB
# colors = {
# (255, 127, 39): 0,
# (34, 177, 76): 1,
# (163, 73, 164): 2,
# (237, 28, 36): 3,
# (255, 201, 14): 4,
# (63, 72, 204): 5
# }
out = []
for x in range(flag.width):
for y in range(flag.height):
if flag.getpixel((x, y)) in colors:
out.append(colors[flag.getpixel((x, y))])
longstr = "".join(map(str,out))
splitstr = [longstr[i:i+3] for i in range(0, len(longstr), 3)]
sol = [chr(int(x, 6)) for x in splitstr]
print("".join(sol))
A local copy of this script is provided here, as well as a cropped image containing only the flag picture here.