Хакер - Сны Фемиды. Ломаем виртуальную машину Themida
hacker_frei
МВК
Themida считается одним из самых сложных инструментов защиты программ от нелицензионного копирования — не только из‑за обфускации и навороченных механизмов антиотладки, но и из‑за широкого использования виртуализации. В предыдущей статье мы узнали, как сбросить триал в защищенной Themida программе. Теперь настало время поковыряться в ее виртуальной машине.
WARNING
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
В прошлых статьях я уже упоминал об одной очень зловредной особенности серьезных протекторов: чтобы осложнить жизнь хакерам, разработчики обфусцируют критические участки скомпилированного кода в макрокоманды виртуальной машины, щедро разбавленные ловушками и безумным кодом. При таком раскладе объем кода может увеличиваться в тысячи раз, делая реверс чудовищно сложным. Вдобавок его можно делать мутирующим, реализуя одну и ту же команду сотнями разных способов. Мы уже сталкивались с подобным в статье про Enigma, теперь попробуем разобрать более сложный способ виртуализации на примере снятия триала с одного из графических плагинов.
Итак, условия задачи: у нас есть 64-битная библиотека, привязанная к определенному ознакомительному периоду. Халявный сброс триала, методы которого я описывал в предыдущих статьях, невозможен: при загрузке библиотека стучится на сервер, который проверяет текущую дату и дает добро на запуск. При отсутствии соединения программа просто не работает. Код упакован и зашифрован, исследованию в дизассемблере не подлежит, но детекторы не видят на нем никакого известного протектора. В общем, ситуация начинает слегка пугать.
Но мы не из пугливых! Загружаем программу в наш любимый отладчик x64dbg и при помощи описанного мною в предыдущих статьях замечательного плагина ScyllaHide подбираем антиантиотладочную конфигурацию (Themida). Теперь включаем уже опробованный нами в боях плагин Themidie и наконец получаем возможность поковыряться в расшифрованном и распакованном коде (включая его трассировку). Чтобы расшифрованный код было удобнее изучать, я сдампил его еще одним описанным ранее плагином — Scylla. Если теперь мы загрузим сдампленный код в дизассемблер IDA, то увидим примерно такую картину.

Бегло взглянув на карту нашего модуля, мы увидим коротенькую синюю полоску слева. Она подсказывает, что в начале модуля сосредоточен нормальный вменяемый код функций, который по каким‑то причинам (обычно скорость исполнения) не подвергся виртуализации. Пестрая полоска с преобладанием коричневого цвета справа — шитый код виртуальной машины Themida, перемежающийся обработчиками команд.
Побродив немного отладчиком по дебрям кода и слегка расстроившись, переходим к более прогрессивному методу работы. Попробуем отследить и проанализировать трассу между вызываемыми невиртуализированными функциями. В качестве отправного пункта берем функцию HTTP-запроса лицензии на сервер, благо она по понятным причинам не обфусцирована. Ставим на нее точку останова и при ее достижении жмем Ctrl-F9, выполняя функцию до возврата в обработчик шитого кода, из которого она вызывается.
На входе обнаруживаем такой огромный кусок бессмысленных команд, что его бесполезно пытаться пройти вручную. Возложим эту тяжкую задачу на отладчик. Для этого откроем вкладку «Трассировка» (крайняя справа) и правой кнопкой мыши выберем «Начать выполнение трассировки». Затем для быстроты запускаем трассировку через Ctrl-Alt-F8 (трассировка с обходом). Памятуя о том, что невиртуализированный код находится в секции с меньшими адресами, в качестве условия остановки ставим RIP до начала секции, содержащей виртуализированный код.

В окне трассировки шустро побежали исполняемые команды, а вот и остановка на следующем вызове какой‑то функции. Ого, нам потребовалась пара десятков тысяч шагов, чтобы преодолеть промежуток между ними! Это нас не пугает: попробуем проанализировать полученную трассу.
Для начала нас интересует, каким образом реализована псевдокоманда вызова невиртуализированной функции. В самом конце трассы находим такой участок кода, соответствующего обработчику вызова функции и коду перехода на него от обработчика предыдущей псевдокоманды. Попробуем по нему понять принцип действия виртуальной машины:
33DC | 00007FFD071D113F | 48:89EF | mov rdi,rbp <--- RBP указывает на текущий фрейм кода
33DD | 00007FFD071D1142 | 48:81C7 36010000 | add rdi,136
33DE | 00007FFD071D1149 | 0907 | or dword ptr ds:[rdi],eax
33DF | 00007FFD071D114B | 48:25 FFFF0000 | and rax,FFFF <--- RAX 16-битный параметр текущей команды, представляющий собой
33E0 | 00007FFD071D1151 | 48:C1E0 03 | shl rax,3 <--- индекс в таблице 64-битных адресов обработчиков команд шитого кода
33E1 | 00007FFD071D1155 | 48:01C2 | add rdx,rax <--- базовый адрес, на который в RDX
33E2 | 00007FFD071D1158 | 4C:8B02 | mov r8,qword ptr ds:[rdx] <--- Элемент по этому индексу указывает на обработчик следующей команды, то есть 7FFD07011D20
33E3 | 00007FFD071D115B | 48:89EB | mov rbx,rbp
33E4 | 00007FFD071D115E | 48:81C3 EB000000 | add rbx,EB
33E5 | 00007FFD071D1165 | 48:8103 22000000 | add qword ptr ds:[rbx],22 <--- [RBP+EB] указатель на текущую команду шитого кода, ее размер был 22h байта, сдвинуть его на следующую команду
33E6 | 00007FFD071D116C | 41:FFE0 | jmp r8 <--- после чего перейти на ее обработчик
33E7 | 00007FFD07011D20 | 48:C7C2 00000000 | mov rdx,0
33E8 | 00007FFD07011D27 | 49:89E8 | mov r8,rbp
33E9 | 00007FFD07011D2A | 49:81C0 EB000000 | add r8,EB
33EA | 00007FFD07011D31 | 4D:8B00 | mov r8,qword ptr ds:[r8] <--- R8=[RBP+EB] указатель на новую команду шитого кода
33EB | 00007FFD07011D34 | 49:81C0 06000000 | add r8,6
33EC | 00007FFD07011D3B | 41:8B10 | mov edx,dword ptr ds:[r8] <--- В RDX 32-битный параметр псевдокоманды по смещению 6 байт
33ED | 00007FFD07011D3E | 48:89E9 | mov rcx,rbp
33EE | 00007FFD07011D41 | 48:81C1 01000000 | add rcx,1 <--- [RBP+1] <--- Базовый адрес модуля
33EF | 00007FFD07011D48 | 48:0311 | add rdx,qword ptr ds:[rcx] <--- Их сумма дает абсолютный адрес вызываемой функции в RDX
33F0 | 00007FFD07011D4B | 49:C7C6 00000000 | mov r14,0
33F1 | 00007FFD07011D52 | 49:89E8 | mov r8,rbp
33F2 | 00007FFD07011D55 | 49:81C0 EB000000 | add r8,EB
33F3 | 00007FFD07011D5C | 4D:8B00 | mov r8,qword ptr ds:[r8] <--- R8=[RBP+EB] указатель на текущую команду шитого кода
33F4 | 00007FFD07011D5F | 49:81C0 04000000 | add r8,4
33F5 | 00007FFD07011D66 | 6645:8B30 | mov r14w,word ptr ds:[r8] <--- В R14 16-битный параметр псевдокоманды по смещению 4 байта
33F6 | 00007FFD07011D6A | 49:01E6 | add r14,rsp <--- Это размер зарезервированного места в стеке под сохраненные регистры
33F7 | 00007FFD07011D6D | 49:8916 | mov qword ptr ds:[r14],rdx <--- Адрес вызываемой функции на стек для перехода
33F8 | 00007FFD07011D70 | 49:81C6 08000000 | add r14,8 <--- Место в стеке для адреса возврата из нее
33F9 | 00007FFD07011D77 | 48:C7C2 00000000 | mov rdx,0
33FA | 00007FFD07011D7E | 49:89E8 | mov r8,rbp
33FB | 00007FFD07011D81 | 49:81C0 EB000000 | add r8,EB
33FC | 00007FFD07011D88 | 4D:8B00 | mov r8,qword ptr ds:[r8] <--- R8=[RBP+EB] указатель на текущую команду шитого кода
33FD | 00007FFD07011D8B | 49:81C0 00000000 | add r8,0
33FE | 00007FFD07011D92 | 41:8B10 | mov edx,dword ptr ds:[r8] <--- В RDX 32-битный параметр псевдокоманды по смещению 0 байт
33FF | 00007FFD07011D95 | 48:89E8 | mov rax,rbp |
3400 | 00007FFD07011D98 | 48:05 01000000 | add rax,1 |
3401 | 00007FFD07011D9E | 48:0310 | add rdx,qword ptr ds:[rax] <--- [RBP+1] — базовый адрес модуля, их сумма дает
3402 | 00007FFD07011DA1 | 49:8916 | mov qword ptr ds:[r14],rdx <--- адрес возврата из вызываемой функции на стек
3403 | 00007FFD07011DA4 | 48:89EB | mov rbx,rbp
3404 | 00007FFD07011DA7 | 48:81C3 7F000000 | add rbx,7F
3405 | 00007FFD07011DAE | C703 00000000 | mov dword ptr ds:[rbx],0 <--- По всей видимости, [RBP+7F] флаг реентерабельности?
3406 | 00007FFD07011DB4 | 41:58 | pop r8
3407 | 00007FFD07011DB6 | 41:59 | pop r9
3408 | 00007FFD07011DB8 | 41:5A | pop r10
3409 | 00007FFD07011DBA | 41:5B | pop r11
340A | 00007FFD07011DBC | 41:5C | pop r12
340B | 00007FFD07011DBE | 41:5D | pop r13
340C | 00007FFD07011DC0 | 41:5E | pop r14
340D | 00007FFD07011DC2 | 41:5F | pop r15
340E | 00007FFD07011DC4 | 5F | pop rdi
340F | 00007FFD07011DC5 | 5E | pop rsi
3410 | 00007FFD07011DC6 | 5D | pop rbp
3411 | 00007FFD07011DC7 | 5B | pop rbx
3412 | 00007FFD07011DC8 | 5A | pop rdx
3413 | 00007FFD07011DC9 | 59 | pop rcx
3414 | 00007FFD07011DCA | 58 | pop rax
3415 | 00007FFD07011DCB | 9D | popfq
3416 | 00007FFD07011DCC | C2 0000 | ret 0 <--- Перейти к вызываемой функции
Итак, мы примерно разобрали одну (из нескольких тысяч) команду шитого кода Themida — вызов невиртуализированной функции. Выглядит она примерно так.

Что же делать дальше? По‑хорошему надо пройтись по таблице адресов обработчиков виртуальных команд, благо она у нас есть, разобраться, что делает каждая из них, и «распрямить» наш безумный код. Однако это путь не из приятных: команд ну очень много, причем они мутируют, то есть одна и та же команда может присутствовать в разных вариациях.
Возможно, после всего описанного тебя уже не сильно напугает то, что в одном исполняемом файле может использоваться даже несколько разных мутировавших виртуальных машин (разные системы команд с переставленными параметрами, длинами и так далее). Поэтому для начала попробуем найти более короткий путь. Поскольку трасса даже между двумя соседними вызовами функций получается чудовищно длинной, попытаемся залогировать хотя бы сами эти вызовы. Обрати внимание, что регистры для совместимости вроде как сохраняются в одном порядке, поэтому поищем код их сохранения (41 58 41 59 41 5A...). Нашлось примерно две сотни мутированных обработчиков вызова невиртуализированных функций. Но расставлять руками точки останова на них очень муторно, поэтому пишем следующий скрипт:
findallmem 0, "41584159415A415B415C415D415E415F5F5E5D5B5A59589DC20000"
mov i,0
loop:
cmp i, $result
je continue
bp ref.addr(i)+18
bpcnd ref.addr(i)+18, 0
bpl ref.addr(i)+18, "called from: "{rip}" called: "{[rsp]}" return to: "{[rsp+8]}" rsp: "{rsp}" PC: "{[mem.base(rip)+128EDDC+EB]}"
inc i
jmp loop
continue:
То есть мы ищем все места в коде, где есть такая последовательность восстановления регистров, и ставим на нее условную точку останова, логирующую текущее состояние виртуальной машины. Волшебное число 128EDDC получено эмпирически как смещение текущего фрейма относительно базы модуля, а волшебное число EB — как смещение до программного счетчика виртуальной машины из приведенного выше фрагмента кода. После отработки скрипта получаем две сотни условных точек останова, которые пишут в лог трассу вызовов примерно в таком виде:
called from: 7FFD069D8E12 called: 7FFD158B6D30 return to: 7FFD06B8067F rsp: 6B694FA710 PC: 7FFD06AE481F
called from: 7FFD069D8E12 called: 7FFD158B6DB0 return to: 7FFD06B807E5 rsp: 6B694FA710 PC: 7FFD06AE4A3C
called from: 7FFD06A8ACB5 called: 7FFD0507CB40 return to: 7FFD06B8094D rsp: 6B694FA710 PC: 7FFD06AE4BC3
called from: 7FFD069D8E12 called: 7FFD158B6EC0 return to: 7FFD06B80A70 rsp: 6B694FA710 PC: 7FFD06AE4DA6
called from: 7FFD0698CFB6 called: 7FFD158B6D30 return to: 7FFD06B80BC8 rsp: 6B694FA710 PC: 7FFD06AE506B
called from: 7FFD069E6E0F called: 7FFD158B6DB0 return to: 7FFD06B80D0C rsp: 6B694FA710 PC: 7FFD06AE5294
called from: 7FFD06A146C8 called: 7FFD0507CB60 return to: 7FFD06B80EB9 rsp: 6B694FA710 PC: 7FFD06AE542D
called from: 7FFD069D8E12 called: 7FFD158B6EC0 return to: 7FFD06B81002 rsp: 6B694FA710 PC: 7FFD06AE5616
called from: 7FFD0698CFB6 called: 7FFD158B6D30 return to: 7FFD06B8114F rsp: 6B694FA710 PC: 7FFD06AE58EB
called from: 7FFD0698CFB6 called: 7FFD158B6DB0 return to: 7FFD06B812C2 rsp: 6B694FA710 PC: 7FFD06AE5B0B
called from: 7FFD06AA94FE called: 7FFD0507CC60 return to: 7FFD06B8143D rsp: 6B694FA710 PC: 7FFD06AE5C9A
called from: 7FFD069E6E0F called: 7FFD158B6EC0 return to: 7FFD06B815EB rsp: 6B694FA710 PC: 7FFD06AE5E87
called from: 7FFD0698CFB6 called: 7FFD158B6D30 return to: 7FFD06B81741 rsp: 6B694FA710 PC: 7FFD06AE60CE
called from: 7FFD069E6E0F called: 7FFD158B6DB0 return to: 7FFD06B81875 rsp: 6B694FA710 PC: 7FFD06AE62DC
called from: 7FFD06A146C8 called: 7FFD0507CE60 return to: 7FFD06B819C5 rsp: 6B694FA710 PC: 7FFD06AE647E
called from: 7FFD069D8E12 called: 7FFD158B6EC0 return to: 7FFD06B81B3A rsp: 6B694FA710 PC: 7FFD06AE6679
called from: 7FFD06AA94FE called: 7FFD0506B620 return to: 7FFD06B81C91 rsp: 6B694FA710 PC: 7FFD06AE6874
called from: 7FFD069E6E0F called: 7FFD158B6D30 return to: 7FFD06B81DB7 rsp: 6B694FA710 PC: 7FFD06AE6A49
called from: 7FFD069E6E0F called: 7FFD158B6DB0 return to: 7FFD06B81F01 rsp: 6B694FA710 PC: 7FFD06AE6C6E
called from: 7FFD069E6E0F called: 7FFD158BE0D0 return to: 7FFD06B82060 rsp: 6B694FA710 PC: 7FFD06AE6E97
called from: 7FFD069E6E0F called: 7FFD158D0E50 return to: 7FFD06B821AE rsp: 6B694FA710 PC: 7FFD06AE7098
called from: 7FFD069D8E12 called: 7FFD158BDDB0 return to: 7FFD06B822C9 rsp: 6B694FA710 PC: 7FFD06AE725F
called from: 7FFD069D8E12 called: 7FFD158B6EC0 return to: 7FFD06B82405 rsp: 6B694FA710 PC: 7FFD06AE7446
called from: 7FFD069E6E0F called: 7FFD158B6D30 return to: 7FFD06B82569 rsp: 6B694FA710 PC: 7FFD06AE7633
called from: 7FFD0698CFB6 called: 7FFD158B6DB0 return to: 7FFD06B826D3 rsp: 6B694FA710 PC: 7FFD06AE7845
Указатель стека мы проверяем для контроля вызовов виртуализированных функций, а в последней колонке содержится указатель на текущую исполняемую команду шитого кода. Два таких лога, запущенные со включенным и отключенным интернетом, дают нам возможность найти развилку в шитом коде с точностью до вызова невиртуализированной функции.
Для более точного нахождения развилки нам придется искать команду условного оператора и ее обработчик. С этой целью снова повторяем последовательность действий, описанную в начале этой статьи — останавливаемся на последнем вызове невиртуализированной функции перед развилкой и запускаем более детальную трассировку при включенном и отключенном интернете. Придется повозиться, сравнивая ну очень длинные трассы, но в итоге мы находим обработчик условного оператора. Реализация его чрезвычайно сложна, настолько, что тут нет места, чтобы привести ее полностью. Да и смысла никакого в этом нет: код мутирует и почти ни один шаблон не действует.
Более того, злобные кодеры из Oreans еще сильнее усложнили нам жизнь, добавив в команду дополнительный 8-битный параметр. От его значения зависит флаг, на который реагирует условный переход. И разумеется, при мутации команд эти параметры меняются. Само место развилки выглядит примерно так:
2564 | 00007FFD6A21E0CE | 49:89E8 | mov r8,rbp <--- RBP указывает на текущий фрейм кода
2565 | 00007FFD6A21E0D1 | 49:81F5 01000000 | xor r13,1
2566 | 00007FFD6A21E0D8 | 49:01D5 | add r13,rdx
2567 | 00007FFD6A21E0DB | 4C:31CA | xor rdx,r9
2568 | 00007FFD6A21E0DE | 49:81C0 EB000000 | add r8,EB
2569 | 00007FFD6A21E0E5 | 4C:01EA | add rdx,r13
256A | 00007FFD6A21E0E8 | 48:09D2 | or rdx,rdx
256B | 00007FFD6A21E0EB | 4D:8B00 | mov r8,qword ptr ds:[r8] <--- R8=[RBP+EB] указатель на новую команду шитого кода
256C | 00007FFD6A21E0EE | 49:81C0 0A000000 | add r8,A
256D | 00007FFD6A21E0F5 | 48:81C9 80000000 | or rcx,80
256E | 00007FFD6A21E0FC | 45:8A08 | mov r9b,byte ptr ds:[r8] <--- В R9 битный идентификатор флага по смещению A байт
256F | 00007FFD6A21E0FF | 41:80F9 E4 | cmp r9b,E4 <--- В этой реализации команды идентификатор E4 соответствует Zero flag
2570 | 00007FFD6A21E103 | 0F84 19000000 | je 7FFD6A21E122
2571 | 00007FFD6A21E122 | 48:81E2 90000000 | and rdx,90
2572 | 00007FFD6A21E129 | 49:81E6 04000000 | and r14,4
2573 | 00007FFD6A21E130 | 4D:09CD | or r13,r9
2574 | 00007FFD6A21E133 | 41:54 | push r12
2575 | 00007FFD6A21E135 | 41:81E4 40000000 | and r12d,40 <--- Регистр флагов уже загружен в r12d, маска 40h — установлен бит Zero
2576 | 00007FFD6A21E13C | 0F84 18000000 | je 7FFD6A21E15A <-------------------Развилка
2577 | 00007FFD6A21E15A | 49:C7C5 00000000 | mov r13,0
2578 | 00007FFD6A21E161 | 48:81C1 78000000 | add rcx,78
2579 | 00007FFD6A21E168 | 48:81E9 88000000 | sub rcx,88
257A | 00007FFD6A21E16F | 41:5C | pop r12
257B | 00007FFD6A21E171 | 49:C7C5 00000000 | mov r13,0
257C | 00007FFD6A21E178 | 49:C7C5 00040000 | mov r13,400
257D | 00007FFD6A21E17F | 41:80F9 60 | cmp r9b,60
Код данного обработчика мы поменять не можем (он используется в бесчисленном количестве других мест), однако мы можем модифицировать команду шитого кода, ведь она содержит в своих параметрах адреса и обеих веток виртуального кода, и их обработчиков. Разумеется, придется вносить исправления в уже распакованный и расшифрованный код (пример халявного решения данной проблемы я приводил в предыдущей статье), а еще — озадачиться борьбой с проверкой контроля целостности.
Итак, мы достигли поставленной цели: сравнили два лога и нашли развилку в коде между ветками с правильным ответом сервера и без него. Пропатчив код, мы можем излечить наш подопытный графический плагин от излишней алчности.
Мы рассмотрели лишь общие принципы реализации виртуальной машины Themida. К слову сказать, у Oreans есть и другие похожие продукты, например Code Virtualizer. Эта статья, возможно, поможет кому‑то в борьбе и с подобными виртуальными машинами и обфускаторами.
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei