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
|
|
Getting information from the container
After loading small.tar
, we see three new Docker images:
|
|
When we run the p1 container as requested, a password is requested:
With docker inspect
, we see that the binary which is run is /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:
|
|
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:
- From Decimal
- From Base64
We get the following program:
|
|
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:
|
|
We run our password decoding program. The password is 428bb1004de3f7639b60849dd2b17b59
.
We enter that password in the container, and it replies:
|
|
We submit the flag:
|
|
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.
|
|
The Base64 data decodes to the following:
|
|
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:
|
|
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
|
|
|
|
The key is 2557599983
, and we get password: 6e049bc38a417522cf424b4f157bbd0f
. We enter that to finally retrieve the 3rd chunk of the flag:
|
|
We are ready to submit the full flag:
|
|
Quantum Kraken Device - the Skeleton Key
Description
|
|
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)
|
|
We show the initial state vector for confirmation:
|
|
We want to get |->
, which on X axis is the opposite of |+>
.
If we do:
- H on |0> : we get |+>
- Z on |+>
In our interface with the badge, we enter the following commands:
|
|
We confirm we have reached the desired state:
|
|
To validate that, we hash the state vector
|
|
and provide the hash to the calibration test:
|
|
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:
|
|
There I struggle like hell to reach the desired state.
|
|
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:
|
|
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:
|
|
I submit this last hash, and get a flag :
|
|
By the way, at each calibration step, a LCD at the bottom of the badge lights up. Nice.
Calibration 2.a
Description
|
|
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:
|
|
I initialize the machine:
|
|
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:
|
|
To get there:
|
|
Calibrate 2.c
In this final step, we are asked, with 3 qubits, to entangle every qubits.
The desired state vector is below:
|
|
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:
- H 2
- CNOT(1,2)
- 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:
|
|
and complete the calibration:
|
|
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.
We retrieve the payload: 464c41472d30383966333765633330616166663036653036643863333731656338653865643337383634313763
.
CyberChef easily converts this hexadecimal string, or simple Python line:
|
|
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:
|
|
SailorKidz (A encryption)
This challenge was showing an image with an encrypted text.
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:
|
|