Files
Tajna-tretej-stolicy/reverse-ancient-processor/WRITEUP.md
2026-04-22 10:58:32 +03:00

69 lines
5.2 KiB
Markdown
Raw 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.
<h1 align="center">Ancient Processor</h1>
<p align="center">
<img src="https://img.shields.io/badge/category-Reverse-blueviolet" alt="Reverse"/>
<img src="https://img.shields.io/badge/points-912-orange" alt="912 pts"/>
</p>
Мы получаем stripped ELF-бинарник `checker`. По названию таска и описанию можно уловить намек, что внутри сидит эмулятор какого-то «древнего процессора» — значит, вероятно, что сработает reverse эмулятора и восстановление его байткода.
## Решение
Открываем `checker` в IDA или Ghidra и сразу ищем главный цикл интерпретатора — он выдаёт себя большим `switch`'ем по опкодам. По коду VM восстанавливается набор инструкций:
| Категория | Опкоды |
|---|---|
| Стек | `PUSH`, `POP`, `DUP`, `SWAP`, `ROT` |
| Арифметика | `ADD`, `SUB`, `XOR`, `MUL`, `AND`, `OR`, `NOT`, `SHL`, `SHR`, `MOD` |
| Память / переходы | `LOAD`, `STORE`, `JMP`, `JZ`, `CALL`, `RET` |
| I/O | `READ`, `PRINT`, `HALT` |
| Антианализ | `SYSCALL`, `CHECKTIME`, `SELFMOD2` |
Самое интересное лежит в `vm.c`: перед выполнением программы в массив `code` сначала копируется `decoy_program`, а затем поверх него пишется уже расшифрованный `real_program_encrypted`. То есть первая программа — декорация, её можно даже не разбирать. А реальная цель — именно зашифрованная. Рагадка тут, на самом деле, элементарная: каждый байт XOR'ится с 8-байтным ключом. Можно либо выгрузить уже готовый массив из памяти в рантайме, либо вытащить ключ и шифротекст из бинарника и раскрутить всё статикой.
После расшифровки байткод раскладывается в повторяющийся шаблон проверки очередного символа:
```text
READ
PUSH xor_key
XOR
PUSH add_key
ADD
PUSH sub_key
SUB
[иногда MUL]
PUSH expected
CMP
JZ fail
```
Каждый символ флага проверяется независимо, и в обычном случае формула получается такой:
$$\bigl((ch \oplus k_{\mathrm{xor}}) + k_{\mathrm{add}} - k_{\mathrm{sub}}\bigr) \bmod 256 = \mathrm{expected}$$
Отсюда символ восстанавливается в одну строчку:
$$ch = \bigl((\mathrm{expected} + k_{\mathrm{sub}} - k_{\mathrm{add}}) \bmod 256\bigr) \oplus k_{\mathrm{xor}}$$
Но, вохможно и к сожалению, это еще не конец. Если на этом месте пройтись по всем символам и подставить константы — часть результатов не сойдётся. На этом этапе можно логично предположить, что причиной этому скорее всего служит самоизменяющийся код. Инструкция `SELFMOD2` меняет байты прямо внутри выполняющейся программы, и несколько уже прочитанных символов флага используются для модификации XOR-ключей в будущих проверках:
| Символ-источник | Модифицирует ключ символа |
|:---:|:---:|
| 5 | 12 |
| 10 | 18 |
| 15 | 25 |
Для этих позиций статический `xor_key` из байткода брать нельзя — надо считать его динамически с учётом уже восстановленной части флага. Здесь заложена еще одна мелкая подлянка: каждый четвёртый символ перед сравнением умножается на константу (`MUL`), и чтобы его обратить, нужен [modular inverse](https://en.wikipedia.org/wiki/Modular_multiplicative_inverse) по модулю 256.
> Обратное $a^{-1} \bmod 256$ существует только для нечётных $a$ (чтобы $\gcd(a, 256) = 1$) — множители `MUL` в байткоде специально подобраны нечётными, иначе символ флага был бы однозначно невосстановим.
Остальные «защиты» можно игнорировать — это шум:
| Инструкция | Что делает | Эффект |
|---|---|---|
| `SYSCALL` | Читает байт из `/dev/urandom`, кладёт `rnd ^ rnd` в стек | Стабильный `0` |
| `CHECKTIME` | XOR'ит значение с `0xFF` при медленном выполнении | Не триггерится в памяти |
## Флаг
`caplag{v1rtu4l_m4ch1n3_m4st3r}`