Хакер - В обход стражи. Как вскрывают приложения, защищенные аппаратным ключом Sentinel

Хакер - В обход стражи. Как вскрывают приложения, защищенные аппаратным ключом Sentinel

hacker_frei

https://t.me/hacker_frei

МВК

Ре­верс‑инжи­ниринг написан­ных на Java при­ложе­ний, если раз­работ­чики не позабо­тились заранее об их защите, обыч­но не пред­став­ляет труд­ностей. Имен­но что­бы усложнить жизнь хакерам и иссле­дова­телям, исполь­зуют­ся раз­личные инс­тру­мен­ты, один из которых — защита с аппа­рат­ным клю­чом Sentinel. Сегод­ня мы рас­смот­рим спо­соб обхо­да такой защиты.

Тем, кто читал мои пре­дыду­щие статьи, пос­вящен­ные за­щите при­ложе­ний на Java, извес­тно самое сла­бое мес­то такой защиты: проз­рачность байт‑кода и невыно­симая лег­кость вос­ста­нов­ления его до исходни­ков.

По­это­му раз­работ­чики таких инс­тру­мен­тов стре­мят­ся как мож­но хит­рее спря­тать код от любопыт­ных глаз хакеров, исполь­зуя раз­личные при­емы: обфуска­цию, ком­пиляцию в натив, шиф­рование. Наг­лядный при­мер пос­ледне­го мы начали раз­бирать в статье «Без­защит­ная Java. Лома­ем Java bytecode encryption». Тог­да мы оста­нови­лись на инлайн‑пат­чинге рас­шифро­ван­ного байт‑кода пря­мо из биб­лиоте­ки аген­та JVMTI. Сегод­ня мы про­дол­жим эту тему и научим­ся пат­чить непос­редс­твен­но шиф­рован­ный код.

В качес­тве при­мера мы возь­мем при­ложе­ние, защищен­ное Sentinel Licensing API Java Class Library — спе­циаль­ной биб­лиоте­кой защиты Java-клас­сов, пос­тавля­емой к клю­чам типа Hasp. Почитать о том, как устро­ена эта биб­лиоте­ка, мож­но на сай­те раз­работ­чика. Само при­ложе­ние защище­но клю­чом Hasp, в его рабочем катало­ге при­сутс­тву­ют харак­терные для Sentinel фай­лы hasp_rt.exehasp_windows_x64_34344.dllhaspvlib_34344.dll и спе­цифи­чес­кие для Java HASPJava.dllHASPJava_x64.dllsntljavaclsrt.dllsntljavaclsrt_x64.dll. Detect It Easy пред­ска­зуемо ука­зыва­ет на NetHASP dongle reference и Hardlock dongle reference.

Мы по опы­ту зна­ем, что ломать эти биб­лиоте­ки «в лоб» луч­ше даже не пытать­ся, они весь­ма сурово вир­туали­зова­ны и защище­ны от отладки и модифи­кации. Поэто­му смот­рим на ком­пилиро­ван­ные клас­сы, содер­жащи­еся внут­ри JAR. Там тоже все мрач­но: кро­ме глав­ного клас­са, все осталь­ные пошиф­рованы с высокой энтро­пией, при­мер­но как опи­сано в статье «Без­защит­ная Java. Лома­ем Java bytecode encryption». За исклю­чени­ем того, что крип­тор под­клю­чает­ся не как JavaAgent при запус­ке при­ложе­ния, а из глав­ного клас­са, по сути пред­став­ляюще­го собой заг­рузчик при­ложе­ния c единс­твен­но откры­тым кодом:

public static void main(String[] stringArray) {

try {

String string;

String string2 = System.getProperty("java.class.path");

int n = Math.max(string2.lastIndexOf("\"), string2.lastIndexOf("/"));

String string3 = string = string2.substring(0, ++n);

if (JavaClsEntry.isWindows()) {

string3 = string3 + "sntljavaclsrt";

if (0 == System.getProperty("sun.arch.data.model").compareTo("64")) {

string3 = string3 + "_x64";

}

string3 = string3 + ".dll";

} else if (JavaClsEntry.isLinux()) {

string3 = string3 + "libsntljavaclsrt_x86_64.so";

} else {

return;

}

File file = new File(string3);

string3 = file.getAbsolutePath();

System.load(string3);

Class<?> clazz = Class.forName("com.MainApp");

Method method = clazz.getMethod("main", String[].class);

method.invoke(null, new Object[]{stringArray});

}

catch (Exception exception) {

exception.printStackTrace();

}

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

Как же нам теперь быть? Ведь, пос­коль­ку здесь не про­исхо­дит пря­мая под­мена JIT-ком­пилято­ра через JavaAgent, пред­ложен­ный в упо­мяну­той выше статье спо­соб не годит­ся. Поп­робу­ем под­клю­чить­ся отладчи­ком непос­редс­твен­но к JIT-ком­пилято­ру. Нем­ного покурив докумен­тацию, обна­ружи­ваем в биб­лиоте­ке JVM.DLL такую фун­кцию:

JVM_DefineClassWithSource(JNIEnv *env, const char *name, jobject loader, const jbyte *buf, jsize len, jobject pd, const char *source)

Как вид­но из опи­сания, эта фун­кция ком­пилиру­ет класс из мас­сива байт‑кода. Дли­на мас­сива, наз­вание клас­са и даже исходно­го Java-модуля при­лага­ется. При наличии клю­ча, уста­новив в нашем любимом отладчи­ке x64dbg точ­ку оста­нова на вход этой фун­кции, мы получа­ем в регис­тре R9 рас­шифро­ван­ный класс, начиная с сиг­натуры CAFEBABE, и его дли­ну в регис­тре R14. В ито­ге его мож­но лег­ко сдам­пить на диск сле­дующей коман­дой:

savedata MainApp.class, R9, R14

В некото­рых, наибо­лее прос­тых слу­чаях, ког­да зашиф­рован толь­ко глав­ный класс, это­го впол­не дос­таточ­но, что­бы снять защиту целиком, — прос­то запако­выва­ем рас­шифро­ван­ный глав­ный класс вмес­то ста­рого и пере­опре­деля­ем его в манифес­те. Если зашиф­рован­ных клас­сов нем­ного, то их тоже мож­но сдам­пить по оче­реди и бороть­ся с при­вяз­кой к клю­чу уже в рас­шифро­ван­ных клас­сах. Обыч­но подоб­ная защита пред­полага­ет наличие в модуле уже Java-клас­сов Aladdin/Hasp.classAladdin/HaspApiVersion.classAladdin/HaspStatus.class и Aladdin/HaspTime.class. Одна­ко зашиф­рован­ных клас­сов может быть очень мно­го, так что дам­пить их вруч­ную по оче­реди неп­родук­тивно.

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

На пер­вый взгляд, задача выг­лядит жут­ковато — как я уже говорил, сен­тинелов­ские биб­лиоте­ки по‑взрос­лому пок­рипто­ваны и вир­туали­зиро­ваны, вдо­бавок сра­жают­ся с отладчи­ком при попыт­ке их трас­сиров­ки. Но мы все‑таки поп­робу­ем отло­вить момент рас­шифров­ки. Для начала прос­то прер­вемся в отладчи­ке, ког­да появ­ляет­ся сооб­щение об отсутс­твии клю­ча. По сте­ку вызовов мы с удив­лени­ем обна­ружи­ваем, что уши про­вер­ки клю­ча в сен­тинелов­ской биб­лиоте­ке sntljavaclsrt_x64 тор­чат из кер­нелов­ской фун­кции чте­ния фай­ла ReadFile, а еще точ­нее — из ее низ­коуров­невого вызова NtReadFile.

В биб­лиоте­ке sntljavaclsrt_x64 есть даже соот­ветс­тву­ющая экспор­тиру­емая фун­кция, под­меня­ющая сис­темную. Про­верить это дос­таточ­но прос­то: уста­новив точ­ку оста­нова на вызов ReadFile из java.dll (выделе­но на сте­ке вызовов на скрин­шоте), мы обна­ружи­ваем, что ReadFile счи­тыва­ет уже впол­не себе рас­шифро­ван­ные бло­ки байт‑кода пор­циями по 0x400 байт.

Та­ким обра­зом, мы узна­ли механизм встра­ива­ния сен­тинелов­ской защиты в JIT-ком­пилятор Java. Одна­ко на этот раз нам хотелось бы пой­ти даль­ше, чем в пре­дыду­щей статье, и выяс­нить алго­ритм шиф­рования байт‑кода. Для это­го мы сна­чала ста­вим точ­ку оста­нова на вызов ReadFile из java.dll.

Что­бы отфиль­тро­вать все побоч­ные чте­ния из дру­гих фай­лов, уста­новим филь­тр на чте­ние бло­ков толь­ко раз­мером в 0x400 байт. Для это­го в усло­вии оста­нов­ки про­писы­ваем r8==0x400. По оста­нов­ке в этой точ­ке адрес буфера чте­ния будет находить­ся в регис­тре RDX — ста­вим точ­ку оста­нова на пер­вый байт это­го буфера. Выпол­нение оста­нав­лива­ется, ког­да в буфер записы­вают­ся зашиф­рован­ные дан­ные. Чте­ние бай­та из это­го буфера тор­мозит­ся в очень инте­рес­ном мес­те — мы видим обфусци­рован­ный код sntljavaclsrt_x64:

00007FFC46EB31F5 | movzx ecx,byte ptr ds:[r12+rax]

00007FFC46EB31FA | mov edx,edi

00007FFC46EB31FC | shr edx,18

00007FFC46EB31FF | add edx,ecx

00007FFC46EB3201 | movzx ecx,dl

00007FFC46EB3204 | movzx r11d,byte ptr ds:[r13+rcx]

00007FFC46EB320A | lea rcx,qword ptr ds:[7FFC46EB1A46]

00007FFC46EB3211 | mov edx,ecx

00007FFC46EB3213 | and edx,ebp

00007FFC46EB3215 | xor rcx,rbp

00007FFC46EB3218 | lea rcx,qword ptr ds:[rcx+rdx*2]

00007FFC46EB321C | jmp rcx

В регис­тре R12 — ука­затель на зашиф­рован­ный буфер, в регис­тре RDI — сме­щение в нем, в регис­тре R13 — ука­затель на некую таб­лицу переко­диров­ки. Резуль­тат это­го фраг­мента кода хра­нит­ся в регис­тре R117FFC46EB1A46 — слег­ка закоди­рован­ный адрес сле­дующей инс­трук­ции. Нем­ного поп­рыгав по обфусци­рован­ному коду, находим сле­дующий фраг­мент:

00007FFC46EB32DF | mov ecx,edi

00007FFC46EB32E1 | shr ecx,10

00007FFC46EB32E4 | add ecx,r11d

00007FFC46EB32E7 | movzx ecx,cl

00007FFC46EB32EA | movzx ecx,byte ptr ds:[r13+rcx+100]

00007FFC46EB32F3 | mov edx,edi

00007FFC46EB32F5 | shr edx,8

00007FFC46EB32F8 | add edx,ecx

00007FFC46EB32FA | movzx ecx,dl

00007FFC46EB32FD | movzx edx,byte ptr ds:[r13+rcx+200]

00007FFC46EB3306 | lea rcx,qword ptr ds:[7FFC46EB1A22]

00007FFC46EB330D | mov ebx,ecx

00007FFC46EB330F | and ebx,ebp

00007FFC46EB3311 | xor rcx,rbp

00007FFC46EB3314 | lea rcx,qword ptr ds:[rcx+rbx*2]

00007FFC46EB3318 | jmp rcx

Тут на выходе резуль­тат уже в RDX. И наконец, финаль­ный фраг­мент, который пред­став­ляет собой конец цик­ла по всем бай­там (дли­на в R9) и условный переход на начало:

00007FFC46EB3273 | movzx ecx,dl

00007FFC46EB3276 | add ecx,edi

00007FFC46EB3278 | movzx ecx,cl

00007FFC46EB327B | movzx ecx,byte ptr ds:[r13+rcx+300]

00007FFC46EB3284 | mov byte ptr ds:[r12+rax],cl

00007FFC46EB3288 | add edi,1

00007FFC46EB328B | add rax,1

00007FFC46EB328F | mov ecx,r8d

00007FFC46EB3292 | and ecx,ebp

00007FFC46EB3294 | lea rdx,qword ptr ds:[7FFC46EB1A92]

00007FFC46EB329B | mov ebx,edx

00007FFC46EB329D | and ebx,ebp

00007FFC46EB329F | xor rdx,rbp

00007FFC46EB32A2 | cmp rax,r9

00007FFC46EB32A5 | lea rcx,qword ptr ds:[r10+rcx*2]

00007FFC46EB32A9 | lea rdx,qword ptr ds:[rdx+rbx*2]

00007FFC46EB32AD | cmove rdx,rcx

00007FFC46EB32B1 | jmp rdx

Как вид­но, нес­мотря на всю наворо­чен­ность защиты, алго­ритм рас­шифров­ки прос­той как две копей­ки. Он пред­став­ляет собой четыре таб­личных пре­обра­зова­ния. В перево­де на понят­ный язык прог­рамми­рова­ния он выг­лядит при­мер­но так (array — рас­кодиру­емый фраг­мент, table — таб­лица):

for (int i=0;i<array.Length;i++)

{

byte r11 = table[(byte)(array[i] + (i >> 0x18))];

byte ecx = table[(byte)(r11 + (i >> 0x10))+0x100];

byte edx = table[(byte)(ecx + (i >> 0x8)) + 0x200];

array[i] = table[(byte)(edx + i)+0x300];

}

Таб­лица переко­диров­ки раз­мером 0x400 байт, ука­затель на которую находит­ся в регис­тре R13, прек­расно дам­пится на диск из отладчи­ка при помощи сле­дующей кон­соль­ной коман­ды:

savedata table.bin, R13, 0x400

Пос­ле это­го мож­но писать прос­тень­кий пакет­ный рас­кодиров­щик для любого количес­тва клас­сов.

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

for (int i=0;i<array.Length;i++)

{

byte a=byte(indexOf(&table[0x300],array[i])-i);

byte b=byte(indexOf(&table[0x200],a)-(i >> 0x8));

byte c=byte(indexOf(&table[0x100],b)-(i >> 0x10));

array[i]=byte(indexOf(&table,c)-(i >> 0x18));

}

Это озна­чает, что теперь нам не нуж­но извра­щать­ся, конс­тру­ируя инлайн‑патч. Имея валид­ный ключ, мы можем рас­шифро­вать нуж­ный класс, поп­равить его, нап­ример при помощи dirtyJoe (как — я опи­сал в статье «Гряз­ный Джо. Взла­мыва­ем Java-при­ложе­ния с помощью dirtyJOE»), затем сно­ва закоди­ровать файл и перепа­ковать его в JAR-архив.

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



Report Page