Insomni'hack CTF 2023 - Still counting on you
There was only one crypto challenge at the Insomni’hack CTF 2023, and that was this one. Our application runs on a server, and we have a Python file that shows us how it works.
The python file consists of 4 functions:
xorshift128
encrypt_message
encrypt_user_message
menu
We have the ability to encrypt a message of your choice using the encrypt_user_message
function and we can get the encrypted admin_message
which most likely also contains the flag.
|
|
Each message gets encrypted by ChaCha20_Poly1305
which is an authenticated encryption mechanism.
It uses ChaCha20
to encrypt messages and Poly1305
to generate a message authentication code.
If we look how ChaCha20
works we can see that it is a stream cipher.
The basic idea of a stream cipher is that given a secret key K
and a nonce N
we can generate a secret one-time pad which has the same size as the message M
we try to encrypt. The one-time pad is also referred to as a key stream.
The key observation for us is that given the same key K
and the same nonce N
we will receive the same key stream.
Solution
If we look at how the nonce
is created in our python file we can see that the only source of “randomness” is the x = int(time.time())
.
int(time.time())
gives us the current timestamp in seconds since we cast it to an integer.
This means that, if we can encrypt 2 ciphers simultaneously (within one second), we can get the same nonce N
and, as a result, also the same key stream.
If we know one of the two plaintext text then we can decrypt the second unknown cipher text like so
$$\begin{eqnarray} c1 = flag ⊕ key\_stream \\\ c2 = chosen\_pt ⊕ key\_stream \end{eqnarray}$$
Now since we know the chosen_pt
we can get the flag by just xoring the two cipher text and then xoring chosen_pt
and we are left with the flag.
$$\begin{eqnarray} chosen\_pt ⊕ flag = c1 ⊕ c2 \\\ flag = (chosen\_pt ⊕ flag) ⊕ chosen\_pt \end{eqnarray}$$
We created a python script that created two connections in parallel.
One connection encrypts our chosen_pt
(which in our case was just b'1'*100
) and the second retrieves the encrypted flag.
We can also check if we received the same key stream if the nonce we receive at the start of the string is the same.
|
|
c1
is the encrypted flag and c2
is our encrypted plaintext that we choose.
We first have to remove the nonce
and the tag
since we don’t need those values.
|
|
And then we find the flag
|
|