+++ author = "Maik de Kruif" title = "Spycam" subtitle = "Beginners Quest 10 - Google CTF" date = 2021-11-07T12:41:00+01:00 description = "A writeup for challenge 10 of the beginners quests of the Google CTF." cover = "img/writeups/google-ctf/2021/beginners-quest/10/cover.png" tags = [ "Google CTF", "Beginners Quest", "ctf", "hacking", "writeup", "hardware", ] categories = [ "ctf", "writeups", "hacking", "hardware", ] +++ ## Story line ### New York - Office Complex New York is hot, and you are on your way to the office complex. It seems like it is well guarded, even though you are expected under the alias of the assassin, perhaps it will be a better idea to sneak inside the building, unseen? You climb through a window on the side of the building. Inside you spot more guards, quick, hide behind a desk. Now you have to sneak past the guards into the main office. ### Challenge: Spycam (hardware) You manage to find some exposed wires and quickly hook them up to your portable terminal. It seems to be a live feed of the internal CCTV system. If you can manage to decode the signal you might find something interesting, maybe a code or a password to get past the locked door. ### After solving Congratulations, you successfully sneaked past the guards, and now you are inside the main office. Look over there, a safe case! Wait, what, it is open, no way! It’s only a photo inside, what a disappointment... But wait, don’t get hasty now, it seems like it’s a harbor in the picture, and there is something scribbled on the back, it’s coordinates to the harbor which seems to be located in Singapore. ## Attachment [attachment.zip](/files/writeups/google-ctf/2021/beginners-quest/10/attachment.zip) ## Recon The attachment contains one file: `chall.tar.gz`. Extracting this file gives seven `csv` files of about 25MB: - 1.csv - 2.csv - 3.csv - 4.csv - 5.csv - 6.csv - 7.csv They all contain 600255 lines. A sample of the first csv file: ```text -0.0018051198211097765 ,4.25 ,-0.05 ,-0.05 ,-0.18 -0.001805079821043734 ,4.25 ,-0.05 ,-0.08 ,-0.18 -0.0018050398209776917 ,4.3 ,-0.05 ,-0.08 ,-0.18 -0.0018049998209116493 ,4.3 ,-0.05 ,-0.08 ,-0.18 -0.0018049598208456068 ,4.25 ,-0.05 ,-0.08 ,-0.2 -0.0018049198207795644 ,4.25 ,-0.05 ,-0.05 ,-0.18 -0.001804879820713522 ,4.25 ,-0.05 ,-0.05 ,-0.18 ``` ## Solving I found this challenge to be pretty difficult as it gives us very little information to start with. The description says something about CCTV footage, so these CSVs probably contains some kind of image or video. I don't know any format though that looks like this. The first column in the CSV file seems to only be incrementing. This could be a timing signal for something like [VGA](https://en.wikipedia.org/wiki/Video_Graphics_Array). I though of this because [Ben Eater](https://www.youtube.com/channel/UCS0N5baNlQWJCUrhCEo8WlA) makes great videos about how computers work and made [a video about creating a graphics card](https://www.youtube.com/watch?v=l7rce6IQDWs). I recommend you go watch it if you don't know how VGA works. I also remmend you to watch [this follow-up video about RGB in VGA](https://www.youtube.com/watch?v=uqY3FMuMuRo). With my basic understanding about VGA, I tried to make sense of the data. As I said, the first column seems to be the timing, but the others are still unclear. I continued by taking a look at the value range of the columns using a Python script. ```py filename = "1.csv" with open(filename, "r") as file: min_max = [[float(value), float(value)] for value in file.readline().split(",")] for line in file: for index, value in enumerate(map(float, line.split(","))): min_max[index][0] = min(min_max[index][0], value) min_max[index][1] = max(min_max[index][1], value) for index, [low, high] in enumerate(min_max): print(f"#index: {index}") print(f"{low=}") print(f"{high=}") print(f"rng={high-low}") print() ``` ```py #index: 0 low=-0.0018051198211097765 high=0.022205119821109773 rng=0.02401023964221955 #index: 1 low=-0.35 high=4.8 rng=5.1499999999999995 #index: 2 low=-0.4 high=0.28 rng=0.68 #index: 3 low=-0.38 high=0.28 rng=0.66 #index: 4 low=-0.43 high=0.15 rng=0.58 ``` The output shows the ranges of the last three indexes are about the same. This hints at color values, and, if you know VGA, this makes sense as the value would be between 0 and 0.7 volts. In today's images, the range is defined in a byte with a value between 0 and 255. So later these numbers will have to multiplied by `255/0.7`. The purpose of the second column is, however, still unclear. From the range I could see it goes from 0 to 5 and from scrolling through the CSV file I could see it only turned to 0 twice. To confirm this, I wrote the following script: ```py filename = "1.csv" x_values = set() with open(filename, "r") as file: for index, line in enumerate(file): _, x, _, _, _ = map(float, line.split(",")) x_values.add(x) # Only print every 1000 lines as the output would be too cluttered otherwise if index % 1000 == 0 and round(x) == 0: print(f"{index=}, {x=}") print(f"{x_values=}") ``` Which returns the following output: ```py index=44000, x=0.4 index=45000, x=0.4 index=461000, x=0.4 index=462000, x=0.4 x_values={-0.35, 0.45, 0.4, 0.35, 4.2, 4.25, 4.3, 4.35, 4.8, 4.15, -0.25} ``` The output shows it's a HIGH LOW signal which is probably used as a sync signal to tell the screen when to start and stop reading. To only get the part we need, I wrote the following script: ```py import glob for filename in glob.glob("*.csv"): should_read = False previous_was_zero = False line_offset = 0 timing_offset = 0 counter = 0 with open(filename, "r") as file: for line in file: timing, sync, r, g, b = map(float, line.split(",")) if sync > 3: if previous_was_zero: should_read = True timing_offset = timing elif should_read and not previous_was_zero: should_read = False break previous_was_zero = sync < 3 line_offset += len(line) if should_read: counter += 1 print(f"{counter=}, timing={(timing-timing_offset)*1e3}") ``` In this script I count the number of lines that I want to use and also printed the timings in (probably) milliseconds. I want these numbers so I can find out what the resolution of the VGA signal is. The output was roughly the same for all seven files: ```py counter=415492, timing=16.619707440046454 counter=415492, timing=16.619707440046454 counter=415491, timing=16.61966743998041 counter=415491, timing=16.619667439980415 counter=415491, timing=16.619667439980415 counter=415491, timing=16.61966743998041 counter=415491, timing=16.619667439980415 ``` This means the frame is about 415492 pixels in total and it takes 16.6 ms to draw it. With this information I went to [TinyVGA](http://tinyvga.com/vga-timing). This website contains the timings for all VGA resolutions. From it's catalog, I found [`640 x 480 @ 60 Hz`](http://tinyvga.com/vga-timing/640x480@60Hz) to be the best match as it the total amount of pixels would be `800 * 525 = 420000`, with a total frame time of `16.683217477656 ms`. This is pretty close to our values, so I tried to render a picture using it's vertical refresh rate of `31.46875 kHz`. The script I used is the following: ```py import glob from PIL import Image width = 800 # amount of pixels in one line height = 525 # amount of lines in whole frame vertical_refresh = 31468.75 # vertical refresh rate in Hz lowest_voltage = -0.4 # lowest voltage of a signal for filename in glob.glob("*.csv"): img = Image.new("RGB", (width, height), (255, 255, 255)) should_read = False previous_was_zero = False line_offset = 0 timing_offset = 0 with open(filename, "r") as file: for line in file: timing, sync, r, g, b = map(float, line.split(",")) if sync > 3: if previous_was_zero: should_read = True timing_offset = timing elif should_read and not previous_was_zero: should_read = False break previous_was_zero = sync < 3 line_offset += len(line) if should_read: timing -= timing_offset y = int(timing*vertical_refresh) x = int((timing*vertical_refresh-y)*width) if not (0 <= x < width and 0 <= y < height): print(x, y) r = (r-lowest_voltage)*(255/0.7) g = (g-lowest_voltage)*(255/0.7) b = (b-lowest_voltage)*(255/0.7) img.putpixel((x, y), tuple(map(int, [r, g, b]))) img.save(f"out/{filename}.png") ``` After running this script, I got the following images: {{< figure class="small inline" src="/img/writeups/google-ctf/2021/beginners-quest/10/1.csv.png" title="1.csv.png" >}} {{< figure class="small inline" src="/img/writeups/google-ctf/2021/beginners-quest/10/2.csv.png" title="2.csv.png" >}} {{< figure class="small inline" src="/img/writeups/google-ctf/2021/beginners-quest/10/3.csv.png" title="3.csv.png" >}} {{< figure class="small inline" src="/img/writeups/google-ctf/2021/beginners-quest/10/4.csv.png" title="4.csv.png" >}} {{< figure class="small inline" src="/img/writeups/google-ctf/2021/beginners-quest/10/5.csv.png" title="5.csv.png" >}} {{< figure class="small inline" src="/img/writeups/google-ctf/2021/beginners-quest/10/6.csv.png" title="6.csv.png" >}} {{< figure class="small inline" src="/img/writeups/google-ctf/2021/beginners-quest/10/7.csv.png" title="7.csv.png" >}} Upon looking at image number 7, I saw some text on the image. The RGB values are a little offset though which makes it unreadable. I could have fixed it by manually moving the x and y around, but an easier fix is just only using one color. I did this by changing this line: ```diff - img.putpixel((x, y), tuple(map(int, [r, g, b]))) + img.putpixel((x, y), tuple(map(int, [r, r, r]))) ``` This gave the following image: {{< figure src="/img/writeups/google-ctf/2021/beginners-quest/10/7_rrr.csv.png" title="7.csv.png (red)" >}} It looked like I got the flag, but when submitting `CTF{vlde0_g?aphi?s_4???y}` I got a message saying it was the wrong key. It looks like the OCR failed, and the text is not readable in it's current form. So I tried using only blue: ```diff - img.putpixel((x, y), tuple(map(int, [r, g, b]))) + img.putpixel((x, y), tuple(map(int, [b, b, b]))) ``` {{< figure src="/img/writeups/google-ctf/2021/beginners-quest/10/7_bbb.csv.png" title="7.csv.png (blue)" >}} This is still pretty bad, but at least I can see something. From the image I made the following changes: - The "g" should be a capital "G" - The last question mark should be a "4" The other two were still unreadable, but from guessing I replaced the second question mark with a "c" and the other two that are left with an "r". ## Solution The flag is correct! It's `CTF{V1de0_Graphics_4rr4y}`.