---
tags: Writeup
---
# PBCTF2021
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.
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.
## 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
```
#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Util.number import *
from flag import flag
import json
import os
import string
ACCEPTABLE = string.ascii_letters + string.digits + string.punctuation + " "
class GoodHash:
def __init__(self, v=b""):
self.key = b"goodhashGOODHASH"
self.buf = v
def update(self, v):
self.buf += v
def digest(self):
cipher = AES.new(self.key, AES.MODE_GCM, nonce=self.buf)
enc, tag = cipher.encrypt_and_digest(b"\0" * 32)
return enc + tag
def hexdigest(self):
return self.digest().hex()
if __name__ == "__main__":
token = json.dumps({"token": os.urandom(16).hex(), "admin": False})
token_hash = GoodHash(token.encode()).hexdigest()
print(f"Body: {token}")
print(f"Hash: {token_hash}")
inp = input("> ")
if len(inp) > 64 or any(v not in ACCEPTABLE for v in inp):
print("Invalid input :(")
exit(0)
inp_hash = GoodHash(inp.encode()).hexdigest()
if token_hash == inp_hash:
try:
token = json.loads(inp)
if token["admin"] == True:
print("Wow, how did you find a collision?")
print(f"Here's the flag: {flag}")
else:
print("Nice try.")
print("Now you need to set the admin value to True")
except:
print("Invalid input :(")
else:
print("Invalid input :(")
```
### 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
After reading many articles in the internet about AES-GCM, below is the encryption scheme.
![](https://i.imgur.com/S4Eebgb.png)
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)**
#### GHASH Explained
For detailed info, you can read [wiki](https://en.wikipedia.org/wiki/Galois/Counter_Mode), 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
```
GHASH Pseudocode:
----------------------------------------------
x = bytes(16) # Initialize x with 16 bytes of 0
padded_a = pad(a) # Append extra '\x00' until the size is divisible by 16
padded_c = pad(c) # Append extra '\x00' until the size is divisible by 16
for i in range(0,len(padded_a),16):
x = xor(x, padded_a[i:i+16])•H
for i in range(0,len(padded_c),16):
x = xor(x, padded_c[i:i+16])•H
# Notes:
# conv is illustration function that will convert integer to 16 bytes representation
# Example:
# 1 -> b'\x00\x00\x00\x00\x00\x00\x00\x01'
# 8 -> b'\x00\x00\x00\x00\x00\x00\x00\x08'
# 11 -> b'\x00\x00\x00\x00\x00\x00\x00\x0b'
return xor(x, conv(len(padded_a)+len(padded_c)))•H
```
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.
```
block_1 = '{"token": "xxxxx'
block_2 = 'xxxxxxxxxxxxxxxx'
block_3 = 'xxxxxxxxxxx", "a'
block_4 = 'dmin": false}\x00\x00\x00'
```
- GHASH will initialize x with 16 bytes of `\x00`. `final_x` will be used as the AES-GCM first block input (substitute of `iv||counter`)
```
x0 = '\x00'*16
x1 = xor(x0, block_1)•H
x2 = xor(x1, block_2)•H
x3 = xor(x2, block_3)•H
x4 = xor(x3, block_4)•H
final_x = xor(x4, ('\x00'*15 + '\x3d'))•H # \x3d is nonce length representation in bytes, \x3d == 61
```
- 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:
```
Body: {"token": "c6ffded97492efee4eca42ac218a7088", "admin": false}
Hash: e7f0421f828d7a7ae902cb2dfbd3a3fa00e1953b0ebd647acc121c352b208156e51c5823199fb39e7de53e4f4c43ece2
```
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 is `aaaabbbbccccdddd`, so:
- `new_block2='aaaabbbbccccdddd'`
- `new_x2=xor(x1, block_2)•H`
- Apply preimage attack to the `new_block3`, so that the `new_block3` hash value will be the same as `target_x3`.
- Calculate the correct value by do:
- `new_block3=xor((target_x3⋅H^-1), new_x2)`
- And then, for the `new_block4`, reuse the given `block4`, which is `dmin": 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 to `target_x3`, which mean the value of `new_finalx` will be equals to `target_finalx`, and because it is the same **hash collision will happen!!**
Important note during crafting the payload:
- We need to brute force the `new_block2` string until the correct value of `new_block3` consist of the allowed characters.
### Solution
So, here is my full solution.
```python3
from Crypto.Util.number import *
from Crypto.Cipher import AES
from multiprocessing import Process
import random
acceptable = string.ascii_letters + string.digits + '!#$%&()*+,-.:;<=>?@[]^_`|~ '
key = b'goodhashGOODHASH'
def tobin(x, n):
x = Integer(x)
nbits = x.nbits()
assert nbits <= n
return [0] * (n - nbits) + x.bits()[::-1]
def frombin(v):
return int("".join(map(str, v)), 2 )
X = GF(2).polynomial_ring().gen()
poly = X**128 + X**7 + X**2 + X**1 + 1
F = GF(2**128, name='a', modulus=poly)
def toF(x):
# Little endian, so need bit reverse
x = frombin(tobin(x, 128)[::-1])
return F.fetch_int(x)
def fromF(x):
# Little endian, so need bit reverse
x = x.integer_representation()
x = frombin(tobin(x, 128)[::-1])
return x
def field_mult(a, b):
return fromF(toF(a) * toF(b))
def rand_str():
global acceptable
n = 16
return ''.join(random.choice(acceptable) for _ in range(n)).encode()
# Calculate H and H^-1
ex = False
Ek = AES.new(key, AES.MODE_ECB)
hkey = Ek.encrypt(b'\x00'*(16))
h = bytes_to_long(hkey)
inverse_h = fromF(toF(h)^-1)
target_json = b'{"token": "a617eb720dd41c256119c8b8e64d50d1", "admin": false}'
padded_target_json = target_json + b'\x00'*((16-len(target_json)%16)%16)
target_x = [0] # initialize with 0
for i in range(0, len(padded_target_json), 16):
block = padded_target_json[i:i+16]
x = field_mult(target_x[-1]^^bytes_to_long(block), h)
target_x.append(x)
def solve():
global target_x
global h
global inverse_h
global target_json
# Craft payload
new_x = [0]
new_block1 = b'{"admin":true, "'
new_x.append(field_mult(new_x[-1]^^bytes_to_long(new_block1), h))
while True:
new_block2 = rand_str()
new_x2 = field_mult(new_x[-1]^^bytes_to_long(new_block2), h)
new_block3 = long_to_bytes(field_mult(target_x[3], inverse_h)^^new_x2)
# print new_block1+new_block2+new_block3+target_json[48:], target_x[3] == field_mult(new_x2^^bytes_to_long(new_block3), h)
if not any(v not in acceptable.encode() for v in new_block3):
payload = new_block1+new_block2+new_block3+target_json[48:]
print('[+] Payload:', payload)
exit()
def finds():
global ex
while True:
if ex:
break
solve()
if __name__ == '__main__':
proc = []
for _ in range(6):
p = Process(target=finds)
p.start()
proc.append(p)
input()
ex = True
for p in proc:
p.join()
```
Running the code will give you this
![](https://i.imgur.com/YUXDmvl.png)
Putting one of the found payload will give you the flag
![](https://i.imgur.com/iFpncht.png)
`Flag: pbctf{GHASH_is_short_for_GoodHash_:joy:}`
### References
- [StackExchange question about preimage attack on GHASH](https://crypto.stackexchange.com/questions/32574/why-does-a-fixed-key-not-make-macs-like-cbc-mac-or-gmac-into-secure-hash-functio)
- [AES-GCM Implementation](https://jhafranco.com/2013/05/31/aes-gcm-implementation-in-python/)
- [Wikipedia about AES-GCM](https://en.wikipedia.org/wiki/Galois/Counter_Mode)
- [MSLC Writeup about GCM (which I reuse some of their codes. Thanks)](http://mslc.ctf.su/wp/boston-key-party-ctf-2016-gcm-crypto-9pts-2/)
## Social Media
Follow me on [Twitter](https://twitter.com/chovid99)