30 авг. 2015 г.

Внутренние классы. Часть 7 – безопасность

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

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

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

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

S0001

S0002И так! У нас есть класс PassCheck, у которого есть private поле PASSWORD содержащее пароль. Так же у него есть метод, который позволяет проверить переданное в него значение.

И затем выводит соответствующее сообщение.

Но прошу заметить этот метод не возвращает значение private поля, а лишь проверяет соответствует ли переданный аргумент паролю.

S0003В классе Main просто происходит создание экземпляра класса PassCheck и передача в его метод значения полученного из командной строки.

Пример запуска программы приведен слева.

Как мы видим программа очень простая.

Но в ней нет метода выводящего пароль. Мы же хотим его получить.

S0004И так представим ситуацию что у нас есть только класс файлы с этой программой, то есть файлы имеющие расширение .class, но нет исходных текстов. И нам надо как то получить заветный пароль.

Теперь начинаем исследование наших .class файлов при помощи утилиты javap. Сразу же хочу предупредить некоторых умников, что да, если запустить javap с ключом –c, то в дизассемблированном коде можно увидеть наш пароль в чистом виде, но это не то что мы хотим. Мы хотим написать свою программу которая выведет значение private поля на экран.

Если мы дадим команду javap -p PassCheck.class, то увидим следующий вывод на экран:

S0005

Мы видим что у класса PassChek есть private поле PASSWORD, а так же метод, который ни чего не возвращает (void), а просто получает значение типа String и как можно предположить, сравнивает его с паролем. Ну мы то знаем что сравнивает :) сами же писали. Но сейчас мы как будто потеряли исходники и забыли пароль.

 

S0006

В классе Main как видим ни чего интересного нет. Мы можем предположить что там просто создается экземпляр класса PassChek и в него передается аргумент командной строки.

То есть нам надо работать с классом PassCheck и получить значение поля PASSWORD.

Для этого постараемся использовать внутренние классы, так как они имеют доступ даже к private полям внешнего класса. Но тут есть одна заковырка. Внутренний класс в априори должен быть в том же пакете что и внешний. Для этого нам придется создать еще один проект и в нем создать пакет pro.java.inner07. И в этом пакете мы создадим свой класс Main, назвав его MainHack, и свой класс PassCheck, название его надо нам будет сохранить и внутри него создадим внутренний класс. Затем откомпилируем все это дело и в каталог где лежат наши атакуемые .class файлы перепишем MainHack.class и внутренний класс, внешний же оставим на месте, так как нам надо атаковать именно класс PassCheck в его изначальном виде.

S0007

S0008

Мы создали два класса. Наш класс PassCheck содержит внутренний класс, который к тому же наследуется от внешнего класса PassCheck, а значит наследует и поле PASSWORD.

Хотя, в принципе, наследоваться было и не особо нужно, но это задел на следующую тему, где мы будем более подробно рассматривать наследование.

Класс MainHack создает экземпляр класса PassCheck и вызывает на нем метода сравнения паролей. Затем создает экземпляр внутреннего класса Hack и вызывает на нем унаследованный им и переопределенный метод comparePassword().

Этот метод на самом деле ни чего не сравнивает, а должен просто вывести значение поля PASSWORD.

То что у нас получилось я зафиксировал в коммите Git под шагом 1. Это если кто то захочет повторить этот эксперимент.

S0011

Запуск и работа класса MainHack и модифицированного класса PassCheck показана слева. Теперь к сути. У нас получилось три файла:

S0012

И нас интересует файл PassCheck$Hack.class.

 

Что будет если его подставить в пакет где у нас находится настоящий атакуемый нами класс PassCheck?

S0013

И естественно для его запуска нам нужен будет класс MainHack. На скрине слева показаны те файлы которые мы скопировали в пакет с атакуемым классом PassCheck.class. Запустим MianHack и посмотрим что будет…

S0014

И мы наблюдаем очень интересное поведение. Мы видим что атакуемый класс PassCheck, отработал, но правда с ошибками с которыми мы чуть позже разберемся. А пока надо понять что в классе MainHack и в классе PassCheck$Hack нет кода который бы выводил сообщение "Пароль не правильный", он есть только в классе PassCheck, что и доказывает что наш класс MainHack запустил атакуемый нами класс PassCheck. Мы так же видим номера строк где вылезла ошибка. Про трассировку ошибок мы еще поговорим но гораздо позже. А пока обращаем внимание на строку под номером 18, которая находится во внутреннем классе PasccChek$Hack.

S0015

То есть как видно из отрывка кода, ошибку вызывает строка отмеченная стрелкой:

println("PASSWORD = " + PASSWORD);

Но есть и хорошая новость! Этот облом показывает что наш класс PassCheck$Hack выполняется, и что очень важно пытается получить доступ, но получает по шаловливым ручкам.

Давайте закомментируем эту строку. Откомпилируем наш класс PassCheck$Hack и попытаемся запустить все снова.

S0016

Когда мы закомментировали 18 строку выполнение нашего класса PassCheck$Hack прошло без ошибок! То есть экземпляр нашего класса PassCheck$Hack подцепляется к экземпляру атакуемого класса PassCheck. Сбой происходит только тогда, когда наш класс хочет получить доступ к private полю. Теперь осталось понять почему вылетает ошибка и как это обойти.

Для этого придется поговорить о более продвинутых вещах, чем начальное изучение Java :)

И так, поехали!

В Java Language Specification (section 13.1.11) говорится: "A construct emitted by a Java compiler must be marked as synthetic if it does not correspond to a construct declared explicitly or implicitly in source code, unless the emitted construct is a class initialization method".

Ну и кратко на русском. Любы конструкции добавленные компилятором в байт-код класса, не имеющие соответствующих конструкций в исходном коде, должны помечаться как синтетические. Исключение составляют конструкторы по умолчанию и инициализационные блоки.

Еще одно упоминание о синтетических методах можно найти в документации на метод Member.isSynthetic().

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

Чтобы это продемонстроровать расскоментируем строку println("PASSWORD = " + PASSWORD); но не будем запускать программу, а просто откомпилируем и посмотрим при помощи javap.

S0017

Как мы видим в подчеркнутой красной линией строке, появился новый статический метод с именем access$0. Это и есть синтетический метод который был создан компиляротом. В нашем коде класса PassCheck такого метода нет. Этот метод был создан для того чтобы предоставить доступ к private члену внешнего класса, нашему внутреннему классу. Синтетические методы создаются для доступа к каждому private члену. Если бы у нас было несколько private членов и обращений к ним, то для каждого бы был создан отдельный синтетический метод. Надо заметить, что синтетические метды создаются при прямом доступе к ним, если доступ к ним предоставляют public методы, то синтетические методы создаваться не будут. На этом, пока с синтетическими методами закончим и пойдем дальше.

А дальше без хирургического вмешательства ни как. Тут еще надо понять, что в изначальном, атакуемом (откомпилированном) классе PassCheck содержится пароль, который мы хотим увидеть, но там нет метода который его ввыводит. И нет синтетического метода который бы обращался к private полю PASSWORD. Поэтому нам туда его надо внеднить. То есть пропатчить непосредственно файл PassCheck.class.

Для этого сравним откомпилированные классы (файлы .class) – атакуемый и атакующий.

S0018

Как видим разница есть. Ну это и естественно :) И поэтому нам надо пропатчить атакуемый файл CheckPass.class.

После того как он будет пропатчен изменениями из атакующего файла PassCheck.class, надо еще будет переписать файлы MainHack.class и PassCheck$Hack.class в каталог с атакуемым классом. Копирование этих классов уже было показано на одном из сркинов выше. Ну и теперь запускаем наш MainHack.class…

S0019

Тадааааммммм! Мы получили то что хотели. Правда с хирургическим вмешательством.

Конечно до приватного поля можно было добраться и через reflection, но эту тему мы пока не проходили поэтому оставим ее на потом :)

11 комментариев:

  1. "Наш класс PassCheck содержит внутренний класс, который к тому же наследуется от внешнего класса PassCheck, а значит наследует и поле PASSWORD."

    А разве private поля класса (даже если класс наследник является внутренним классом) можно наследовать?
    Мне кажется здесь ошибочное утверждение.

    ОтветитьУдалить
    Ответы
    1. При наследовании наследуется все, в том числе и private поля и методы.

      Удалить
    2. Тогда я чего-то не понимаю. Вот простой пример:

      package Test;

      public class Outer {
      private String str = "OuterStr";

      class Inner extends Outer{
      public void printStr(){System.out.println(this.str);}
      }
      }

      При попытке обратиться к переменной str по ссылке this наследника (в методе printStr()) среда разработки пишет следующее: 'str' has private access in 'Test.Outer'. И в то же время, с любым другим модификаторм доступа (public str, protected str и str) программа компилируется и выводит на печать "OuterStr".
      Из чего я делаю вывод, что private поля класса (даже если класс наследник является внутренним классом класса родителя) наследовать нельзя.
      Или я чего-то не понимаю в наследовании?
      Поправьте меня пож., если я не прав.

      Удалить
    3. Создайте public метода в классе Outer, который выводит на печать private str, и затем вызовите этот метод из класса наследника, и увидите значение своего унаследованного private str.

      Вообще я напишу отдельную статью на эту тему, так как похоже очень у многих смутное понимание этой темы.

      Удалить
  2. Этот комментарий был удален автором.

    ОтветитьУдалить
  3. "После того как он будет пропатчен изменениями из атакующего файла PassCheck.class, надо еще будет переписать файлы MainHack.class и PassCheck$Hack.class в каталог с атакуемым классом. Копирование этих классов уже было показано на одном из сркинов выше."

    Не понял зачем еще раз переписывать файлы MainHack.class и PassCheck$Hack.class в каталог с атакуемым классом? Мы же уже их переписали ранее, когда иксэпшены получили. Достаточно было просто хирургию провести на файле PassCheck.class и все. Или я не прав?

    ОтветитьУдалить
    Ответы
    1. Потому что строка println("PASSWORD = " + PASSWORD); была расскоментирована и класс был заново откомпилирован.

      Удалить
  4. Доброго времени!
    "И поэтому нам надо пропатчить атакуемый файл CheckPass.class.

    После того как он будет пропатчен изменениями из атакующего файла PassCheck.class"
    Но как пропатчить-то? Мы же так и не дошли до конца)

    ОтветитьУдалить
  5. Этот комментарий был удален автором.

    ОтветитьУдалить
  6. Но для изменения бинароного кода там нужно указать сторонний двоичный редактор? ExamDiff Pro только для сравнения или там тоже есть редактор?

    ОтветитьУдалить