Я думаю, ты достаточно внимательно следишь за темой, поэтому тебе уже не нужно объяснять, что такое обфускатор. Но на всякий случай кратко напомню: это семейство программ, которые максимально запутывают код, затрудняя его анализ хакером или реверс‑инженером. Обычно такое актуально для скриптовых и высокоуровневых языков, компилируемых не в натив, а в промежуточный шитый код, из которого исходники восстанавливаются относительно легко различными декомпиляторами.
Так получилось, что до этого мы уделяли много внимания обфускаторам .NET, JavaScript и прочих языков, незаслуженно обойдя вниманием Java. Попробую исправить это упущение: сегодня мы рассмотрим необычный обфускатор — Zelix KlassMaster. Его необычность заключается даже не в том, что он под Java, а, скорее, в стране происхождения — он создан командой австралийцев. Шучу, конечно, в этом тоже нет ничего необычного. В общем, это очередной типичный обфускатор для Java, который мы выбрали в качестве примера для обучения.
Забегая вперед, скажу, что под версии этого обфускатора вплоть до 11-й уже имеется готовый деобфускатор ZelixKiller, поэтому, если тебе попалась старая версия и лень читать дальше, можешь им воспользоваться. Мы же попробуем самостоятельно разобрать версию Zelix 12.0.2 , не поддерживаемую упомянутым деобфускатором, и написать на нее свой деобфускатор.
Итак, у нас есть некое приложение для работы с электронной почтой, реализованное на Java. По понятным причинам Detect It Easy нам никак не поможет в определении обфускатора. На Zelix KlassMaster нам указывает строковая константа ZKM12., содержащаяся в пуле констант каждого класса.

На эту константу нет видимых ссылок из кода, и она содержит версию обфускатора Zelix KlassMaster, поэтому для других версий цифры будут иными.
Сразу смиренно принимаем неприятный факт, что исходные имена классов внутри архива . потеряны и называются a, — сейчас только ленивый оставляет их на всеобщее обозрение. Гораздо более неприятным сюрпризом оказывается то, что, во‑первых, в скомпилированном 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(, a(, a(, возвращающих 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]; }
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»
