Как уже говорилось вложенные классы имеют доступ ко всем членам внешнего класса включая даже private поля и методы. Может возникнуть вопрос, каким образом вложенные классы получают дополнительные привилегии доступа, ведь они преобразуются в обычные, а виртуальной машине вообще о них ничего не известно?
Все это изучим на одном примере, где мы попытаемся получить доступ к закрытому полю класса, используя подставные классы. Как уже говорилось внутренние классы имеют доступ ко все членам внешнего класса. Виртуальная же машина ни чего не знает о внутренних классах, так как компилятор преобразует все внутренние классы в классы верхнего уровня и именует их по правилам которые мы уже обсуждали несколько раз.
Идея в том чтобы создать клас который примитивно содержит в private поле пароль. И у него будет только один метод который проверяет строку введенную как параметр на предмет совпадает она или нет с паролем хранящемся в приватном поле. Но этот метод не возвращает значение этого поля, а лишь выводит сообщение правильный пароль или нет.
Затем в другом проекте мы попробуем создать внутренний класс, который постарается получить доступ к этому приватному полю.
И так! У нас есть класс PassCheck, у которого есть private поле PASSWORD содержащее пароль. Так же у него есть метод, который позволяет проверить переданное в него значение.
И затем выводит соответствующее сообщение.
Но прошу заметить этот метод не возвращает значение private поля, а лишь проверяет соответствует ли переданный аргумент паролю.
В классе Main просто происходит создание экземпляра класса PassCheck и передача в его метод значения полученного из командной строки.
Пример запуска программы приведен слева.
Как мы видим программа очень простая.
Но в ней нет метода выводящего пароль. Мы же хотим его получить.
И так представим ситуацию что у нас есть только класс файлы с этой программой, то есть файлы имеющие расширение .class, но нет исходных текстов. И нам надо как то получить заветный пароль.
Теперь начинаем исследование наших .class файлов при помощи утилиты javap. Сразу же хочу предупредить некоторых умников, что да, если запустить javap с ключом –c, то в дизассемблированном коде можно увидеть наш пароль в чистом виде, но это не то что мы хотим. Мы хотим написать свою программу которая выведет значение private поля на экран.
Если мы дадим команду javap -p PassCheck.class, то увидим следующий вывод на экран:
Мы видим что у класса PassChek есть private поле PASSWORD, а так же метод, который ни чего не возвращает (void), а просто получает значение типа String и как можно предположить, сравнивает его с паролем. Ну мы то знаем что сравнивает :) сами же писали. Но сейчас мы как будто потеряли исходники и забыли пароль.
В классе Main как видим ни чего интересного нет. Мы можем предположить что там просто создается экземпляр класса PassChek и в него передается аргумент командной строки.
То есть нам надо работать с классом PassCheck и получить значение поля PASSWORD.
Для этого постараемся использовать внутренние классы, так как они имеют доступ даже к private полям внешнего класса. Но тут есть одна заковырка. Внутренний класс в априори должен быть в том же пакете что и внешний. Для этого нам придется создать еще один проект и в нем создать пакет pro.java.inner07. И в этом пакете мы создадим свой класс Main, назвав его MainHack, и свой класс PassCheck, название его надо нам будет сохранить и внутри него создадим внутренний класс. Затем откомпилируем все это дело и в каталог где лежат наши атакуемые .class файлы перепишем MainHack.class и внутренний класс, внешний же оставим на месте, так как нам надо атаковать именно класс PassCheck в его изначальном виде.
Мы создали два класса. Наш класс PassCheck содержит внутренний класс, который к тому же наследуется от внешнего класса PassCheck, а значит наследует и поле PASSWORD.
Хотя, в принципе, наследоваться было и не особо нужно, но это задел на следующую тему, где мы будем более подробно рассматривать наследование.
Класс MainHack создает экземпляр класса PassCheck и вызывает на нем метода сравнения паролей. Затем создает экземпляр внутреннего класса Hack и вызывает на нем унаследованный им и переопределенный метод comparePassword().
Этот метод на самом деле ни чего не сравнивает, а должен просто вывести значение поля PASSWORD.
То что у нас получилось я зафиксировал в коммите Git под шагом 1. Это если кто то захочет повторить этот эксперимент.
Запуск и работа класса MainHack и модифицированного класса PassCheck показана слева. Теперь к сути. У нас получилось три файла:
И нас интересует файл PassCheck$Hack.class.
Что будет если его подставить в пакет где у нас находится настоящий атакуемый нами класс PassCheck?
И естественно для его запуска нам нужен будет класс MainHack. На скрине слева показаны те файлы которые мы скопировали в пакет с атакуемым классом PassCheck.class. Запустим MianHack и посмотрим что будет…
И мы наблюдаем очень интересное поведение. Мы видим что атакуемый класс PassCheck, отработал, но правда с ошибками с которыми мы чуть позже разберемся. А пока надо понять что в классе MainHack и в классе PassCheck$Hack нет кода который бы выводил сообщение "Пароль не правильный", он есть только в классе PassCheck, что и доказывает что наш класс MainHack запустил атакуемый нами класс PassCheck. Мы так же видим номера строк где вылезла ошибка. Про трассировку ошибок мы еще поговорим но гораздо позже. А пока обращаем внимание на строку под номером 18, которая находится во внутреннем классе PasccChek$Hack.
То есть как видно из отрывка кода, ошибку вызывает строка отмеченная стрелкой:
println
("PASSWORD = " + PASSWORD);Но есть и хорошая новость! Этот облом показывает что наш класс PassCheck$Hack выполняется, и что очень важно пытается получить доступ, но получает по шаловливым ручкам.
Давайте закомментируем эту строку. Откомпилируем наш класс PassCheck$Hack и попытаемся запустить все снова.
Когда мы закомментировали 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.
Как мы видим в подчеркнутой красной линией строке, появился новый статический метод с именем access$0. Это и есть синтетический метод который был создан компиляротом. В нашем коде класса PassCheck такого метода нет. Этот метод был создан для того чтобы предоставить доступ к private члену внешнего класса, нашему внутреннему классу. Синтетические методы создаются для доступа к каждому private члену. Если бы у нас было несколько private членов и обращений к ним, то для каждого бы был создан отдельный синтетический метод. Надо заметить, что синтетические метды создаются при прямом доступе к ним, если доступ к ним предоставляют public методы, то синтетические методы создаваться не будут. На этом, пока с синтетическими методами закончим и пойдем дальше.
А дальше без хирургического вмешательства ни как. Тут еще надо понять, что в изначальном, атакуемом (откомпилированном) классе PassCheck содержится пароль, который мы хотим увидеть, но там нет метода который его ввыводит. И нет синтетического метода который бы обращался к private полю PASSWORD. Поэтому нам туда его надо внеднить. То есть пропатчить непосредственно файл PassCheck.class.
Для этого сравним откомпилированные классы (файлы .class) – атакуемый и атакующий.
Как видим разница есть. Ну это и естественно :) И поэтому нам надо пропатчить атакуемый файл CheckPass.class.
После того как он будет пропатчен изменениями из атакующего файла PassCheck.class, надо еще будет переписать файлы MainHack.class и PassCheck$Hack.class в каталог с атакуемым классом. Копирование этих классов уже было показано на одном из сркинов выше. Ну и теперь запускаем наш MainHack.class…
Тадааааммммм! Мы получили то что хотели. Правда с хирургическим вмешательством.
Конечно до приватного поля можно было добраться и через reflection, но эту тему мы пока не проходили поэтому оставим ее на потом :)