5.2 KiB
Ancient Processor
Мы получаем 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}