Tags: aes crypto
Rating:
# Help a Santa helper? (Crypto)
Description:
```
Hey, I found some secret server on which Krampus stores his private details.
It seems like he has some kind of difficult crypto challenge instead of a login, if we pass that we should get access to valuable information.
Let's give it a try, what do you say?
Target: nc challs.xmas.htsp.ro 1004
Author: Gabies
```
Files:
- [server.py](server.py)
- [chall.py](chall.py)
- [PoW.py](PoW.py)
Try to netcat in, same as last crypto chall, ask for a hash:
```
Provide a hex string X such that sha256(unhexlify(X))[-5:] = 88976
```
Using the same pwntools script:
```py
from pwn import *
p = remote("challs.xmas.htsp.ro" ,1004)
p.recvuntil("= ")
h = p.recvuntil("\n")[:-1]
ans = pwnlib.util.iters.mbruteforce(lambda x: hashlib.sha256(x.encode()).hexdigest()[-5:] == h.decode() , string.ascii_lowercase, length = 10)
p.sendline(ans.encode().hex())
```
Result:
```
python3 solve.py
[+] Opening connection to challs.xmas.htsp.ro on port 1004: Done
[+] MBruteforcing: Found key: "byytn"
[*] Switching to interactive mode
Good, you can continue!
Hello, thanks for answering my call, this time we have to break into Krampus' server and recover a stolen flag.
We have to solve a hash collision problem to get into the server.
Sadly, we're on a hurry and you only have 2 actions you can make until we get detected.
Choose what you want to do:
1. hash a message
2. provide a collision
3. exit
$ 1
Ok, give me a hex-encoded string to hash.
Give me a message.
$ deadbeef
Here is your hash: b'b01385bb41ec9d0956545c328a5160ae'.
Choose what you want to do:
1. hash a message
2. provide a collision
3. exit
$ 2
Now give me two different hex-encoded strings to check for a collision.
Give me a message.
$ deadbeef
Give me a message.
$ deadbeef
Oh, you silly, that was not it, you should try one more time!
[*] Got EOF while reading in interactive
```
As you can see, we can only have two actions:
1. hash a message
2. provide a collision
## Analyse source code
### server.py
```py
from chall import *
from PoW import *
from text import *
import os
from binascii import *
def is_hex(s):
for c in s:
if not c in "0123456789abcdef":
return False
return True
def collision(h1, h2):
return h1.string != h2.string and h1.digest() == h2.digest()
if not PoW(5):
exit()
action_cnt = 2
seed = os.urandom(16)
print (intro.format(action_cnt))
for i in range(action_cnt):
print (menu)
x = input()
if not x in ["1", "2", "3"]:
print(invalid_input)
exit()
if x == "1":
print (hash_message)
print (get_msg)
msg = input()
if not is_hex(msg):
print (bad_input)
exit()
h = Hash(seed)
h.update(unhexlify(msg))
print (show_hash.format(h.hexdigest()))
if x == "2":
print (collision_message)
print (get_msg)
msg1 = input()
print (get_msg)
msg2 = input()
if not (is_hex(msg1) and is_hex(msg2)):
print (bad_input)
exit()
h1 = Hash(seed)
h2 = Hash(seed)
h1.update(unhexlify(msg1))
h2.update(unhexlify(msg2))
if (collision(h1, h2)):
print (win.format(FLAG))
else:
print (lose)
exit()
if x == "3":
print (goodbye)
exit()
```
After looking the server.py, we know that `seed` is generate randomly, and we can get the flag only if `collision` return true
```py
def collision(h1, h2):
return h1.string != h2.string and h1.digest() == h2.digest()
...
...
h1 = Hash(seed)
h2 = Hash(seed)
h1.update(unhexlify(msg1))
h2.update(unhexlify(msg2))
if (collision(h1, h2)):
print (win.format(FLAG))
```
### chall.py
```py
import os
from Crypto.Cipher import AES
from binascii import hexlify
def xor(a, b):
return bytes([x^y for x,y in zip(a, b)])
def pad(msg, block_size):
if len(msg) % block_size == 0:
return msg
return msg + bytes(block_size - len(msg) % block_size)
class Hash:
def __init__(self, seed = None):
if seed == None:
seed = os.urandom(16)
self.perm = AES.new(seed, AES.MODE_ECB)
self.get_elem = self.perm.encrypt
self.hash = bytes(16)
self.string = b""
def update(self, msg):
msg = pad(msg, 16)
for i in range(0, len(msg), 16):
self.string += msg[i:i+16]
self.hash = xor(msg[i:i+16], self.get_elem(xor(self.hash, msg[i:i+16])))
def digest(self):
return self.hash
def hexdigest(self):
return hexlify(self.digest())
```
After looking the chall.py, we know it is using **AES ECB** to encrypt our message
But the `update` function does some special things:
```py
def update(self, msg):
msg = pad(msg, 16)
for i in range(0, len(msg), 16):
self.string += msg[i:i+16]
self.hash = xor(msg[i:i+16], self.get_elem(xor(self.hash, msg[i:i+16])))
```
We know the `get_elem` is the AES ECB encrypt function
And the `hash` is always 16 bytes, because it not appending
It does something like this if written in math term:
![image1](image1.gif)
Where `m` is message, h is `hash`, E is `get_elem`
If convert to diagram is something like this:
![image2](image2.png)
## Finding the collision
In the `collision` function, state that **we need to give two different message but produce the same hash**
We can only hash a message once then we need to provide the two message. (Because of max 2 actions)
There is a flaw exist in this custom made hashing
If we send a only 16 bytes message, it return the hash that is message XOR the encrypted message:
![image3](image3.gif)
Then what happen if we send `h XOR m` for the 2nd block of the message?
![image4](image4.gif)
As you can see, if we put the 2nd block message as hash XOR message, it must produce hash with all zero!
So we give `m` for first message and `m+(h^m)+m` for second message to get the collision!
Because `m` produce `h`, `m+(h XOR m)+m` also produce `h` (0 XOR h = h), so we get different message but same hash!
![image5](image5.gif)
## Solving
Written a [python script](solve.py) to solve this:
```py
from pwn import *
# Pad function from the chall.py
def pad(msg, block_size):
if len(msg) % block_size == 0:
return msg
return msg + bytes(block_size - len(msg) % block_size)
p = remote("challs.xmas.htsp.ro" ,1004)
p.recvuntil("= ")
h = p.recvuntil("\n")[:-1]
ans = pwnlib.util.iters.mbruteforce(lambda x: hashlib.sha256(x.encode()).hexdigest()[-5:] == h.decode() , string.ascii_lowercase, length = 10)
p.sendline(ans.encode().hex())
p.sendline('1')
p.sendlineafter("message.\n","SKR".encode().hex())
p.recvuntil("Here is your hash: b'")
h = bytes.fromhex(p.recvuntil("'")[:-1].decode())
m = pad(b"SKR",16)
hm = xor(m,h)
p.sendline('2')
p.sendlineafter("message.\n",(m).hex())
p.sendlineafter("message.\n",(m+hm+m).hex())
p.interactive()
```
And we got the flag!!
```
python3 solve.py
[+] Opening connection to challs.xmas.htsp.ro on port 1004: Done
[+] MBruteforcing: Found key: "tgao"
[*] Switching to interactive mode
Damn, that was a really clever approach, you should be proud of yourself.
Here's the flag: X-MAS{C0l1i5ion_4t7ack5_4r3_c0o1!_4ls0_ch3ck_0u7_NSUCRYPTO_fda233}
[*] Got EOF while reading in interactive
```
## Flag
> X-MAS{C0l1i5ion_4t7ack5_4r3_c0o1!_4ls0_ch3ck_0u7_NSUCRYPTO_fda233}