Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Сегодня мы продолжим знакомиться с обфускаторами, принципами их анализа и борьбы с ними. Наш пациент — обфускатор для 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