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

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

hacker_frei

https://t.me/hacker_frei

МВК 

Код на 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.dlljava.dlljli.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

Здесь в локаль­ной перемен­ной [rsp+1C0h+var_170] име­ется струк­тура callbacks, которая очи­щает­ся по адре­су 18002C0F8. Затем по адре­су 18002C102 в нее затал­кива­ется обра­бот­чик ClassFileLoadHook, и, наконец, сле­дует вызов SetEventCallbackscall qword ptr [rax+3C8h]. Итак, мы наш­ли callback-фун­кцию, выпол­няющую рас­шифров­ку байт‑кода клас­са перед его ком­пиляци­ей. В нашем слу­чае это sub_18002BDC0. По спе­цифи­кации гуг­лим опи­сание дан­ного обра­бот­чика:

void JNICALL

eventHandlerClassFileLoadHook(

jvmtiEnv * jvmtienv,

JNIEnv * jnienv,

jclass class_being_redefined,

jobject loader,

const char* name, // Имя класса

jobject protectionDomain,

jint class_data_len, // Размер класса

const unsigned char* class_data, // Загруженный из файла криптованный байт-код

jint* new_class_data_len, // Размер декриптованных данных

unsigned char** new_class_data // Декриптованные данные

)

Те­перь, запус­тив прог­рамму в отладчи­ке x6dbg, уста­нав­лива­ем брейк‑пой­нт на этот обра­бот­чик. Брейк‑пой­нт будет сра­баты­вать на заг­рузке и рас­шифров­ке каж­дого клас­са. На вхо­де в него парамет­ром name (регистр rsi) мы видим имя клас­са, а по завер­шении обра­бот­чик отдаст нам в new_class_data рас­шифро­ван­ный байт‑код, который мож­но дам­пить и спо­кой­но деком­пилиро­вать. Даль­нейшие наши дей­ствия — дело тех­ники: пер­во‑напер­во ста­вим логиро­вание заг­ружа­емых клас­сов (текст жур­нала {s:rsi}).

Точ­ка оста­нова на обра­бот­чике

Те­перь при работе прог­раммы в жур­нал отладчи­ка будет записы­вать­ся пос­ледова­тель­ность заг­ружа­емых прог­раммой клас­сов, по которой мы лег­ко отсле­жива­ем класс, ответс­твен­ный за зап­рос на сер­вер и про­вер­ку лицен­зии. Допус­тим, мы наш­ли этот класс и, для при­мера, у нас он называ­ется com/coreui/app/license/wizard/licenseWizard. Добавив в этот брейк‑пой­нт усло­вие для оста­нов­ки (strcmp(utf8(rsi),"com/coreui/app/license/wizard/licenseWizard")), мы можем оста­нав­ливать прог­рамму на нуж­ном нам клас­се, который потом вруч­ную сдам­пим в файл и затем деком­пилиру­ем любым пон­равив­шимся нам деком­пилято­ром.

Де­ком­пилиро­вав его, мы обна­ружи­ваем и мес­то одно­бай­тового пат­ча — для отвязки от про­вер­ки лицен­зии дос­таточ­но закоро­тить метод j(), пос­тавив в байт‑коде ret (B1) по сме­щению 1B36 от начала клас­са. Фак­тичес­ки задача решена: пра­вить байт‑код без переком­пиляции мы уже уме­ем. Одна­ко тут не все так прос­то.

Пра­вить нам при­дет­ся крип­тован­ный код, а ана­лиз крип­тора JavaLoader в IDA показы­вает, что пок­рипто­ван он отнюдь не XOR’ом, а по‑взрос­лому, несим­метрич­ным алго­рит­мом с исполь­зовани­ем эллипти­чес­ких кри­вых. То есть ха­ляв­ный спо­соб пат­ча здесь не годит­ся. Более того: пос­коль­ку алго­ритм несим­метрич­ный, мы даже перек­рипто­вать исправ­ленный код не смо­жем, не зная при­ват­ного клю­ча.

В ито­ге скла­дыва­ется тра­гико­мичес­кая ситу­ация: мы вро­де бы сло­мали про­вер­ку лицен­зии, но работать она может толь­ко из отладчи­ка. Ины­ми сло­вами, мы оста­нав­лива­емся на заг­рузке нуж­ного клас­са, пос­ле его рас­шифров­ки пат­чим руками байт‑код и запус­каем при­ложе­ние даль­ше. Мож­но, конеч­но, авто­мати­зиро­вать этот про­цесс скрип­том, но все рав­но выг­лядит как‑то нес­портив­но для ува­жающе­го себя хакера. Как же нам вык­рутить­ся из этой неп­рият­ной ситу­ации?

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

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

// Декрипт байт-кода, возвращает в rax длину декриптованного блока или 0 при неудаче

18002BF0C E8 0F FA FF FF call sub_18002B920

18002BF11 8B D0 mov edx, eax

18002BF13 85 C0 test eax, eax

18002BF15 7F 10 jg short loc_18002BF27 // Если 0, то ошибка

18002BF17 48 8B D5 mov rdx, rbp // rbp — имя класса

18002BF1A 48 8D 0D 57 76 0E 00 lea rcx, aDecryptionFail ; "Decryption failed: %s\n"

18002BF21 E8 CA FC FF FF call sub_18002BBF0 // Выход из программы с ошибкой

18002BF26 CC db 0CCh

18002BF27 loc_18002BF27:

18002BF27 49 8B 0F mov rcx, [r15] // Добавление к байт-коду сигнатуры CAFEBABE

18002BF2A 48 8B 84 24 A0 00 00 00 mov rax, [rsp+A0] // Указатель на расшифрованный байткод

18002BF32 48 89 08 mov [rax], rcx

18002BF35 83 C2 08 add edx, 8

При­дет­ся пожер­тво­вать про­вер­кой пра­виль­нос­ти рас­шифров­ки, ведь мы и так уве­рены, что у нас все дек­рипту­ется пра­виль­но. А если нет, прог­рамма все рав­но работать не будет. Так что мы исполь­зуем этот «прос­вет» в пару десят­ков бай­тов с поль­зой. Дру­гое допуще­ние — у нас не хва­тит мес­та на пол­ную про­вер­ку име­ни клас­са, при­дет­ся огра­ничить­ся пос­ледни­ми 4 бай­тами. В про­екте явно нет дру­гого клас­са с име­нем дли­ной 45 байт и окон­чани­ем -ard. Итак, нам нуж­но пос­ле рас­шифров­ки байт‑кода про­верить имя клас­са с окон­чани­ем -ard в задан­ной позиции и в слу­чае успе­ха поменять в рас­шифро­ван­ном байт‑коде байт по сме­щению 1B36 на B1:

18002BF13 | 48:8B8424 A0000000 | mov rax,qword ptr ss:[rsp+A0] // Указатель на расшифрованный байт-код

18002BF1B | 817D 2E 61726400 | cmp dword ptr ss:[rbp+2A],647261 // Имя класса+42=="ard"\0?

18002BF22 | 75 08 | jne javaloader.18002BF2C

18002BF24 | C680 361B0000 B1 | mov byte ptr ds:[rax+1B36],B1 // Если да, то поменять нужный байт в байт-коде на ret

18002BF2B | 90 | nop

18002BF2C | 49:8B0F | mov rcx,qword ptr ds:[r15]

18002BF2F | 90 | nop

18002BF30 | 90 | nop

18002BF31 | 90 | nop

За­пус­каем прог­рамму, про­веря­ем — все работа­ет! Итак, мы разоб­рались с одним из спо­собов сок­рытия байт‑кода в при­ложе­нии на Java. Я думаю, что, вни­матель­но про­читав статью, ты уже понял, что этот спо­соб далеко не единс­твен­ный и количес­тво вари­антов и модифи­каций зависит толь­ко от извра­щен­ной фан­тазии раз­работ­чика. Тем не менее, наде­юсь, эта статья под­ска­жет пыт­ливому уму нап­равле­ние, в котором сле­дует дви­гать­ся, если воз­никнут нес­тандар­тные ситу­ации. Воз­можно, я даже ког­да‑нибудь опи­шу самые инте­рес­ные из них попод­робнее.

Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei



Report Page