Беззащитная Java. Ломаем Java bytecode encryption

Код на Java не так прост, как кажет­ся. На пер­вый взгляд взлом Java-при­ложе­ния выг­лядит нес­ложной задачей, бла­го под­ходящих деком­пилято­ров великое мно­жес­тво. Но если тебе доведет­ся стол­кнуть­ся с защитой Bytecode encryption, задача мно­гок­ратно усложня­ется. В этой статье я под­робно рас­ска­жу, как бороть­ся с этой напастью.

У начина­юще­го прог­раммис­та, нем­ного осво­ивше­го Java, соз­дает­ся лож­ное впе­чат­ление, что писать прог­раммы на ней не прос­то, а очень прос­то. У начина­юще­го хакера подоб­ное впе­чат­ление может сло­жить­ся о взло­ме написан­ных на Java прог­рамм. И вправ­ду, делов‑то: берешь обыч­ный ZIP-архи­ватор, рас­паковы­ваешь JAR-фай­лы, затем выбира­ешь деком­пилятор на свой вкус и деком­пилиру­ешь получен­ные CLASS-фай­лы хочешь по одно­му, а хочешь — весь про­ект разом. На выходе получа­ешь исходни­ки про­екта на блю­деч­ке.

warning

Статья име­ет озна­коми­тель­ный харак­тер и пред­назна­чена для спе­циалис­тов по безопас­ности, про­водя­щих тес­тирова­ние в рам­ках кон­трак­та. Автор и редак­ция не несут ответс­твен­ности за любой вред, при­чинен­ный с при­мене­нием изло­жен­ной информа­ции. Рас­простра­нение вре­донос­ных прог­рамм, наруше­ние работы сис­тем и наруше­ние тай­ны перепис­ки прес­леду­ются по закону.

Прав­да, иног­да при­ходит­ся повозить­ся с обфуска­цией или поковы­рять­ся гряз­ными руками в JVM-байт‑коде (про­цесс я опи­сывал в статье «Гряз­ный Джо. Взла­мыва­ем Java-при­ложе­ния с помощью dirtyJOE»). Все рав­но это выг­лядит нам­ного про­ще маеты с натив­ным кодом под злоб­ными про­тек­торами типа «Фе­миды» или даже дот­нет‑при­ложе­ниями.

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

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

При бли­жай­шем рас­смот­рении нам бро­сает­ся в гла­за, что исполня­емый модуль прог­раммы — прос­тень­кий заг­рузчик Java Runtime Environment, вызыва­ющий javaw c длин­нющей коман­дной стро­кой, которая содер­жит перечень JAR-модулей и биб­лиотек. В кон­це это­го спис­ка мы видим имя глав­ного клас­са.

За­пус­тив поиск по JAR-архи­вам, мы находим сам файл клас­са. И вот здесь нас под­жида­ет неп­рият­ный сюр­приз: все име­ющиеся в нашем рас­поряже­нии деком­пилято­ры нап­рочь отка­зыва­ются работать с этим фай­лом, и даже dirtyJOE руга­ется на непод­держи­ваемый фор­мат. Открыв файл в hex-редак­торе, мы обна­ружи­ваем, что руга­ется он впол­не спра­вед­ливо: от нор­маль­ного ском­пилиро­ван­ного CLASS-фай­ла здо­рово­го челове­ка здесь оста­лась толь­ко сиг­натура CAFEBABE, все осталь­ное содер­жимое запол­нено высоко­энтро­пий­ным белым шумом упа­кован­ных или зашиф­рован­ных дан­ных.

Вид зашиф­рован­ного фай­ла Main.class в HEX-редак­торе

Гля­дя на эту кар­тину, я испы­тывал силь­ное чувс­тво дежавю. С чем‑то подоб­ным я уже стал­кивал­ся, ког­да раз­бирал bytcode-обфуска­тор PHP SourceGuardian или хотя бы тот же .NET Reactor: кто‑то явно вкли­нива­ется в про­цесс заг­рузки JVM-байт‑кода и рас­шифро­выва­ет его на лету. Но как такое мож­но реали­зовать на Java?

Для отве­та на этот воп­рос поп­робу­ем слег­ка углу­бить­ся в теорию фун­кци­они­рова­ния Java-машины. Пос­коль­ку она столь же кросс‑плат­формен­на, как и .NET (на самом деле в опре­делен­ном смыс­ле даже более кросс‑плат­формен­на), то байт‑код JVM не интер­пре­тиру­ется, а однократ­но ком­пилиру­ется в плат­формен­но зависи­мый натив­ный код при заг­рузке клас­са. Мы даже зна­ем, как называ­ется дан­ный про­цесс, — JIT-ком­пиляция (just in time, «вре­мен­ная ком­пиляция на лету»). А зна­чит, точ­но так же, как и в .NET, мы можем при­атта­чить­ся во вре­мя работы прог­раммы отладчи­ком x64dbg к про­цес­су javaw.exe, что поз­волит нам отла­живать ском­пилиро­ван­ный натив­ный код как род­ной.

Прав­да, удо­воль­ствия в этом мало, пос­коль­ку ском­пилиро­ван­ный код нед­ружес­твен­но выг­лядит (в отли­чие, ска­жем, от резуль­тата JIT-ком­пиляции того же .NET). Код силь­но опти­мизи­рован, мно­гопо­точен и шустр, но край­не неудо­бен для ревер­синга.

При­мер откомпи­лиро­ван­ного JIT натив­ного кода

Ко­неч­но, опре­делен­ные вкус­ности име­ются и там. К при­меру, если покопать­ся в модулях jvm.dll, java.dll, jli.dll и дру­гих, то мож­но обна­ружить мно­го стан­дар­тных базовых фун­кций, упро­щающих про­цесс отладки. Воз­можно, я ког­да‑нибудь рас­ска­жу о них в дру­гой статье. Мно­го матери­ала по этой теме мож­но най­ти в интерне­те: нап­ример, перевод­ные статьи, пуб­ликовав­шиеся на «Хаб­рахаб­ре»: «Java HotSpot JIT ком­пилятор — устрой­ство, монито­ринг и нас­трой­ка» и «Как работа­ет Graal — JIT-ком­пилятор JVM на Java». Но сей­час цель у нас дру­гая: разоб­рать­ся, как имен­но про­исхо­дит про­цесс шиф­рования и дешиф­рования байт‑кода, а так­же вос­ста­новить исходный код Java из шиф­рован­ного. В .NET, к при­меру, нам уда­лось най­ти вход JIT-ком­пилято­ра, поп­робу­ем это сде­лать и для Javа.

Сно­ва кос­нусь теории, не углубля­ясь в под­робнос­ти. По сути, для реали­зации под­мены байт‑кода в JVM обыч­но исполь­зует­ся два основных интерфей­са, каж­дый из которых реали­зует собс­твен­ный под­ход. Один из них называ­ется JVMCI — JVM compiler interface — и слу­жит непос­редс­твен­но для под­клю­чения собс­твен­ного JIT-ком­пилято­ра Java, написан­ного опять же на Java. По понят­ным при­чинам это явно не наш слу­чай (у нас все клас­сы зашиф­рованы, начиная с глав­ного).

А вот вто­рой, JVM Tool Interface (JVMTI), похоже, имен­но то, что нам надо, поэто­му рас­смот­рим его попод­робнее. JVM Tool Interface — это чер­тов­ски полез­ный интерфейс вза­имо­дей­ствия с вир­туаль­ной машиной JVM. Он поз­воля­ет рас­ширить ее фун­кци­ональ­ность, не зат­рагивая код. Для пол­ного опи­сания воз­можнос­тей это­го инс­тру­мен­та одной статьи не хва­тит, поэто­му я сно­ва отсы­лаю любопыт­ных к мат­части.

Все полез­няшки это­го интерфей­са реали­зуют­ся через так называ­емые аген­ты — внеш­ние пла­гины. У них мно­жес­тво фун­кций, но глав­ное — они дают пол­ный дос­туп к заг­ружа­емо­му байт‑коду и кон­троль над ним. Имен­но это нам сей­час и нуж­но. Аген­ты заг­ружа­ются из javaw, для это­го нуж­но ука­зать спе­циаль­ные парамет­ры в манифес­те или же в коман­дной стро­ке. К при­меру, самый рас­простра­нен­ный тип аген­тов — javaagent. Они написа­ны на Java и под­клю­чают­ся через соот­ветс­тву­ющую коман­дную стро­ку javaagent:agent.jar. Этот тип аген­тов тоже име­ет пол­ный дос­туп к байт‑коду, его под­мена час­то исполь­зует­ся в обфуска­ции и модифи­кации кода, но не в нашем слу­чае.

В нашем при­ложе­нии задей­ство­ван натив­ный JVMTIAgent, вызов которо­го мож­но обна­ружить, вни­матель­но про­ана­лизи­ровав коман­дную стро­ку и най­дя в ней сле­дующий параметр: -agentlib:JavaLoader. JVMTIAgent пред­став­ляет собой динами­чес­кую биб­лиоте­ку (в слу­чае Windows это DLL, а в слу­чае, нап­ример, «Линук­са» — SO), из которой экспор­тиру­ются какие‑то из сле­дующих фун­кций:

  • JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved); — эта фун­кция вызыва­ется при запус­ке аген­та, если он ука­зан в парамет­ре коман­дной стро­ки -agentpath: или -agentlib:, как в нашем слу­чае;
  • JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char* options, void* reserved); — дан­ная фун­кция вызыва­ется, если агент не заг­ружа­ется при запус­ке, тог­да мы сна­чала под­клю­чаем­ся к целево­му про­цес­су, а затем отправ­ляем коман­ду соот­ветс­тву­юще­му целево­му про­цес­су для заг­рузки аген­та;
  • JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm); — фун­кция вызыва­ется при уда­лении аген­та, опци­ональ­но.

Мы уже обна­ружи­ли в рабочем катало­ге динами­чес­кую биб­лиоте­ку с ори­гиналь­ным наз­вани­ем JavaLoader.dll, при заг­рузке которой в дизас­сем­блер IDA и находим иско­мые фун­кции — перед нами дей­стви­тель­но JVMTIAgent, дек­рипту­ющий байт‑код при заг­рузке нуж­ного клас­са. Но как он это дела­ет?

Сно­ва покурим спе­цифи­кацию JVM Tool Interface, ссыл­ку на которую я при­вел выше. Общий прин­цип работы аген­та — уста­нов­ка собс­твен­ных поль­зователь­ских callback-обра­бот­чиков на опре­делен­ные события. В дан­ном слу­чае нас инте­ресу­ет событие JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, вызыва­емое сра­зу пос­ле заг­рузки мас­сива байт‑кода нуж­ного клас­са из фай­ла, но перед JIT-ком­пиляци­ей дан­ного клас­са. При­мер­ная реали­зация уста­нов­ки такого обра­бот­чика выг­лядит так:

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) { jvmtiEventCallbacks callbacks; jvmtiEnv * jvmtienv = jvmti(agent); jvmtiError jvmtierror; memset(&callbacks, 0, sizeof(callbacks)); callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook; // Новый обработчик JVMTI_EVENT_CLASS_FILE_LOAD_HOOK jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv, &callbacks, sizeof(callbacks)); // Установка обработчиков jvmtierror = (*jvmtienv)->SetEventNotificationMode(jvmtienv, JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, (jthread)NULL); // Разрешаем обработку события JVMTI_EVENT_CLASS_FILE_LOAD_HOOK

По­копав­шись при помощи IDA в коде про­цеду­ры Agent_OnLoad, находим соот­ветс­тву­ющее мес­то.

Ус­танов­ка обра­бот­чика события JVMTI_EVENT_CLASS_FILE_LOAD_HOOK в аген­те JavaLoader

Источник: xakep.ru

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *