Перейти к основному содержимому

WriteUp Crypto CTFZone 2024

·6 минут· loading · loading ·
Babaika
Crypto
Автор
babaika
cR4.sh / base64 enjoyer
Оглавление

welcome
Недавно прошел ctfzone, хочу поделиться райтапом на задание по криптографии.

Cold Siemens
#

Описание: I recently discovered a huge problem in my cryptosystem. Hope it’s SQRe now!

nc cold_siemens.ctfz.zone 1188

import os
import random
from math import log
from Crypto.Util.number import long_to_bytes
from gmpy2 import iroot
from secret import flag

TARGET_LEN = 666
left_pad = random.randint(TARGET_LEN // 3, TARGET_LEN // 2)
right_pad = TARGET_LEN - left_pad - len(flag) 
#генерация случайных байтов заданной длины слева и справа от самого флага
flag = os.urandom(left_pad) + flag + os.urandom(right_pad) 

def sqrsqr(x: int, prec: int) -> tuple[int, int]:
    return int(iroot(x * 10 ** (prec * 4), 4)[0]) % 10**prec 


class Server:
    def __init__(self, bl: int, skip: int = 1000):
        self.bl = bl
        self.K = random.randrange(0, 2**self.bl)
        self.cipher = None
        self.key = None
        random.seed(self.K)

    def init_cipher(self, keylen: int):
        alpha = sqrsqr(self.K, prec=keylen)
        alpha_list = list(str(alpha).zfill(keylen))
        random.shuffle(alpha_list)
        self.key = long_to_bytes(int("".join(alpha_list)))

    def encrypt(self, m: bytes) -> bytes:
        keylen = round(log(10 ** len(m)) / log(10))
        self.init_cipher(keylen)
        sc = (len(m) + len(self.key) - 1) // len(self.key)
        return bytes([x ^ y for x, y in zip(m, self.key * sc)])


S = Server(bl=256)

print("Encrypted flag: ", S.encrypt(flag).hex())
while True:
    try:
        msg = bytes.fromhex(input("m: "))
        if len(msg) > 285:
            print("Message to big to encrypt, sorry")
        else:
            print("Encrypted msg: ", S.encrypt(msg).hex())
    except Exception as e:
        print(e)
        exit()

Мы имеем дело с симметричным методом шифрования, который использует дробную часть какого-то большого числа для формирования ключа.

Разберёмся в работе кода
#

Функция sqrsqr возвращает последние prec цифр, 4-го корня из x. Метод init_cipher создаёт ключ для шифрования. Он делает это с помощью применения функции sqrsqr к случайно сгенерированному числу K, а затем перемешивает результат. Метод encrypt шифрует входящее сообщение m используя XOR-операцию.

Пример работы:

Alt text
При подключении к сервису выводится зашифрованный флаг; усложняет задачу то, что помимо самого флага в сообщении присутствуют случайные байты слева и справа от нужной нам информации. Именно за их счёт длина сообщения увеличена до 666 символов; из-за ограничений длины ввода есть возможность восстановить дробную часть только до 285. Поэтому возникает проблема — для восстановления флага нам потребуется 666 знаков.
Мы можем отправлять сообщения длиной n и получать информацию о n цифрах. Поскольку эти цифры переставлены, нужно восстановить их одну за другой.

Решение
#

Для решения задачи нам необходимо написать код, который будет посимвольно восстанавливать число K, генерировать ключ на его основе и расшифровывать флаг. Разберём по функциям на примере решения Sarkoxed:

Функция find_decimal
#

def find_decimal(key: str, i: int, alpha: list):
    original_counts = [alpha.count(str(i)) for i in range(10)]
    for k in range(0, 100):
        for j in range(2, len(key) + 1, 2):
            tmp = int(key[:j], 16)
            if len(str(tmp)) == i - k:
                tmpalpha = str(tmp).zfill(i)

                diff = [
                    tmpalpha.count(str(i)) - oc for i, oc in enumerate(original_counts)
                ]
                if set(diff) != set([0, 1]):
                    continue
                return tmpalpha

Создается список original_counts, в котором для каждой цифры от 0 до 9 подсчитывается количество её вхождений в списке alpha. Внешний цикл проходит по значениям k от 0 до 99, а внутренний цикл перебирает подстроки строки key, начиная с индекса 2 и увеличивая длину подстроки на 2 (т.е. рассматриваются только чётные длины). Для каждой подстроки происходит преобразование из шестнадцатеричной системы в десятичную. Код проверяет, совпадает ли длина десятичного числа tmp с i - k. Если длина совпадает, создается строка tmpalpha, которая содержит десятичное число, дополненное ведущими нулями до длины i. Создаётся список diff, который содержит разницу между количеством вхождений каждой цифры в tmpalpha и значениями из original_counts. Также происходит проверка на коллизии: set(diff) != set([0, 1]). Если же все условия успешно выполняются, функция возвращает строку tmpalpha.

Функция recover_fractional_part
#

def recover_fractional_part(r, b):
    alpha = []
    #tqdm используется для красивого отображения прогресса выполнения
    for i in tqdm(range(1, b + 1)): 
        r.sendline(b"00" * i)
        key = re.findall(r"Encrypted msg:  (.*)\n", r.recvline().decode())[0]
        alpha_ = list(find_decimal(key, i, alpha))
        assert len(alpha_) == len(alpha) + 1

        for d in alpha.copy():
            if d in alpha_:
                del alpha_[alpha_.index(d)]
            else:
                alpha.append(d)
                break
        else:
            alpha.append(alpha_[0])
    r.close()

    return int("".join(alpha))

b = 285
host, port = "localhost", 1188
r = remote(host, port)
flag = bytes.fromhex(re.findall(r"Encrypted flag:  (.*)\n", r.recvline().decode())[0])
alpha = recover_fractional_part(r, b)
r.close()

Создаётся список alpha для хранения восстановленных значений. На наш сервис отправляется строка, состоящая из нулей, мы получаем ответ и извлекаем зашифрованное сообщение. После чего вызывается функция find_decimal, результат преобразуется в список alpha_. Проверка длины alpha_ гарантирует, что было найдено новое значение. В конце функция возвращает целое число, полученное путем объединения всех элементов списка alpha в строку и преобразования в целое число. То есть мы просто отправляем нули, чтобы получить ключевой поток и восстановить дробную часть.

Основной код
#

Для начала нам придётся немного порассуждать. Если alpha это дробная часть нашего числа, а x это последние цифры (функция sqrsqr в оригинальном кода). Тогда: $$(x + alpha)^4 \approx K $$ $$K - K \approx 0 $$ $$(x + alpha)^4 - (x + alpha)^4\approx 0 $$ $$(x^4 + 4 * alpha * x^3 + 6 * alpha^2 * x^2 + 4 * alpha^3 * x + alpha^4) - K\approx 0 $$ Сведем это к задаче о минимальном векторе. И получим следующую решетку из аналогичной задачи о квадратном корне: $$ \begin{bmatrix} 1 & 0 & 0 & 0 & \lfloor alpha^4 * B \rfloor \\ 0 & 1 & 0 & 0 & \lfloor4 alpha^3 * B \rfloor \\ 0 & 0 & 1 & 0 & \lfloor6 alpha^2 * B \rfloor \\ 0 & 0 & 0 & 1 & \lfloor4 alpha * B \rfloor \\ 0 & 0 & 0 & 0 & B \end{bmatrix} $$

Решетка это подмножество векторов многомерного евклидова пространства, которые образуют дискретную подгруппу относительно операции сложения. Криптография на решётках Мы знаем, что существует линейная комбинация последнего столбика, такая что результирующее число будет недалеко от нуля. Остальные столбики в единицах, так как они будут хранить информацию о коэффициентах многочлена. Они будут примерно около получившегося в последнем столбике нуля. B - точность. Опытным путём можно обнаружить, что нужно примерно 290 знаков алфавита. У нас есть 285, остальные 5 мы можем перебрать (extra). Соответственно: B = 10^(b+extra) = 10^290

extra = 290 - b
for t in tqdm(range(10**extra)):
    B = 10 ** (b + extra)
    alpha_t = alpha * 10**extra + t
    t = Rational(alpha_t) / B
    M = Matrix(
        [
            [1, 0, 0, 0, floor(t**4 * B)],
            [0, 1, 0, 0, floor(4 * t**3 * B)],
            [0, 0, 1, 0, floor(6 * t**2 * B)],
            [0, 0, 0, 1, floor(4 * t * B)],
            [0, 0, 0, 0, B],
        ]
    )

    L = M.LLL()
    tmp = M.solve_left(L[0])
    R = tmp[0] * tmp[1]
    K = int(-tmp[0] * tmp[-1] + R**4)

    def sqrsqr(x: int, prec: int) -> tuple[int, int]:
        return int(iroot(x * 10 ** (prec * 4), 4)[0]) % 10**prec

    m = flag
    keylen = round(log(10 ** len(m)) / log(10))
    random.seed(K)
    alpha_ = sqrsqr(K, prec=keylen)
    alpha_list = list(str(alpha_).zfill(keylen))
    random.shuffle(alpha_list)
    key = long_to_bytes(int("".join(alpha_list)))
    sc = (len(m) + len(key) - 1) // len(key)
    d = bytes([x ^ y for x, y in zip(m, key * sc)])
    if b"CTFZone" in d:
        print(d[d.index(b"CTFZONE") :])
        exit()

Создается матрица M, элементы которой рассчитываются на основе t и B, к ней применяется алгоритм LLL (Lenstra–Lenstra–Lovász), основная цель которого — найти короткие векторы в решетке, созданной на основе данной матрицы, то есть, простыми словами, “уменьшить матрицу”. Рассчитывается число K и происходит генерация ключа на его основе, который затем используется для расшифровки сообщения m с помощью операции XOR. Далее идёт проверка наличия строки “CTFZone” в зашифрованных данных d. Если она найдена, программа выводит данные, начиная с первой найденной позиции. Ура, флаг у нас!

End
#

На этом всё, спасибо за прочтение! Всем хорошего дня, надеюсь вам понравилось!!

over