April 25

Учёт провизии

CTF: АльфаЦТФ2026

Автор: @frankegoesdown

Категория: Misc / Forensics

Теги: excel, crypto, reverse

Описание задачи

В терминале учёта провизии требуется ввести правильный пароль.

Дан файл Учёт провизии.xlsx. При открытии — «терминал» на листе Panel с полем ввода пароля. Нужно найти пароль, чтобы на экране отобразился флаг.

Разведка

XLSX — это ZIP с XML внутри. Распаковываем архив и смотрим структуру:

xl/workbook.xml — описание листов
xl/worksheets/
  sheet1.xml — Panel (видимый)
  sheet2.xml — s1 (скрытый)
  sheet3.xml — s2 (скрытый)
  sheet4.xml — s3 (скрытый)
xl/sharedStrings.xml — строковые константы
unzip "Учёт провизии.xlsx" -d unpacked

В workbook.xml видим три скрытых листа: s1, s2, s3. Все три участвуют в верификации пароля и рендере результата.

Анализ листов

Panel (sheet1) — фронтенд

Ячейка B11 — сообщение валидации (через IF со ссылкой на s1). Ячейки B14 и далее — пиксели «экрана»: каждая ячейка берёт бит из s3 и закрашивается условным форматированием → pixel art.

s1 (sheet2) — проверка пароля

Лист содержит многослойную линейную систему над полем Z₂₅₁ (простое p = 251). Слой 1: входной вектор x (8 байт пароля) проходит через матрицу 8×8 M1 с аффинным сдвигом c1. Слой 2: второй набор SUMPRODUCT-формул смешивает v1 и x с константами c2. Слой 3: аналогично смешивает v1, v2 и несколько отдельных линейных комбинаций. Целевой вектор v3 сравнивается с [92, 97, 27, 240, 199, 80, 217, 23]. Несмотря на трёхслойность, вся система линейна по входу x — итого одна система A_total · x ≡ targets - b_total (mod 251).

s2 (sheet3) — поточный шифр

65-строчный ГПСЧ (ЛЦСР-подобный), реализованный формулами Excel. Состояние инициализируется байтами пароля. Генерирует поток ключевых байт через BITXOR/BITLSHIFT/BITRSHIFT операции.

s3 (sheet4) — шифртекст и расшифровка

Ячейки D2:M27 — 260 байт шифртекста (10×26 блоков). Столбец B — ключевой поток из s2. Ячейки P2:Y27 — расшифровка: P[i] = XOR(D[i], B[i]). Расшифрованные байты — биты 26×80 пиксельного изображения (терминал).

Решение

Гауссово исключение над Z₂₅₁

Строим матрицу A_total (8×8) и вектор b_total (8), извлекая коэффициенты из XML всех трёх слоёв. Затем решаем систему методом Гаусса по модулю 251:

P = 251

def modinv(a, p):
    return pow(a, p - 2, p)

def gauss_mod(A, b, p):
    n = len(b)
    M = [A[i][:] + [b[i]] for i in range(n)]
    for col in range(n):
        pivot = next(r for r in range(col, n) if M[r][col] % p != 0)
        M[col], M[pivot] = M[pivot], M[col]
        inv = modinv(M[col][col], p)
        M[col] = [(v * inv) % p for v in M[col]]
        for r in range(n):
            if r != col and M[r][col]:
                factor = M[r][col]
                M[r] = [(M[r][j] - factor * M[col][j]) % p for j in range(n + 1)]
    return [M[i][n] for i in range(n)]

# targets из s1: [92, 97, 27, 240, 199, 80, 217, 23]
x = gauss_mod(A_total, [(t - b_total[i]) % P for i, t in enumerate(targets)], P)
# → [120, 108, 109, 97, 116, 114, 105, 120]
password = bytes(x).decode()  # → "xlmatrix"

Верификация через реализацию шифра

Реализуем ГПСЧ из s2 на Python, воспроизводя Excel-формулы:

keystream = generate_keystream(b"xlmatrix", 260)
plaintext = [ct ^ ks for ct, ks in zip(ciphertext, keystream)]

# Рендер bitmap 26×80 пикселей
for row in range(26):
    line = ""
    for col in range(80):
        bit = plaintext[row * 10 + col // 8] >> (7 - col % 8) & 1
        line += "█" if bit else " "
    print(line)
    

На экране появляется флаг в терминальном шрифте.

Ввод пароля

Открываем Excel, вводим xlmatrix в поле пароля на листе Panel. Формулы пересчитываются, pixel art отображает флаг.

Вывод

Задача многоуровневая:

1. Распаковка XLSX → анализ скрытых листов

2. Обратная инженерия формул → выявление линейной структуры системы валидации

3. Линейная алгебра над Z₂₅₁ → Гауссово исключение даёт пароль xlmatrix

4. Воспроизведение поточного шифра → расшифровка шифртекста

5. Pixel art → флаг

Ключевой инсайт: несмотря на три слоя SUMPRODUCT-формул с разными промежуточными переменными, вся система является составной аффинной функцией от входа — и как таковая решается методом Гаусса, а не брутфорсом.