Like in 2021 and 2022, I contributed some challenges for Firebird’s internal CTF, which are from the Hong Kong University of Science and Technology. This time I wrote three crypto challenges: Randomsum, Shelter and Threerider.
There were 24 teams participating. There were three solves for Randomsum, while Shelter and Threerider were unsolved during the CTF.
Randomsum
Challenge Summary
I am leaking random bits of the RSA factors. Who doesn’t like leaks?
Attachment: chall.py • output.txt
We are given a 2048-bit RSA modulus n, a public exponent e = 65537 and an encrypted flag c. We are also given t, which consists of 2048 bits (1318 distinct bits) from the factors, p and q. The objective is to recover the flag.
Solution
Since we are given t (i.e., 1318 distinct bits of p and q), we can immediately apply from section 4.3.11 to factorize n. In short, p and q can be obtained with branch-and-prune algorithm by checking n\ \text{mod}\ 2, n\ \text{mod}\ 2^2 and so on.
Solution script
Shelter
Challenge Summary
I hope my account is intact under the hackers’ attack!
nc HOST PORT
We are given an authentication service which allows us to
- [register] create a new user with a given username and password,
- [sign in] sign in to a user with a set of credentials by returning a token,
- [auth] retrieve the username from the token.
Remarkably, the token is a ciphertext of username=[USERMAME]&password=[PASSWORD]
, using AES-CBC with a fixed key and a random IV. It is notable that the auth endpoint is vulnerable to the padding oracle attack.
On the other hand, we are also given a hacker’s exploit which attempts to steal the username and password from a given token. The goal is to create a valid token such that the hacker is unable to retrieve its credentials with the crack
function.
Solution
The padding oracle and its attacks
Suppose that we are given a function \mathcal{O} that tells us whether a ciphertext c has a valid PKCS#7 padding, i.e.,
\mathcal{O}(c) = \begin{cases} \color{green}{\checkmark} & \text{if}\ c\ \text{has a valid padding} \\ \color{red}{✗} & \text{otherwise} \end{cases}.
There are a lot of instances vulnerable to the padding oracle attack. It affected multiple frameworks like Ruby on Rails2, ASP.NET and so on.
Let’s get to the details of the padding oracle attack, which happens in the cipher block chaining (CBC) mod of operation. If we let m_1, c_0 and c_1 being respectively a plaintext, an IV and a ciphertext (all of them are 16-byte long), then the below relation holds:
m_1 = c_0 \oplus \text{Decrypt}_k(c_1).
The general idea for the padding oracle attack is to change c_0 and keep c_1 fixed. We can recover \text{Decrypt}_k(c_1) by checking whether m_1 has a valid padding. For example, we want to recover the below ciphertext:
Assuming that the ciphertext decrypts to 68656c6c6f20776f726c642104040404
. Passing the ciphertext to \mathcal{O} would return \color{green}{\checkmark} because it has a valid padding (the message ends with four 04
s).
Of course, as the hacker we do not have the access to the plaintext. We have to use \mathcal{O} to recover the plaintext byte-by-byte. Let’s follow the algorithm given from __recover_block
and see how \mathcal{O} recovers the plaintext. To begin, it sets
\begin{aligned} & c_0 = \texttt{01010101010101010101010101010101} \quad \text{and} \\ & c_1 = \texttt{1d53a4e415b0893fc386fbea776b7198}. \end{aligned}
The ciphertext decrypts to 69656f6e6a2470697b646f2b09080b0a
, and it does not have a valid padding, i.e., \mathcal{O}(c_0 \ || \ c_1) = \color{red}{✗}. We then send \mathcal{O}(c_0′ \ || \ c_1), where
\begin{aligned} c_0′ &= \texttt{00000000000000000000000000000001} \oplus \texttt{01010101010101010101010101010101} \\ &= \texttt{01010101010101010101010101010100}. \end{aligned}
The corresponding plaintext is 69656f6e6a2470697b646f2b09080b0b
, which also imply that \mathcal{O}(c_0′ \ || \ c_1) = \color{red}{✗}. The below figure shows the results of the 256 possible attempts:
Eventually, we will send \mathcal{O}(c_0” \ || \ c_1) with
\begin{aligned} c_0” &= \texttt{0000000000000000000000000000000b} \oplus \texttt{01010101010101010101010101010101} \\ &= \texttt{0101010101010101010101010101010a}. \end{aligned}
The plaintext is now 69656f6e6a2470697b646f2b09080b01
and it has a valid PKCS#7 padding, i.e., \mathcal{O}(c_0” \ || \ c_1) = \color{green}{\checkmark}. Knowing this as a valid plaintext, we immediately know that the last byte of the plaintext (denoted by m_1”) is a 01
. Therefore, we know that the last byte of \text{Decrypt}_k(c_1) is \texttt{0a} \oplus \texttt{01} = \texttt{0b}, using the relation
\begin{aligned} & m_1” = c_0” \oplus \text{Decrypt}_k(c_1) \\ \Longrightarrow \quad & \texttt{{\color{gray}[15 bytes]}01} = \texttt{{\color{gray}[15 bytes]}0a} \oplus \text{Decrypt}_k(c_1) \\ \Longrightarrow \quad & \text{Decrypt}_k(c_1) = \texttt{{\color{gray}[15 bytes]}01} \oplus \texttt{{\color{gray}[15 bytes]}0a} = \texttt{{\color{gray}[15 bytes]}0b}. \end{aligned}
Subsequently, we can recover the second last byte. Define c_0^{(3)} by
\begin{aligned} c_0^{(3)} &= \texttt{0000000000000000000000000000000b} \oplus \texttt{02020202020202020202020202020202} \\ &= \texttt{02020202020202020202020202020209}. \end{aligned}
The plaintext for c_0^{(3)} \ || \ c_1 is 6a666c6d6927736a78676c280a0b0802
, thus \mathcal{O}(c_0^{(3)} \ || \ c_1) = \color{red}{✗}. We can keep increasing the counter for the second-last byte. Eventually, the counter will be increased to \texttt{0a}… and this is its corresponding c_0^{(4)}:
\begin{aligned} c_0^{(4)} &= \texttt{00000000000000000000000000000a0b} \oplus \texttt{02020202020202020202020202020202} \\ &= \texttt{02020202020202020202020202020809}. \end{aligned}
The plaintext for c_0^{(4)} \ || \ c_1 is 6a666c6d6927736a78676c280a0b0202
, thus \mathcal{O}(c_0^{(4)} \ || \ c_1) = \color{green}{\checkmark}. Thus the last two bytes of \text{Decrypt}_k(c_1) is \texttt{0a0b}, since
\begin{aligned} & m_1^{(4)} = c_0^{(4)} \oplus \text{Decrypt}_k(c_1) \\ \Longrightarrow \quad & \texttt{{\color{gray}[14 bytes]}0202} = \texttt{{\color{gray}[14 bytes]}0a0b} \oplus \text{Decrypt}_k(c_1) \\ \Longrightarrow \quad & \text{Decrypt}_k(c_1) = \texttt{{\color{gray}[14 bytes]}0202} \oplus \texttt{{\color{gray}[14 bytes]}0809} = \texttt{{\color{gray}[14 bytes]}0a0b}. \end{aligned}
We can repeat the process to recover \text{Decrypt}_k(c_1). With that, we can easily obtain m_1 by the identity we mentioned at the very beginning:
m_1 = c_0 \oplus \text{Decrypt}_k(c_1).
By the way, since we will be getting the error message from the auth endpoint, it could tell us if the padding is correct. With that said, this endpoint has a padding oracle and thus cannot escape from the padding oracle vulnerability.
A corner case
I was not entirely correct in the previous section. When recovering the last byte, the trailing byte may not be 01
for the padding to be PKCS#7-compliant. Instead, it could end with 0202
(or 030303
, 04040404
, …).
For instance, let’s look at the case m_1 = \texttt{{\color{gray}[14 bytes]}0201} and c_0 = \texttt{{\color{gray}[14 bytes]}017a}. The below are the 256 oracle calls that recovers the last byte of m_1. Note that c_1 below is kept constant in the attempts.
We can see that attempts #123 and #124 have valid paddings, i.e.,
\mathcal{O}(\texttt{{\color{gray}[14 bytes]}017b{\color{gray}[16 bytes]}}) = \mathcal{O}(\texttt{{\color{gray}[14 bytes]}017a{\color{gray}[16 bytes]}}) = \color{green}{\checkmark}.
However, the function stops at the first successful attempt, it assumed that the ciphertext \texttt{{\color{gray}[14 bytes]}017b{\color{gray}[16 bytes]}} would decrypt to \texttt{{\color{gray}[15 bytes]}01}, which is wrong. Thus it fails to recover the correct m_1.
Notably, there are some message-ciphertext pairs that the hacker fails to handle, and one of them being m_1 = \texttt{{\color{gray}[14 bytes]}0201} and c_1 = \texttt{{\color{gray}[14 bytes]}01}c^* with (c^* \oplus 3) < c^*.
Applying to the challenge
I used the username foo
and password xxxxxxxx[02]
([02]
is the byte with ASCII value 2). In that way, the plaintext would be username=foo&password=xxxxxxxx\x02
, which is 31 bytes long and a [01]
will be appended as a padding due to PKCS#7. We can generate ciphertexts repeatedly using the signin
command until the ciphertext ends with 0102
, 0103
, 0106
, …, 01fe
, 01ff
(all of them satisfy c^* \oplus 3 < c^*).
There are, in average, one out of 512 tokens that the hacker fails to decrypt.
How do we fix the attack script?
To handle the corner case, we need to check the plaintext so that it does not end with 0202
, 030303
and so on. We will change the second last byte of c_0 to a different value. In that way, the second last byte of m_1 will be changed as well. If m_1 is still PKCS#7-compliant, then that means that m_1 ends with a 01
. In the below figure, attempts #123+ and #124+ are the additional checks imposed.
Solution script
Threerider
Challenge Summary
The deadline for writing challenges is coming again!
Mystiz, who claimed himself not well-known reusing challenges, decided to free-ride and plagarize challenges from HKCERT CTF 2020. Uh, I mean, HKCERT CTF 2021. Maybe you can reuse the solve script last year for the flag.
Ciphertext:
Attachment: challenge.py
Suppose that the flag matches the regular expression firebird\{\w{53}\}
. We define an encryption algorithm, \mathcal{E}, with a 16-byte key k_1k_2…k_{16}. Let m be the message we would like to encrypt (all of the counters are set to zero):
- Encrypt m with AES-CTR using the key
k1 00 00 00 ... 00
(k_1 followed by 15 null bytes) and denote it by t_1. - Encrypt t_1 with AES-CTR using the key
k2 00 00 00 ... 00
and denote it by t_2. - …
- Encrypt t_{15} with AES-CTR using the key
k16 00 00 00 ... 00
and denote it by t_{16}. - Return t_{16} as the ciphertext.
\mathcal{E} is used to encrypt the flag and we are given its ciphertext. The objective is to recover the flag. Notably, the challenge is highly referenced from Tenet and Tenet: The plagarism in HKCERT CTF 2020 and 2021 respectively.
Solution
We can borrow the idea from FreeRider. To get started, we can build a 133 \times 256 matrix A and a 133 \times 1 matrix b, with entries being 0 or 1:
A = \left[\begin{matrix} b_{0, j_1} & b_{1, j_1} & b_{2, j_1} & … & b_{255, j_1} \\ b_{0, j_2} & b_{1, j_2} & b_{2, j_2} & … & b_{255, j_2} \\ && \vdots \\ b_{0, j_{133}} & b_{1, j_{133}} & b_{2, j_{133}} & … & b_{255, j_{133}} \\ \end{matrix}\right], b = \left[\begin{matrix} m_{j_1} + c_{j_1} \\ m_{j_2} + c_{j_2} \\ \vdots \\ m_{j_{133}} + c_{j_{133}} \end{matrix}\right]
Similar to FreeRider, there is a 256 \times 1 matrix x such that A \cdot x = b\ (\text{mod}\ 2). In particular, if x = (x_0, x_1, …, x_{255}) then x_j = 1 if and only if the byte j occurs in the key for an odd number of times.
For example, if the key is 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0e
, then
\begin{cases} x_j = 1 & \text{if}\ j = 0, 1, …, 13 \\ x_j = 0 & \text{otherwise} \end{cases}.
It is remarkable that there are at most sixteen from x_0, x_1, …, x_{255} such that x_j = 1. For simplicity, let’s assume that there are exactly sixteen x_j’s such that x_j = 1. In that case, if we pick m entries x_{j_1}, x_{j_2}, …, x_{j_m} randomly, then the probability of x_{j_1} = x_{j_2} = … = x_{j_m} = 0 would be
\frac{240}{256} \cdot \frac{239}{255} \cdot … \cdot \frac{240-k+1}{256-k+1}.
Since we have 133 equations and 256 unknowns, we will just randomly pick 123 x_j’s and assumed that to be zero. From above, the probability for them to actually be zero is around 1 in 56000.
Eventually, we can just repeatedly assume some of the unknowns to be zero and solve the equations. If there are less than 16 non-zero x_j’s, then that is very likely to be the vector we are looking for.
Solution script
- Gabrielle de Micheli, Nadia Heninger (2020) “Recovering cryptographic keys from partial information, by example”
https://hal.science/hal-03045663/document ↩︎ - Juliano Rizzo, Thai Duong (2010) “Practical Padding Oracle Attacks”
https://www.usenix.org/legacy/event/woot10/tech/full_papers/Rizzo.pdf ↩︎