Как работает ProGuard


    Если вы когда-либо задумывались о безопасности своего приложения, либо как-то хотели оптимизировать свой код, то, наверняка, вы знаете что такое ProGuard. Возможно, вы уже пострадали от него или смогли превозмочь документацию, парочку статей на просторах и разобрались, что там к чему.

    В данной статье я не буду рассказывать ни о том, как прописывать keep rules или о каких-то полезных опциях. На мой взгляд для того чтобы втащить в проект ProGuard вполне достаточно просмотреть прикрепленные к нему туториалы. Я буду разбирать как именно работает ProGuard с точки зрения кода. И если вы заинтересованы — добро пожаловать под кат.

    Многие заблуждаются на счет ProGuard, ошибочно полагая, что это обфускатор, коим он не является по нескольким причинам:

    • Он не перемешивает код
    • Он не шифрует код

    Главная задача ProGuard — поменять имена объектов, классов, методов, тем самым затрудняя анализ кода для реверс-инженера. Помимо этого он еще и оптимизирует код, удаляя неиспользуемые в программе ресурсы. Но, в конечном итоге, в классическом понимании обфускатором его назвать нельзя.

    Итак, с чем же мы имеем дело?


    Вообще, ProGuard — это open-source утилита, которая работает с java-кодом. Да и написан он тоже на java. Ребята, которые занимаются им, разрабатывают еще и DexGuard, сорсы которого так же, если пошуршать, можно найти на просторах, так как они периодически сливаются. Но вообще DexGuard считается платным, по факту являясь более хардовой версией того же ProGuard.

    Так что можно сделать вывод, что ProGuard — это jar-файл, который переставляет символы в нашем коде, оптимизирует его и, вроде как, повышает безопасность. По-умолчанию ProGuard работает с 26 прописными и строчными буквами английского алфавита.

    Правила для proguard перетаскиваются из проекта в проект и считаются неприкосновенными, потому что не трогай — так работает, а иначе могут начаться какие-то адовые красные строки, как фиксить которые никто не знает, да и знать не хочет. А составил эти правила какой-то оракул, чтобы добраться до которого нужно семь раз вокруг себя обернуться, превратиться в птицу и лететь на юго-запад два часа сорок три минуты.

    Ну раз никто туда не хочет и никому туда не надо, давайте залезем в сорсы.

    Если посмотреть на директории проекта proguard, то можно сразу обозначить его главные функции.


    Пока что вроде все понятно. Потому давайте заглянем в главный класс.

    public class ProGuard   {
        
        //…
    private final MultiValueMap<String, String> injectedClassNameMap = new MultiValueMap<String, String>();
    
       //...
        
        /**
         * The main method for ProGuard.
         */
        public static void main(String[] args)
        {
            if (args.length == 0)
            {
                System.out.println(VERSION);
                System.out.println("Usage: java proguard.ProGuard [options ...]");
                System.exit(1);
            }
    
            // Create the default options.
            Configuration configuration = new Configuration();
    
            try
            {
                // Parse the options specified in the command line arguments.
                ConfigurationParser parser = new ConfigurationParser(args,
                                                                     System.getProperties());
                //...

    Ну и напоремся здесь на очевидный метод main, в котором мы видим как задаются дефолтные правила и парсятся установленные самим разработчиком.

    Помимо этого есть вполне ожидаемый объект injectedClassNameMap, с помощью которого мы и получаем файлик build/outputs/proguard/release/mapping.txt который выглядит примерно так:


    Так что если нам вдруг захочется вскрыть собственный код и привести его в читаемый вид, с помощью mapping.txt можно это сделать. Для этого при публикации apk-файла нужно загрузить версию mapping.txt в Google Play Console.

    Теперь можно посмотреть на парсер конфигураций, которые задает разработчик.

    public class ConfigurationParser    {
        
        //...
        
        /**
         * Parses and returns the configuration.
         * @param configuration the configuration that is updated as a side-effect.
         * @throws ParseException if the any of the configuration settings contains
         *                        a syntax error.
         * @throws IOException if an IO error occurs while reading a configuration.
         */
        public void parse(Configuration configuration)
        throws ParseException, IOException
        {
            while (nextWord != null)
            {
                lastComments = reader.lastComments();
    
                // First include directives.
                if      (ConfigurationConstants.AT_DIRECTIVE                                     .startsWith(nextWord) ||
                         ConfigurationConstants.INCLUDE_DIRECTIVE                                .startsWith(nextWord)) configuration.lastModified                          = parseIncludeArgument(configuration.lastModified);
                else if (ConfigurationConstants.BASE_DIRECTORY_DIRECTIVE                         .startsWith(nextWord)) parseBaseDirectoryArgument();
    
                // Then configuration options with or without arguments.
                else if (ConfigurationConstants.INJARS_OPTION                                    .startsWith(nextWord)) configuration.programJars                           = parseClassPathArgument(configuration.programJars, false);
                else if (ConfigurationConstants.OUTJARS_OPTION                                   .startsWith(nextWord)) configuration.programJars                           = parseClassPathArgument(configuration.programJars, true);
    
                //…
    
    else if (ConfigurationConstants.KEEP_CLASSES_WITH_MEMBER_NAMES_OPTION            .startsWith(nextWord)) configuration.keep                                  = parseKeepClassSpecificationArguments(configuration.keep, false, true,  true,  null);
                else if (ConfigurationConstants.PRINT_SEEDS_OPTION                               .startsWith(nextWord)) configuration.printSeeds                            = parseOptionalFile();
    
                // After '-keep'.
                else if (ConfigurationConstants.KEEP_DIRECTORIES_OPTION                          .startsWith(nextWord)) configuration.keepDirectories                       = parseCommaSeparatedList("directory name", true, true, false, true, false, true, true, false, false, configuration.keepDirectories);
    
        //...

    Ух, я говорила не обфускатор-не обфусцирует, а тут смотрите целая директория obfuscate. Как же так?


    Если посмотреть на приведенный выше скрин, то можно легко найти классы, которые отвечают за переименование объектов (SimpleNameFactory, ClassRenamer...). Как я и говорила выше, по дефолту используется 26 латинских символов.

    public class SimpleNameFactory implements NameFactory
    {
        private static final int CHARACTER_COUNT = 26;
    
        private static final List cachedMixedCaseNames = new ArrayList();
        private static final List cachedLowerCaseNames = new ArrayList();
    
        private final boolean generateMixedCaseNames;
        private int  index = 0;
        //…

    В классе SimpleNameFactory, есть специальный метод для проверки printNameSamples(), который выдаст нам вполне ожидаемые значения

     public static void main(String[] args)
        {
            System.out.println("Some mixed-case names:");
            printNameSamples(new SimpleNameFactory(true), 60);
            System.out.println("Some lower-case names:");
            printNameSamples(new SimpleNameFactory(false), 60);
            System.out.println("Some more mixed-case names:");
            printNameSamples(new SimpleNameFactory(true), 80);
            System.out.println("Some more lower-case names:");
            printNameSamples(new SimpleNameFactory(false), 80);
    
        }
    
        private static void printNameSamples(SimpleNameFactory factory, int count)
        {
            for (int counter = 0; counter < count; counter++)
            {
                System.out.println("  ["+factory.nextName()+"]");
            }
        }

    Some mixed-case names:
    [a]
    [b]
    [c]
    [d]
    [e]
    [f]
    [g]
    [h]
    [i]
    [j]
    [k]
    ...

    За “обфускацию” отвечает класс Obfuscator, в котором есть один единственный метод execute, куда передается весь собранный пулл классов самого проекта и всех библиотек, добавленных в него.

    public class Obfuscator
    {
        private final Configuration configuration;
        //...
        public void execute(ClassPool programClassPool,
                            ClassPool libraryClassPool) throws IOException
        {
            // Check if we have at least some keep commands.
            if (configuration.keep         == null &&
                configuration.applyMapping == null &&
                configuration.printMapping == null)
            {
                throw new IOException("You have to specify '-keep' options for the obfuscation step.");
            }
            
        //...

    Помимо в ProGuard присутствует оптимизация, которую запускает класс Optimizer, тем самым выполняя очень важные функцию по очистка неэксплуатируемых ресурсов. Тут так же учитываются заданные самим разработчиком параметры. Так что если вы хотите быть уверены в сохранности кода, то вы всегда можете прописать для него правила. Оптимизация запускается из класса ProGuard.

     /**
         * Performs the optimization step.
         */
        private boolean optimize(int currentPass,
                                 int maxPasses) throws IOException
        {
            if (configuration.verbose)
            {
                System.out.println("Optimizing (pass " + currentPass + "/" + maxPasses + ")...");
            }
    
            // Perform the actual optimization.
            return new Optimizer(configuration).execute(programClassPool,
                                                        libraryClassPool,
                                                        injectedClassNameMap);
        }
    

    Работу proguard можно разделить на несколько этапов:

    1. Считывание заданных правил
    2. Оптимизация
    3. Удаление ресурсов, помеченных в оптимизации
    4. Переименование объектов
    5. Запись проекта в указанную директорию в переработанном виде

    Запустить proguard можно вручную командой:

    java -jar proguard.jar @android.pro

    Где proguard.jar — собранный ProGuard проект, а android.pro — правила для его работы с параметрами входных и выходных данных.

    Почему писать собственный ProGuard слишком больно


    Фактически, пока я лазила по коду ProGuard, я видела в графе автора только одно имя — Eric Lafortune. Путем быстрого гугления я нашла его личный сайт, если кому-то интересно, вы можете ознакомиться с ним.

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

    Почему использовать ProGuard бесполезно


    ProGuard работает по правилам, которые заведомо известны и если вы не задаете правила и просто включаете его в своем проекте, получить доступ к коду для злоумышленника не составит особого труда, потому что обратные преобразователи давно написаны и лежат в открытом доступе. Конечно же, если вы более подробно изучите тему и добавить правила будет уже сложнее, но совсем немного. Какой выход?

    Компании, для которых сокрытие их кода приоритетно, форкаются от ProGuard и модифицируют его под свои нужды, тем самым получая на выходе уникальное решение.

    Зачем, почему, тебе нечем заняться?


    Вообще, proguard — не огромная утилита и ничего сверхъестественного там не происходит, так что вполне можно изучить исходники потратив пару вечеров за кружкой чая и поглаживая кота. Зачем вам это нужно? Для того чтобы более детально знать инструменты, с которыми работаете и понимать, что они делают с вашим кодом, на самом ли деле они так нужны вам. Это относится не только к ProGuard, но и к любому другому стороннему коду, который вы используете в своем проекте. Ваш код — это ваша зона ответственности и будьте чистоплотны в ней, иначе какой вообще смысл заниматься разработкой?

    Статья написана около полугода назад, а proguard постоянно развивается, потому какие-то фрагменты уже могут не совпадать.

    P.S. Все подборки я публикую как всегда в телеграм канале @paradisecurity, а ссылку можно найти в моем профиле, либо найти в поиске телеграм по названию.
    Поделиться публикацией

    Комментарии 12

      +4
      КДПВ неприличная.
        0
        Поддерживаю, поменяйте КПДВ, пока НЛО не увидело, а то у нас тут курс на англоязычную аудиторию, и такая дрянь явно не способствует репутации ресурса.

        p.s. но в голове, конечно, зазвучал голос Гаврилова :) «А ну иди сюда...» (С)
          +1
          Я поискала по ресурсу и вроде как такая лексика используется в свободном доступе, если администрация скажет поменять — безусловно уберу. А вообще, примерно такой код я встречала. Обычно так выглядят вскрытые трояны:) Exosphere
            0
            Да, всё нормально. Мало ли как человек называет переменные и функции :-) Но откровенный мат, конечно, порицается, например, КДПВ "+уяк-+уяк и в продакшен" убрали даже с замазанной частью.
            0
            Хорошая, кстати, мысль про англоязычную версию, но пост пока на русском. Спасибо, обсудим с коллегами. Прецедент. Boomburum зовём за окончательной экспертизой.
              +2
              Ну прям вот не криминал, конечно, но всё же желательно стараться выбирать нейтральные фото (и текстовые формулировки), чтобы не создавать проблем на ровном месте.

              сс miproblema
          +2
          Форкать прогард смысла нет, файлы конфигурации очень гибкие вплоть до ручного задания имен определенных классов, автор видимо особо не вникал в настройку тулзы. Перемешать код можно используя опцию repackageclasses и вместе с optimizationpasses > 3 дает очень неплохой вариант обфускации, естественно с опциями renamesourcefileattribute и keepattributes. А вообще статья не рассказывает о работе прогарда, как он строит AST и строит ли, как определяет неиспользуемый код, преобразует скомпилированные классы на лету или нет и т.д. И вообще гугл сейчас отказался от прогарда в сторону своего велосипеда R8/D8
            0
            Изначально статья была не о keep rules и правилах оптимизации и обфускации, я написала это в самом начале. Я читала о R8/D8. Насколько я знаю пока это очень сыро, а прогарду больше пятнацати лет. Вот тут описано все с точки зрения создателя proguard что к чему — www.guardsquare.com/en/blog/proguard-and-r8
            +1
            r8 уже дефолт в студии 3.4, он уже лучше работает с котлином, чем прогард, зная гугл и его вечные альфа релизы уже пора переезжать, мы уже полгода назад съехали с прогарда jakewharton.com/digging-into-d8-and-r8
              0
              Интересно, тогда я посмотрю более подробно. Спасибо)
              0
              Что-то непонятное написано про бесполезность прогарда. Что имеется ввиду про получение доступа к коду? Если говорить про мобильники, то он и так всегда есть. И какие правила обратного преобразования есть в сети? Маппинги?
                0

                По итогам статьи не очень понятно, есть ли в алгоритме обфусцирования рандомизация. Иными словами, запустив сборку два раза подряд получу ли я тот же самый результат.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое