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

5.2 KiB
Raw Blame History

Ancient Processor

Reverse 912 pts

Мы получаем 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-байтным ключом. Можно либо выгрузить уже готовый массив из памяти в рантайме, либо вытащить ключ и шифротекст из бинарника и раскрутить всё статикой.

После расшифровки байткод раскладывается в повторяющийся шаблон проверки очередного символа:

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 по модулю 256.

Обратное a^{-1} \bmod 256 существует только для нечётных a (чтобы \gcd(a, 256) = 1) — множители MUL в байткоде специально подобраны нечётными, иначе символ флага был бы однозначно невосстановим.

Остальные «защиты» можно игнорировать — это шум:

Инструкция Что делает Эффект
SYSCALL Читает байт из /dev/urandom, кладёт rnd ^ rnd в стек Стабильный 0
CHECKTIME XOR'ит значение с 0xFF при медленном выполнении Не триггерится в памяти

Флаг

caplag{v1rtu4l_m4ch1n3_m4st3r}