69 lines
5.2 KiB
Markdown
69 lines
5.2 KiB
Markdown
<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}`
|