Я-Профи 2021. Отборочный этап. Реверс задач магистерского уровня
@ Rakovsky Stanislav | Monday, Dec 20, 2021 | 15 minutes read | Update at Monday, Dec 20, 2021

В этот раз нас ждали очень интересные таски, часть из которых была подготовлена SPbCTF.

В этом году было 8 задач:

  1. Криптография с Эль Гамалем - 11 pts
  2. Утечка информации по сети, pcap - 12 pts
  3. AES-подобная криптография - 13 pts
  4. Крипта, цикличные коды, БХЧ - 14 pts
  5. Пентест веб-приложения - 11 pts
  6. Реверс модуля трояна - 12 pts
  7. Реверс pdf-файла - 13 pts
  8. Реверс крипты - 14 pts

Под катом - разбор второй, шестой, седьмой и восьмой задач.

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


Утечка информации по сети, pcap

Реверс модуля трояна

Реверс pdf-файла

Реверс крипты

Задача 2, 12 баллов

Злоумышленник организовал скрытый канал передачи видеопотока с использованием протокола TCP. Нужно определить способ кодирования и найти секретный ключ.

Дамп трафика: https://disk.yandex.ru/d/eQCWL3_sCcBOXw

Формат ответа: itmo{...}

mag5.pcap

Нас ждет незашумленный pcap-файл весом почти 12 метров, состоящмй из одного tcp-стрима.

Делаем Follow tcp stream - Show data as Hex Dump

00000000  00 00 00 01 09 30 00 00  01 41 9b ad 49 e1 0f 26   .....0.. .A..I..&
00000010  53 02 0d 7f fe da a6 58  00 ae f2 5f fc 37 e1 38   S......X ..._.7.8
00000020  46 a7 56 eb a6 e7 11 74  f0 c7 cc 93 1f 07 7a f7   F.V....t ......z.
00000030  a9 84 ae c8 1a 25 85 8b  30 b7 d6 f0 3b 79 f9 9e   .....%.. 0...;y..

Фмммм… Гуглим магию 00 00 00 01 09 30 00 00 - попадаем на статьи по работе с фреймами в кодеке H.264.

Извлекаем файл (Show data as Raw - Save As...)

Выбор прост:

  1. Погуглить онлайн-инструменты по конвертации файлов .h264 в какой-нибудь воспринимаемый vlc
  2. Качаем плеер, который может работать с сырым .h264

Вот этот сайт справился на ура, только ему нужно давать файл сразу с правильным расширением .h264.

Видеопоток - три с половиной минуты нечто похожее на конфетти, где на промежутках от 0:41-0:45, 2:21-2:25 высвечивается флаг, содержащий в себе хэш длиной в 32 символа…

Излучаю недовольство небольшой уцуцугой в конце и весьма солидной длиной флага, которую нужно перепечатать.

Флаг моего варианта: itmo{d3177be8ad0b41a5817268b36d17ff6d}


Задача 6, 12 баллов

6. (12)
Задание подготовлено сообществом SPbCTF https://vk.com/spbctf

В ходе расследования инцидента был обнаружен модуль трояна, генерирующий ключ шифрования файлов на основе окружения системы.

Валера декомпилировал модуль в исходный код, и укатил в отпуск, так и не достав сам ключ. Ключ нужен срочно! У нас 4 часа.

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

Исходные данные: https://disk.yandex.ru/d/qGUHZyVavWtTgg

Рекомендуемое ПО: Java JDK или онлайн-компилятор, например ideone.com

Формат ответа: yaprofi{...}

Main.java

Как решивший этот таск могу сказать, что вам лучше не доверять авторам таска в плане возможности использовать онлайн-компилятор - установка IDE сэкономит вам нервы и время.

Так вот. Использовался предположительно данный популярный обфускатор.

У нас есть файл длиной в 439 строк, который спокойно работает с openjdk-17, но ничего не выводит. Как минимум сейчас.

Изучаем код:

private static void lllIIIl() {
    llllI = new String[lllll[110]];
    Main.llllI[Main.lllll[1]] = Main.llIlllI((String)"", (String)"ORoIg");
    Main.llllI[Main.lllll[7]] = Main.llIllll((String)"qKw+p9G/ik0=", (String)"XXmKJ");
    Main.llllI[Main.lllll[5]] = Main.llIlllI((String)"Qw==", (String)"cyLkY");
    Main.llllI[Main.lllll[3]] = Main.lllIIII((String)"Nq3yhVASS9c=", (String)"WKQoN");
    Main.llllI[Main.lllll[4]] = Main.llIllll((String)"qBYVopZrQ9A=", (String)"prhZS");

Заготовка строк.

private static void processDetails(ProcessHandle lllIllllIllIIll) throws NoSuchAlgorithmException {
    String lllIllllIllIlll = Main.text((Optional)lllIllllIllIIll.info().command());
    if (lllIllllIllIlll.equals(lIllI[lIlll[lllll[0]]])) {
        return;
    }
    Path lllIllllIllIIIl = Paths.get(lllIllllIllIlll, new String[lIlll[lllll[1]]]);
    String lllIllllIllIlII = Main.getWithoutExtension((String)lllIllllIllIIIl.getFileName().toString());
    MessageDigest lllIllllIlIllll = MessageDigest.getInstance(lIllI[lIlll[lllll[2]]]);
    lllIllllIlIllll.update(lllIllllIllIlII.getBytes());
    byte[] lllIllllIlllIlI = lllIllllIlIllll.digest();
    int lllIllllIllIlIl = Main.find((Object[])hashes, (Object)Main.bytesToHex((byte[])lllIllllIlllIlI));
    if (lllIllllIllIlIl < 0) return;
    Main.procName[lllIllllIllIlIl] = lllIllllIllIlII;
}

Очень примечательно, что у этой функции осталось искомое название.

public static void main(String[] arrstring) throws NoSuchAlgorithmException {
    String lllIllllIIlIlIl = Main.getOsName();
    for (int lllIllllIIllIIl = Main.lIlll[Main.lllll[1]]; lllIllllIIllIIl < hashes.length; ++lllIllllIIllIIl) {
        Main.procName[lllIllllIIllIIl] = null;
...
      do {
        if (lllIllllIIlIIll2 >= hashes.length) {
            MessageDigest lllIllllIIlIIlll2 = MessageDigest.getInstance(lIllI[lIlll[lllll[3]]]);
            lllIllllIIlIIlll2.update(((String)lllIllllIIllIIl).getBytes());
            byte[] lllIllllIIlIIlI = lllIllllIIlIIlll2.digest();
            System.out.println("yaprofi{" + Main.bytesToHex((byte[])lllIllllIIlIIlI).toLowerCase() + "}");
            return;
        }
...

Функция генерации флага + тут есть еще несколько хороших переменных.

Рекомендую поковырять файл, переименовать нужные переменные - и именно через нормальную IDE, а не текстовый редактор типа notepad++, так как названия функций включены в названия других переменных, что может смутить вас, так как вы пытаетесь дать осмысленные названия, а они будут пересекаться где-то в других местах.

Достаточно интересная техника против обратной разработки, как по мне.

Причёсываем main:

public static void main(String[] arrstring) throws NoSuchAlgorithmException {
    String osName = Main.getOsName();
    for (int i = Main.ints[Main.key_array[1]]; i < hashes.length; ++i) {
        Main.procName[i] = null;
        if (strings_array[key_array[8]].length() < strings_array[key_array[39]].length()) continue;
        return;
    }
    if (!osName.equals(common_strings[ints[key_array[5]]])) return;
    ProcessHandle.allProcesses().forEach(process_id_array -> {
        try {
            Main.processDetails((ProcessHandle)process_id_array);
            strings_array[key_array[81]].length();
            "".length();
        }
        catch (NoSuchAlgorithmException stackTrace) {
            stackTrace.printStackTrace();
        }
        if (((key_array[82] + key_array[2] - key_array[83] + key_array[8] ^ key_array[51] + key_array[84] - key_array[85] + key_array[86]) & (key_array[74] + key_array[87] - key_array[88] + key_array[1] ^ key_array[89] + key_array[90] - key_array[22] + key_array[8] ^ -strings_array[key_array[91]].length())) >= ((key_array[92] ^ key_array[45] ^ (key_array[93] ^ key_array[94])) & (key_array[61] + key_array[95] - key_array[96] + key_array[49] ^ key_array[97] + key_array[98] - key_array[76] + key_array[36] ^ -strings_array[key_array[99]].length()))) return;
        return;
    });
    Object raw_flag = new String();
    int index = ints[key_array[1]];
    do {
        if (index >= hashes.length) {
            MessageDigest md5_instnc = MessageDigest.getInstance(common_strings[ints[key_array[3]]]);
            md5_instnc.update(((String)raw_flag).getBytes());
            byte[] key_digest = md5_instnc.digest();
            System.out.println("yaprofi{" + Main.bytesToHex((byte[])key_digest).toLowerCase() + "}");
            return;
        }
        if (procName[index] == null) {
            return;
        }
        raw_flag = (String)raw_flag + procName[index];
        ++index;
    } while (((key_array[14] ^ key_array[61]) & (key_array[62] ^ key_array[53] ^ key_array[63])) != (key_array[64] ^ key_array[12]));
}

Еще один довод использовать IDE: возможность отлаживать код.

if (!osName.equals(common_strings[ints[key_array[5]]])) return;

Такс, встав на строке со сравнением нашей системной версии становится понятно, что первая часть условий - запускаться на Windows 10. Не вижу проблем отлаживатьсяся и на семерке, тем более что эту проверку мы можем спокойно обойти.

ProcessHandle.allProcesses().forEach(process_id_array -> {
    try {
        Main.processDetails((ProcessHandle)process_id_array);
        strings_array[key_array[81]].length();
        "".length();
    }
    catch (NoSuchAlgorithmException stackTrace) {
        stackTrace.printStackTrace();
    }
    if (((key_array[82] + key_array[2] - key_array[83] + key_array[8] ^ key_array[51] + key_array[84] - key_array[85] + key_array[86]) & (key_array[74] + key_array[87] - key_array[88] + key_array[1] ^ key_array[89] + key_array[90] - key_array[22] + key_array[8] ^ -strings_array[key_array[91]].length())) >= ((key_array[92] ^ key_array[45] ^ (key_array[93] ^ key_array[94])) & (key_array[61] + key_array[95] - key_array[96] + key_array[49] ^ key_array[97] + key_array[98] - key_array[76] + key_array[36] ^ -strings_array[key_array[99]].length()))) return;
    return;
});

То есть мы итерируемся по PID-ам, передавая из в processDetails. Давайте разберем ее.

После разбора логики она выглядит так:

    private static void processDetails(ProcessHandle pid) throws NoSuchAlgorithmException {
        String proc_full_path = Main.text((Optional)pid.info().command());
        if (proc_full_path.equals(common_strings[ints[key_array[0]]])) {
            return;
        }
        Path proc_path = Paths.get(proc_full_path, new String[ints[key_array[1]]]);
        String proc_name = Main.getWithoutExtension((String)proc_path.getFileName().toString());
        MessageDigest md5_algo = MessageDigest.getInstance(common_strings[ints[key_array[2]]]);
        md5_algo.update(proc_name.getBytes());
        byte[] md5_digest = md5_algo.digest();
        int hash_index = Main.find((Object[])hashes, (Object)Main.bytesToHex((byte[])md5_digest));
        if (hash_index < 0) return;
        Main.procName[hash_index] = proc_name;
    }

То есть скрипт ищет определенные процессы по md5-кам от их названий, и если найдет - запишет название в массив. Данный массив впоследствии будет использоваться для создания флага. Если проигнорировать это условие и переиначить проверку в main, то будет генерироваться флаг от пустой строки, что нам очень нежелательно.

Давайте посмотрим, какие значения ожидаются:

Гугление и труд всё перетрут, вооружаемся онлайновыми радужными таблицами:

ECCBC87E4B5CE2FE28308FD9F2A7BAF3 # 3
9DD4E461268C8034F5C8564E155C67A6 # x
035963B147F3B4B278D6DAD324C642C6 # telegram
2765D621AF8A58B78B4D528BD5EF7F6B # icq
5412E9B841A3A6322E0E966B0118E4A3 # viber
57BA23B78C1FD7C8AC4BF325F6F40D9A # whatsup

Дело за малым, объявляем их в любом удобном месте. Например, можем переопределить processDetails, раз они здесь и назначаются:

private static void processDetails(ProcessHandle prochndl) throws NoSuchAlgorithmException {
    Main.procName[0] = "3";
    Main.procName[1] = "x";
    Main.procName[2] = "telegram";
    Main.procName[3] = "icq";
    Main.procName[4] = "viber";
    Main.procName[5] = "whatsup";
}

Убеждаемся, что нигде по коду нет контроля целостности, запускаем наш новый код, получаем флаг:

yaprofi{7616f47cfa1f99616f1dcf8155ea2669}

Задача 7, 13 баллов

Задание подготовлено сообществом SPbCTF https://vk.com/spbctf

В этом PDF флаг написан на странице №2. Но страницы №2 нет! Разберитесь, в чём дело, и восстановите флаг.

Исходные данные: https://disk.yandex.ru/i/W_pX_Cd3_Yvr3A

Рекомендуемое ПО: hex-редактор, например 010 Editor.

Формат ответа: itmo{...}

spbctf.pdf

PDF task - начало

Честно, по баллам это очень недооцененный таск.

Простой пользователь и индивидуальный предприниматель mrvos экспортнул страницу spbctf в вк в pdf-формат, при этом слегка пошаманив:

И, как описано в таске, отсутствует вторая из одиннадцати страниц.

И тут нужно почувствовать баланс:

  • с одной стороны, эту задачу можно гарантированно решить за время проведения контеста;
  • с другой стороны, эту задачу сделал Влад Росков для магистров, вряд ли она будет простой :)

Давайте бренштормить.

Первая мысль - грепнуть флаг. Не работает.

Вторая мысль - вытащить все картинки из стримов с помощью онлайн-тулзяки и найти флаг. Увы, флаг так не нашелся.

Третья мысль - строки можно выделить и скопировать (это называется born digital pdf - формат смешанного контета, когда слайды представлены не просто в виде цельных картинок, а имеют разбивку на боксы, которые хранят в себе текст и медиа), значит можно поискать футеры страниц и найти нужный стрим. Не работает.

На этом моменте я перешел решать последний реверс, чтобы вернуться к задаче попозже)

Стримы

Придется ковырять стримы - составные элементы pdf-файлов. Открыв в PdfStreamDumper, можем порадоваться 227 стримам, что честно не так много.

Стримы иерархичны - у каждого из них есть свой Object Index, у некоторых проставлены /Parent, некоторые ссылаются на своих /Kids

Внемли нашему зову, сила питона:

python3 pdf-parser.py spbctf.pdf | python2 pdfobjflow.py

Да, очень красивое и широкое дерево отношений. Можете открыть его в новом окне

python3 ./pdfid_v0_2_8/pdfid.py spbctf.pdf
PDFiD 0.2.8 spbctf.pdf
 PDF Header: %PDF-1.4
 obj                  674
 endobj               673
 stream               273
 endstream            273
 xref                   3
 trailer                3
 startxref              3
 /Page                 12
 /Encrypt               0
 /ObjStm               16
 /JS                    1
 /JavaScript            0
 /AA                    0
 /OpenAction            0
 /AcroForm              0
 /JBIG2Decode           0
 /RichMedia             0
 /Launch                0
 /EmbeddedFile          0
 /XFA                   0
 /URI                 660
 /Colors > 2^24         0

Видим, что страниц 12.

python3 ./pdf-parser.py -t "/Page" spbctf.pdf

Выхлоп слегка упрощу

obj 2063 0
 Type: /Page
  <<
    /Parent 475 0 R
    /StructParents 0
    /Type /Page
  >>


obj 1 0
 Type: /Page
  <<
    /Parent 475 0 R
    /StructParents 1
    /Type /Page
  >>


obj 42 0
 Type: /Page
  <<
    /Parent 475 0 R
    /StructParents 2
    /Type /Page
  >>


obj 66 0
 Type: /Page
  <<
    /Parent 475 0 R
    /StructParents 3
    /Type /Page
  >>


obj 89 0
 Type: /Page
  <<
    /Parent 475 0 R
    /StructParents 4
    /Type /Page
  >>


obj 111 0
 Type: /Page
  <<
    /Parent 475 0 R
    /StructParents 5
    /Type /Page
  >>


obj 120 0
 Type: /Page
  <<
    /Parent 475 0 R
    /StructParents 6
    /Type /Page
  >>


obj 129 0
 Type: /Page
  <<
    /Parent 475 0 R
    /StructParents 7
    /Type /Page
  >>


obj 141 0
 Type: /Page
  <<
    /Parent 476 0 R
    /StructParents 8
    /Type /Page
  >>


obj 156 0
 Type: /Page
  <<
    /Parent 476 0 R
    /StructParents 9
    /Type /Page
  >>


obj 174 0
 Type: /Page
  <<
    /Parent 476 0 R
    /StructParents 10
    /Type /Page
  >>


obj 1 0
 Type: /Page
  <<
    /Parent 475 0 R
    /StructParents 1
    /Type /Page
  >>

Да, пайшарм, это то, что мне нужно.

Достаточно примечательно наблюдать дублирование obj 1 0. Мне оно не нравится. Чем они отличаются?

obj 1 0
    /Contents 2 0 R


obj 1 0
     /Contents 2245 0 R

А какие значения у других страниц?

  • 2063 - [2154 0 R 2155 0 R 2160 0 R 2164 0 R 2165 0 R 2166 0 R 2167 0 R 2172 0 R]
  • 42 - 43 0 R
  • 66 - 67 0 R
  • 89 - 90 0 R
  • 111 - 112 0 R
  • 120 - 121 0 R
  • 129 - 130 0 R
  • 141 - 142 0 R
  • 156 - 157 0 R
  • 174 - 175 0 R

Как по мне, налицо закономерность. Но наивно исправить не получится - страница не появляется.

Но можно заметить, что после объекта 1 идет переопределение объектов 477 и 478 и последующих.

img_1.png

Может рискнем их вырезать? Увы, вырезав последние стримы, мы ничего не добились.

Так, а что насчет их общих родителей?

474 0 obj
<</Count 10/Kids[475 0 R 476 0 R]/Type/Pages>>
endobj
475 0 obj
<</Count 7/Kids[2063 0 R   42 0 R   66 0 R   89 0 R 111 0 R 120 0 R 129 0 R]/Parent 474 0 R/Type/Pages>>
endobj
476 0 obj
<</Count 3/Kids[141 0 R 156 0 R 174 0 R]/Parent 474 0 R/Type/Pages>>
endobj

Первый объект не указан у своего родителя в детях. Звучит как локальная трагедия. Давайте исправим, добавив 475-му его ребенка.

474 0 obj
<</Count 10/Kids[475 0 R 476 0 R]/Type/Pages>>
endobj
475 0 obj
<</Count 8/Kids[2063 0 R   1 0 R   42 0 R   66 0 R   89 0 R 111 0 R 120 0 R 129 0 R]/Parent 474 0 R/Type/Pages>>
endobj
476 0 obj
<</Count 3/Kids[141 0 R 156 0 R 174 0 R]/Parent 474 0 R/Type/Pages>>
endobj

Фух. Решили. Пожалуйста, не показывайте этот таск детям) Мы попробовали несколько различных теорий, и правильно пошли от мысли, что нужно смотреть в отношения между объектами. Жалко, что родитель может вот так просто забыть о своем ребенке, а pdf не может в контроль двухсторонних связей.


Задача 8, 14 баллов

Задание подготовлено сообществом SPbCTF https://vk.com/spbctf

Представляем вашему вниманию вторую версию нашего невзламываемого, наибыстрейшего, пост-асимметричного, FIPS 140-2=138 сертифицированного криптоалгоритма. Наша запатентованная технология гарантирует, что даже если ЦРУ дешифрует один слой защиты, оно всё ещё ничего не получит: ведь слоёв два.

Именно поэтому вы не сможете расшифровать флаг, хранящийся в файле encrypted_flag.dat

Исходные данные: https://disk.yandex.ru/d/vpslCAwc9iIOmA

Формат ответа: itmo{...}

spbctf.pdf

И на закуску таск на реверс криптоалгоритма.

Дан js-файл, который позволяет зашифровать и расшифровать предложенный файл. Код на 132 страницы, оставлю только кусок с шифрованием:

var key = crypto.createHash('sha512').update(passphrase, 'utf8').digest();
var input = fs.readFileSync(inputFile);

console.log("First pass...");
var output = Buffer.from(input);
for (var i = 0; i < output.length; i++) {
    output[i] += key[i % key.length];
}

console.log("Second pass...");
input = Buffer.from(output.toString('hex'));
var output = Buffer.from(input);
for (var i = 0; i < output.length; i++) {
    output[i] += key[i % key.length];
}

fs.writeFileSync(outputFile, output);
console.log("Written " + output.length + " bytes");

state = 0;
rl.setPrompt('> ');

Так, использование в качестве ключа sha512(passphrase)… Хорошо, мы теперь знаем, что пароль тут не предполагается перебирать.

На первом слое ключ побайтово циклично add8-ится, а на втором…

input = Buffer.from(output.toString('hex'));

Хо, замечательный алгоритм, который увеличивает выходной файл в два раза, в чем можно убедиться, запустив скрипт)

Так, расчехляем jupyter (во время решения тасков рекомендую использовать именно jupyter, а не pycharm, за счет удобства разработки без необходимости перезапускать скрипт).

Так, но как эта особенность алгоритма нам поможет с расшифровкой? Давайте искать ограничения.

Пусть весь файл состоит из блоков длиной 64 байта (64 символа - длина дайджеста sha256). Последний блок может быть короче.

Также encrypt(block[i], key) - процедура шифрования одного блока.

Операция hexlify хэширует один блок, возвращает два блока. При этом энтропия выходных блоков уменьшается: они могут
иметь значения только в одном из двух диапазонов: ord('0') - ord('9') и ord('a') - ord('f').

Затем эти блоки повторно шифруются. Получается, что в рамках каждого блока мы можем уточнить диапазон значений, который может
принимать каждый байт ключа - с 256 значений до 16. При этом выходных блоков у нас 2n, и применяя эту логику в отношении
всех имеющихся блоков, мы можем еще сильнее уменьшить энтропию.

Непонятно? Мне тоже. Давайте реализовывать.

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

Поделим шифротекст на блоки второй стадии:

enc = open("encrypted_flag.dat", "rb").read()

blocks = [enc[i*64: i*64+64] for i in range(len(enc)//64 + 1)]

Обозначим все возможные варианты каждого байта ключа:

keylen = 64
variants = []
for k in range(keylen):
    variants.append(list(range(256)))

Теперь пройдемся с ограничением:

allowed_af = b"0123456789abcdef"

for block in blocks:
    # проходимся по каждому байту блока
    for block_elem in range(len(block)):
        # print("block", block_i, block[block_elem])
        cur_variant = variants[block_elem % keylen]

        # to_delete - лист с недопустимыми значениями, которые мы выкинем из текущей позиции ключа

        # можно сильно оптимизировать, копируя cur_variant, тогда на каждом блоке
        # мы будем затрачивать меньше времени на проверку вариантов,
        # но даю код таким, как я его написал во время соревнования - он тоже имеет право на существование

        # знаете, классно думать об оптимизациях во время написания райтапа, это может помочь в будущем при
        # схожих ситуациях

        to_delete = []
        for cuva in cur_variant:
            resu = block[block_elem] - cuva
            # выравниваем значение блока после обращения второй стадии шифрования
            # можно также использовать uint8 в ctypes или malduck
            if resu < 0:
                resu+=256
            # выкидываем плохой вариант
            if resu not in allowed_af:
                to_delete.append(cuva)
        for _ in to_delete:
            cur_variant.remove(_)
        print(cur_variant)
        # Sanity check - у нас массив не может остаться пустым
        if not cur_variant:
            print("!!! EMPTY")
print(variants)

В итоге получились такие значения ключа:

[[9],
 [248, 249],
 [54],
 [31],
 [97],
 [232],
 [67],
 [102],
 [7],
 [221],
 [227],
 [62],
 [14],
 [119],
 [199],
 [238],
 [74],
 [244],
 [23],
 [193],
 [82],
 [122],
 [150],
 [71, 72],
 [143],
 [202],
 [67],
 [194],
 [229],
 [10],
 [9],
 [37],
 [35],
 [181],
 [159],
 [28],
 [109],
 [53],
 [199],
 [255],
 [193],
 [215],
 [14],
 [127],
 [205],
 [9],
 [207],
 [6],
 [245],
 [254],
 [96],
 [59],
 [67],
 [94],
 [231],
 [180],
 [157],
 [13],
 [14],
 [95],
 [34],
 [134],
 [46],
 [145, 146]]

Видим, что в трех байтах у нас могут быть два значения: это порождает 8 различных ключей.

Но зачем плодить дополнительный код, если мы на соревновании и может просто выбрать нужные нам байты)

def decodex(blob, variants):
    ans = []
    for blob_i in range(len(blob)):
        cur_key = variants[blob_i % keylen]

        resu = blob[blob_i] - cur_key[0]
        if resu < 0:
            resu += 256
        ans.append(resu)
    return bytes(ans)

import binascii, hexdump
dx = decodex(binascii.unhexlify(decodex(enc, variants)), variants)
hexdump.hexdump(dx)
00000000: 50 4B 03 04 14 00 00 00  08 00 38 7B 75 53 12 03  PK........8{uS..
00000010: BA CE F0 01 00 00 E7 13  00 00 08 00 00 00 66 6D  ..............fm
00000020: 61 67 2E 74 78 74 ED 58  4D 4B 03 32 10 BD E7 57  ag.txt.XMK.2...W
00000030: 44 C2 82 1E BA DE 7B 2D  88 17 41 50 28 1E DB BC  D.....{-..AP(...

О, это архив. Мы видим fmag.txt, и благодаря 16-разрядной сетке можем смело сказать, что в последнем байте флага должно быть значение 146, а не 145.

00000220: 08 00 38 7A 75 53 12 03  BA CE F0 02 00 00 E7 12  ..8zuS..........
00000230: 00 00 08 00 24 00 00 00  00 00 00 00 20 00 00 00  ....$....... ...
00000240: 00 00 00 00 66 6C 61 67  2E 74 78 75 0A 00 20 00  ....flag.txu.. .
00000250: 00 00 00 00 01 00 18 01  43 02 B5 CA D1 DE D7 01  ........C.......

И аналогично с 24-м байтом флага - 72, а не 71.

Всё, открываем архив, достаем флаг. Он дан в виде аскии-арта, но состоит из 8 символов, что очень приятно)

Heeey! Congratz! :)

Your flag:

........................................................................................................................................................................................................................................................................
........................................................................................................................................................................................................................................................................
........................................................................................................................................................................................................................................................................
..............%%WW......................................................................%%%%##..........##WW..........%%##%%%%%%........##WW%%%%WW......%%WW................##WWWWWW%%..........##WWWW%%......WW%%WW##WW##%%........WWWW##%%........WW##%%..............
..............WW%%............##%%....................................................%%WW............##WWWW........%%%%......%%##....WW##......WWWW....WWWW..............##%%......####......%%##....%%##....WW%%................WWWW....WW%%..........##WW............
..............................%%##....................................................##WW..........##%%WWWW....................WW%%..............##WW..%%%%..........................##WW..%%WW........%%##..WW%%..............##%%........WW%%........WWWW............
............######........##WW##%%####......##..##%%..##WW........%%##WW##............##%%..............####..................##WW..............##%%....####..WWWWWW................WWWW....WWWW........%%%%..##%%..##WW##..................WWWW........WW%%............
..............%%WW............WW##..........%%WW..WWWW..%%WW....##WW....WW##........%%##................WW##..............WWWWWW............%%##WW......WWWW%%....%%WW..........%%%%%%........WWWW....%%##%%..WWWW%%....%%%%..............WWWW............WW%%..........
..............##%%............%%%%..........%%%%..%%%%..%%%%..%%##........%%WW......%%%%................##WW..................##WW..............%%%%....####........%%WW............%%WW........%%####..%%WW..............%%##..........WW##..............WW%%..........
..............##%%............WW%%..........####..%%%%..####..%%%%........WW%%........%%WW..............####....................WWWW..............%%##..##WW........##WW..............####..............####..............WWWW........WW##..............##WW............
..............%%%%............%%%%..........##%%..##%%..%%%%..%%WW........%%##........WWWW..............##%%....................%%##..............%%%%..%%##........WW##..............##WW....##........##WW..####........%%%%......WW%%................WW##............
..............##%%............%%##....WWWW..##WW..##%%..####....WW##....##%%..........WWWW..............##%%........%%WW......%%%%....##WW......##%%....%%##WW....WW##....##WW......WW%%......%%%%....%%WW......WW##....%%##......%%##..................####............
..........WW####WWWW%%..........%%%%##WW....##WW..##%%..WW%%......%%%%WWWW..............WW####......%%%%##%%WWWW......##%%%%WWWW........WW%%######......%%%%..##%%WW........WW%%WW####..........WWWW##WW..........%%WW##%%......%%%%%%##WWWWWW##....%%WW##..............
........................................................................................................................................................................................................................................................................
........................................................................................................................................................................................................................................................................
........................................................................................................................................................................................................................................................................
........................................................................................................................................................................................................................................................................
........................................................................................................................................................................................................................................................................

-- Made by SPbCTF (vk.com/spbctf)


На этом с мага-уровнем всё!

В этом году приятно решать)

Cats!

Under construction

Wow! Flipable!

Hello from another side of the Moon!

Looking for a flag? Okay, take this:
LFXXKIDBOJSSA43XMVSXIIC6LYQCAIA=

About me

Under construction. You can try to contact me and fill this field… haha… ha…