March 27

Суровая жаба. Изучаем защиту Excelsior JET для программ на Java

МВК

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

Стро­ка 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. При­чем похоже, что наз­вания всех опи­сате­лей клас­сов рас­шифро­выва­ются сра­зу в одной про­цеду­ре при стар­те прог­раммы. В таком слу­чае поп­робу­ем извлечь из нее спи­сок всех клас­сов и положе­ние стро­ки име­ни клас­са в струк­туре опи­сате­ля.

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

sub_9BA640 endp
```

Итак, мы наконец‑то получи­ли локали­зацию стро­ки име­ни внут­ри опи­сате­ля клас­са — отно­ситель­ное сме­щение до него. Попут­но мы обна­ружи­ли еще одну инте­рес­ную струк­туру, адрес которой дан­ная про­цеду­ра получа­ет на вход. Назовем ее условно «струк­тура CDES» по сиг­натуре 53454443h в начале. Выг­лядит она так.

Струк­тура CDES

Это самая базовая струк­тура 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-адрес таблицы имен методов по адресам

Стра­тегия поис­ка такова: ищем в коде адрес прис­ваива­ния сиг­натуры 53454443h и, начиная с это­го мес­та, по мас­кам команд и сме­щений извле­каем нуж­ные адре­са. Идея весь­ма сколь­зкая, пос­коль­ку в каж­дой новой вер­сии ком­пилято­ра авто­ры могут менять этот ини­циали­заци­онный код, из‑за чего нам при­дет­ся перера­баты­вать алго­ритм, но как рабочий вари­ант такой под­ход сго­дит­ся.

Ты, навер­ное, заметил, что байт‑шиф­роваль­щик F9h тоже не ини­циали­зиру­ется в этой про­цеду­ре, он берет­ся из дру­гой стран­ной струк­туры, находя­щей­ся в самом начале сек­ции _config с сиг­натурой CPB.

Струк­тура

Как видишь, сме­щение это­го бай­та в струк­туре рав­но 1Ch. Пос­ледний кусочек паз­ла встал на мес­то, и оста­ется толь­ко пожелать уда­чи энту­зиас­там ревер­са это­го хит­рого ком­пилято­ра.