Unsafe Java II - Мутагенез земноводных

Дата публикации 12 сен 2006

Unsafe Java II - Мутагенез земноводных — Архив WASM.RU

Unsafe Java - Мутагенез земноводных

Unsafe Java II - Мутагенез земноводных

Первая статья была посвящена классам, объектам и общим принципам работы с памятью виртуальной машины. Сейчас пришло время сделать еще один шаг вглубь и посмотреть на важнейшие составные части классов, а именно функции. В плане практического применения мы научимся:

  • изменять байткод после загрузки
  • вызывать функции, не импортируя их

Но сначала небольшое лирическое отступление. После появления "Unsafe Java I" я получил большое количество отзывов, общий смысл которых сводился к "а нафиг все это нужно?". Поэтому пользуюсь случаем подчеркнуть, что никоим образом не призываю применять ниже описанное в повседневной работе. Случаи, требующие настолько глубокого копания в виртуальной машине Явы действительно достаточно редки. Однако они существуют. И в конце концов бывает просто приятно "перелезть через забор" :smile3:

Примеры к этой статье, за исключением простейших, оформлены для разнообразия в виде небольшого crackme. Так что имеет наверное смысл сначала попробовать разобраться с ним собственными силами, а потом уже читать "решение". Хотя особо нетерпеливые или ленивые читатели могут конечно сделать и наоборот. Для тех новичков, которые читают по-немецки: по адресу http://www.buha.info/board/showthread.php?t=52196 можно найти подробную пошаговую (не поленился же DarkTom, за что ему большое спасибо) инструкцию, как исследовать этот, а значит и подобные, crackme.

Все примеры протестированы с Sun Java под Windows версии от 1.4.2_08 до 1.5.0_08 включительно. К своему стыду должен признаться, что несмотря на обещание, так пока до Linux'a и не добрался. Так что у кого есть время - может заняться. Байткод примеров скомпилирован версией 1.4.2_08, другие компиляторы могут немного отличаться, что приведет к другим смещениям в коде. Соответственно если у кого-то примеры в собственной компиляции не работают, первым делом сравните получившийся байткод с моим. Что поделать, хотите работать на низком уровне - учитесь не доверять компилятору.

1. Где живут функции

Как всегда, первым делом нужно определить, где копать. Поэтому посмотрим снова на уже знакомые нам по первой части структуры классов и поищем в них поля, отвечающие за хранение функций и кода. Так как внутренняя жизнь виртуальной машины довольно, я бы даже сказал неоправданно, часто претерпевает более или менее мелкие именения, то придется нам рассмотреть в общей сложности три случая. Опять же, здесь я приведу только те поля, которые в дальнейшем действительно понадобятся.

Версии 1.4.x, где x>7

Код (Text):
  1.  
  2. class_struct{
  3.     ... ...
  4.     100 functions   // указатель на массив с элементами типа func_struct*
  5.     ... ...
  6.     128 constantpool    // указатель типа constantpool_struct*
  7.     ... ...
  8. }
  9.  

Код (Text):
  1.  
  2. func_struct{
  3.     0   magic
  4.     4   class           // class_struct*
  5.     8   constantpool        // constantpool_struct*
  6.     12  ...
  7.     16  strsize         // размер структуры в DWORD'ах
  8.     18  ...
  9.     ... ...
  10.     32  name_index      // индекс имени функции в массиве констант
  11.                         // (т.е. в структуре constantpool_struct)
  12.     34  sig_index       // индекс сигнатуры (описание параметров и
  13.                         // возвращаемого значения) в массиве констант
  14.     36  ...
  15.     40  bc_length       // длина байткода в байтах
  16.     ... ...
  17.     48  invocation_counter  // количество вызовов функции
  18.     ... ...
  19.     56  code            // псевдоуказатель на машинный код
  20.     ... ...
  21.     72  bytecode        // здесь начинается сам байткод
  22.     ... ...
  23. }
  24.  

Код (Text):
  1.  
  2. constantpool_struct{
  3.     ... ...
  4.     8   pool_size   // количество констант + 1
  5.     12  tags        // constant_tags*
  6.     16  pool_cache  // указатель на кэш методов, смотри пояснения в тексте
  7.     ... ...
  8.     28  constants   // начало массива констант
  9.     ... ...
  10. }
  11.  

Код (Text):
  1.  
  2. constant_tags {
  3.     ... ...
  4.     8   length  // длина массива
  5.     12  array   // байтовый массив
  6.     ....
  7. }
  8.  

Возможно кстати, что приведенные выше смещения справедливы также и для более старых версий, т.к. значения x<8 я просто не проверял.

Версии 1.5.x, где x<6

Код (Text):
  1.  
  2. func_struct
  3. {
  4.     0   magic
  5.     4   class_struct
  6.     8   funcconst       // funcconst_struct*
  7.     12  constantpool        // constantpool_struct*
  8.     ... ...
  9.     24  strsize         // размер структуры в DWORD'ах
  10.     26  ...
  11.     ... ...
  12.     36  invocation_counter  // количество вызовов функции
  13.     ... ...
  14.     44  code            // псевдоуказатель на машинный код
  15.     ... ...
  16. }
  17.  

Код (Text):
  1.  
  2. funcconst_struct{
  3.     0   magic
  4.     4   class_struct
  5.     ... ...
  6.     24  strsize     // размер структуры в DWORD'ах
  7.     ... ...
  8.     30  bc_length   // длина байткода в байтах
  9.     32  name_index  // индекс имени функции в массиве констант
  10.                     // (т.е. в структуре constantpool_struct)
  11.     34  sig_index   // индекс сигнатуры (описание параметров и
  12.                     // возвращаемого значения) в массиве констант
  13.     36  ...
  14.     ... ...
  15.     48  bytecode    // здесь начинается сам байткод
  16.     ... ...  
  17. }
  18.  

Остальные структуры совпадают с версиями 1.4.x

Версии 1.5.x, где x>5

Код (Text):
  1.  
  2. funcconst_struct{
  3.     0   magic
  4.     4   class_struct
  5.     ... ...
  6.     32  strsize     // размер структуры в DWORD'ах
  7.     ... ...
  8.     38  bc_length   // длина байткода в байтах
  9.     40  name_index  // индекс имени функции в массиве
  10.                     // констант (т.е. в структуре constantpool_struct)
  11.     42  sig_index   // индекс сигнатуры (описание параметров и
  12.                     // возвращаемого значения) в массиве констант
  13.     44  ...
  14.     ... ...
  15.     48  bytecode    // здесь начинается сам байткод
  16.     ... ...
  17. }
  18.  

Код (Text):
  1.  
  2. constantpool_struct{
  3.     ... ...
  4.     8   pool_size   // количество констант + 1
  5.     12  tags        // constant_tags*
  6.     16  pool_cache  // указатель на кэш методов, смотри главу 3
  7.     ... ...
  8.     32  constants   // начало массива констант
  9.     ... ...
  10. }
  11.  

Остальные структуры соответствуют предыдущему случаю 1.5.x с x<6

Внимательный читатель наверняка уже заметил, что я скачу "галопом по Европам". Неразобранными остались например constantpool_struct и constant_tags - интересные структуры с достаточно нетривиальным устройством, про кэш методов вообще по сути ничего не сказано, кроме того, что он есть. За бортом остались и отличия серверной VM, хотя все примеры написаны так, что будут работать и с ключом -server тоже. На самом деле я начал было писать полноценные объяснения, но статья быстро разрослась до совершенно неприличного размера и пришлось их снова убрать, переместив в следующую часть. Так что желающие непременно получить полную картину смотрят исходники и/или терпеливо ждут третьей части цикла.

2. Модификация байткода

На настоящий момент метод ClassLoader.defineClass0(...), задачей которого является физическая загрузка класса и инициализация его структур, представляет собой своеобразный переломный пункт в жизненном цикле байткода. Официально считается, что изменять загружаемый класс можно только до передачи его (в виде байтового массива) в defineClass0. Действительно, создание и редактирование классов "на лету" до загрузки - вручную или с помощью многочисленных библиотек типа BCEL - пользуется устойчивой популярностью. И при необходимости успешно отлавливаются простой заменой стандартного класса ClassLoader на свой собственный. Нас же интересует сейчас именно общий случай, то есть возможность изменения байткода в любой момент работы программы.

Как ни странно, никаких особых ухищрений с нашей стороны не потребуется. Достаточно знать где лежит код и четко представлять, в каком виде он там лежит. Сложностей с местонахождением возникнуть не должно, смотрим предыдущую главу. Придется только опять вычислять смещения в зависимости от версии Явы - Sun'овцы видимо все еще находятся в процессе творческого поиска. В отношении формата есть однако свои нюансы.

Правила ассемблирования и формат скомпилированного кода полностью описаны в VM spec, особенно обратите внимание, что все числовые значения хранятся по схеме big endian. В целом код в памяти будет таким же, как и в class файле, за одним большим исключением: ссылки на константы классов, полей и функций в constant pool'e заменяются ссылками на кэш, то есть по сути просто на порядковый номер обьекта. Сейчас разберем на наглядном примере.

В методе main класса TestSelfmod есть строчка

Код (Text):
  1.  
  2.     Unsafe unsafe = UnsafeUtilEx.unsafe;
  3.  

Компилируется она в

Код (Text):
  1.  
  2.     0002    B20011  getstatic #0011 &lt;sun.misc.Unsafe crackme.UnsafeUtilEx.unsafe&gt;
  3.     0005    4D  astore_2
  4.  

Для удобства код отформатирован так же, как показывает его JavaBite (http://www.wasm.ru/baixado.php?mode=tool&id=284). 0x0011 - номер константы (тип Fieldref) в массиве констант. Но в памяти окажется следущее:

Код (Text):
  1.  
  2.     B20100
  3.     4D
  4.  

Здесь 0x01 - индекс в кэше методов. По индексу 0x00 будет лежать метод TestSelfmod.<init>, а по индексу 0x02 - поле UnsafeUtilEx.vm1_5. То есть порядок констант в constant pool'e сохраняется и в кэше.

Итак с форматом мы тоже разобрались, можно править. Просто пишем свой код поверх старого, простенький пример смотрите в классе TestSelfmod. Полезной работы здесь всего три строчки:

Код (Text):
  1.  
  2.         int i = 0;
  3.     ...
  4.         i += 23;
  5.     ...
  6.         System.out.println(i);
  7.  

По идее программа должна вывести 23 на консоль. Но с помощью

Код (Text):
  1.  
  2.     unsafe.putAddress(address+0x85, 0xB22A0184L);
  3.  

мы подставляем в команду 0x840117 (т.е. iinc 1 23) число 42 (результат - 0x84012A, не забывайте про big endian) и именно его и получаем на выходе. Заметьте, что функция изменяет собственный код во время выполнения - классическая самомодификация. Еще одним примером является TestLoop, там вызванная функция изменяет вызывающую, чтобы выйти из бесконечного цикла (ifne заменяется на ifeq).

Также посмотрите на строки

Код (Text):
  1.  
  2.     unsafe.putAddress(key_func_data, unsafe.getAddress(key_func_data) ^ 0xEA6716E2L);
  3.     unsafe.putAddress(key_func_data+4, unsafe.getAddress(key_func_data+4) ^ 0xB21E0C9AL);
  4.     unsafe.putAddress(key_func_data+8, unsafe.getAddress(key_func_data+8) ^ 0x7F131577L);
  5.     unsafe.putAddress(key_func_data+12, unsafe.getAddress(key_func_data+12) ^ 0x003D86EFL);
  6.  

в ValidatorMain. А когда поймете, что и как они делают, начинайте экспериментировать сами.

На самом деле конечно не все так безоблачно и при экспериментах придется учитывать еще как минимум два момента. Момент первый: весь код проходит через верификатор. Абсолютно весь, в том числе и тот, который добавляется динамически. Так что вписать на место кода всякий мусор не удастся, это должны быть более или менее осмысленные команды. Правила верификатора (почти все) описаны все в той же VM spec. Есть правда возможность отложить по времени некоторые проверки, сыграв на чрезмерном "уме" верификатора - его умении распознавать "мертвый" код. Для такого кода выполняются по-видимому только самые общие проверки и в то же время нам ничто не мешает с помощью свежеприобретенных умений в нужный момент послать его на выполнение, занопив например безусловный переход.

Кроме того не следует забывать, что Ява - язык полукомпилируемый. На некотором моменте (когда именно, зависит от настроек) байткод будет пропущен через настоящий компилятор и выполнятся будет уже машинный код. В связи с этим при изменении байткода обычно имеет смысл выставлять значения invocation_counter и code в ноль. Если код уже прошел через HotSpot compiler, то таким образом будет произведена принудительная перекомпиляция. Если функция правится еще до ее первого вызова, сбросом результатов компиляции можно естественно пренебречь.

3. Подмена функций

Как известно, имена всех использующихся классов и функций лежат в скомпилированном class файле открытым текстом. Поэтому первым инструментом при исследовании написанных на Яве программ и является не какой-нибудь хитрый декомпилятор, а обыкновенный grep. Присутствие java.io.* выдает работу с файлами, BigInteger.modPow может быть признаком RSA и так далее. Для разработчиков этот факт естественно неприятен. Встает вопрос, можно ли вызвать функцию так, чтобы её не было видно в импорте?

Первое что приходит в голову - использовать reflection. Самой функции тогда действительно не будет видно. Беда в том, что вместо нее появится пакет java.lang.reflect, в частности например Method.invoke, который не менее безошибочно обратит на себя внимание. Поэтому уточним задачу: нужно вызвать функцию так, чтобы вызывающий класс остался "чистым". Импортирование Unsafe естественно тоже запрещено.

Разберемся сначала с теорией. Как уже упоминалось выше, вызов функции в байткоде осуществляется по ее порядковому номеру в кэше. По этому индексу в кэше лежит в свою очередь указатель на соответствующую func_struct (строго говоря не только и не всегда, но в самые тонкости лезть не будем). Чтобы переправить функцию A на B, достаточно подменить этот самый указатель, и тогда последующие обращения к A будут на самом деле вызывать B, несмотря на то, что код останется прежним. Тот же самый алгоритм будет работать кстати и в отношении открытых переменных (полей) классов.

Допустим, мы по каким-либо причинам хотим спрятать возведение числа в степень, то есть вызов Math.pow(...). Давайте подумаем и распишем необходимые для этого действия. Во-первых нужен конечно подходяший вызов какой-нибудь функции A, которую нам не жалко подменить. С этим сложностей не предвидится - можно взять из стандарных библиотечных, можно просто определить собственную. Значительно сложнее и интересней будет вторая половина задачи, т. е. получение указателя на Math.pow. Чтобы найти указатель в массиве class_struct.functions нужно для начала загрузить класс Math (нигде не сказано, что он уже присутствует в системе) и получить указатель на его class_struct. Так как Math не должен присутствовать в импорте, то загружать придется через Class.forName("java.lang.Math").

Все бы хорошо, но тогда в импорте окажется Class.forName. Она правда встречается на порядок чаще, чем reflection и особых подозрений вряд ли вызовет, но как известно лучше уж перестараться, чем наоборот. Поэтому вызов Class.forName мы тоже спрячем по описанной выше схеме, с той только разницей, что Class уже загружен и получить его структуру - задача тривиальная. План действий выглядит в итоге следующим образом:

  1. Определение "ненужных" функций A и B
  2. Получение указателя на Class.forName
  3. Перенаправление B на Class.forName
  4. Вызов B с параметром "java.lang.Math"
  5. Получение указателя на Math.pow
  6. Перенаправление A на Math.pow
  7. Вызов A

Теперь проследим эти теоретические построения в коде. ValidatorMain.wktm станет у нас Class.forName, а KeyValidator.edtb сыграет почетную роль Math.pow. Вспомогательная функция ValidatorMain.atbz возьмет на себя задачу, которую мы до сих пор в наших рассуждениях ловко обошли молчанием, а именно собственно поиск нужного указателя в массиве functions. Казалось бы тривиальное действие, но дело в том, что определенного порядка в расположении функций судя по всему не существует, он меняется от версии к версии практически случайным образом и задается похоже методом среднепотолочного тыка. Посмотрите например на безобразную конструкцию в TestLoop.doSomething, где мы имеем дело всего с двумя функциями и я жестко задал их номер в массиве. Поэтому остается только громоздкий поиск по имени и сигнатуре, что и проделывает ValidatorMain.atbz.

Отсюда по шагам, сверяйтесь с кодом примеров. Получаем указатель на Class.forName:

Код (Text):
  1.  
  2.     long class_struct = unsafe.getAddress(unsafe.getAddress(this_struct+15*4)+4);
  3.     long func_data = atbz(class_struct, 0x4E726F66, 0x25);
  4.  

Пишем его в кэш на место ValidatorMain.wktm и загружаем Math:

Код (Text):
  1.  
  2.     unsafe.putAddress(this_const_cache+33*4, func_data);
  3.     Object obj = wktm(s);
  4.  

С целью немного усложнить crackme строка s тоже собирается динамически, но это уже мелочи. Получаем указатель на Math.pow:

Код (Text):
  1.  
  2.     long math_struct = unsafe.getAddress(UnsafeUtilEx.ObjectToAddress(v)+8);
  3.     long math_func_data = atbz(math_struct, 0x00776F70, 0x5);
  4.  

Заменяем KeyValidator.edtb на Math.pow:

Код (Text):
  1.  
  2.     unsafe.putAddress(k_const_cache+13*4, math_func_data);
  3.  

Теперь вызов edtb(name, i) в KeyValidator.mshv возведет name в степень i. Причем импорт класса KeyValidator остался совершенно "чистым".

4. Заключение

Не исключено, что при применение описанных приемов в "промышленном масштабе" придется учитывать ряд дополнительных, не упомянутых здесь тонкостей. Некоторое представление о них можно получить отсюда: http://www.cracklab.ru/f/index.php?action=vthread&topic=5363&forum=2&page=-1 С другой стороны большинство замечаний bsl_zcs носят опять же чисто академический характер: опции -XX: точно так же недокументированы, как и Unsafe, и систем скажем с -XX:CompileThreshold=2 мне еще никогда не встречалось и очень вряд ли встретятся.

До сих пор я цитировал только отдельные поля из структур виртуальной машины. В следующей, третьей, части я приведу полные (по возможности :smile3:) структуры с описанием.

Приложения

unsafe_java_2_code.zip (8 KB) - Примеры к статье

java_crackme.zip (4 KB) - Crackme

(c) Stiver, 09.09.2006


0 1.663
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532