• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Изучаем обфускатор для Java и придумываем собственный деобфускатор

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,178
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
Сегодня мы продолжим знакомиться с обфускаторами, принципами их анализа и борьбы с ними. Наш пациент — обфускатор для Java под названием Zelix KlassMaster, внутреннее устройство которого мы подробно исследуем.
Я думаю, ты достаточно внимательно следишь за темой, поэтому тебе уже не нужно объяснять, что такое обфускатор. Но на всякий случай кратко напомню: это семейство программ, которые максимально запутывают код, затрудняя его анализ хакером или реверс‑инженером. Обычно такое актуально для скриптовых и высокоуровневых языков, компилируемых не в натив, а в промежуточный шитый код, из которого исходники восстанавливаются относительно легко различными декомпиляторами.

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


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

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

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

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

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

Для просмотра ссылки Войди или Зарегистрируйся
К слову, декомпилятор Для просмотра ссылки Войди или Зарегистрируйся все‑таки кое‑как справляется с задачей.

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

Код:
//   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];
    }
Нетрудно догадаться, что этот код динамически заполняет массив q расшифрованными значениями строк из массива p. Откуда берется массив p? Ответ на этот вопрос нам дает внимательный анализ метода static void <clinit>(). Метод тоже содержит в себе ловушки для декомпилятора, поэтому восстанавливается несколько криво даже при помощи FernFlower:


Код:
/*
     * Unable to fully structure code
     */
    static {
        block20: {
            block19: {
                // Общее количество зашифрованных строк
                var5 = new String[15];
                var3_1 = 0;
                // Первая строка, содержащая зашифрованный код строковых констант
                var2_2 = "...";
                var4_3 = var2_2.length();
                // Длина самой первой строковой константы, зашифрованной внутри var2_2
                var1_4 = 18;
                var0_5 = -1;
lbl8:
                // 2 sources


                while (true) {
                    // Маска xor уникальна для каждого класса
                    v0 = 67;
                    v1 = ++var0_5;
                    v2 = var2_2.substring(v1, v1 + var1_4);
                    v3 = -1;
                    break block19;
                    break;
                }
lbl14:
                // 1 sources


                while (true) {
                    var5[var3_1++] = v4.intern();
                    if ((var0_5 += var1_4) < var4_3) {
                        var1_4 = var2_2.charAt(var0_5);
                        ** continue;
                    }
                    // Вторая строка, содержащая зашифрованный код строковых констант
                    var2_2 = "...";
                    var4_3 = var2_2.length();
                    // Длина самой первой строковой константы, зашифрованной внутри второй строки var2_2
                    var1_4 = 9;
                    var0_5 = -1;
lbl23:
                    // 2 sources


                    while (true) {
                        // Маска xor уникальна для каждого класса
                        v0 = 74;
                        v5 = ++var0_5;
                        v2 = var2_2.substring(v5, v5 + var1_4);
                        v3 = 0;
                        break block19;
                        break;
                    }
                    break;
                }
lbl29:
                // 1 sources


                while (true) {
                    var5[var3_1++] = v4.intern();
                    if ((var0_5 += var1_4) < var4_3) {
                        var1_4 = var2_2.charAt(var0_5);
                        ** continue;
                    }
                    break block20;
                    break;
                }
            }
            v6 = v2.toCharArray();
            v7 = v6.length;
            var6_6 = 0;
            v8 = v0;
            v9 = v6;
            v10 = v7;
            if (v7 > 1) ** GOTO lbl86
            do {
                v11 = v8;
                v9 = v9;
                v12 = v9;
                v13 = v8;
                v14 = var6_6;
                while (true) {
                    switch (var6_6 % 7) {
                        case 0: {
                            v15 = 36;
                            break;
                        }
                        ... // case из шести значений, представляющий собой шесть масок для последовательных операций xor, уникален для каждого класса
                        default: {
                            v15 = 64;
                        }
                    }
                    v12[v14] = (char)(v12[v14] ^ (v13 ^ v15));
                    ++var6_6;
                    v8 = v11;
                    if (v11 != 0) break;
                    v11 = v8;
                    v9 = v9;
                    v14 = v8;
                    v12 = v9;
                    v13 = v8;
                }
lbl86:
                // 2 sources


                v16 = v9;
                v10 = v10;
            } while (v10 > var6_6);
            v4 = new String(v16);
            switch (v3) {
                default: {
                    ** continue;
                }
                ** case 0:
lbl96:
                // 1 sources


                ** continue;
            }
        }
        p.p = var5;
Логика этого кода слегка ломает мозг, однако при ближайшем рассмотрении ничего особо сложного в нем нет. Конечно, жизнь вверх ногами среди кенгуру накладывает определенный отпечаток на специфику австралийского программирования, но при должном старании и упорстве даже такая логика вполне постижима. К примеру, эквивалент вышеприведенного кода на С# выглядит примерно вот так:

Код:
private static String a(int n, string cArray)
        {
            char [] cArray2;


            int n2;
            int n3;
            int n4;


            n4 = n;
            cArray2 = cArray.ToCharArray();
            n3 = cArray.Length;
            n2 = 0;


            if (n3 <= n2) return "";


            do
            {


                int n5 = n2;
                cArray2[n5] = (char)(cArray2[n5] ^ (n4 ^ (mask7[(n2 % 7)])


                ));
                ++n2;


            } while (n3 > n2);


            return new string(cArray2);


        }
...
      // Расшифровка строковых констант, содержащихся в первой зашифрованной строке var2_2
        while (true)
            {
                int v0 =v0_0;


                int v1 = ++var0_5;
                string v2 = var2_2.Substring(v1, var1_4);
                int v3 = -1;


                var5[var3_1++] = a(v0, v2);


                if ((var0_5 += var1_4) < var4_3)
                {
                    var1_4 = var2_2[var0_5];
                    continue;
                }
                else break;
            }
            var2_2 = cryptStr2;
            var4_3 = cryptStr2.Length;
            var1_4 = var1_4_1;
            var0_5 = -1;
            // Расшифровка строковых констант, содержащихся во второй зашифрованной строке var2_2
            while (true)
            {


                int v0 = v0_1;
                int v4 = ++var0_5;
                string v2 = var2_2.Substring(v4, var1_4);
                int v3 = 0;


                var5[var3_1++] = a(v0, v2);
                if ((var0_5 += var1_4) < var4_3)
                {
                    var1_4 = var2_2[var0_5];
                    continue;
                }
                break;
            }
Массив строк var5, получающийся на выходе этого кода, и есть упомянутый выше массив p. Еще одной странностью австралийского программирования лично для меня стало то, что, анализируя код, обфусцированный более поздней версией ZKM15.0.2 , я обнаружил: разработчики не усложнили, а, наоборот, упростили алгоритм шифрования строк, отказавшись от финального преобразования private static String a(int n, int n2).

То есть массив var5 из фрагмента кода выше в этом случае уже содержит расшифрованные строки, которыми заполняется массив private static final String[] a, содержащийся в каждом классе. В дальнейшем строковые константы просто выбираются из этого массива.

Что касается разбираемой нами сейчас версии 12.0.2, то перекодировка строковых констант местами там еще более параноидальная, чем описано выше. Разработчики решили, что для некоторых строковых констант недостаточно упомянутого мною шифрования и их на всякий случай надо зашифровать еще раз другим алгоритмом. К примеру, по какой‑то понятной лишь сумчатым причине они решили посильнее спрятать слово Register в заголовке диалогового окна регистрации. Для этого они создали специальный класс util.T с совсем уж ламерским алгоритмом шифрования следующего вида:

Код:
public class T {
    public static String a(String string) {
        StringBuffer stringBuffer = new StringBuffer();
        for (int i = 0; i < string.length(); ++i) {
            stringBuffer.append(T.a(string.charAt(i)));
        }
        return stringBuffer.toString();
    }


    public static char a(char c) {
        if (c >= 'A' && c <= 'Z' && (c = (char)(c + 13)) > 'Z') {
            c = (char)(c - 26);
        }
        if (c >= 'a' && c <= 'z' && (c = (char)(c + 13)) > 'z') {
            c = (char)(c - 26);
        }
        return c;
    }
}
В итоге обращение к строковой константе Register выглядит в деобфусцированном виде как T.a((String)"Ertvfgre ") (или окончательно обфусцированном T.a((String)f3.a(1631, 16759))). Строка Name: - T.a((String)"Anzr:")) и так далее. В других версиях я таких странных извращений не встречал, хотя, возможно, это уже импровизация собственной паранойи пользователей обфускатора.

В заключение постараюсь кратко рассказать, как можно использовать эту информацию для дальнейшего анализа обфусцированного кода и, возможно, написания собственного деобфускатора.

По‑хорошему, конечно, неплохо бы накодить свой собственный декомпилятор class-файлов, обходящий ловушки в коде, подобные описанным выше. Благо сейчас нет особой проблемы в декомпиляторах с открытым кодом, которые можно форкнуть.

Но лично я посоветовал бы начать с малого. К примеру, взять за основу тот же FernFlower (код которого, кстати, тоже открыт) и строить свой деобфускатор в формате постпроцессора над восстановленным им Java-кодом. Проанализировав достаточное количество классов, обфусцированных при помощи Zelix KlassMaster 12.0.2, можно сказать, что основной источник зашифрованных строковых констант — методы static void <clinit>() и private static String a(int, int). Они конструируются обфускатором достаточно консервативно, чтобы после декомпиляции можно было легко извлечь исходные данные для описанного выше алгоритма расшифровки строковых констант: зашифрованные строковые данные, их длины и размеры, массивы xor-масок и перекодировки байтов. Причем используя при этом обычные регулярные выражения.

Далее следует искать по восстановленному Java-коду теми же регулярками конструкции типа a(int, int), считать их строковые эквиваленты и заменять их в коде. Таким же образом на следующем шаге можно заменять и выражения вида T.a((String)"...")), тем самым окончательно вытащив все зашифрованные строки на свет.

Скорее всего, уже этого будет достаточно для полноценного анализа восстановленного кода, но гордые австралийцы хвастаются тем, что приготовили еще массу нескучных аттракционов для развлечения пытливых умов: Integer/Long Constant Encryption, Reference Obfuscation, Method Parameter Obfuscation. Так что простор для изучения этой темы открывается поистине бескрайний.
 
Activity
So far there's no one here
Сверху Снизу