Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
На какие только ухищрения не приходится идти разработчикам программ на Java, чтобы усложнить взлом и реверс! Однако у всех подобных приложений есть слабое место: в определенный момент исполнения программа должна быть передана в JVM в исходных байт‑кодах, дизассемблировать который очень просто. Чтобы избежать этого, некоторые программисты вовсе избавляются от JVM-байт‑кода. Как хакеры обычно поступают в таких случаях? Сейчас разберемся!
warning
Статья написана в исследовательских целях, имеет ознакомительный характер и предназначена для специалистов по безопасности. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Использование или распространение ПО без лицензии производителя может преследоваться по закону.
Авторы одной программы, суровые сибирские программисты, решили поступить совсем радикальным способом: скомпилировали Java-код в натив, причем (по их собственному утверждению) с обфускацией и оптимизацией, как бы противоречиво это ни звучало. Фактически они пожертвовали кросс‑платформенностью (ну и зачем она, спрашивается, нужна в уже скомпилированной программе, заточенной под определенную архитектуру?).
Уж не знаю, насколько такой подход способствует оптимизации, — исследованное мной приложение чертовски неторопливо и прожорливо к ресурсам компьютера, а главное, занимает несколько сот мегабайт. Но реверс‑инженерам предложенный разработчиками подход сильно усложняет жизнь. Лично я не нашел в паблике внятного мануала по организации данных в таких программах, и во многих обзорах эта технология считается лучшей для защиты Java-приложений от взлома и декодинга. Называется она Excelsior JET.
Что ж, попробуем изучить эту технологию при помощи подручных средств. В качестве подопытного кролика возьмем одно из офлайновых веб‑приложений, о которых я многократно рассказывал в своих статьях. В качестве дизассемблера по старой традиции воспользуемся IDA.
Несмотря на то что код не запакован, не виртуализирован и практически не обфусцирован, поначалу задача реверса кажется неподъемной — в дизассемблированном коде напрочь отсутствуют не только названия классов и методов, но и читаемые текстовые строки. Между тем мы точно знаем, что и строки, и названия классов, методов, и даже номера строк в коде все‑таки хранятся.
Дело в том, что программа пишет в лог стек вызовов при возникновении исключений — там присутствуют и полные названия методов с классами, и даже имена исходных файлов Java, из которых они были скомпилированы вместе с номерами строк, выполняющих вложенные вызовы.
Это вдохновило меня на дальнейшие поиски. Как минимум при входе в каждый метод информация о нем каким‑то образом должна заноситься в отладочный стек. Бегло рассмотрев код, находим первую зацепку. На подавляющем большинстве процедур начало кода выглядит следующим образом (схожие места помечены стрелкой):
add rsp, 0FFFFFFFFFFFFFFF8h mov eax, [rsp-0C00h] ; <--------------------- 1 lea rax, unk_9EEDFC8 ; <--------------------- 2 mov [rsp], rax ; <--------------------- 3 add rsp, 0FFFFFFFFFFFFFFF8h mov eax, [rsp+8+var_C08] ; <--------------------- 1 lea rax, unk_9F2E060 ; <--------------------- 2 mov [rsp+8+var_8], rax ; <--------------------- 3 push rbx push rbp push rsi push rdi push r12 push r13 push r14 add rsp, 0FFFFFFFFFFFFFF60h mov eax, [rsp+0D8h+var_CD8] ; <--------------------- 1 lea rax, unk_ABFB080 ; <--------------------- 2 mov [rsp+0D8h+var_D8], rax ; <--------------------- 3 push rbx push rbp push rsi push rdi push r12 push r13 push r14 add rsp, 0FFFFFFFFFFFFFFC0h mov eax, [rsp+78h+var_C78] ; <--------------------- 1 lea rax, unk_ABFB040 ; <--------------------- 2 mov [rsp+78h+var_78], rax ; <--------------------- 3
Строка 1 чисто рудиментарная и никакой полезной нагрузки (во всяком случае, в приведенных выше примерах) не несет. Здесь в eax
присваивается значение, лежащее на стеке выше текущего положения на C00h
байт. Можно предположить, что это своеобразная защита от переполнения, — при вызове каждой процедуры на стеке гарантированно должен быть запас из C00h
байт.
А вот следующие две строки вызывают интерес: при входе в каждую процедуру следом за адресом возврата на стек кладется уникальный адрес некоей структуры, причем адрес практически всегда уникальный. Структура эта не инициализирована при загрузке программы, поэтому придется подключать к работе отладчик.
Здесь нас ожидает первая подножка: наш любимый x64dbg не годится. Не знаю и не хочу разбираться, специально ли это задумано авторами или стало следствием прожорливости Excelsior JET, но при запуске приложения из x64dbg программа сразу же кончает жизнь самоубийством с предсмертным сообщением о нехватке памяти. Приаттачиться к работающей программе можно, однако работать она все равно не хочет, ссылаясь на ту же самую проблему.
По счастью, создатели старенького легендарного отладчика OllyDbg перед тем, как проект закрылся, успели сделать тестовую 64-битную версию своей программы, очень сырую, с урезанными возможностями, но не конфликтующую с капризной и жадной до ресурсов софтиной. Итак, загружаем исследуемую программу в OllyDbg и останавливаем ее на начале любой из подобных процедур. Структура, ссылку на которую упорно кладут на стек, выглядит примерно так.
Структура, ссылку на которую кладут на стек
Видно, что у каждой процедуры есть своя собственная запись размером 0x40
байт. Не знаю, как они правильно называются, давай для удобства называть их структура-40 по их размеру. Назначение полей этой структуры малопонятно, за исключением указателя на процедуры (выделено синим) и по нулевому смещению указателя на другую, более интересную структуру, выделенного зеленым. У соседних записей ссылка на эту новую структуру одинакова, и, если присмотреться, в ней явно видно полное имя класса. Структура инициализирована в исходном коде, но без имени класса и некоторых полей.
Описатель класса, инициализированный в исходном коде (справа) и во время работы программы (слева)
При первом же взгляде на блок данных в месте, где должно располагаться имя класса, возникают смутные сомнения, что этот блок просто зашифрован каким‑то нехитрым шифром типа XOR. Дела продвигаются: у нас наметились уже два направления дальнейшей работы — определить соответствие процедур классам в изначальном коде и расшифровать их имена.
Как ни странно, метод решения у этих задач один: ставим точку останова типа Memory
на интересный нам адрес и ждем в засаде, пока не поймается изменяющий его кусок кода.
Начнем с расшифровки имен классов. Ставим Memory breakpoint
на первый байт строки java/
по адресу 72B7718
и запускаем программу. Наша ловушка сразу срабатывает на простенькой процедуре расшифровки:
jmp short loc_93A54A
На входе RCX-адрес зашифрованной строки и RDX-адрес расшифрованной строки (в нашем случае исходный RCX). А еще R8-байт, с которым строка ксорится, в нашем случае это F9h
.
loc_93A542: ; CODE XREF: sub_93A540+40↓j add rcx, 1 add rdx, 1loc_93A54A: ; CODE XREF: sub_93A540↑j movsx eax, byte ptr [rcx] ; EAX <- текущий байт строки test eax, eax jz short loc_93A56F cmp r8d, eax jz short loc_93A561 ; Проверки на конец строки — 0 или F9h mov r9d, eax xor eax, r8d ; EAX <- EAX XOR R8D movsx eax, al jmp short loc_93A57B; ---------------------------------------------------------------------------loc_93A561: ; CODE XREF: sub_93A540+14↑j mov r9d, eax mov r10d, r9d mov r9d, eax mov eax, r10d jmp short loc_93A57B; ---------------------------------------------------------------------------loc_93A56F: ; CODE XREF: sub_93A540+F↑j xor r9d, r9d mov r10d, r9d mov r9d, eax mov eax, r10dloc_93A57B: ; CODE XREF: sub_93A540+1F↑j ; sub_93A540+2D↑j mov [rdx], al ; Текущий байт <- новое значение EAX test r9d, r9d jnz short loc_93A542 retn
Предчувствия нас не обманули: это простейший XOR с фиксированным байтом F9h
. Причем похоже, что названия всех описателей классов расшифровываются сразу в одной процедуре при старте программы. В таком случае попробуем извлечь из нее список всех классов и положение строки имени класса в структуре описателя.
Источник: xakep.ru