HeroCTF v6 - Halloween [crypto]
My solve from a noob point a view for the halloween challenge at heroCTF v6.
Difficulty : Hard
Team : Langskip
Boo! Do you believe in ghost ? I sure don’t
Author : alol
TL;DR
The GOST cipher used for the challenge is badly implemented.
Using the CTR mode, the counter reset after 256 count. So we can regenerate the keystream used to encrypt the flag and decrypt the flag.
Introduction
For this challenge, a server sends us a welcome message with the encrypted flag inside. It will then encrypt each message (encoded in hexadecimal) we send to it.
The algorithm used is an implementation of GOST, a symmetrical algorithm like AES.
Solving
Challenge source code
The challenge code is quite simple.
chall.py
|
|
- A 32-byte random key is generated
- An 8-byte initialization vector is generated
cipher
is initialized with gostcrypto with the CTR mode, the key, the IV and the algorithm we don’t yet know ‘kuznechik’.
Once this is done, the server sends us a string with the encrypted flag in it, for example :
It's almost Halloween, time to get sp00361e2084c41bd4ddf3748a49442d2c99670f6d235d2429e3d6e445f07d5415f2ebd38bee6bf50fecd23ae33db8a76c48d1ab7bb7591512b354762afbfa7153e494f8eb3bd93ad1151d3c36f4c1913400ky 👻!
So far, there’s no obvious vulnerability, so you’ll have to dive into the source code (it’s a tough challenge, though!).
And then you can send strings of characters encoded in hex and the server will reply with its encrypted version.
Reading gostcrypto module source code
For python modules, just look on pypi.
After a quick google search on pypi you’ll find this page.
And in “project links” you’ll find a link to github with all the source code and a user doc but the doc won’t be useful.
Reading the parts used for encryption
Let’s dive straight into the source code, i.e. the gostcrypto.gostcipher
module at this page.
There are two files:
- gost_34_12_2015.py
- gost_34_13_2015.py
A quick look at the first file reveals that this is where the ‘kuznechik’ and ‘magma’ algo are defined. Here we have the source code for encrypting plaintext in ECB mode. In other words, classic encryption without mode, as with AES:
In the second file, all modes are defined, including CTR
on line 802 in the GOST34132015ctr
class: class GOST34132015ctr(GOST34132015Cipher):
.
It contains the encrypt()
, decrypt()
, counter()
, _inc_ctr()
methods.
The simplest is the decrypt()
method:
gost_34_13_2015.py:GOST34132015Cipher:decrypt()
|
|
super().decrypt(data)
will not alter data, it corresponds to the decrypt()
method of the GOST34132015Cipher
class inherited by GOST34132015ctr
which does :
gost_34_13_2015.py:GOST34132015ctr:decrypt()
|
|
So super().decrypt(data)
checks whether the data is of type bytes or bytearray
and then encrypts it with self.encrypt(data)
.
Encrypting a ciphertext with CTR mode is like deciphering it.
The CTR mode is a stream cipher which means that the cipher can be xoried with the plaintext and therefore the plaintext size is equal to the cipher size. If we know the plaintext, we can xor it with the ciphertext to recover the keystream, and if we know the keystream, we can xor it with the ciphertext to recover the plaintext.
Then, the counter()
method only returns the counter and the method _inc_ctr()
increments the counter.
At class initialization, gostcrypto.gostcipher.new("kuznechik", key, gostcrypto.gostcipher.MODE_CTR, init_vect=iv)
in the challenge amounts to executing the __init__()
method, which takes the algo, key and IV as parameters:
- A counter is defined as
iv + b"\x00"*8
, i.e. the first half of the counter is a random nonce and the lower half is the counter starting at 0. - The
GOST34132015
class (and notGOST34132015Cipher
, as the latter doesn’t have a__init__
method, so the class above is initialized instead) is initialized withsuper().__init__(algorithm,key)
, which defines :- the algorithm, i.e. ‘kuznechik’ for us
self._cipher_obj: CipherObjType = GOST34122015Kuznechik(key)
, defines_cipher_obj
withkuznechik
, which will be used to encrypt our plain text.
CTR mode execution flow
The execution flow is defined in the encrypt()
method line 855
- The current counter (starting at 0) is encrypted and the result stored in
gamma
:gamma = self._cipher_obj.encrypt(self._counter)
. - The counter is incremented:
self._counter = self._inc_ctr(self._counter)
- a XOR operation is applied between the encrypted counter and the current block:
result + add_xor(self._get_block(data, i), gamma)
The ‘if’ block corresponds to the same thing, but for a block that isn’t 16 characters long. By the way, block size is defined here
|
|
We could go and analyze the kuznechik
function in depth, but there were no papers talking about an exploitable vulnerability there so i slowed down.
So I decided to test all the functions of the GOST34132015ctr class.
Analysis of the counter incrementation
Testing _inc_ctr()
, we notice something interesting. Here’s the code:
gost_34_13_2015.py:GOST34132015ctr:_inc_ctr()
|
|
Actually i didn’t see anything at first sight, i just put the fonction in a jupyter notebook and tested a lil.
I discovered that by taking an example counter: 12916ac66cc1cbfe0000000000000000
, notice that the part that must be incremented is 0000000000000000
being 8 bytes long and therefore $ 256^{8} = 18446744073709551616 $ counter possibilities.
Except that, in this implementation, the counter rises to 00000000000000ff
and then, on the next increment, resets to 0000000000000000
.
As a result, there are only 256 possible counter values, which greatly reduces the list of possibilities and allows us to recover the keystream used to encrypt the flag.
Knowing that the counter is incremented with each block, we need to find out how many times the counter has been incremented to encrypt the flag, subtract this number from 256, increment the counter by the number we obtain and recover the keystream by sending a null byte string of the same size as the flag (since the keystream is encrypted with the plaintext, sending null bytes enables us to directly recover the key because 0 xor n = n).
Exploit
solve_halloween.py
|
|
flag : Hero{5p00ky_5c4ry_fl4w3d_cryp70_1mpl3m3n74710ns_53nd_5h1v3r5_d0wn_y0ur_5p1n3}
Conclusion
I really liked the challenge and found it intersting. I learned source code “auditing” and a new cipher.
The code was well written so easy to understand, and there was no horrible mathematics only understandable by PhDs.