
Название — Callfuscated
Категория — Reversing
Сложность — Insane
Ссылка — https://app.hackthebox.com/challenges/Callfuscated
Вступление
Наша задачка запрашивает пароль и на правильном печатает Correct. Validate the challenge using the flag: %s. Внутри труп, разложенный обфускатором: виртуальная машина со своим байткодом на 586 ячеек, MBA поверх каждой арифметической операции, opaque-предикаты, раздувающие CFG, и фирменная нарезка кода на тысячи call/pop- гаджетов (собственно как и в названии, Call-ob-fuscated).
Что за зверь
Арсенал, который навешан на функцию проверки:
VM. Пароль проверяет не нативный код, а интерпретатор поверх массива
program[]. В дизассемблере видно диспетчер и таблицу опкодов, а не алгоритм. Значит, реверсить придётся дважды: сначала саму VM, потом её байткод.MBA. Каждая примитивная операция размазана в цепочку смешанной булево-арифметической эквивалентности. На статике выглядит жутко, на трассе схлопывается в одно значение.
Opaque predicates. Ветки с заранее известным исходом, которые существуют только чтобы удвоить граф и увести дизассемблер в мёртвые пути.
Junk / call-обфускация. Каждая реальная инструкция завёрнута в
call gadget; gadget: pop r8; <insn>; call next. Отсюда десятки тысяч инструкций на ровном месте.
Общий знаменатель у всех четырёх один: они переживают статику, но не переживают исполнение. На конкретном прогоне MBA — это число, opaque — это всегда одна и та же ветка, junk — это шум с предсказуемым адресом возврата. Значит, всё решается динамикой.
Разведка
$ file crackme crackme: ELF 64-bit LSB executable, x86-64, dynamically linked, stripped $ echo AAAA | ./crackme Welcome to callfuscated crackme. To register enter your password: Incorrect flag. Try again
Строки дают якоря: %63s (scanf пишет в буфер 0x40f080), Correct...%s, Incorrect flag. Try again. Диспетчер VM в дизассемблере узнаётся мгновенно:
mov eax, [rbp-0x18] ; pc mov eax, [rbp+rax*4-0x950] ; program[pc] -> opcode test eax, eax jne <handler>
В фрейме — pc, длина программы, sp, пара регистров и стек данных. Стековая машина, классика.
План
Свой трассировщик на
ptrace— записать поток исполнения на известном вводе (ABCDEFGH…).Свой дампер — вытащить
program[](586 ячеек) прямо из памяти.Свой эмулятор x86-64 на Python, который проигрывает записанный поток управления и валидируется порегистрово против настоящей трассы. Поток управления от пароля не зависит (VM ветвится по опкодам), так что одна выверенная трасса = универсальный движок для любого ввода.
Декодировать опкоды VM.
Обратить проверку.
Инструменты и грабли
Трассировщик. PTRACE_SINGLESTEP в цикле, лог rip и нужных регистров после каждой инструкции, с отсечкой по диапазону образа:
ptrace(PTRACE_TRACEME, 0, 0, 0); execv(bin, argv); while (1) { ptrace(PTRACE_SINGLESTEP, pid, 0, 0); waitpid(pid, &status, 0); if (WIFEXITED(status)) break; ptrace(PTRACE_GETREGS, pid, 0, ®s); if (regs.rip >= 0x401000 && regs.rip < 0x40d000) fprintf(out, "%llx eax=%x edx=%x ecx=%x esi=%llx rax=%llx\n", regs.rip, (u32)regs.rax, (u32)regs.rdx, (u32)regs.rcx, regs.rsi, regs.rax); }
На ABCDEFGH — 187 064 инструкции. Отдельный дампер снимает program[] и память VM.
Эмулятор. Парсинг листинга objdump в (addr, mnemonic, operands), обработчики на ~30 реально встречающихся опкодов (mov/movzx/movsx/lea/add/sub/and/or/xor/imul/shl/shr/push/pop/call/leave/…), затем прогон трассы с порегистровой сверкой на каждом шаге. Расхождение = баг. Два были показательными.
rand()зовётся из четырёх мест, 192 раза. VM подмешиваетrand(). Наивное предположение «все вызовы возвращаются в0x40923a» — мимо: сайтов четыре (409235,40ad5b,40af37,40b10b), суммарно 192 вызова, каждый возвращается вcall+5. Старая логика «мотать до0x40923a» съедала сотни живых инструкций. Фикс: адрес возврата считать обобщённо как(адрес call)+5и захватить все 192 результата по порядку.
WORD— подстрокаDWORD. Детект размера операнда былif k in text for k in ('BYTE','WORD','DWORD','QWORD').'WORD' in 'DWORD PTR' == True, поэтому все 32/64-битные обращения к памяти резались до 16 бит и всплывало это только на значениях больше0xffff. Лечится границей слова:
def opsize(op): m = re.search(r'\b(BYTE|WORD|DWORD|QWORD) PTR', op) return {'BYTE':1,'WORD':2,'DWORD':4,'QWORD':8}[m.group(1)] if m else None
После фиксов:
trace lines 187064 rand_seq 192 REACHED final 40b537 idx 187018 eax=ffffffff rand_used 192
187 064 инструкции воспроизведены побайтово, финал eax = 0xffffffff — ровно то, что бинарник считает для неверного пароля.
Разбор VM
На готовом эмуляторе снимаю снимок стека данных на каждом диспетче и раскручиваю программу из 586 ячеек. Вся «криптография» опкодов, спрятанная за MBA, оказывается арифметикой уровня первого курса:
PUSH imm— константа на стек.H2— сложение. Именно им считается адрес:0x40cbb5 + 0x24cb = 0x40f080, т.е.&input[0]. Каждый блок символа берёт0x40f080 + i.DEREF— чтение байта ввода по адресу.H5— умножение аккумулятора на 256.H7— прибавление байта.
То есть каждая четвёрка символов сворачивается в 32-битное big-endian число:
acc = (((c0)*256 + c1)*256 + c2)*256 + c3 == uint32_be(c0 c1 c2 c3)
Дальше на группу подмешиваются две константы через G8 и G3 — обе декодируются в XOR. Групп восемь (32 символа), у каждой своя пара:
Группа | G8 | G3 |
|---|---|---|
0 | 0x0915033a | 0x41414141 |
1 | 0x427d7872 | 0x11111111 |
2 | 0x30310a00 | 0x55555555 |
3 | 0x2a052e32 | 0x5a5a5a5a |
4 | 0xcff5ecdf | 0xaaaaaaaa |
5 | 0x1914031e | 0x77777777 |
6 | 0xf6f7c6ad | 0x99999999 |
7 | 0x6c6a524e | 0x33333333 |
Финальное 32-битное значение — битовая маска: 0xffffffff на мусоре, 0 на верном пароле, каждая сошедшаяся группа гасит свои биты. Условие группы сводится к uint32_be(4 символа) == G8 ^ G3, то есть символы восстанавливаются в лоб:
groups = [(0x0915033a,0x41414141),(0x427d7872,0x11111111), (0x30310a00,0x55555555),(0x2a052e32,0x5a5a5a5a), (0xcff5ecdf,0xaaaaaaaa),(0x1914031e,0x77777777), (0xf6f7c6ad,0x99999999),(0x6c6a524e,0x33333333)] flag = b''.join((g8 ^ g3).to_bytes(4,'big') for g8,g3 in groups) print(flag.decode())
Группа 0: 0x0915033a ^ 0x41414141 = 0x4854427b → HTB{ и таким образом собираем наш флаг.
Результат

Вот как показывается в консоли:
$ echo 'HTB{******_**_***_********_*_**}' | ./crackme Welcome to callfuscated crackme. To register enter your password: Correct. Validate the challenge using the flag: HTB{******_**_***_********_*_**}
Полученный флаг буквально описывает, что сделал обфускатор - нарезал одну verify() на тысячи гаджетов вокруг VM. VM + MBA + opaque + junk впечатляют только на статике; у всей четвёрки одна общая дырка и им всё равно надо исполниться.
Задачка была очень интересной :)
