Се­год­ня мы про­дол­жим зна­комить­ся с обфуска­тора­ми, прин­ципами их ана­лиза и борь­бы с ними. Наш паци­ент — обфуска­тор для Java под наз­вани­ем Zelix KlassMaster, внут­реннее устрой­ство которо­го мы под­робно иссле­дуем.

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

Так получи­лось, что до это­го мы уде­ляли мно­го вни­мания обфуска­торам .NET, JavaScript и про­чих язы­ков, незас­лужен­но обой­дя вни­мани­ем Java. Поп­робую испра­вить это упу­щение: сегод­ня мы рас­смот­рим необыч­ный обфуска­тор — Zelix KlassMaster. Его необыч­ность зак­люча­ется даже не в том, что он под Java, а, ско­рее, в стра­не про­исхожде­ния — он соз­дан ко­ман­дой авс­тра­лий­цев. Шучу, конеч­но, в этом тоже нет ничего необыч­ного. В общем, это оче­ред­ной типич­ный обфуска­тор для Java, который мы выб­рали в качес­тве при­мера для обу­чения.

За­бегая впе­ред, ска­жу, что под вер­сии это­го обфуска­тора вплоть до 11-й уже име­ется готовый де­обфуска­тор ZelixKiller, поэто­му, если тебе попалась ста­рая вер­сия и лень читать даль­ше, можешь им вос­поль­зовать­ся. Мы же поп­робу­ем самос­тоятель­но разоб­рать вер­сию Zelix 12.0.2 , не под­держи­ваемую упо­мяну­тым деоб­фуска­тором, и написать на нее свой деоб­фуска­тор.

Итак, у нас есть некое при­ложе­ние для работы с элек­трон­ной поч­той, реали­зован­ное на Java. По понят­ным при­чинам Detect It Easy нам никак не поможет в опре­деле­нии обфуска­тора. На Zelix KlassMaster нам ука­зыва­ет стро­ковая кон­стан­та ZKM12.0.2, содер­жаща­яся в пуле кон­стант каж­дого клас­са.

На эту кон­стан­ту нет видимых ссы­лок из кода, и она содер­жит вер­сию обфуска­тора Zelix KlassMaster, поэто­му для дру­гих вер­сий циф­ры будут ины­ми.

Сра­зу сми­рен­но при­нима­ем неп­рият­ный факт, что исходные име­на клас­сов внут­ри архи­ва .jar потеря­ны и называ­ются a, b, c, d, ... — сей­час толь­ко ленивый оставля­ет их на все­общее обоз­рение. Гораз­до более неп­рият­ным сюр­при­зом ока­зыва­ется то, что, во‑пер­вых, в ском­пилиро­ван­ном JVM-коде нап­рочь отсутс­тву­ют тек­сто­вые стро­ки в явном виде, а во‑вто­рых, некото­рые методы не деком­пилиру­ются. К при­меру, вот так выг­лядит код некото­рых методов, вос­ста­нов­ленный онлайн‑деком­пилято­ром jdec.app.

А вот резуль­тат работы популяр­ного офлайн‑деком­пилято­ра JD-GUI.

К сло­ву, деком­пилятор FernFlower все‑таки кое‑как справ­ляет­ся с задачей.

Оце­нив получен­ные резуль­таты, я нем­ного уди­вил­ся тому, что прос­тая и при­митив­ная конс­трук­ция:

// 56: aload #4
// 58: invokevirtual length : ()I
// 61: bipush #24
// 63: iload_2
// 64: ifeq -> 311
// 67: if_icmplt -> 297
// 70: goto -> 77

Она экви­вален­тна вот такому полура­зоб­ранно­му коду:

v0 = var4_3.length();
v1 = 24;
if (!var2_6) break block38;
if (v0 >= v1) {
}
** GOTO lbl43

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

Ос­тановим­ся под­робнее на более акту­аль­ной проб­леме — вос­ста­нов­лении зна­чений тек­сто­вых кон­стант, ведь без них мы даже не понима­ем, какой класс за какое дей­ствие прог­раммы отве­чает. Бег­ло прос­мотрев код любого клас­са, замеча­ем оби­лие ссы­лок на конс­трук­ции вида a(-6001, -16879), a(-6007, 18765), a(-6006, 20811)..., воз­вра­щающих String. Обра­щаем вни­мание, что пос­ледним методом каж­дого клас­са явля­ется метод такого вида:

private static String a(int n, int n2) {
int n3 = (n ^ 0xFFFFE88D) & 0xFFFF; // Маска для xor варьируется от класса к классу произвольным образом
if (q[n3] == null) {
int n4;
int n5;
char[] cArray = p[n3].toCharArray();
switch (cArray[0] & 0xFF) {
case 0: {
n5 = 63;
break;
}
// Длинный case по всем значениям от 0 до 255, представляющий собой по сути табличное преобразование, уникальное для каждого класса
case 254: {
n5 = 212;
break;
}
default: {
n5 = 197;
}
}
int n6 = n5;
int n7 = (n2 & 0xFF) - n6;
if (n7 < 0) {
n7 += 256;
}
if ((n4 = ((n2 & 0xFFFF) >>> 8) - n6) < 0) {
n4 += 256;
}
int n8 = 0;
while (n8 < cArray.length) {
int n9 = n8 % 2;
int n10 = n8;
char[] cArray2 = cArray;
char c = cArray[n10];
if (n9 == 0) {
cArray2[n10] = (char)(c ^ n7);
n7 = ((n7 >>> 3 | n7 << 5) ^ cArray[n8]) & 0xFF;
} else {
cArray2[n10] = (char)(c ^ n4);
n4 = ((n4 >>> 3 | n4 << 5) ^ cArray[n8]) & 0xFF;
}
++n8;
}
q[n3] = new String(cArray).intern();
}
return q[n3];
}

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

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

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

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

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

    Подписаться

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