Post

Insomni'hack 2025 - Passvault (level 1 & 2)

Insomni'hack 2025 - Passvault (level 1 & 2)

Introduction

Insomni’hack 2025 took place from March 10 to 15, 2025, at the SwissTech Convention Center in Lausanne, Switzerland. Organized (now) by Orange Cyberdefense Switzerland, this event has grown from a modest hacking contest in 2008 to one of Europe’s largest cybersecurity gatherings.

For this edition, I prepared a 3-stage hardware challenge Passvault, which only one team managed to solve even few other teams where not far away from the final solution.

Final Scoreboard

Hardware

Before diving into the technical details, let me introduce the device:

DeviceDevice teardown

As shown above, the device consists of a Nordic nRF5340 Development Kit, equipped with a custom-designed adapter that enables interfacing with a MikroTik module containing NXP’s SE050C secure element.

All ressources, 3d print and adapter can be found on the repository

Passvault 1/3

Description

1
2
3
4
5
Introducing Passvault !

Like any password vault, simply remember your master password to access it.

Even if you forget it, Passvault's got your back with its built-in feature to jog your memory !

Solution

As mentionned in the description, the system relies on a user-defined master password for access. This could be verified by the participant on the built-in shell exposed on the USB interface which only provided an unlock command.

Fortunately, the system includes a built-in “memory jogger” functionality which assist users in recalling a forgotten master password.

When the device is powered on, a careful observer will notice that the LED sequence played at startup is not only consistent but also loops afters some time.

By observing the LEDs more closely, subtle timing differences become noticeable between the moment when a pattern is displayed and the LEDs being totally off. Long story short, the sequence looks as follow:

TimingDescription
[3600 ms]Startup animation
[500 ms]Pattern
[200 ms]Short pause
[500 ms]Pattern
[500 ms]Long pause
[3600 ms]Stop animation

The entire sequence last about ~50 seconds before repeating, meaning there are ~40 patterns to extract.

Assuming an ASCII string is being transmitted nibble by nibble (4-bits at a time), one of the LED should be lighten up rarely. Indeed, as characters range from 0x00 to 0x7F the upper bit (bit7) will always be 0. In the present case, this can be easily correlated to LED4 on the board.

Here is how the flag could be obtained with some scripting on a prerecorded video:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import cv2
import time

class LED:
    def __init__(self, name:str, x:int, y:int, w:int, h:int, thres_on:int,thres_off:int):
        self.name = name
        self.x = x
        self.y = y
        self.w = w
        self.h = h
        self.thres_on = thres_on
        self.thres_off = thres_off
        self.status = 0
        self.brightness = 0

def main():
    # Path to input video
    video_path = "video.mp4"
    
    # Output video path
    output_path = 'decoded_output.avi'

    # Open the video
    cap = cv2.VideoCapture(video_path)
    if not cap.isOpened():
        print(f"Error: Could not open video file: {video_path}")
        return
    
    # Get video properties
    fps    = 60
    width  = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')

    # Set up video writer
    out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
    
    # define leds zones and threshold
    leds = [
        LED("LED1",366, 183, 5, 7, 215, 210),
        LED("LED2",368, 238, 5, 7, 215, 210),
        LED("LED3",329, 184, 5, 7, 219, 210),
        LED("LED4",330, 239, 5, 7, 219, 210),
    ]

    old_nibble = 0
    nibbles = []

    nb_frame = 0
    while cap.isOpened():
        nibble = 0
        ret, frame = cap.read()
        if not ret:
            break

        # skip some frames to get proper decoding
        if nb_frame < 155 or nb_frame in [788, 904, 905] or nb_frame > 1100:
            nb_frame += 1
            continue
        
        for i, led in enumerate(leds):
            gray = cv2.cvtColor(frame[led.y:led.y+led.h, led.x:led.x+led.w], cv2.COLOR_RGB2GRAY)
            cv2.rectangle(frame, (led.x, led.y), (led.x + led.w, led.y + led.h), (0, 0, 0), 1)
            led.brightness = gray.mean()
            
            if led.status == 0 and led.brightness >= led.thres_on:
                led.status = 1
            elif led.status == 1 and led.brightness < led.thres_off:
                led.status = 0
            
            if led.status == 1:
                cv2.putText(frame, led.name + " " + str(led.status), (10, 30*(i+1)), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
            else:
                cv2.putText(frame, led.name + " " + str(led.status), (10, 30*(i+1)), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
            
            nibble |= led.status << i


        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

        # Print nibble in binary (4 bits) and decimal
        if old_nibble != nibble:
            old_nibble = nibble
            nibbles.append(hex(nibble)[2:])
        try:
            flag = bytes.fromhex("".join("".join(nibbles).split("0")))
            cv2.putText(frame, flag.decode(), (120,300), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 0), 2)
        except:
            pass
        
        cv2.imshow("LEDs", frame)
        out.write(frame)
        nb_frame += 1
    
    time.sleep(5)
    cap.release()
    out.release()
    cv2.destroyAllWindows()
    
    print(f"Final flag => {flag}")
    
if __name__ == "__main__":
    main()

And here is the resulting video with opencv on-screen additions:

Decoding the flag using Python and OpenCV

Flag

INS{F1r5t_Ch4r_1s_(}

Passvault 2/3

Description

1
2
3
Just before the CTF began, the PSIRT was made aware a critical vulnerability that lets you bypass the master password. ;(

Can you find it ?

Solution

When reseting the device while being connected, one can observed that the master password is the following output:

1
2
3
4
5
6
7
8
9
10
11
__________                      ____   ____            .__   __
\______   \_____    ______ _____\   \ /   /____   __ __|  |_/  |_
|     ___/\__  \  /  ___//  ___/\   Y   /\__  \ |  |  \  |\   __\
|    |     / __ \_\___ \ \___ \  \     /  / __ \|  |  /  |_|  |
|____|    (____  /____  >____  >  \___/  (____  /____/|____/__|
                \/     \/     \/               \/
Version: SE-050C2
[I] Decrypting Master password
[I] Done decrypting Master password

$

In this challenge, the system uses an NXP SE050 secure element (SE) to store and protect the master password. Under normal conditions, the microcontroller fetches the master password over the I²C interface (SDA and SCL lines) from the SE050 and validates user input against it via the unlock command

However, if the SE050 is physically removed or rendered non-functional (e.g., by shorting SDA/SCL), the master password cannot be retrieved anymore.

Because of a lack of fail-safe mechanism, the firmware will simply continue booting and expose the shell to the user. Since the password has not been populated into the array, any null user input corresponding will be therefore treated as the correct one.

Input unlock "" to retrieve the flag on the shell.

This technique can be useful for bypassing a BIOS password on a computer, as explained here

Flag

INS{P4ssv4ul7_SE_Byp4ss3d}

This post is licensed under CC BY 4.0 by the author.