Init. commit
This commit is contained in:
163
SuiGeneris-Reverse/README.md
Normal file
163
SuiGeneris-Reverse/README.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# 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
|
||||
```
|
||||
158
SuiGeneris-Reverse/solver.py
Normal file
158
SuiGeneris-Reverse/solver.py
Normal file
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MAGIC_TTAB = 0x42415454 # 'TTAB'
|
||||
HDR_FMT = "<6I"
|
||||
|
||||
|
||||
def rol32(x: int, r: int) -> int:
|
||||
r &= 31
|
||||
return ((x << r) | (x >> (32 - r))) & 0xFFFFFFFF
|
||||
|
||||
|
||||
def prng_step(state: int, tweak: int) -> int:
|
||||
x = (state ^ tweak) & 0xFFFFFFFF
|
||||
x ^= (x << 13) & 0xFFFFFFFF
|
||||
x ^= (x >> 17) & 0xFFFFFFFF
|
||||
x ^= (x << 5) & 0xFFFFFFFF
|
||||
return x & 0xFFFFFFFF
|
||||
|
||||
|
||||
def alu_op_int(a: int, b: int) -> int:
|
||||
return (a + b) & 0xFFFFFFFF
|
||||
|
||||
|
||||
def hash_op_int(a: int, b: int) -> int:
|
||||
return (a + b + 0x9E3779B9) & 0xFFFFFFFF
|
||||
|
||||
|
||||
def load_firmware(path: Path):
|
||||
blob = path.read_bytes()
|
||||
if len(blob) < struct.calcsize(HDR_FMT):
|
||||
raise ValueError("firmware too small")
|
||||
magic, version, code_len, data_len, flag_len, target = struct.unpack_from(HDR_FMT, blob, 0)
|
||||
if magic != MAGIC_TTAB or version != 1:
|
||||
raise ValueError("bad firmware header")
|
||||
|
||||
off = struct.calcsize(HDR_FMT)
|
||||
code_sz = code_len * 4
|
||||
data_sz = data_len * 4
|
||||
if len(blob) < off + code_sz + data_sz:
|
||||
raise ValueError("truncated firmware")
|
||||
|
||||
code = list(struct.unpack_from(f"<{code_len}I", blob, off))
|
||||
off += code_sz
|
||||
data = list(struct.unpack_from(f"<{data_len}I", blob, off))
|
||||
return {
|
||||
"code": code,
|
||||
"data": data,
|
||||
"flag_len": flag_len,
|
||||
"target": target,
|
||||
}
|
||||
|
||||
|
||||
def solve(
|
||||
fw,
|
||||
prefix: str | None,
|
||||
suffix: str | None,
|
||||
alphabet: str | None,
|
||||
no_format: bool,
|
||||
) -> bytes:
|
||||
data = fw["data"]
|
||||
flag_len = fw["flag_len"]
|
||||
target = fw["target"]
|
||||
if flag_len < 2 or len(data) < 4:
|
||||
raise ValueError("bad firmware constants")
|
||||
|
||||
seed_r14 = data[0]
|
||||
seed_r15 = data[1]
|
||||
seed_prng = data[2]
|
||||
|
||||
output_base = 3 + 3 * flag_len
|
||||
if len(data) < output_base + flag_len + 1:
|
||||
raise ValueError("bad firmware layout")
|
||||
|
||||
consts = data[3:output_base]
|
||||
c_mul = consts[0::3]
|
||||
c_rot = consts[1::3]
|
||||
c_tw = consts[2::3]
|
||||
|
||||
outputs = data[output_base:output_base + flag_len]
|
||||
target2 = data[output_base + flag_len]
|
||||
|
||||
recovered = []
|
||||
r15 = seed_r15
|
||||
for out in outputs:
|
||||
b = (out - r15 - 0x9E3779B9) & 0xFFFFFFFF
|
||||
if b > 0xFF:
|
||||
raise RuntimeError("invalid output stream: byte out of range")
|
||||
recovered.append(b)
|
||||
r15 = out
|
||||
|
||||
flag = bytes(recovered)
|
||||
|
||||
# format constraints
|
||||
if not no_format:
|
||||
if prefix is None or suffix is None or alphabet is None:
|
||||
raise ValueError("format constraints require prefix/suffix/alphabet")
|
||||
if not flag.startswith(prefix.encode("ascii")):
|
||||
raise RuntimeError("prefix mismatch")
|
||||
if not flag.endswith(suffix.encode("ascii")):
|
||||
raise RuntimeError("suffix mismatch")
|
||||
mid = flag[len(prefix): len(flag) - len(suffix)]
|
||||
alpha = set(alphabet.encode("ascii"))
|
||||
if any(ch not in alpha for ch in mid):
|
||||
raise RuntimeError("alphabet mismatch")
|
||||
|
||||
# verify accumulators match firmware targets
|
||||
prng = seed_prng
|
||||
r14 = seed_r14
|
||||
r15 = seed_r15
|
||||
mul_out = 0
|
||||
for i, ch in enumerate(flag):
|
||||
stale_mul = mul_out
|
||||
stale_prng = prng
|
||||
mul_out = (ch + c_mul[i]) & 0xFFFFFFFF
|
||||
rot = rol32(r14, c_rot[i])
|
||||
alu = alu_op_int(rot, stale_prng)
|
||||
r14 = hash_op_int(stale_mul, alu)
|
||||
prng = prng_step(prng, c_tw[i])
|
||||
r15 = hash_op_int(r15, ch)
|
||||
|
||||
if r14 != target or r15 != target2:
|
||||
raise RuntimeError("verification failed against firmware targets")
|
||||
|
||||
return flag
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Recover flag from firmware.bin.")
|
||||
ap.add_argument("firmware", nargs="?", default="public/firmware.bin")
|
||||
ap.add_argument("--prefix", default="caplag{", help="known flag prefix")
|
||||
ap.add_argument("--suffix", default="}", help="known flag suffix (1 char)")
|
||||
ap.add_argument("--alphabet", default="0123456789ABCDEF", help="alphabet for middle bytes")
|
||||
ap.add_argument("--no-format", action="store_true", help="disable prefix/suffix/alphabet constraints")
|
||||
args = ap.parse_args()
|
||||
|
||||
fw = load_firmware(Path(args.firmware))
|
||||
flag = solve(
|
||||
fw,
|
||||
prefix=None if args.no_format else args.prefix,
|
||||
suffix=None if args.no_format else args.suffix,
|
||||
alphabet=None if args.no_format else args.alphabet,
|
||||
no_format=args.no_format,
|
||||
)
|
||||
try:
|
||||
print(flag.decode("ascii"))
|
||||
except UnicodeDecodeError:
|
||||
print(flag)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user