After Width: | Height: | Size: 367 KiB |
After Width: | Height: | Size: 326 KiB |
After Width: | Height: | Size: 359 KiB |
After Width: | Height: | Size: 390 KiB |
After Width: | Height: | Size: 340 KiB |
After Width: | Height: | Size: 335 KiB |
After Width: | Height: | Size: 415 KiB |
After Width: | Height: | Size: 159 KiB |
After Width: | Height: | Size: 214 KiB |
After Width: | Height: | Size: 367 KiB |
@ -0,0 +1,321 @@ |
||||
+++ |
||||
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}`. |