Init. commit

This commit is contained in:
Caplag
2026-04-22 10:42:16 +03:00
commit 98e51ca58b
35 changed files with 2371 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
<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}`