Автор @МВК, aka Mikhail Kondakov
На какие только ухищрения не приходится идти разработчикам программ на Java, чтобы усложнить взлом и реверс! Однако у всех подобных приложений есть слабое место: в определенный момент исполнения программа должна быть передана в JVM в исходных байт‑кодах, дизассемблировать который очень просто. Чтобы избежать этого, некоторые программисты вовсе избавляются от JVM-байт‑кода. Как хакеры обычно поступают в таких случаях? Сейчас разберемся!
Авторы одной программы, суровые сибирские программисты, решили поступить совсем радикальным способом: скомпилировали Java-код в натив, причем (по их собственному утверждению) с обфускацией и оптимизацией, как бы противоречиво это ни звучало. Фактически они пожертвовали кросс‑платформенностью (ну и зачем она, спрашивается, нужна в уже скомпилированной программе, заточенной под определенную архитектуру?).
Уж не знаю, насколько такой подход способствует оптимизации, — исследованное мной приложение чертовски неторопливо и прожорливо к ресурсам компьютера, а главное, занимает несколько сот мегабайт. Но реверс‑инженерам предложенный разработчиками подход сильно усложняет жизнь. Лично я не нашел в паблике внятного мануала по организации данных в таких программах, и во многих обзорах эта технология считается лучшей для защиты Java-приложений от взлома и декодинга. Называется она Excelsior JET.
Что ж, попробуем изучить эту технологию при помощи подручных средств. В качестве подопытного кролика возьмем одно из офлайновых веб‑приложений, о которых я многократно рассказывал в своих статьях. В качестве дизассемблера по старой традиции воспользуемся IDA.
Несмотря на то что код не запакован, не виртуализирован и практически не обфусцирован, поначалу задача реверса кажется неподъемной — в дизассемблированном коде напрочь отсутствуют не только названия классов и методов, но и читаемые текстовые строки. Между тем мы точно знаем, что и строки, и названия классов, методов, и даже номера строк в коде все‑таки хранятся.
Дело в том, что программа пишет в лог стек вызовов при возникновении исключений — там присутствуют и полные названия методов с классами, и даже имена исходных файлов Java, из которых они были скомпилированы вместе с номерами строк, выполняющих вложенные вызовы.
Это вдохновило меня на дальнейшие поиски. Как минимум при входе в каждый метод информация о нем каким‑то образом должна заноситься в отладочный стек. Бегло рассмотрев код, находим первую зацепку. На подавляющем большинстве процедур начало кода выглядит следующим образом (схожие места помечены стрелкой):
Строка 1 чисто рудиментарная и никакой полезной нагрузки (во всяком случае, в приведенных выше примерах) не несет. Здесь в eax присваивается значение, лежащее на стеке выше текущего положения на C00h байт. Можно предположить, что это своеобразная защита от переполнения, — при вызове каждой процедуры на стеке гарантированно должен быть запас из C00h байт.
А вот следующие две строки вызывают интерес: при входе в каждую процедуру следом за адресом возврата на стек кладется уникальный адрес некоей структуры, причем адрес практически всегда уникальный. Структура эта не инициализирована при загрузке программы, поэтому придется подключать к работе отладчик.
Здесь нас ожидает первая подножка: наш любимый x64dbg не годится. Не знаю и не хочу разбираться, специально ли это задумано авторами или стало следствием прожорливости Excelsior JET, но при запуске приложения из x64dbg программа сразу же кончает жизнь самоубийством с предсмертным сообщением о нехватке памяти. Приаттачиться к работающей программе можно, однако работать она все равно не хочет, ссылаясь на ту же самую проблему.
По счастью, создатели старенького легендарного отладчика OllyDbg перед тем, как проект закрылся, успели сделать тестовую 64-битную версию своей программы, очень сырую, с урезанными возможностями, но не конфликтующую с капризной и жадной до ресурсов софтиной. Итак, загружаем исследуемую программу в OllyDbg и останавливаем ее на начале любой из подобных процедур. Структура, ссылку на которую упорно кладут на стек, выглядит примерно так.
Видно, что у каждой процедуры есть своя собственная запись размером 0x40 байт. Не знаю, как они правильно называются, давай для удобства называть их структура-40 по их размеру. Назначение полей этой структуры малопонятно, за исключением указателя на процедуры (выделено синим) и по нулевому смещению указателя на другую, более интересную структуру, выделенного зеленым. У соседних записей ссылка на эту новую структуру одинакова, и, если присмотреться, в ней явно видно полное имя класса. Структура инициализирована в исходном коде, но без имени класса и некоторых полей.
При первом же взгляде на блок данных в месте, где должно располагаться имя класса, возникают смутные сомнения, что этот блок просто зашифрован каким‑то нехитрым шифром типа XOR. Дела продвигаются: у нас наметились уже два направления дальнейшей работы — определить соответствие процедур классам в изначальном коде и расшифровать их имена.
Как ни странно, метод решения у этих задач один: ставим точку останова типа Memory на интересный нам адрес и ждем в засаде, пока не поймается изменяющий его кусок кода.
Начнем с расшифровки имен классов. Ставим Memory breakpoint на первый байт строки java/ по адресу 72B7718 и запускаем программу. Наша ловушка сразу срабатывает на простенькой процедуре расшифровки:
На входе RCX-адрес зашифрованной строки и RDX-адрес расшифрованной строки (в нашем случае исходный RCX). А еще R8-байт, с которым строка ксорится, в нашем случае это F9h.
Предчувствия нас не обманули: это простейший XOR с фиксированным байтом F9h. Причем похоже, что названия всех описателей классов расшифровываются сразу в одной процедуре при старте программы. В таком случае попробуем извлечь из нее список всех классов и положение строки имени класса в структуре описателя.
sub_9BA640 proc near ; На входе RCX — адрес структуры CDES
; ---------------------------------------------------------------------------
loc_9BA66A: ; CODE XREF: sub_9BA640+6B↓j
mov rcx, rax
mov rsi, rax
call sub_9C4540 ; Возвращает RAX — адрес текущего описателя класса
mov ecx, [rax+74h]
and ecx, 20h
cmp ecx, 20h ; Если бит 20h в двойном слове по смещению 74h от начала описателя класса установлен — название класса уже расшифровано
jz short loc_9BA6A0
mov ecx, [rax+14h] ; Двойное слово по смещению 14h от начала описателя класса — смещение до зашифрованного имени
movsxd rcx, ecx
add rcx, rax ; Абсолютный адрес строки имени в ECX
mov rdx, rcx ; И в RDX тоже
mov r8d, ebp ; Байт‑шифровальщик F9h
mov rdi, rax
call sub_93A540 ; Расшифровать имя
mov eax, [rdi+74h]
or eax, 20h
mov [rdi+74h], eax ; Установить флаг расшифрованности имени
sub_9BA640 endp
```
Итак, мы наконец‑то получили локализацию строки имени внутри описателя класса — относительное смещение до него. Попутно мы обнаружили еще одну интересную структуру, адрес которой данная процедура получает на вход. Назовем ее условно «структура CDES» по сигнатуре 53454443h в начале. Выглядит она так.
Это самая базовая структура Excelsior JET. Помимо байта, которым шифруются текстовые строки (выделен красным), из нее идут ссылки на все базовые структуры и таблицы. Находится она по смещению 8 от начала секции _bss и, к сожалению, не инициализирована в исходном коде. К вопросу ее инициализации и получения из нее интересующих нас структур и таблиц мы вернемся чуть позже, для начала же попробуем локализовать таблицу описателей классов. Немного повозившись с процедурой sub_9C4500, находим внутри следующий код:
Сама таблица описателей классов выглядит так.
Судя по всему, в ней содержатся не только описатели классов, но еще и множество других элементов, назначение которых ты можешь при желании выяснить самостоятельно. Нас же пока интересуют ее элементы, начиная с номера, выделенного на рисунке синим цветом. Однако и эти элементы вовсе не обязательно описатели классов, надо внимательно смотреть на флажки в заголовке структуры.
Теперь вернемся к инициализации «структуры-40». Записи «структуры-40» хоть в исходном коде и не инициализированы, однако расположены по вполне фиксированным адресам, на которые можно ставить бряки. Для примера берем первый же адрес 9F2E060 из одной такой процедуры. По остановке в данной точке мы получаем процедуру, инициализирующую «структуры-40» для каждого метода заданного класса. Насколько я понимаю, это происходит каждый раз при создании объекта. В упрощенном виде эта процедура выглядит примерно так (интересные места я выделил комментариями):
Разумеется, описанная процедура значительно сложнее и выполняет множество других функций, но на данный момент мы ищем вполне определенные фичи, и, похоже, мы их нашли. Резюмируя, попробуем для наглядности нарисовать примерную схему описателя класса.
На этом рисунке оранжевым обозначен тип блока (описатель класса — 3 или 4), красным — имя класса и смещение на него относительно начала описателя, зеленым — указатель на таблицу описателей классов, синим — таблица методов класса и смещение на нее относительно базового адреса.
Ко всему прочему мы выяснили еще один интересный факт. Несмотря на то что нативное приложение 64-битное, адреса методов внутри описателя классов — 32-битные смещения относительно базового адреса модуля. На самом деле, если внимательно присмотреться, мы найдем в описателе класса таблицу с прямыми длинными ссылками (как я понимаю, на методы из внешних классов), но нам она в данный момент не нужна.
Не знаю, как создатели приложения выкручиваются в случае с большими исполняемыми модулями, не адресуемыми 32 битами. Возможно, они имеют несколько секций и «структур CDES». Мне, во всяком случае, таковые не попадались, поэтому вернемся к нашим баранам, то бишь к жабам
Мы разобрали, какие классы скомпилированы в нативный код приложения и какие методы соответствуют каждому классу. Но хотелось бы знать имена этих методов. Ведь я уже упоминал в начале статьи, что приложение при возникновении исключения легко показывает стек вызовов с полными названиями классов, методов, с именами исходных файлов и даже с номерами строк. Последние, конечно, нам не особо нужны, но имена методов знать бы хотелось.
Направление, в котором следует копать в данном случае, очевидно: если обработчик исключений знает имена классов — спросим его об этом! Поскольку мы теперь знаем адреса методов каждого класса, тупо ставим бряки на всякий случай на все методы класса. Ну, например, на Throwable, а еще лучше на StackTrace.
Поискав в памяти загруженного модуля, мы находим такой класс — его полное имя
Указанный метод по адресу возврата определяет имя метода, из которого был выполнен вызов, имя исходного Java-файла, содержащего этот метод, и номер строки. Забегая вперед, скажу, что его полное имя
Надеюсь, к настоящему моменту я тебя не слишком напугал обилием новых структур с собственными именами? Как ты, вероятно, заметил, приведенный выше кусок кода содержит целых три новых термина: таблица имен методов по адресам, структура описания метода и индекс зашифрованной строки. Остановимся на каждом из них подробнее.
Чтобы при возникновении ошибки отследить метод и строку вызова, Excelsior Jet хранит упорядоченные таблицы соответствия адрес — метод, и для каждого метода существует таблица «адрес — строка». Таблица соответствия имен методов по адресам, как ты уже догадался, абсолютно адресуется из «структуры CDES» 64-битным адресом по смещению E8h. На рисунке «Структура CDES» она выделена фиолетовым. Структура у нее совершенно прозрачная, она показана на следующей иллюстрации.
Первое 32-битное слово (выделено красным) — количество элементов в таблице. Каждый элемент занимает 12 байт (первые два элемента выделены фиолетовым) и соответствует одному скомпилированному методу. Первое 32-битное слово элемента (выделено желтым) — относительный адрес точки входа метода. Последнее 32-битное слово (выделено голубым) — относительный адрес структуры описания метода.
Как видно, здесь применен такой же мухлеж с 32-битной адресацией кода и данных внутри 64-битного приложения: вместо нормальных 64-битных адресов используются 32-битные смещения относительно базы модуля, хранящегося в «структуре CDES».
Вернемся ко второй найденной структуре — структуре описания метода, которую я так назвал из‑за скудности собственной фантазии. Она не слишком отличается от таблицы имен методов по адресам и тоже представляет собой упорядоченную таблицу c размером в начале (выделено красным) и 12-байтовым элементом (первый элемент выделен фиолетовым).
Отличие от предыдущей таблицы состоит в том, что в самом начале структуры описания метода идут три 32-битных слова. Первое из них (на рисунке обведено оранжевым) — индекс класса, содержащего метод в таблице описателей классов. Второе (обведено синим) — индекс зашифрованного имени метода, а третье (обведено зеленым) — индекс зашифрованного имени исходного Java-файла.
Записи устроены следующим образом. Первый 32-битный элемент — смещение кода, в который компилируется Java-строка относительно начала метода, второй — номер строки в исходном Java-файле, а третий, судя по всему, не задействован вообще, поскольку я не видел его ненулевых значений. Поскольку обе описанные структуры представляют собой упорядоченные массивы (по сути, упорядоченные словари), любое место кода быстро и однозначно идентифицируется по ним методом дихотомии.
Таким образом, можно значительно облегчить реверс скомпилированных приложений, просто пробежавшись по этим двум структурам и расставив в коде метки соответствия классам, методам и строкам. Предоставляю тебе самостоятельно заняться написанием подобных скриптов или плагинов, а нам осталось разобраться, что означает индекс зашифрованной строки из комментария к предыдущему фрагменту кода.
Как я уже говорил, из‑за паранойи разработчиков в Excelsior Jet, помимо имен классов, шифруются вообще все‑все‑все текстовые строки, причем одним и тем же алгоритмом — тупым XOR с хранящимся в «структуре CDES» байтом‑шифровальщиком (у нас это F9h). Но если имена классов стоят на своих местах в соответствующих структурах описания класса, то почти все остальные строки собраны в одном зашифрованном блоке, на который указывает абсолютный адрес по смещению C8h в «структуре CDES» (на рисунке «Структура CDES» выделено зеленым). Таким образом, упомянутые выше индексы представляют собой смещения относительно начала этого блока. Здесь я снова обращу твое внимание: создатели компилятора явно делают допущение, что в любом 64-битном приложении размер этого блока будет адресоваться 32 битами.
И в заключение для тех, кого эта немного сумбурная статья вдохновила написать более дружественный к пользователю декомпилятор, скрипт или плагин к отладчику, вернемся к вопросу поиска и локализации основных структур данных, упомянутых в статье. Как я уже говорил, основная «структура CDES», из которой идут ссылки на главные структуры, блоки и таблицы, инициализируется после запуска программы. То есть в уже запущенной программе все эти адреса посмотреть можно, но из дизассемблера придется искать. Попробуем это сделать. Инициализируется эта структура в самое начало секции _bss, при помощи IDA мы легко находим код инициализации:
Стратегия поиска такова: ищем в коде адрес присваивания сигнатуры 53454443h и, начиная с этого места, по маскам команд и смещений извлекаем нужные адреса. Идея весьма скользкая, поскольку в каждой новой версии компилятора авторы могут менять этот инициализационный код, из‑за чего нам придется перерабатывать алгоритм, но как рабочий вариант такой подход сгодится.
Ты, наверное, заметил, что байт‑шифровальщик F9h тоже не инициализируется в этой процедуре, он берется из другой странной структуры, находящейся в самом начале секции _config c сигнатурой CPB.
Как видишь, смещение этого байта в структуре равно 1Ch. Последний кусочек пазла встал на место, и остается только пожелать удачи энтузиастам реверса этого хитрого компилятора.
На какие только ухищрения не приходится идти разработчикам программ на Java, чтобы усложнить взлом и реверс! Однако у всех подобных приложений есть слабое место: в определенный момент исполнения программа должна быть передана в JVM в исходных байт‑кодах, дизассемблировать который очень просто. Чтобы избежать этого, некоторые программисты вовсе избавляются от JVM-байт‑кода. Как хакеры обычно поступают в таких случаях? Сейчас разберемся!
Авторы одной программы, суровые сибирские программисты, решили поступить совсем радикальным способом: скомпилировали 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
А вот следующие две строки вызывают интерес: при входе в каждую процедуру следом за адресом возврата на стек кладется уникальный адрес некоей структуры, причем адрес практически всегда уникальный. Структура эта не инициализирована при загрузке программы, поэтому придется подключать к работе отладчик.
Здесь нас ожидает первая подножка: наш любимый x64dbg не годится. Не знаю и не хочу разбираться, специально ли это задумано авторами или стало следствием прожорливости Excelsior JET, но при запуске приложения из x64dbg программа сразу же кончает жизнь самоубийством с предсмертным сообщением о нехватке памяти. Приаттачиться к работающей программе можно, однако работать она все равно не хочет, ссылаясь на ту же самую проблему.
По счастью, создатели старенького легендарного отладчика OllyDbg перед тем, как проект закрылся, успели сделать тестовую 64-битную версию своей программы, очень сырую, с урезанными возможностями, но не конфликтующую с капризной и жадной до ресурсов софтиной. Итак, загружаем исследуемую программу в OllyDbg и останавливаем ее на начале любой из подобных процедур. Структура, ссылку на которую упорно кладут на стек, выглядит примерно так.
Видно, что у каждой процедуры есть своя собственная запись размером 0x40 байт. Не знаю, как они правильно называются, давай для удобства называть их структура-40 по их размеру. Назначение полей этой структуры малопонятно, за исключением указателя на процедуры (выделено синим) и по нулевому смещению указателя на другую, более интересную структуру, выделенного зеленым. У соседних записей ссылка на эту новую структуру одинакова, и, если присмотреться, в ней явно видно полное имя класса. Структура инициализирована в исходном коде, но без имени класса и некоторых полей.
При первом же взгляде на блок данных в месте, где должно располагаться имя класса, возникают смутные сомнения, что этот блок просто зашифрован каким‑то нехитрым шифром типа XOR. Дела продвигаются: у нас наметились уже два направления дальнейшей работы — определить соответствие процедур классам в изначальном коде и расшифровать их имена.
Как ни странно, метод решения у этих задач один: ставим точку останова типа Memory на интересный нам адрес и ждем в засаде, пока не поймается изменяющий его кусок кода.
Начнем с расшифровки имен классов. Ставим Memory breakpoint на первый байт строки java/ по адресу 72B7718 и запускаем программу. Наша ловушка сразу срабатывает на простенькой процедуре расшифровки:
Код:
jmp short loc_93A54A
Код:
loc_93A542: ; CODE XREF: sub_93A540+40↓j
add rcx, 1
add rdx, 1
loc_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, r10d
loc_93A57B: ; CODE XREF: sub_93A540+1F↑j
; sub_93A540+2D↑j
mov [rdx], al ; Текущий байт <- новое значение EAX
test r9d, r9d
jnz short loc_93A542
retn
sub_9BA640 proc near ; На входе RCX — адрес структуры CDES
Код:
...
movsx edx, byte ptr [rcx+0ACh] ; По смещению ACh в этой структуре байт-шифровальщик F9h
mov r8d, edx
mov rdx, rcx
mov rcx, rax
mov rbx, rdx
mov ebp, r8d
call sub_9C4500 ; Инициализация указателей на таблицу описателей классов
jmp short loc_9BA6A3
loc_9BA66A: ; CODE XREF: sub_9BA640+6B↓j
mov rcx, rax
mov rsi, rax
call sub_9C4540 ; Возвращает RAX — адрес текущего описателя класса
mov ecx, [rax+74h]
and ecx, 20h
cmp ecx, 20h ; Если бит 20h в двойном слове по смещению 74h от начала описателя класса установлен — название класса уже расшифровано
jz short loc_9BA6A0
mov ecx, [rax+14h] ; Двойное слово по смещению 14h от начала описателя класса — смещение до зашифрованного имени
movsxd rcx, ecx
add rcx, rax ; Абсолютный адрес строки имени в ECX
mov rdx, rcx ; И в RDX тоже
mov r8d, ebp ; Байт‑шифровальщик F9h
mov rdi, rax
call sub_93A540 ; Расшифровать имя
mov eax, [rdi+74h]
or eax, 20h
mov [rdi+74h], eax ; Установить флаг расшифрованности имени
Код:
...
cmp ecx, edx
jle short loc_9BA66A ; Следующий класс
...
retn
```
Итак, мы наконец‑то получили локализацию строки имени внутри описателя класса — относительное смещение до него. Попутно мы обнаружили еще одну интересную структуру, адрес которой данная процедура получает на вход. Назовем ее условно «структура CDES» по сигнатуре 53454443h в начале. Выглядит она так.
Это самая базовая структура Excelsior JET. Помимо байта, которым шифруются текстовые строки (выделен красным), из нее идут ссылки на все базовые структуры и таблицы. Находится она по смещению 8 от начала секции _bss и, к сожалению, не инициализирована в исходном коде. К вопросу ее инициализации и получения из нее интересующих нас структур и таблиц мы вернемся чуть позже, для начала же попробуем локализовать таблицу описателей классов. Немного повозившись с процедурой sub_9C4500, находим внутри следующий код:
Код:
mov rax, [rdx+0C0h] ; Адрес таблицы описателей классов по смещению 0C0h от начала структуры CDES (на предыдущем рисунке выделено синим)
...
mov edx, [rax-2Ch] ; Номер первого рассматриваемого элемента в таблице — по смещению -2Ch от начала таблицы (на следующем рисунке выделено синим)
mov [rcx+10h], edx
mov edx, [rax-2Ch] ; Номер первого рассматриваемого элемента в таблице — по смещению -2Ch от начала таблицы (на следующем рисунке выделено синим)
mov r8d, [rax-28h] ; Количество рассматриваемых элементов в таблице — по смещению -28h от начала таблицы (на следующем рисунке выделено зеленым)
lea eax, [rdx+r8] ; Номер последнего рассматриваемого элемента в таблице (на следующем рисунке выделено красным)
sub eax, 1
mov [rcx+14h], eax
jmp short loc_9C44C7
; ---------------------------------------------------------------------------
loc_9C44BE: ; В цикле перебираем все элементы таблицы, начиная с первого рассматриваемого на предмет принадлежности к описателям класса
mov eax, [rcx+10h]
add eax, 1
mov [rcx+10h], eax
loc_9C44C7: ; CODE XREF: sub_9C4480+3C↑j
mov rax, [rcx]
mov edx, [rcx+10h]
mov rax, [rax+rdx*8]
movsx edx, word ptr [rax+8]
cmp edx, 3 ; У описателей класса 16-битное слово по смещению 8 от начала структуры должно быть 3
jz short loc_9C44E3
movsx edx, word ptr [rax+8]
cmp edx, 4
jnz short sub_9C4480 ; ...или 4
loc_9C44E3: ; CODE XREF: sub_9C4480+58↑j
mov [rcx+18h], rax ; Если элемент таблицы удовлетворяет этим условиям, возвращаем его адрес
jmp short locret_9C44FE
Судя по всему, в ней содержатся не только описатели классов, но еще и множество других элементов, назначение которых ты можешь при желании выяснить самостоятельно. Нас же пока интересуют ее элементы, начиная с номера, выделенного на рисунке синим цветом. Однако и эти элементы вовсе не обязательно описатели классов, надо внимательно смотреть на флажки в заголовке структуры.
Теперь вернемся к инициализации «структуры-40». Записи «структуры-40» хоть в исходном коде и не инициализированы, однако расположены по вполне фиксированным адресам, на которые можно ставить бряки. Для примера берем первый же адрес 9F2E060 из одной такой процедуры. По остановке в данной точке мы получаем процедуру, инициализирующую «структуры-40» для каждого метода заданного класса. Насколько я понимаю, это происходит каждый раз при создании объекта. В упрощенном виде эта процедура выглядит примерно так (интересные места я выделил комментариями):
Код:
sub_936500 proc near
...
loc_936576: ; Цикл по всем элементам таблицы методов класса
...
mov [r11], r9 ; Место, на котором срабатывает breakpoint, R9-адрес текущего описателя класса — по смещению 0 «структуры-40»
...
mov ebp, [r9+0B0h] ; Относительный адрес таблицы методов находится по смещению B0h внутри описателя класса
test ebp, ebp
jz short loc_93673D
movsxd rbp, ebp
and rbp, rax
mov rsi, [r9+30h] ; По смещению 30h внутри описателя класса — указатель на «структуру CDES»
mov rsi, [rsi+58h] ; По смещению 58h внутри «структуры CDES» — базовый адрес исполняемого модуля 400000h, на рисунке «Структура CDES» выделен оранжевым
add rbp, rsi ; Абсолютный адрес таблицы методов класса в RBP
jmp short loc_93673F
; ---------------------------------------------------------------------------
loc_93673D: ; CODE XREF: sub_936500+228↑j
xor ebp, ebp
loc_93673F: ; CODE XREF: sub_936500+23B↑j
test rbp, rbp
jz short loc_936764
mov ebx, [rbp+rbx*4+0] ; RBP[RBX] — относительный адрес текущего метода
test ebx, ebx
jz short loc_93675F
movsxd rbx, ebx
mov r9, [r9+30h] ; По смещению 30h внутри описателя класса — указатель на «структуру CDES»
and rbx, rax
mov r9, [r9+58h] ; По смещению 58h внутри «структуры CDES» — базовый адрес исполняемого модуля 400000h, на рисунке «Структура CDES» выделен оранжевым
add r9, rbx ; Абсолютный адрес текущего в R9
...
mov [rdx+r10+18h], r9 ; Адрес метода по смещению 18h «структуры-40»
...
jnz loc_936576 ; Перейти к обработке следующего метода класса и следующего блока «структуры-40»
На этом рисунке оранжевым обозначен тип блока (описатель класса — 3 или 4), красным — имя класса и смещение на него относительно начала описателя, зеленым — указатель на таблицу описателей классов, синим — таблица методов класса и смещение на нее относительно базового адреса.
Ко всему прочему мы выяснили еще один интересный факт. Несмотря на то что нативное приложение 64-битное, адреса методов внутри описателя классов — 32-битные смещения относительно базового адреса модуля. На самом деле, если внимательно присмотреться, мы найдем в описателе класса таблицу с прямыми длинными ссылками (как я понимаю, на методы из внешних классов), но нам она в данный момент не нужна.
Не знаю, как создатели приложения выкручиваются в случае с большими исполняемыми модулями, не адресуемыми 32 битами. Возможно, они имеют несколько секций и «структур CDES». Мне, во всяком случае, таковые не попадались, поэтому вернемся к нашим баранам, то бишь к жабам
Мы разобрали, какие классы скомпилированы в нативный код приложения и какие методы соответствуют каждому классу. Но хотелось бы знать имена этих методов. Ведь я уже упоминал в начале статьи, что приложение при возникновении исключения легко показывает стек вызовов с полными названиями классов, методов, с именами исходных файлов и даже с номерами строк. Последние, конечно, нам не особо нужны, но имена методов знать бы хотелось.
Направление, в котором следует копать в данном случае, очевидно: если обработчик исключений знает имена классов — спросим его об этом! Поскольку мы теперь знаем адреса методов каждого класса, тупо ставим бряки на всякий случай на все методы класса. Ну, например, на Throwable, а еще лучше на StackTrace.
Поискав в памяти загруженного модуля, мы находим такой класс — его полное имя
com/excelsior/jet/runtime/excepts/stacktrace/StackTrace. Методов у этого класса немного, порядка двадцати, поэтому, просто установив точки останова на каждый из них, получаем срабатывание по адресу D193C0.Указанный метод по адресу возврата определяет имя метода, из которого был выполнен вызов, имя исходного Java-файла, содержащего этот метод, и номер строки. Забегая вперед, скажу, что его полное имя
com/excelsior/jet/runtime/excepts/stacktrace/StackTrace/a, и, к сожалению, код перед целевой компиляцией обфусцируется. Схематически он выглядит вот так:
Код:
sub_D193C0 proc near ; На входе в RDX — адрес возврата со стека почему-то минус 1, то есть адрес байта, предшествующего адресу возврата из call
...
mov rcx, [rax+30h] ; RCX <- Адрес CDES
mov r9, [rcx+0E8h] ; CDES[E8h] — таблица имен методов по адресам, на рисунке «Структура CDES» выделена фиолетовым
test r9, r9
jz loc_D19667
mov r10d, [r9]
test r10d, r10d
jz loc_D19667
test rdx, rdx
jz short loc_D19411
mov r10, [rcx+58h] ; Базовый адрес исполняемого модуля
sub rdx, r10 ; RDX <- относительный адрес возврата
...
call sub_D1A100 ; Эта процедура ищет в таблице имен методов по адресам относительный адрес RDX, то есть участок кода, внутри которого он находится
test rax, rax ; На выходе в RAX — абсолютный адрес найденного элемента таблицы имен методов по адресам, содержащего адрес возврата
jz loc_D19662
mov ecx, 0FFFFFFFFh
mov edx, [rax+8] ; 32-битное слово по смещению 8 в найденном элементе — относительный адрес структуры описания метода
test edx, edx
jz short loc_D1944D
movsxd rdx, edx
and rdx, rcx
mov r8, [rsi+58h] ; Базовый адрес исполняемого модуля
add rdx, r8 ; Получаем абсолютный адрес структуры описания метода
jmp short loc_D1944F
; ---------------------------------------------------------------------------
loc_D1944D: ; CODE XREF: sub_D193C0+7C↑j
xor edx, edx
loc_D1944F: ; CODE XREF: sub_D193C0+8B↑j
mov eax, [rax] ; 32-битное слово по смещению 0 найденного элемента таблицы — стартовый адрес метода
sub ebx, eax ; Смещение от начала метода до адреса возврата
mov r8, rcx
mov rcx, rdx
mov edx, ebx
mov r12, rcx
mov r13, r8
call sub_D1A000 ; Методом половинного деления ищем в структуре описания метода номер строки по относительному смещению адреса возврата
,,,
mov ecx, [rax+4] ; Номер строки исходного Java-файла
mov edx, [r12+4] ; Индекс зашифрованной строки имени метода
mov eax, [r12+8] ; Индекс зашифрованной строки имени исходного Java-файла, содержащего метод
...
retn
sub_D193C0 endp
Чтобы при возникновении ошибки отследить метод и строку вызова, Excelsior Jet хранит упорядоченные таблицы соответствия адрес — метод, и для каждого метода существует таблица «адрес — строка». Таблица соответствия имен методов по адресам, как ты уже догадался, абсолютно адресуется из «структуры CDES» 64-битным адресом по смещению E8h. На рисунке «Структура CDES» она выделена фиолетовым. Структура у нее совершенно прозрачная, она показана на следующей иллюстрации.
Первое 32-битное слово (выделено красным) — количество элементов в таблице. Каждый элемент занимает 12 байт (первые два элемента выделены фиолетовым) и соответствует одному скомпилированному методу. Первое 32-битное слово элемента (выделено желтым) — относительный адрес точки входа метода. Последнее 32-битное слово (выделено голубым) — относительный адрес структуры описания метода.
Как видно, здесь применен такой же мухлеж с 32-битной адресацией кода и данных внутри 64-битного приложения: вместо нормальных 64-битных адресов используются 32-битные смещения относительно базы модуля, хранящегося в «структуре CDES».
Вернемся ко второй найденной структуре — структуре описания метода, которую я так назвал из‑за скудности собственной фантазии. Она не слишком отличается от таблицы имен методов по адресам и тоже представляет собой упорядоченную таблицу c размером в начале (выделено красным) и 12-байтовым элементом (первый элемент выделен фиолетовым).
Отличие от предыдущей таблицы состоит в том, что в самом начале структуры описания метода идут три 32-битных слова. Первое из них (на рисунке обведено оранжевым) — индекс класса, содержащего метод в таблице описателей классов. Второе (обведено синим) — индекс зашифрованного имени метода, а третье (обведено зеленым) — индекс зашифрованного имени исходного Java-файла.
Записи устроены следующим образом. Первый 32-битный элемент — смещение кода, в который компилируется Java-строка относительно начала метода, второй — номер строки в исходном Java-файле, а третий, судя по всему, не задействован вообще, поскольку я не видел его ненулевых значений. Поскольку обе описанные структуры представляют собой упорядоченные массивы (по сути, упорядоченные словари), любое место кода быстро и однозначно идентифицируется по ним методом дихотомии.
Таким образом, можно значительно облегчить реверс скомпилированных приложений, просто пробежавшись по этим двум структурам и расставив в коде метки соответствия классам, методам и строкам. Предоставляю тебе самостоятельно заняться написанием подобных скриптов или плагинов, а нам осталось разобраться, что означает индекс зашифрованной строки из комментария к предыдущему фрагменту кода.
Как я уже говорил, из‑за паранойи разработчиков в Excelsior Jet, помимо имен классов, шифруются вообще все‑все‑все текстовые строки, причем одним и тем же алгоритмом — тупым XOR с хранящимся в «структуре CDES» байтом‑шифровальщиком (у нас это F9h). Но если имена классов стоят на своих местах в соответствующих структурах описания класса, то почти все остальные строки собраны в одном зашифрованном блоке, на который указывает абсолютный адрес по смещению C8h в «структуре CDES» (на рисунке «Структура CDES» выделено зеленым). Таким образом, упомянутые выше индексы представляют собой смещения относительно начала этого блока. Здесь я снова обращу твое внимание: создатели компилятора явно делают допущение, что в любом 64-битном приложении размер этого блока будет адресоваться 32 битами.
И в заключение для тех, кого эта немного сумбурная статья вдохновила написать более дружественный к пользователю декомпилятор, скрипт или плагин к отладчику, вернемся к вопросу поиска и локализации основных структур данных, упомянутых в статье. Как я уже говорил, основная «структура CDES», из которой идут ссылки на главные структуры, блоки и таблицы, инициализируется после запуска программы. То есть в уже запущенной программе все эти адреса посмотреть можно, но из дизассемблера придется искать. Попробуем это сделать. Инициализируется эта структура в самое начало секции _bss, при помощи IDA мы легко находим код инициализации:
Код:
sub_743B80 proc near ; CODE XREF: sub_743FA0+25↓p
mov dword ptr [rcx], 53454443h ; Сигнатура CDES
...
lea rax, unk_9E31718 ; Hardcoded-адрес таблицы описателей классов
mov [rcx+0C0h], rax
lea rax, unk_14F3F6F4 ; Hardcoded-адрес блока зашифрованных строк
mov [rcx+0C8h], rax
lea rax, unk_15718A54
mov [rcx+0D0h], rax
lea rax, unk_9ED8428
mov [rcx+0E0h], rax
lea rax, aExfs ; "EXFS\a"
mov [rcx+0D8h], rax
lea rax, unk_1571F50C
mov [rcx+0E8h], rax ; Hardcoded-адрес таблицы имен методов по адресам
Ты, наверное, заметил, что байт‑шифровальщик F9h тоже не инициализируется в этой процедуре, он берется из другой странной структуры, находящейся в самом начале секции _config c сигнатурой CPB.
Как видишь, смещение этого байта в структуре равно 1Ch. Последний кусочек пазла встал на место, и остается только пожелать удачи энтузиастам реверса этого хитрого компилятора.