Илья Медведовский - Атака на Internet
При загрузке объекта ActiveX поведение браузера зависит от настроек его системы безопасности – как подписанные, так и неподписанные (либо заверенные неизвестной организацией) объекты могут быть либо автоматически загружены или отвергнуты, либо предъявлены пользователю, с тем чтобы дальнейшее решение принимал он. В Internet Explorer также можно задать разные настройки для различных зон безопасности – для локальной сети, Internet, отдельных подозрительных хостов, и наоборот, достойных особого доверия.
JavaScript, VBScript и т. п. представляют собой упрощенные языки подготовки сценариев, код которых встраивается непосредственно в html-файл и выполняется браузером. Они непригодны для реализации серьезных приложений, в них отсутствуют средства для работы с файлами, сетевого взаимодействия и т. д. Но они широко используются во вспомогательных целях, в качестве средства первоначальной обработки результатов, для оформления, «оживления» html-документов и т. д. Казалось бы, что ограничения, присущие этим языкам, делают их абсолютно безопасными, в действительности же львиная доля ошибок в браузерах связана именно с реализацией этих простейших средств разработки.
Безопасность Java-приложений
Java – язык, разработанный Sun Microsystems изначально для приложений бытовой электроники и позднее перенесенный в Internet, что стало для него вторым рождением. Различают обычные Java-приложения и апплеты, предназначенные для загрузки по сети и выполнения в окне браузера.
Вопросы безопасности Java-апплетов заслуживают того, чтобы о них говорить отдельно, поскольку это единственное распространенное средство разработки клиентских приложений, в котором решение проблемы безопасности предлагается уже на уровне архитектуры.
Основным достоинством Java-приложений является независимость от клиентской платформы. В отличие от традиционных приложений, транслирующихся в исполняемые коды процессора, Java-приложения транслируются в так называемый байт-код, интерпретируемый в дальнейшем виртуальной Java-машиной. При этом байт-код независим от платформы, на которой он в дальнейшем будет выполняться, – достаточно, чтобы для этой платформы существовала Java-машина. Поскольку большинство основных функций реализовано на уровне виртуальной Java-машины, это приводит к существенному уменьшению размеров байт-кода, что является как достоинством, так и недостатком Java-приложений. Так как байт-код интерпретируется виртуальной машиной, производительность Java-приложений уступает производительности традиционных откомпилированных программ. Частично с этим удается бороться, применяя компиляторы времени исполнения (JIT – just in time compilers), осуществляющие компиляцию приложения при его загрузке в «родной» для данного процессора код. Также возможен вызов функций, реализованных на других языках программирования (С, С++) и откомпилированных для данной платформы, – так называемый native code (родной код). Он применяется при реализации наиболее критичных ко времени исполнения фрагментов кода.
Другим достоинством Java-приложений является защищенность. Во-первых, сам язык способствует написанию более надежных и устойчивых к сбоям программ. Помимо строгой типизации, управления доступом, работы с исключениями, знакомых программистам и по С++, в Java добавлена автоматическая «сборка мусора» (освобождение неиспользуемой памяти), проверка на выход за границы массива, возможность указать, что данный метод или объект не может быть изменен или переопределен. В языке нет указателей и переопределенных операторов.
Все эти нововведения помогают создавать более безопасный код. Рассмотрим теперь особенности Java, вынуждающие писать безопасный код.
По мере развития Java развивалась и система безопасности. В JDK 1.0 (Java Development Kit) основу системы безопасности составляли три компонента – Verifier (верификатор), ClassLoader (загрузчик классов) и SecurityManager (менеджер безопасности). Эта модель известна под названием sandbox (песочница), в ней выполняются Java-приложения, загруженные из сети (рис. 10.1).
Рис. 10.1. Модель безопасности JDK 1.0
Для полноценного функционирования модели безопасности каждый ее компонент должен работать безошибочно, поскольку только четкая совместная работа компонентов обеспечивает контроль над приложением во время загрузки и исполнения кода.
Первый рубеж обороны – верификатор, проверяющий загружаемый байт-код на корректность, так как у нас нет никакой гарантии, что загружаемый код был получен в результате работы компилятора Java, а не подправлен вручную или не сгенерирован специальным «враждебным» компилятором. После того как код прошел верификацию, гарантируется, что файл класса имеет корректный формат, параметры всех операций имеют правильный тип, в коде не происходит некорректных преобразований типов (например, целого числа в указатель), нет нарушений доступа и т. п. Таким образом, проверяется все, что только можно проверить до начала исполнения программы. Верификатор встроен в виртуальную машину и недоступен из Java-программы.
Загрузчики классов определяют, когда и каким образом классы могут быть добавлены в работающую систему. Частью их работы является защита важных составляющих системы, например запрет на загрузку поддельного менеджера безопасности. Они выполняют две основные функции – собственно загрузку байт-кода (с локального диска, по сети, из области памяти и т. д.), определение namespaces (пространства имен) для различных классов и способов их взаимодействия (отделяя, к примеру, локальные классы от загруженных по сети).
Есть два вида загрузчиков – primordial (первичный) и Class Loader Object (реализованный в виде объекта). Первичный существует в единственном экземпляре и является составной частью виртуальной машины. Он занимается загрузкой доверенных классов (обычно находящихся на локальном диске). Загрузчик второго типа представляет собой экземпляр обычного Java-класса, унаследованного от абстрактного класса java.lang.ClassLoader. С его помощью можно осуществить загрузку класса по сети либо динамическое конструирование класса приложением. Метод defineClass преобразует массив байтов в экземпляр класса Class, а экземпляры нового класса создаются с помощью метода newInstance класса Class. Если метод созданного класса ссылается на другие классы, виртуальная машина вызывает метод loadClass его загрузчика, передавая ему имя запрашиваемого класса. При создании экземпляра класса или вызове любого из его методов потребуется также загрузка его предка и других используемых им классов, за это отвечает функция resolveClass.
Примерная реализация класса NetworkClassLoader может выглядеть следующим образом:class NetworkClassLoader
{
String host;
int port;
Hashtable cache = new Hashtable();
NetworkClassLoader(String aHost, int aPort)
{
host = aHost;
port = aPort;
}
private byte loadClassData(String name)[]
{
// собственно загрузка класса
. . .
}
public synchronized Class loadClass(String name, boolean resolve)
{
Class c = cache.get(name);
// Хэш-таблица используется для исключения
if (c == null)
// повторной загрузки класса
{
// и формирования пространства имен
byte data[] = loadClassData(name);
c = defineClass(data, 0, data.length);
cache.put(name, c);
}
if (resolve)
resolveClass(c);
return c;
}
}
ClassLoader loader = new NetworkClassLoader(host, port);
Object main = loader.loadClass("Main", true).newInstance();На самом деле загрузчик сначала должен обратиться к пространству имен первичного загрузчика и затем, лишь не обнаружив там запрашиваемого класса, продолжить поиск по пространству имен ссылающегося класса, чтобы предотвратить подделку встроенных классов. Причем загрузчик работает совместно с менеджером безопасности, определяющим, можно ли загружать данный класс. Весь алгоритм действий загрузчика обычно выглядит следующим образом:
1. Определить, не был ли загружен этот класс раньше, и, если да, вернуть его.
2. Проконсультироваться с первичным загрузчиком на предмет существования внутреннего класса с этим именем.
3. Запросить у менеджера безопасности разрешение на загрузку данного класса.
4. Считать файл класса в виде массива байтов – по сети, с диска и т. п.
5. Создать экземпляр класса Class.
6. Загрузить используемые классы.
7. Передать класс верификатору на проверку.
Загрузчик является существенным элементом модели безопасности, поэтому писать загрузчики следует особо осторожно – малейшая ошибка может привести к полному краху всей системы безопасности. В нормальной ситуации апплеты не имеют возможности устанавливать свои загрузчики.
Класс SecurityManager отвечает за политику безопасности приложения. Он позволяет приложению перед выполнением потенциально опасной операции выяснить, выполняется ли она классом, загруженным первичным загрузчиком, либо с помощью некоторого ClassLoader (к последнему, особенно при загрузке из сети, доверия должно быть гораздо меньше). Далее менеджер безопасности может определить, разрешить ли эту операцию или наложить на нее вето. Класс SecurityManager выявляет ряд методов, начинающихся со слова «check» (checkDelete, checkExec, checkConnect и т. п.), которые вызываются всеми методами стандартной библиотеки, выполняющими потенциально опасные действия (работа с файлами, сетевыми соединениями и т. п.). Выглядит это обычно следующим образом: