Contents

NorthSec CTF 2025

This is a write-up for the following challenges:

  • Containers
  • Quantum Kraken Device - the Skeleton Key. Calibration 1 and 2.
  • Internet Services
  • Automation 101 (Hackademy track)
  • Sailor Kidz (part with “A” ciphertext)

Containers

The theme of this CTF was a cruise ship, CVSS Bonsecours (conference and CTF taking place at Marché Bonsecours in Montréal).

Description:

While the Bonsecours is obviously a cruise ship, civilian ships can be chartered to carry cargo in containers. These containers are smaller than what you are used to see for haulage. To request a physical container, you need to prove ownership of the container to the stevedor.

This cargo is interesting to us. Try to extract the hidden secrets in this container.

We provided a file, small.tar

1
2
docker load -i small.tar
docker run --rm -it small:p1

Getting information from the container

After loading small.tar, we see three new Docker images:

1
2
3
4
$ docker images | grep small
small                                          p1        35398aa0475c   3 weeks ago     95.9MB
small                                          p3        dbae34b95de9   3 weeks ago     95.9MB
small                                          p2        9e78658f2d4a   3 weeks ago     95.9MB

When we run the p1 container as requested, a password is requested:

/images/nsec-container.png

With docker inspect, we see that the binary which is run is /1680322826

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ docker inspect 6bd9e24c85b7
[
    {
        "Id": "6bd9e24c85b79491d8bfdfc282c6b249374788ea98754e64125989d1d31c98c5",
        "Created": "2025-05-18T22:54:00.388126426Z",
        "Path": "/entrypoint",
        "Args": [
            "ruby",
            "/1680322826"
        ],

Understanding the Ruby program

We attach to the container docker exec -it 6bd9e24c85b7 /bin/sh and once in the container, we list the file:

1
2
3
4
5
/ # cat 1680322826 
require 'base64'
eval(Base64.urlsafe_decode64([90, 71, 86, 109, 73, 72, 103, 103, 80, 83, 66, 108, 100, 109, 70, 115, 75, 67, 74, 98, 78, 84, 69, 115, 73, 68, 85, 119, 76, 67, 65, 49, 77, 83, 119, 103, 78, 68, 103, 115, 73, 68, 85, 50, 76, 67, 65, 49, 77, 67, 119, 103, 78, 84, 73, 115, 73, 68, 85, 50, 76, 67, 65, 48, 79, 67, 119, 103, 78, 84, 66, 100, 73, 105, 107, 117, 98, 87, 70, 119, 75, 67, 89, 54, 89, 50, 104, 121, 75, 83, 53, 113, 98, 50, 108, 117, 76, 110, 82, 118, 88, 50, 107, 75, 90, 71, 86, 109, 73, 71, 53, 118, 75, 67, 111, 112, 73, 68, 48, 103, 90, 88, 104, 112, 100, 67, 65, 119, 76, 110, 78, 49, 89, 50, 77, 106, 90, 88, 78, 122, 67, 109, 85, 57, 82, 69, 70, 85, 81, 83, 53, 121, 90, 87, 70, 107, 76, 110, 78, 119, 98, 71, 108, 48, 76, 109, 49, 104, 99, 72, 116, 112, 100, 67, 53, 48, 98, 49, 57, 112, 76, 110, 78, 108, 98, 109, 81, 111, 79, 108, 52, 115, 101, 67, 108, 57, 67, 109, 107, 57, 90, 50, 86, 48, 99, 121, 89, 117, 89, 50, 104, 118, 98, 88, 65, 75, 90, 83, 53, 54, 97, 88, 65, 111, 97, 83, 53, 105, 101, 88, 82, 108, 99, 121, 107, 117, 98, 87, 70, 119, 101, 50, 108, 48, 76, 110, 74, 108, 90, 72, 86, 106, 90, 83, 103, 54, 80, 84, 48, 112, 102, 83, 53, 121, 90, 87, 112, 108, 89, 51, 82, 55, 97, 88, 82, 57, 76, 109, 86, 104, 89, 50, 104, 55, 98, 109, 57, 57, 67, 109, 53, 118, 75, 69, 82, 66, 86, 69, 69, 112, 73, 71, 108, 109, 73, 71, 85, 117, 99, 50, 108, 54, 90, 83, 65, 104, 80, 83, 66, 112, 76, 110, 78, 112, 101, 109, 85, 75].map(&:chr).join))
__END__
3230824790 3230824784 3230824794 3230824704 3230824704 3230824787 3230824786 3230824786 3230824790 3230824710 3230824711 3230824785 3230824708 3230824789 3230824788 3230824785 3230824795 3230824704 3230824788 3230824786 3230824794 3230824790 3230824795 3230824710 3230824710 3230824784 3230824704 3230824787 3230824789 3230824704 3230824791 3230824795/

It’s an “obfuscated”/“hardened” ruby file. We have an array of integers which are convered to characters and form a Base64 string. So, we copy paste the array in CyberChef and select the following rules:

  1. From Decimal
  2. From Base64

We get the following program:

1
2
3
4
5
6
def x = eval("[51,50,51,48,56,50,52,56,48,50]").map(&:chr).join.to_i
def no(*) = exit 0.succ!ess
e=DATA.read.split.map{it.to_i.send(:^,x)}
i=gets.chomp
e.zip(i.bytes).map{it.reduce(:==)}.reject{it}.each{no}
no(DATA) if e.size != i.size

It creates a key, x, and transforms each integers of the array in a character and joins the characters in a string. Then, basically, it XORs each integers of the Ruby’s DATA section (after __END__) with the key and compares with user input character by character. It exits if a character is wrong.

Decrypting the password

So, we just need to XOR the data integers with the key and this will compute the expected password:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
data = """
3230824790 3230824784 3230824794 3230824704 3230824704 3230824787 3230824786 3230824786 3230824790 3230824710 3230824711 3230824785 3230824708 3230824789 3230824788 3230824785 3230824795 3230824704 3230824788 3230824786 3230824794 3230824790 3230824795 3230824710 3230824710 3230824784 3230824704 3230824787 3230824789 3230824704 3230824791 3230824795
"""

x = 3230824802 & 0xffffffff

numbers = list(map(int, data.split()))
password = ''.join(chr((n ^ x)& 0xff) for n in numbers)

print("Password :", password)

We run our password decoding program. The password is 428bb1004de3f7639b60849dd2b17b59. We enter that password in the container, and it replies:

1
Congrats, you found part 1 of the flag! Here it is: FLAG-6b44cae751

We submit the flag:

1
2
3
../askgod submit FLAG-6b44cae751                                               
You sent a valid flag, but no points have been granted.
Message: [containers] small.tar 1/3 of the flag, 2 more to go

HEY! It’s correct, but we didn’t get any point! Grrrr! We need to do the same with contains p2 and p3.

Container p2

So, now we need to handle the password of small:p2. The methodology is exactly the same, but the key and the data differ.

1
2
3
4
require 'base64'
eval(Base64.urlsafe_decode64([90, 71, 86, 109, 73, 72, 103, 103, 80, 83, 66, 108, 100, 109, 70, 115, 75, 67, 74, 98, 78, 84, 65, 115, 73, 68, 85, 119, 76, 67, 65, 49, 78, 105, 119, 103, 78, 84, 73, 115, 73, 68, 85, 48, 76, 67, 65, 48, 79, 67, 119, 103, 78, 84, 73, 115, 73, 68, 85, 51, 76, 67, 65, 49, 77, 121, 119, 103, 78, 84, 70, 100, 73, 105, 107, 117, 98, 87, 70, 119, 75, 67, 89, 54, 89, 50, 104, 121, 75, 83, 53, 113, 98, 50, 108, 117, 76, 110, 82, 118, 88, 50, 107, 75, 90, 71, 86, 109, 73, 71, 53, 118, 75, 67, 111, 112, 73, 68, 48, 103, 90, 88, 104, 112, 100, 67, 65, 119, 76, 110, 78, 49, 89, 50, 77, 106, 90, 88, 78, 122, 67, 109, 85, 57, 82, 69, 70, 85, 81, 83, 53, 121, 90, 87, 70, 107, 76, 110, 78, 119, 98, 71, 108, 48, 76, 109, 49, 104, 99, 72, 116, 112, 100, 67, 53, 48, 98, 49, 57, 112, 76, 110, 78, 108, 98, 109, 81, 111, 79, 108, 52, 115, 101, 67, 108, 57, 67, 109, 107, 57, 90, 50, 86, 48, 99, 121, 89, 117, 89, 50, 104, 118, 98, 88, 65, 75, 90, 83, 53, 54, 97, 88, 65, 111, 97, 83, 53, 105, 101, 88, 82, 108, 99, 121, 107, 117, 98, 87, 70, 119, 101, 50, 108, 48, 76, 110, 74, 108, 90, 72, 86, 106, 90, 83, 103, 54, 80, 84, 48, 112, 102, 83, 53, 121, 90, 87, 112, 108, 89, 51, 82, 55, 97, 88, 82, 57, 76, 109, 86, 104, 89, 50, 104, 55, 98, 109, 57, 57, 67, 109, 53, 118, 75, 69, 82, 66, 86, 69, 69, 112, 73, 71, 108, 109, 73, 71, 85, 117, 99, 50, 108, 54, 90, 83, 65, 104, 80, 83, 66, 112, 76, 110, 78, 112, 101, 109, 85, 75].map(&:chr).join))
__END__
2284605051 2284604970 2284605052 2284604969 2284604968 2284604973 2284605052 2284604960 2284604974 2284604971 2284604972 2284605055 2284605048 2284604972 2284604961 2284605050 2284604974 2284604975 2284605048 2284604969 2284604973 2284605053 2284605052 2284605055 2284605052 2284604960 2284605055 2284604960 2284605055 2284604968 2284604972 2284605050

The Base64 data decodes to the following:

1
2
3
4
5
6
def x = eval("[50, 50, 56, 52, 54, 48, 52, 57, 53, 51]").map(&:chr).join.to_i
def no(*) = exit 0.succ#ess
e=DATA.read.split.map{it.to_i.send(:^,x)}
i=gets&.chomp
e.zip(i.bytes).map{it.reduce(:==)}.reject{it}.each{no}
no(DATA) if e.size != i.size

The key is transformed (same way as p1) to 2284604953. We modify our decoding script with this key + the data at the end, and we get:

1
Password : b3e014e9725fa58c76a04defe9f9f15c

We enter that in the container, at it tells us: “Congrats, you found part 2 of the flag! Here it is: 80580cb18e643e5”.

Container p3

We can’t flag this yet, we’ve got to do this on p3. The entrypoint is file 253297732

1
2
3
4
require 'base64'
eval(Base64.urlsafe_decode64([90, 71, 86, 109, 73, 72, 103, 103, 80, 83, 66, 108, 100, 109, 70, 115, 75, 67, 74, 98, 78, 84, 65, 115, 73, 68, 85, 122, 76, 67, 65, 49, 77, 121, 119, 103, 78, 84, 85, 115, 73, 68, 85, 122, 76, 67, 65, 49, 78, 121, 119, 103, 78, 84, 99, 115, 73, 68, 85, 51, 76, 67, 65, 49, 78, 105, 119, 103, 78, 84, 70, 100, 73, 105, 107, 117, 98, 87, 70, 119, 75, 67, 89, 54, 89, 50, 104, 121, 75, 83, 53, 113, 98, 50, 108, 117, 76, 110, 82, 118, 88, 50, 107, 75, 90, 71, 86, 109, 73, 71, 53, 118, 75, 67, 111, 112, 73, 68, 48, 103, 90, 88, 104, 112, 100, 67, 65, 119, 76, 110, 78, 49, 89, 50, 77, 106, 90, 88, 78, 122, 67, 109, 85, 57, 82, 69, 70, 85, 81, 83, 53, 121, 90, 87, 70, 107, 76, 110, 78, 119, 98, 71, 108, 48, 76, 109, 49, 104, 99, 72, 116, 112, 100, 67, 53, 48, 98, 49, 57, 112, 76, 110, 78, 108, 98, 109, 81, 111, 79, 108, 52, 115, 101, 67, 108, 57, 67, 109, 107, 57, 90, 50, 86, 48, 99, 121, 89, 117, 89, 50, 104, 118, 98, 88, 65, 75, 90, 83, 53, 54, 97, 88, 65, 111, 97, 83, 53, 105, 101, 88, 82, 108, 99, 121, 107, 117, 98, 87, 70, 119, 101, 50, 108, 48, 76, 110, 74, 108, 90, 72, 86, 106, 90, 83, 103, 54, 80, 84, 48, 112, 102, 83, 53, 121, 90, 87, 112, 108, 89, 51, 82, 55, 97, 88, 82, 57, 76, 109, 86, 104, 89, 50, 104, 55, 98, 109, 57, 57, 67, 109, 53, 118, 75, 69, 82, 66, 86, 69, 69, 112, 73, 71, 108, 109, 73, 71, 85, 117, 99, 50, 108, 54, 90, 83, 65, 104, 80, 83, 66, 112, 76, 110, 78, 112, 101, 109, 85, 75].map(&:chr).join))
__END__
2557599961 2557599882 2557599967 2557599963 2557599958 2557599885 2557599884 2557599964 2557599959 2557599886 2557599963 2557599966 2557599960 2557599962 2557599965 2557599965 2557599884 2557599881 2557599963 2557599965 2557599963 2557599885 2557599963 2557599881 2557599966 2557599962 2557599960 2557599885 2557599885 2557599883 2557599967 2557599881/
1
2
3
4
5
6
def x = eval("[50, 53, 53, 55, 53, 57, 57, 57, 56, 51]").map(&:chr).join.to_i
def no(*) = exit 0.succ#ess
e=DATA.read.split.map{it.to_i.send(:^,x)}
i=gets&.chomp
e.zip(i.bytes).map{it.reduce(:==)}.reject{it}.each{no}
no(DATA) if e.size != i.size

/images/nsec2025-containers-key.png

The key is 2557599983, and we get password: 6e049bc38a417522cf424b4f157bbd0f. We enter that to finally retrieve the 3rd chunk of the flag:

1
Congrats, you found part 3 of the flag! Here it is: 7c8a9cafc1c3e1f

We are ready to submit the full flag:

1
2
3
askgod submit FLAG-6b44cae75180580cb18e643e57c8a9cafc1c3e1f 
Congratulations, you score your team 2 points!
Message: [containers] small.tar [1/3]

Quantum Kraken Device - the Skeleton Key

Description

1
2
3
4
5
Wiz, I need you to listen to me. That badge device we gave you isn't a regular key card. It is the Quantum Kraken Device. Our goal is to make a Skeleton Key - the real key to this whole heist. Months of work were involved to get our hands on this, and we've got a contact who provided instructions.

Underneath that unassuming casing is a miniaturized quantum computer that'l help us breach the Bonsecours vault. I hope that your references didnt lie when they told me that you know a thing or two about quantum computing. That piece in your resume caught my attention.

Since we smuggled it aboard, the device is a bit out of whack and needs calibration. Our contact provided a step-by-step guide in the doc below. Once you realign those qubits, we'll be on our way to setting up the quantum comms link with the ship's supercomputer, and get access to everything on board, including the ship's vault. Needless to say we can't fail. Fix this now.

NorthSec Badge 2025

This challenge uses the NorthSec 2025 badge. We must connect to the badge using picocom -b 115200 /dev/ttyACM0 and select the CTF firmware.

The rest of the challenge interacts with the device, will display some information on the LCD and light a led for each step of calibration.

Understanding the PDF

We are provided with a detailed PDF qkd-calibrate.pdf.

The difficulty of this challenge resides in reading and understanding the math instructions around Quantum Computing. Basically, we are told there are 3 gates (see them as functions):

  • X: performs a NOT on a bit
  • H: superposes bits
  • Z: performs a phase flip (negates the “1” bit)

There are 3 calibrations for phase 1, and 3 for phase 2. The difficulty grows at each step.

Calibration 1.a

We are asked: “Using a single qubit, initialize it to a |-> state”

So, first, we initialize the machine with a single qubit (quantum 1 is entered on the serial connection to the badge)

1
2
3
nsec> quantum 1
Initialized 1-qubit state: |0>
Quantum interactive mode. Type 'exit' to quit, 'help' for commands, or 'list' to show applied gates.

We show the initial state vector for confirmation:

1
2
3
4
q>: sv
Statevector:
|0> : (1.000000 + 0.000000i)
|1> : (0.000000 + 0.000000i)

We want to get |->, which on X axis is the opposite of |+>. If we do:

  1. H on |0> : we get |+>
  2. Z on |+>

In our interface with the badge, we enter the following commands:

1
2
3
4
5
6
7
8
q>: g H 0
Applying gate H to qubit 0...
q>: sv
Statevector:
|0> : (0.707107 + 0.000000i)
|1> : (0.707107 + 0.000000i)
q>: g Z 0
Applying gate Z to qubit 0...

We confirm we have reached the desired state:

1
2
3
4
q>: sv
Statevector:
|0> : (0.707107 + 0.000000i)
|1> : (-0.707107 + 0.000000i)

To validate that, we hash the state vector

1
2
q>: hash
Hash: 2f799a918c2578f275e94bc07739c9f8

and provide the hash to the calibration test:

1
2
3
4
5
6
nsec> calibrate 1
Initializing First Calibration Set... 

=> Using a single qubit, initialize it to a |-> state. Then print out the state vector hash and submit to compare the calibration.
Input Calibrate 1a hash: 2f799a918c2578f275e94bc07739c9f8
Correct!

This is not enough to receive a flag, we must perform also calibration 1b and 1c.

Calibration 1.b

We are asked to find a way to each this state: “Using two qubits, initialize each into a superposition state |->. This will put the two qubits into a superposition of all possible measurements. Once done print out the state vector hash and submit to compare the calibration.”

We initialize the machine with 2 qubits:

1
2
3
4
5
6
7
8
9
nsec> quantum 2
Initialized 2-qubit state: |00>
Quantum interactive mode. Type 'exit' to quit, 'help' for commands, or 'list' to show applied gates.
q>: sv
Statevector:
|00> : (1.000000 + 0.000000i)
|01> : (0.000000 + 0.000000i)
|10> : (0.000000 + 0.000000i)
|11> : (0.000000 + 0.000000i)

There I struggle like hell to reach the desired state.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
...
q>: g Z 1
Applying gate Z to qubit 1...
q>: sv
Statevector:
|00> : (0.500000 + 0.000000i)
|01> : (0.500000 + 0.000000i)
|10> : (0.500000 + 0.000000i)
|11> : (0.500000 + 0.000000i)
q>: hash
Hash: 2d5822b91586a076bb43686a7b56e893

This hash is correct.

Calibration 1.c

It gets worse: in 1c, we use now 3 qubits: “Using three qubits, initialize qubit 0 and 2 to a |-> state, while qubit 1 should be initialized to a |+> state. Once done print out the state vector hash and submit to compare the calibration.”

Having struggled to get the reach the state in 1b, I decide to get help from an AI. I explain the various gates, and the name of each axis and states, and tell it to give me the way to reach the desired state.

First, I ask the AI to give me the desired statevector I should reach (the question does not give that, only the corresponding formula in the PDF) and I want to confirm my understanding:

1
2
3
4
5
6
7
8
|000> : (+0.353553 + 0.000000i)
|001> : (-0.353553 + 0.000000i)
|010> : (+0.353553 + 0.000000i)
|011> : (-0.353553 + 0.000000i)
|100> : (-0.353553 + 0.000000i)
|101> : (+0.353553 + 0.000000i)
|110> : (-0.353553 + 0.000000i)
|111> : (+0.353553 + 0.000000i)

Then the AI tells me the way to reach it:

  • g H 0
  • g H 0
  • g Z 0
  • g H 1
  • g H 2
  • g Z 2

It works great:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
q>: g Z 2
Applying gate Z to qubit 2...
q>: sv
Statevector:
|000> : (0.353553 + 0.000000i)
|001> : (-0.353553 + 0.000000i)
|010> : (0.353553 + 0.000000i)
|011> : (-0.353553 + 0.000000i)
|100> : (-0.353553 + 0.000000i)
|101> : (0.353553 + 0.000000i)
|110> : (-0.353553 + 0.000000i)
|111> : (0.353553 + 0.000000i)
q>: hash
Hash: 0b374d293a6cbbc07cc52cec3b1419a5

I submit this last hash, and get a flag :

1
2
3
4
5
6
=> Using three qubits, initialize qubit 0 and 2 to a |-> state, while qubit 1 should be initialized to a |+> state. Once done print out the state vector hash and submit to compare the calibration.
Input Calibrate 1c hash: 0b374d293a6cbbc07cc52cec3b1419a5
Correct!

Your flag is: FLAG-2f7992d5820b374
Congratulations, the first set of calibrations is correct!

By the way, at each calibration step, a LCD at the bottom of the badge lights up. Nice.

Calibration 2.a

Description

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Well done, Wiz! The Quantum Kraken Device is finally showing stabl
e readings on that first calibration. That was the first step, and
 we're not done yet. I have no clue what this means, but our next 
steps now are to wrangle measurement and entanglementfoundational 
quantum phenomenabefore this device can talk to the target ships s
ecurity system.

You got this, right?

I kept that from you, but here's part 2 of the Calibration Manual.
 I need those qubits locked and loaded. Waste no time.

Manual

The second stage of calibration explains qubit entanglement and the operations CNOT and CZ. pdf

Reaching desired state

We are asked “Using two qubits, create the positive Bell Pair below.”

This first stage of calibration 2 isn’t very difficult. I need to entangle two qubits and reach this state:

1
2
3
4
|00> : (+0.707107 + 0.000000i)
|01> : ( 0.000000 + 0.000000i)
|10> : ( 0.000000 + 0.000000i)
|11> : (+0.707107 + 0.000000i)

I initialize the machine:

 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
nsec> quantum 2
Initialized 2-qubit state: |00>
Quantum interactive mode. Type 'exit' to quit, 'help' for commands
, or 'list' to show applied gates.
q>: g H 0
Applying gate H to qubit 0...
q>: g CNOT 0 1
Applying CNOT gate (control: 0, target: 1)...
q>: sv
Statevector:
|00> : (0.707107 + 0.000000i)
|01> : (0.000000 + 0.000000i)
|10> : (0.000000 + 0.000000i)
|11> : (0.707107 + 0.000000i)
q>: hash
Hash: a253ff07533701a5749286e71c111451
q>: exit
Exiting quantum mode...
Quantum state cleared.
nsec> calibrate 2
Initializing Second Calibration Set... 

=> Using two qubits, create a Bell Pair |00>+|11>. Once done print out t
he state vector hash and submit to compare the calibration.
Input Calibrate 2a hash: a253ff07533701a5749286e71c111451
Correct!

Calibration 2.b

In this next stage, we are asked “Using three qubits, create a three-qubit GHZ (Greenberger-Horne-Zeilinger) state”. The explanation of this state is described in the PDF.

The target state vector to reach is the following:

1
2
3
4
5
6
7
8
|000> : (+0.707107 + 0.000000i)
|001> : ( 0.000000 + 0.000000i)
|010> : ( 0.000000 + 0.000000i)
|011> : ( 0.000000 + 0.000000i)
|100> : ( 0.000000 + 0.000000i)
|101> : ( 0.000000 + 0.000000i)
|110> : ( 0.000000 + 0.000000i)
|111> : (+0.707107 + 0.000000i)

To get there:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
q>: g H 0
Applying gate H to qubit 0...
q>: g CNOT 0 1
Applying CNOT gate (control: 0, target: 1)...
q>: g CNOT 0 2
Applying CNOT gate (control: 0, target: 2)...
q>: sv
Statevector:
|000> : (0.707107 + 0.000000i)
|001> : (0.000000 + 0.000000i)
|010> : (0.000000 + 0.000000i)
|011> : (0.000000 + 0.000000i)
|100> : (0.000000 + 0.000000i)
|101> : (0.000000 + 0.000000i)
|110> : (0.000000 + 0.000000i)
|111> : (0.707107 + 0.000000i)
q>: hash
Hash: ad5f29aebd7b59d71fdedaf48c85ea6b

Calibrate 2.c

In this final step, we are asked, with 3 qubits, to entangle every qubits.

The desired state vector is below:

1
2
3
4
5
6
7
8
|000> : (+0.500000 + 0.000000i)
|001> : (+0.500000 + 0.000000i)
|010> : ( 0.000000 + 0.000000i)
|011> : ( 0.000000 + 0.000000i)
|100> : ( 0.000000 + 0.000000i)
|101> : ( 0.000000 + 0.000000i)
|110> : (+0.500000 + 0.000000i)
|111> : (-0.500000 + 0.000000i)

The issue is that we need to use the CZ operation to reach this state, but the device does not implement CZ, so we need to convert CZ into other existing operations.

I struggle with ChatGPT to get the correct conversion. Indeed, by default, it gets it wrong, and I have to debug the problem with the AI, printing the state vector at each step.

After quite some time, I manage to get a correct conversion of CZ into other operations. For instance, a CZ(1,2) can be done:

  1. H 2
  2. CNOT(1,2)
  3. H 2

From there, it’s easier to have ChatGPT reach the desired state (which normally uses CZ) and I replace the CZ by the correct combination. There are a few issues - mostly not performing the entanglement in the correct order, but after a while, I get it correct:

  • g H 2 # Put qubit 2 (MSB) in superposition
  • g CNOT 2 1 # Entangle qubit 2 and 1
  • g H 0 # Put qubit 0 (LSB) in superposition
  • g H 0 # Prepare for CZ replacement on qubit 0
  • g CNOT 1 0 # part of CZ(1,0) replacement (phase flip on |111>)
  • g H 0 # complete CZ(1,0) replacement

I reach the desired state:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
q>: sv
Statevector:
|000> : (0.500000 + 0.000000i)
|001> : (0.500000 + 0.000000i)
|010> : (0.000000 + 0.000000i)
|011> : (0.000000 + 0.000000i)
|100> : (0.000000 + 0.000000i)
|101> : (0.000000 + 0.000000i)
|110> : (0.500000 + 0.000000i)
|111> : (-0.500000 + 0.000000i)
q>: hash
Hash: 354f15b993224f5ff5592b6e6715365f

and complete the calibration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
q>: exit
Exiting quantum mode...
Quantum state cleared.
nsec> calibrate 2
Initializing Second Calibration Set... 

Previously Correct Calibration 2a Detected!

Previously Correct Calibration 2b Detected!

=> Using three qubits, create a Cluster State that has the provided state vector. All qubits must be entangled together. Once done print out the state vector hash and submit to compare the calibration.
Input Calibrate 2c hash: 354f15b993224f5ff5592b6e6715365f
Correct!

Your flag is: FLAG-a253fad5f2354f1
Congratulations, the second set of calibrations is correct!

Internet Services

We are provided with a PCAP file, and the description highlights “d-and-s” (DNS). So, we open the PCAP with wireshark, and look through the DNS packets. A malformed packed (no 405) strikes us.

/images/nsec2025-dns.png

We retrieve the payload: 464c41472d30383966333765633330616166663036653036643863333731656338653865643337383634313763.

CyberChef easily converts this hexadecimal string, or simple Python line:

1
2
python3 -c "import binascii; print(binascii.unhexlify('464c41472d30383966333765633330616166663036653036643863333731656338653865643337383634313763'))"
b'FLAG-089f37ec30aaff06e06d8c371ec8e8ed3786417c'

Automation 101

This was a beginner Hackademy track. The webpage for this challenge was showing “Site under construction”. The solution consisted simply in viewing the source code, to find the flag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html>
    <head>
        <title>NorthSec Hackademy</title>
    </head>
    <body>
        <div class="container">
            Site is under construction...<!-- FLAG-mI4IZqUelrTlXib
WbL06EbBxZomZbX2m (1/2) -->
        </div>
    </body>
</html>

SailorKidz (A encryption)

This challenge was showing an image with an encrypted text.

/images/nsec2025-sailorkidz.png

The encryption is obviously simple, something like a character substitution. A friend tells me about XKCD encryption. Unfortunately, I couldn’t find an OCR that handled all these (very strange) accents, so I did it by hand. I replaced the accented As by something meaningful to me. For example, the A with a small u above, I would replace with au. The A with a small n above: an. Etc.

We decrypt the following message:

1
2
3
4
5
6
7
AHOY! CONGRTULTIONS ON
SOLVIN' DIS DIFFICULT PUZZLE.

LIKE aLL GOOD SaILDORKIDZ,
YE NEEDS a REWaRD. IF YE
WRTS T' HOIST a FLaG, HERE
KE ONE: FLAG-COHAUFYOXLASSYPD