PBCTF 2021
This CTF marked the start of my journey on preparing myself to convert my role from Software Engineer to Security Engineer for the next 4 years. I’ve taken a break from CTF since I graduated from my college. On this CTF, I managed to solve 2 Crypto Challenge
- Alkoloid Stream (134 pts)
- GoodHash (218 pts)
Today, I will explain my solution on solving the GoodHash.
Crypto
GoodHash (218 pts)
I think I made a good hash function based on AES. Could you test this?
nc good-hash.chal.perfect.blue 1337
Author: rbtree file:main.py
Source Code
|
|
Analysis
General key points
I really love this challenge, because I learn a lot of new things during taking my time to solve this challenge. After I read the source code, some key notes that we could infer:
- It use AES-GCM
- It give us the encryption key
goodhashGOODHASH
- It require us to find a hash collision with some constraints:
- The message should be in json format
- The message should only contains characters from the
ACCEPTABLE
variable - The message will need to contain
"admin": true
if we want to get the flag
- Our input is actually being used as the AES-GCM nonce, not as the plaintext.
- If you read the source code, the plaintext message always
\0*32
- Check
digest
method
- Check
- If you read the source code, the plaintext message always
After reading many articles in the internet about AES-GCM, below is the encryption scheme.
Further analysis:
- If you notice, because our plaintext is always 0, we can safely ignore the plaintext and just focus on the nonce (because if you see the image
pt1^Ek(Counter 1) = Ek(Counter 1)
because of null plaintext) - Turns out, the recommended nonce length for AES-GCM is 96 bits (12 bytes), so that the first block of encryption will be
iv||counter
, where iv is 12 bytes and counter is 4 bytes. - If the nonce length is not 96 bit (whether shorter or longer), the nonce will be hashed by using the GHASH algorithm.
- Reading the source code, we know that the targeted input (
'{"token":"xxx", "admin": false}'
) and our input length will always be longer than 12 bytes, which mean our nonce will be always hashed by GHASH method. (This is important note)
- Reading the source code, we know that the targeted input (
GHASH Explained
For detailed info, you can read wiki, but I’ll try to explain it.
Defined as GHASH(H,A,C), it requires 3 inputs: H: The secret key, calculated by Ek(’\0’*16) A: Associated data C: Message that we want to authenticate
|
|
Notes: The multiplication of xor(x1,x2)•H is happen on $GF(2^{128})$ defined by polynomial
$$Poly=x^{128}+x^7+x^2+x+1$$
Vulnerability
So, if you read the source code, you can see that:
- Our input is limited to 64
- Target hash is created from nonce
{"token: "xxxxxxxxxxxxxxxx", "admin": false}
, which length is 61.
I’ve explained above that if the nonce is not equal to 92 bit, by default AES-GCM will GHASH the nonce. Let’s try to simulate it:
- Let say that the nonce is
{"token: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "admin": false}
. Length is 61 - AES-GCM will call
GHASH(H,bytes(),nonce)
(without any associated data) - GHASH will pad the nonce and split it per 16 bytes.
|
|
- GHASH will initialize x with 16 bytes of
\x00
.final_x
will be used as the AES-GCM first block input (substitute ofiv||counter
)
|
|
- After this, the AES-GCM will start and give the encrypted+tag
The vulnerability of this code is we know the secret key H
. Because we know the secret, we can do preimage attack. Consider the below equation, which is how GHASH works:
$$x_i=(x_{i−1}⊕A_i)⋅H$$ where A is the input that we control.
If we know state $x_i$, and we want the next state $x_{i+1}$ equals to let say $P$
$$𝑃=x_{𝑖+1}=(x_𝑖⊕A_{𝑖+1})⋅𝐻$$
We can get the correct $A_{i+1}$ by multiplying it with $H^{-1}$.
$$A_{i+1}=(P⋅H^{-1})⊕x_i$$
Attack Idea
So, based on that preimage attack, we can craft a valid nonce which hash collides with the given hash, yet contains of "admin": true
Connect to the server, we got this:
|
|
First, we need to calculate the GHASH of the body
. By doing the previous simulation, we will have:
target_x1
(hash of block1,{"token": "c6ffd
)target_x2
(hash of block2,ed97492efee4eca4
)target_x3
(hash of block3,2ac218a7088", "a
)target_x4
(hash of block4,dmin": false}\x00\x00\x00
)target_finalx
(hash of nonce length)
After calculating each block hash, we can start crafting our payload:
- Set
block_1
with'{"admin":true, "'
(len = 16). So:new_block1='{"admin":true, "'
new_x1=xor(x0, block_1)•H
- Set
new_block2
with random string length of 16, which all the characters are allowed. Example, the random string isaaaabbbbccccdddd
, so:new_block2='aaaabbbbccccdddd'
new_x2=xor(x1, block_2)•H
- Apply preimage attack to the
new_block3
, so that thenew_block3
hash value will be the same astarget_x3
.- Calculate the correct value by do:
new_block3=xor((target_x3⋅H^-1), new_x2)
- Calculate the correct value by do:
- And then, for the
new_block4
, reuse the givenblock4
, which isdmin": false}\x00\x00\x00
- We can do this because we apply preimage attack on the third block, which mean the value of
new_x3
is equals totarget_x3
, which mean the value ofnew_finalx
will be equals totarget_finalx
, and because it is the same hash collision will happen!!
- We can do this because we apply preimage attack on the third block, which mean the value of
Important note during crafting the payload:
- We need to brute force the
new_block2
string until the correct value ofnew_block3
consist of the allowed characters.
Solution
So, here is my full solution.
|
|
Running the code will give you this
Putting one of the found payloads will give you the flag
Flag: pbctf{GHASH_is_short_for_GoodHash_😂}
References
- StackExchange question about preimage attack on GHASH
- AES-GCM Implementation
- Wikipedia about AES-GCM
- MSLC Writeup about GCM (which I reuse some of their codes. Thanks)
Social Media
Follow me on Twitter