edit

HeroCTF v6: Halloween

Boo! Do you believe in ghosts ? I sure don’t.

Author : Alol

We are given the source code of the challenge server:

#!/usr/bin/env python3
import gostcrypto
import os

with open("flag.txt", "rb") as f:
    flag = f.read()

key, iv = os.urandom(32), os.urandom(8)
cipher = gostcrypto.gostcipher.new(
    "kuznechik", key, gostcrypto.gostcipher.MODE_CTR, init_vect=iv
)

print(f"It's almost Halloween, time to get sp00{cipher.encrypt(flag).hex()}00ky 👻!")

while True:
    print(cipher.encrypt(bytes.fromhex(input())).hex())

Seeing as we are allowed to encrypt multiple things with the same cipher object, my first instinct was counter block reuse which would allow us to recover the keystream by sending arbitrary data – or decrypt the ciphertext simply by sending it back.

This does not work. Clearly the cipher object’s counter persists throughout the encrypt calls.

So let’s look at the source code for the cipher. Nothing in encrypt looks out of place: plaintext blocks are xor’d with encrypted counter blocks. The very definition of CTR mode.

def encrypt(self, data: bytearray) -> bytearray:
    result = bytearray()
    gamma = bytearray()
    data = super().encrypt(data)
    for i in range(self._get_num_block(data)):
        gamma = self._cipher_obj.encrypt(self._counter)
        self._counter = self._inc_ctr(self._counter)
        result = result + add_xor(self._get_block(data, i), gamma)
    # ...

Wikipedia doesn’t mention any flagrant flaws in the cipher, so let’s take that out of the equation. The only real thing left that could be a problem is… _inc_ctr:

def _inc_ctr(self, ctr: bytearray) -> bytearray:
    internal = 0
    bit = bytearray(self.block_size)
    bit[self.block_size - 1] = 0x01
    for i in range(self.block_size):
        internal = ctr[i] + bit[i] + (internal << 8)
        ctr[i] = internal & 0xff
    return ctr

And here’s the obvious problem: that loop, which supposedly does addition, only goes forward, how the hell does it carry?

Well, if it doesn’t carry, it’ll just repeat after 0xff blocks… and hey, we actually do have the simple scenario we first intuited. After obtaining the ciphertext, we just have to send 0xff - len(ciphertext)/block_size arbitrary blocks for padding, and then the counter will be right back where it started.

And thus, our solve script:

from pwn import *

r = remote("crypto.heroctf.fr", 9001)

line = r.recvline().decode()
hex_ct = line[line.find("sp00")+4:line.find("00ky")]

block_size = 16
ct_blocks = (len(hex_ct)//2)//block_size # hex/2 -> bytes/16 -> blocks
pad_blocks_needed = 0xff - ct_blocks
pad = "00"*block_size*pad_blocks_needed

r.sendline(pad + hex_ct)

ct2 = r.recvline().decode()
print(bytes.fromhex(ct2))

We get a bunch of garbage… with the flag at the end: Hero{5p00ky_5c4ry_fl4w3d_cryp70_1mpl3m3n74710ns_53nd_5h1v3r5_d0wn_y0ur_5p1n3}.


Home About Contact