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 1337Author: 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
ACCEPTABLEvariable - The message will need to contain
"admin": trueif 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
digestmethod
- 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_xwill 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_1with'{"admin":true, "'(len = 16). So:new_block1='{"admin":true, "'new_x1=xor(x0, block_1)•H
- Set
new_block2with 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_block3hash 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_x3is equals totarget_x3, which mean the value ofnew_finalxwill 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_block2string until the correct value ofnew_block3consist 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