# SuiGeneris Это не x86. Это не ARM. Это нечто sui generis — единственное в своём роде. ## Разбор решения В начале нам выдается `vm_runner` и `firmware.bin` Попытаемся проверить что делает и для чего задействован бинарь: ```bash file vm_runner strings -n 5 vm_runner | head -n 20 ./vm_runner ./vm_runner firmware.bin ``` Так получим, что, во-первых, это **ELF x86_64**. Также заметим, если не передать никаких аргументов, тогда бинарник напечатает: `usage: %s firmware.bin`. И, наконец, если в качестве аргумента воспользуемся файлов с прошивкой, тогда у нас попросит ввести флаг `Enter flag:` и затем получим ответ `Correct!` или `Wrong.`. То есть задача сводится к тому, чтобы понять, как именно раннер валидирует ввод. Изучим функцию загрузки прошивки. Для этого обратимся к строкам `bad firmware header` и `failed to load firmware`. Там обнаружим следующие факты: - сначала читаются 24 байта заголовка; - проверяются `magic` и `version`; - потом читаются `code_len` и `data_len` 32-битных слов. Общий формат заголовка выглядит следующим образом: | Поле | Тип | Назначение | | --- | --- | --- | | `magic` | `uint32_t` | Сигнатура прошивки | | `version` | `uint32_t` | Версия формата | | `code_len` | `uint32_t` | Длина code-секции (в словах) | | `data_len` | `uint32_t` | Длина data-секции (в словах) | | `flag_len` | `uint32_t` | Ожидаемая длина флага | | `target` | `uint32_t` | Целевой параметр проверки | Выполним быструю проверку: ```python import struct from pathlib import Path b = Path("firmware.bin").read_bytes() magic, version, code_len, data_len, flag_len, target = struct.unpack("<6I", b[:24]) print(hex(magic), version, code_len, data_len, flag_len, hex(target)) print("file_size", len(b)) print("expected", 24 + 4 * code_len + 4 * data_len) ``` В цикле исполнения декод инструкции такой: | Поле | Формула извлечения | Биты | | --- | --- | --- | | `dst` | `insn & 0xFFF` | `0..11` | | `src` | `(insn >> 12) & 0xFFF` | `12..23` | | `guard` | `(insn >> 24) & 0xFF` | `24..31` | То есть инструкция кодируется как: ```text MOVE: dst | (src << 12) | (guard << 24) ``` Логику `guard` удобнее воспринимать как развилку: ```mermaid flowchart TD B{guard == 0xFF?} B -- Да --> C[Выполнить команду] B -- Нет --> D{R_guard even} D -- Да --> E[Пропустить команду] D -- Нет --> C ``` > `guard = 0xFF` всегда запускает команду; иначе команда выполняется только при нечётном `R[guard]`. Сформируем карту портов. Восстановим её из `read_src`/`write_dst`: | Блок | Порты | Назначение | | --- | --- | --- | | Registers | `0x00..0x0F` | `R0..R15` | | ALU | `0x10`, `0x11`, `0x12` | `A`, `B/trigger`, `OUT` | | MUL | `0x20`, `0x21`, `0x22` | `A`, `B/trigger`, `OUT` | | ROT | `0x30`, `0x31`, `0x32` | `A`, `S/trigger`, `OUT` | | PRNG | `0x40`, `0x41`, `0x42` | `seed`, `step/trigger`, `OUT` | | HASH | `0x60`, `0x61`, `0x62` | `A`, `B/trigger`, `OUT` | | INPUT | `0x51` | Чтение байтов введенного флага | | CONST | `0x90` | Чтение 32-битных слов из `data[]` | | OUTPUT | `0x52` | Буфер выхода VM | Теперь попытаемся определить, что делают блоки `ALU/MUL/ROT/HASH/PRNG`. Семантика из `write_dst`: ```c alu(a,b) = a + b mul(a,b) = a + b rot(a,s) = rol(a, s) hash(a,b) = a + b + 0x9E3779B9 prng(x,t) = xorshift32(x ^ t) ``` чтение `*_OUT` просто возвращает последнее значение. Никакой блокировки чтения по `ready` тут нет После выполнения VM в `main` идет финальная проверка: - считается `output_base = 3 + 3 * flag_len`; - проверяется, что длина `data[]` достаточна; - сравниваются `vm.output_buf` и хвост `data[]`; - дополнительно сравнивается `R15` с отдельным словом `target2`. Из этого получается такой `layout`: | Смещение в `data[]` | Содержимое | | --- | --- | | `0` | `seed` для `R14` | | `1` | `seed` для `R15` | | `2` | `seed` для `PRNG` | | `3..(3 + 3*flag_len - 1)` | Константы `c_mul`, `c_rot`, `c_tw` | | далее `flag_len` слов | `expected_output` | | последнее слово | `target2` (целевое значение `R15`) | Если декодировать блок кода VM, он повторяется для каждого байта ввода. Псевдокод получается такой: ```text R14 = seed_r14 R15 = seed_r15 PRNG = seed_prng MUL_OUT = 0 for i in 0..n-1: byte = input[i] stale_mul = MUL_OUT stale_prng = PRNG MUL_OUT = byte + c_mul[i] rot = rol(R14, c_rot[i]) alu = rot + stale_prng R14 = hash(stale_mul, alu) PRNG = prng(PRNG, c_tw[i]) R15 = hash(R15, byte) OUTPUT[i] = R15 ``` В прошивке лежит полный массив `OUTPUT`, а это просто последовательные значения `R15`. Обновление регистра: ```text R15_next = R15_prev + byte + 0x9E3779B9 ``` Отсюда напрямую: ```text byte = R15_next - R15_prev - 0x9E3779B9 ``` Проходим по всем `OUTPUT[i]`, для каждого считаем `byte`, проверяем что это диапазон `0..255`, и получаем весь флаг. ### Автоматический solver В репозитории также прикреплен файл с авторешением: `solver.py`. Запуск можно осуществить при помощи команды: ```bash python3 solve/solver.py firmware.bin ```