Insomni'Hack CTF Teaser 2022 - ExPiltration [Misc/Forensics]

We participated as r3vengers (Ripp3rs + Scavenger Security), and scored 34th out of almost 500 teams, with 6/15 challenges solved.


Oh shit.. (!) Our network has been compromised and data stored on an air-gaped device stolen but we don't know exactly what has been extracted and how? We have 24/7 video surveillance in the server room and nobody has approched the device.. Here is all I have, could you please give us a hand?


I re-uploaded the files here so they can be accessible when the CTF ends:

MD5: 985E340131C72CE632BED09285AFF078
SHA256: 4E3C106C44132D3FB368BA26675169FE4F10289DA7BD637C3B3CC0E570579EFA

This challenge was completed by 73/500 teams.


Downloading and extracting the ZIP file:

We can see a directory structure inside the storage folder, and a .mp4 video file named surveillance-camera42-2022.03.19_part8.mp4. Running an exiftool against the file we can see some information that might be useful for this challenge:

ExifTool Version Number         : 12.16                                                                                                                                                       
File Name                       : surveillance-camera42-2022.03.19_part8.mp4                                                                                                                  
Directory                       : .                                                                                                                                                           
File Size                       : 338 MiB                                                                                                                                                     
File Modification Date/Time     : 2022:03:19 23:42:06+01:00                                                                                                                                   
File Access Date/Time           : 2022:03:19 23:42:06+01:00                                                                                                                                   
File Inode Change Date/Time     : 2022:01:30 13:25:23+01:00                                                                                                                                   
File Permissions                : rw-rw-r--                                                                                                                                                   
File Type                       : MP4                                                                                                                                                         
File Type Extension             : mp4                                                                                                                                                         
MIME Type                       : video/mp4                                                                                                                                                   
Major Brand                     : MP4  Base Media v1 [IS0 14496-12:2003]                                                                                                                      
Minor Version                   : 0.2.0                                                                                                                                                       
Compatible Brands               : isom, iso2, avc1, mp41                                                                                                                                      
Media Data Size                 : 351717939                                                                                                                                                   
Media Data Offset               : 48                                                                                                                                                          
Movie Header Version            : 0                                                                                                                                                           
Create Date                     : 0000:00:00 00:00:00                                                                                                                                         
Modify Date                     : 0000:00:00 00:00:00                                                                                                                                         
Time Scale                      : 1000                                                                                                                                                        
Duration                        : 0:59:33                                                                                                                                                     
Preferred Rate                  : 1                                                                                                                                                           
Preferred Volume                : 100.00%                                                                                                                                                     
Preview Time                    : 0 s                                                                                                                                                         
Preview Duration                : 0 s                                                                                                                                                         
Poster Time                     : 0 s                                                                                                                                                         
Selection Time                  : 0 s                                                                                                                                                         
Selection Duration              : 0 s                                                                                                                                                         
Current Time                    : 0 s                                                                                                                                                         
Next Track ID                   : 3                                                                                                                                                           
Track Header Version            : 0                                                                                                                                                           
Track Create Date               : 0000:00:00 00:00:00                                                                                                                                         
Track Modify Date               : 0000:00:00 00:00:00                                                                                                                                         
Track ID                        : 1
Track Duration                  : 0:59:33
Track Layer                     : 0
Track Volume                    : 0.00%
Image Width                     : 1280
Image Height                    : 720
Time Code                       : 2
Media Language Code             : eng
Graphics Mode                   : srcCopy
Op Color                        : 0 0 0
Compressor ID                   : avc1
Source Image Width              : 1280
Source Image Height             : 720
X Resolution                    : 72
Y Resolution                    : 72
Bit Depth                       : 24
Pixel Aspect Ratio              : 1:1
Video Frame Rate                : 59.94
Matrix Structure                : 1 0 0 0 1 0 0 0 1
Media Header Version            : 0
Media Create Date               : 0000:00:00 00:00:00
Media Modify Date               : 0000:00:00 00:00:00
Media Time Scale                : 60000
Media Duration                  : 0:59:33
Handler Description             : GoPro AVC
Other Format                    : tmcd
Handler Type                    : Metadata
Handler Vendor ID               : Apple
Encoder                         : Lavf58.29.100 
Image Size                      : 1280x720
Megapixels                      : 0.922
Avg Bitrate                     : 788 kbps
Rotation                        : 0

Opening the video, we can see the Raspberry's LEDs starting moving somewhere near the minute 7:

The first thing that came to my mind is "data exfiltration", however this is a little bit confusing as we do have 2 different lights, the left one that can be either green or off, and the right one that can be red or off.

Does the green light mean 1 and when it's off 0?
Does the green light mean 1 and the red one 0?

One light can be on while the other one is off, but they both can be on or off at the same time.

Exploring the storage folder we can find a file named syslog under the /var/log path, that mentions an script inside /usr/bin:

The script contains the following, that made us realize it was indeed a data exfiltration script in binary format.

import os
import time
import binascii

DELAY = 0.05

def init_leds():
        os.system("echo none > /sys/class/leds/led0/trigger")
        os.system("echo none > /sys/class/leds/led1/trigger")

def restore_leds():
        os.system("echo mmc0 > /sys/class/leds/led0/trigger")
        os.system("echo default-on > /sys/class/leds/led1/trigger")

def text_to_bits(text, encoding='utf-8', errors='surrogatepass'):
    bits = bin(int(binascii.hexlify(text.encode(encoding, errors)), 16))[2:]
    return bits.zfill(8 * ((len(bits) + 7) // 8))

def exfiltrate(data):
        stream = text_to_bits(data)
        for b in stream:
                if b=='0':
                        os.system("echo 0 > /sys/class/leds/led0/brightness")
                        os.system("echo 1 > /sys/class/leds/led0/brightness")

                os.system("echo 1 > /sys/class/leds/led1/brightness")
                os.system("echo 0 > /sys/class/leds/led1/brightness")

def find_scret_file(path):
        files = []
        for r, d, f in os.walk(path):
                for file in f:
                        if '.key' in file or '.crt' in file:
                                files.append(os.path.join(r, file))

        for f in files:
                print("[+] Secret file discovered ({0}).. starting exfiltration".format(f))
                with open(f, 'r') as h:
                        data =

def main():


if __name__ == '__main__':

The main() function is doing 3 things:

  • init_leds() -> Initializate the leds.
  • find_scret_file() -> Looking for a file with either .key or .crt extensions in the /home directory. (I looked here but the file was already removed hehe).
  • restore_leds() -> Restores the led values.

The exfiltrate() function contains the following:

def exfiltrate(data):
        stream = text_to_bits(data)
        for b in stream:
                if b=='0':
                        os.system("echo 0 > /sys/class/leds/led0/brightness")
                        os.system("echo 1 > /sys/class/leds/led0/brightness")

                os.system("echo 1 > /sys/class/leds/led1/brightness")
                os.system("echo 0 > /sys/class/leds/led1/brightness")

It receives data as parameter, which is the .crt or .key file found in the /home directory when called from find_scret_file(), and converts it to bits with text_to_bits. If the stream bit is a 0, it turns off the led0 light, so if the stream bit is a 1, it turns on the led0 light.

Have in mind that led0 is the left one (the one that can be green), and the led1 is the right one (which can be red).

Once the led0 gets turned on or off, there is a 0.05 second delay before the led1 gets turned on, another 0.05 second delay before it gets turned off again, and another 0.05 second delay at the end before the for loop starts again.

TL;DR: The led0 stores the data and the led1 works as a synchronization clock, that tells us when to measure whether the led0 is turned on or off.

Having this in mind, there are several possible approaches to solve the challenge; based on an amount of light on a specific field of the video, color difference, template matching, etc.


We decided to do a template matching approach using OpenCV, there is an official tutorial for this technique here.

What we need is 4 different images:

  • Both lights turned off -> "ref-blank.png"
  • Green light turned on -> "ref-green.png"
  • Red light turned on -> "ref-red.png"
  • Both lights turned on -> "ref-red-green.png"

And a folder named "frames-png" where all the video frames will be stored; we will be using ffmpeg to split up the video in frames with the following syntax: ffmpeg -i surveillance-camera42-2022.03.19_part8.mp4 -ss 00:06:00 -to 00:55:00 -filter:v 'fps=30, crop=100:50:680:520' frames-png/out-%08d.png.

It's important noting the fps=30 filter, otherwise there will be too much noise on the frames and the recovered key won't be accurate. The crop filter is used to crop the video in the mentioned coordinates; a 100:50 section starting from the position 680:520, the -ss and -to flag are to only process the video from the 6th to the 55th minute, as before the 7th the LEDs were not even moving.

Even though my approach was correct, my exploits were not dumping the information properly, so I would like to thank our team mate rooted as the exploit used to solve the challenge was created by him!

Exploit used:

import cv2
import os
from tqdm import tqdm

needles = {
    '  ': 'ref-blank.png',
    'g ': 'ref-green.png',
    'r ': 'ref-red.png',
    'rg': 'ref-red-green.png',

needles = {name: cv2.imread(path) for name, path in needles.items()}

def get_score(haystack, needle):
    match = cv2.matchTemplate(haystack, needle, cv2.TM_CCOEFF_NORMED)
    return cv2.minMaxLoc(match)[1]

last_clock = None
frames = sorted(ent.path for ent in os.scandir('frames-png'))
bits      = []
byte_list = []
last_byte = None
recent = ''
partial_byte = ''

with open('bits.txt', 'w') as f:
    for path in frames:
        im = cv2.imread(path)
        scores = {name: get_score(im, needle) for name, needle in needles.items()}
        best = max(scores.items(), key=lambda x: x[1])
        frame = os.path.basename(path).rsplit('.', 1)[0]
        match, score = best
        bit   = 1 if ('g' in match) else 0
        clock = (score > 0.93 and 'r' in match)
        if last_clock is False and clock is True:
            postfix = {'bits': len(bits)}
            if bits and len(bits) % 8 == 0:
                last_byte = chr(int(''.join(map(str, bits[-8:])), 2))
                partial_byte = ''.join(map(str, bits[-8:]))
                rem = len(bits) % 8
                partial_byte = ''.join(map(str, bits[-rem:]))
            if last_byte is not None:
                postfix['byte'] = last_byte
            if byte_list:
                recent = ''.join(byte_list[-32:])
                postfix['recent'] = recent
        print(f"{frame} {match} ({score * 100:.2f}%) {1 if clock else 0} {bit} {partial_byte:<8}   {last_byte or ''!r}   {recent!r}")
        last_clock = clock

Having the dump stored as bits in the file bits.txt, we can now use CyberChef to convert it to a file:

And we can see two different files; the RSA Private Key:

Proc-Type: 4,ENCRYPTED


And the public key:


So we can now parse the public key in order to get the flag, which is stored inside the Subject Distinguished Name -> Organizational Unit:

Flag: INS{F4rFr0m$p33d0fL1ght}