На какие толь­ко ухищ­рения не при­ходит­ся идти раз­работ­чикам прог­рамм на 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, 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

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

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии