Files
2026-03-02 21:44:22 +03:00

163 lines
6.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```