Рекомендация: Защитите свой программный код от уязвимости финализатора Модель предотвращения создания недопустимых классов Нил Д. Мэссон, инженер по поддержке Java, IBM http://www.ibm.com/developerworks/ru/library/j-fv/ Дата: 10.12.2012 Описание: Ваш программный код Java может иметь уязвимость, связанную с процессом финализации. Узнайте о том, как устроена такая уязвимость и как изменить ваш код для предотвращения атак на нее. Финализаторы могут создавать уязвимости в коде Java при их использовании для создания объектов. Данная уязвимость представляет собой разновидность хорошо известного метода использования финализатора для восстановления объекта. Когда какой-либо объект с методом finalize() становится недоступным, он помещается в очередь для будущей обработки. В данной рекомендации объясняется принцип данной уязвимости и показывается, как можно защитить от него свой программный код. Все образцы кода доступны для загрузки. Концепция финализатора заключается в том, чтобы разрешить какому-либо методу Java освобождать собственные ресурсы, которые нужно возвратить в операционную систему. К сожалению, в финализаторе может выполняться любой код Java, что делает возможным использование кода, подобного представленному в листинге 1: Листинг 1. Восстанавливаемый класс Code: public class Zombie { static Zombie zombie; public void finalize() { zombie = this; } } Когда производится вызов финализатора Zombie, он берет финализируемый объект — по ссылке this — — и сохраняет его в статической переменной zombie. Теперь этот объект снова доступен и не может быть подвергнут финализации. Более утонченная версия этого кода позволяет восстанавливать даже частично сконструированный объект. Даже если объект не соответствует какому-либо критерию корректности в инициализаторе, он все равно может быть создан финализатором, как показано в листинге 2: Листинг 2. Создание недопустимого класса Code: public class Zombie2 { static Zombie2 zombie; int value; public Zombie2(int value) { if(value < 0) { throw new IllegalArgumentException("Отрицательное значение Zombie2"); } this.value = value; } public void finalize() { zombie = this; } } В листинге 2 эффект проверки аргумента value сводится на нет существованием метода finalize(). Как осуществляется атака Разумеется, вряд ли кто-то напишет код, подобный представленному в листинге 2. Однако уязвимость может возникнуть в случае создания подкласса такого класса, как показано в листинге 3: Листинг 3. Уязвимый класс Code: class Vulnerable { Integer value = 0; Vulnerable(int value) { if(value <= 0) { throw new IllegalArgumentException("Значение Vulnerable должно быть положительным"); } this.value = value; } @Override public String toString() { return(value.toString()); } } Класс Vulnerable в листинге 3 предназначается для предотвращения задания неположительных значений value. Это намерение ниспровергается методом AttackVulnerable(), как показано в листинге 4: Листинг 4. Класс для ниспровержения класса Vulnerable Code: class AttackVulnerable extends Vulnerable { static Vulnerable vulnerable; public AttackVulnerable(int value) { super(value); } public void finalize() { vulnerable = this; } public static void main(String[] args) { try { new AttackVulnerable(-1); } catch(Exception e) { System.out.println(e); } System.gc(); System.runFinalization(); if(vulnerable != null) { System.out.println("Уязвимый объект " + vulnerable + " создан!"); } } } В методе main() класса AttackVulnerable предпринимается попытка создания экземпляра нового объекта AttackVulnerable. Поскольку значение value находится вне допустимого диапазона, генерируется исключение, захватываемое в блоке catch. Вызовы System.gc() и System.runFinalization() побуждают виртуальную машину выполнить цикл сборки мусора и произвести запуск финализаторов. Эти вызовы не являются обязательными для проведения успешной атаки, но они служат для демонстрации ее конечного результата, т. е. создания объекта Vulnerable с недопустимым значением. Выполнение контрольного примера дает следующий результат: j Code: ava.lang.IllegalArgumentException: Значение Vulnerable должно быть положительным Уязвимый объект 0 создан! Почему Vulnerable имеет значение 0, а не -1? Обратите внимание на то, что в конструкторе Vulnerable в листинге 3 присвоение значения value не производится до проверки аргумента. Поэтому value имеет свое начальное значение, которое в данном случае равно 0. Подобный вид атаки может использоваться для обхода явных проверок безопасности. Например, класс Insecure в листинге 5 призван генерировать исключение SecurityException, если он выполняется под SecurityManager, при этом вызывающий не имеет прав осуществления записи в текущий каталог: Листинг 5. Класс Insecure Code: import java.io.FilePermission; public class Insecure { Integer value = 0; public Insecure(int value) { SecurityManager sm = System.getSecurityManager(); if(sm != null) { FilePermission fp = new FilePermission("index", "write"); sm.checkPermission(fp); } this.value = value; } @Override public String toString() { return(value.toString()); } } Класс Insecure в листинге 5 может быть атакован тем же рассмотренным выше способом, как показано в классе AttackInsecure в листинге 6: Листинг 6. Атака на класс Insecure Code: public class AttackInsecure extends Insecure { static Insecure insecure; public AttackInsecure(int value) { super(value); } public void finalize() { insecure = this; } public static void main(String[] args) { try { new AttackInsecure(-1); } catch(Exception e) { System.out.println(e); } System.gc(); System.runFinalization(); if(insecure != null) { System.out.println("Небезопасный объект " + insecure + " создан!"); } } } Выполнение кода, представленного в листинге 6, под SecurityManager дает следующий результат: Code: java -Djava.security.manager AttackInsecure java.security.AccessControlException: Доступ запрещен (java.io.FilePermission index write) Небезопасный объект 0 создан! Как избежать атаки До тех пор, пока в Java SE 6 не была реализована третья редакция Спецификации языка Java (JLS), единственно возможные способы предотвращения атаки (использование флага initialized, запрет на создание подклассов или создание финализатора final), не являлись удовлетворительными решениями. Использование флага initialized flag Одним из способов предотвращения атаки является использование флага initialized, который устанавливается вtrue после корректного создания объекта. Каждый метод в классе сначала проверяет, установлен ли флаг initialized, и генерирует исключение, если флаг не установлен. Такой подход к написанию программного код утомителен, о нем легко случайно забыть, и он не препятствует созданию производных классов метода злоумышленником. Предотвращение создания производных классов Вы можете объявить создаваемый класс как final. Это означает, что никто не сможет создать подкласс данного класса, и атака не сработает. Однако использование подобной техники лишает вас гибкости возможностей расширения класса для его специализации или добавления дополнительных функций. Создание сборщика мусора final Вы можете создать какой-либо сборщик мусора для создаваемого класса и объявить его как final. Это означает, что ни один подкласс данного класса не сможет объявить какой-либо сборщик мусора. Недостатком подобного подхода является то, что при существовании сборщика мусора объект будет сохраняться дольше, чем было бы в ином случае. Более новый и совершенный способ Чтобы облегчить предотвращение атак подобного рода без использования дополнительного программного кода и ввода ограничений, разработчики Java внесли изменения в JLS (см. раздел Ресурсы), установив, что если до создания java.lang.Object в конструкторе генерируется какое-либо исключение, метод finalize() данного метода выполняться не будет. Однако как можно сгенерировать исключение до создания java.lang.Object? Ведь первой строкой в любом конструкторе должен быть вызов this() или super(). Если конструктор не включает такого явного вызова, то вызов super() добавляется неявным образом. Таким образом, перед созданием какого-либо объекта должен быть создан другой объект того же класса или его суперкласса. В конечном счете это ведет к созданию самого java.lang.Object, а затем и к созданию всех подклассов, прежде чем будет выполнен любой код из создаваемого метода. Чтобы понять, как может быть сгенерировано исключение до создания java.lang.Object, необходимо разбираться в точной последовательности создания объектов. В JLS эта последовательность изложена явным образом. При создании какого-либо объекта виртуальная машина Java: -Выделяет пространство для объекта. -Устанавливает для всех объектных переменных в объекте значения по умолчанию. Это также касается объектных переменных в суперклассах объекта. -Назначает переменные параметров для объекта. -Обрабатывает любой явный или неявный вызов конструктора (вызов к this() или super() в конструкторе). -Инициализирует переменные в классе. -Выполняет оставшуюся часть конструктора. Ключевым моментом является то, что параметры конструктора обрабатываются до начала обработки любого кода внутри конструктора. Это означает, что если вы выполняете проверку во время обработки параметров, вы можете (путем генерирования исключения) предотвратить финализацию своего класса. Это позволяет создать новый вариант класса Vulnerable из листинга 3, показанный в листинге 7: Листинг 7. Класс Invulnerable Code: class Invulnerable { int value = 0; Invulnerable(int value) { this(checkValues(value)); this.value = value; } private Invulnerable(Void checkValues) {} static Void checkValues(int value) { if(value <= 0) { throw new IllegalArgumentException("Значение Invulnerable должно быть положительным"); } return null; } @Override public String toString() { return(Integer.toString(value)); } } В листинге 7 общедоступный конструктор для Invulnerable вызывает частный конструктор, который, в свою очередь, вызывает метод checkValues для создания параметра. Этот метод вызывается до того, как конструктор выполнит вызов для создания суперкласса, который является конструктором для Object.Таким образом, если в checkValues будет сгенерировано исключение, финализация объекта Invulnerable производиться не будет. В программном коде, представленном в листинге 8, предпринимается попытка атаки на Invulnerable: Листинг 8. Попытка ниспровержения класса Invulnerable class Code: class AttackInvulnerable extends Invulnerable { static Invulnerable vulnerable; public AttackInvulnerable(int value) { super(value); } public void finalize() { vulnerable = this; } public static void main(String[] args) { try { new AttackInvulnerable(-1); } catch(Exception e) { System.out.println(e); } System.gc(); System.runFinalization(); if(vulnerable != null) { System.out.println("Неуязвимый объект " + vulnerable + " создан!"); } else { System.out.println("Атака завершилась неудачно"); } } } с добавлением } else { System.out.println("Атака завершилась неудачно"); В версии Java 5, которая была написана в соответствии с более старой версией JLS, создается объект Invulnerable: Code: java.lang.IllegalArgumentException: Значение Invulnerable должно быть положительным Неуязвимый объект 0 создан! Java SE 6 (начиная с общедоступной версии JVM Oracle и JVM IBM SR9) соответствует новейшей спецификации, поэтому объект не создается: Code: java.lang.IllegalArgumentException: Значение Invulnerable должно быть положительным Атака завершилась неудачно Заключение Финализаторы являются одной из неудач языка Java. Хотя сборщик мусора умеет автоматически возвращать в пользование память, которая больше не используется объектами Java, такого же механизма для возвращения в пользование собственных ресурсов (например, собственной памяти, дескрипторов файлов и сокетов) не существует. Стандартные библиотеки, предоставляемые языком Java и взаимодействующие с этими собственными ресурсами, обычно имеют какой-либо метод close() для выполнения надлежащей очистки, однако они также используют финализаторы для предотвращения возможности утечки ресурсов в случае, если какой-либо объект не был закрыт должным образом. Для остальных объектов, как правило, лучше избегать использования финализаторов. Нет никакой гарантии ни в отношении времени, когда финализатор будет запущен, ни даже в отношении того, что он будет запущен вообще. Наличие финализатора означает, что недоступный объект не может быть обработан сборщиком мусора, пока не выполнен финализатор, при этом из-за этого объекта могут сохраняться еще и другие объекты. Это влечет за собой увеличение количества действующих объектов и, следовательно, увеличение потребления памяти из кучи процессом Java. Способность финализатора восстанавливать объект, подлежащий обработке сборщиком мусора, безусловно, является одним из непредусмотренных последствий устройства механизма финализации. Более новые реализации JVM позволяют вам защитить свой код от негативного влияния данного эффекта на безопасность.