Глава 9. Проблемы реализации. Часть I
уничтожить состояние сеанса безопасного канала общения. Библиотека да-
же может содержать специальную функцию для уничтожения состояния, но
что, если разработчик приложения не станет утруждать себя вызовом еще
одной функции? В конце концов, программа будет прекрасно работать и без
обращения к этой функции.
В некоторых объектно-ориентированных языках программирования ре-
шить эту проблему несколько проще. В C++ у каждого объекта есть деструк-
тор, который может автоматически уничтожать его состояние. Это стандарт-
ный прием, применяемый при написании систем безопасности на C++. Если
главная программа ведет себя правильно и уничтожает все ненужные объ-
екты, деструкторы автоматически очистят соответствующие области памя-
ти. Язык C++ гарантирует, что при обработке исключения будут корректно
уничтожены все объекты стека, однако уничтожением объектов, хранящихся
в динамической памяти (“куче”), должна заниматься сама программа. Ес-
ли для завершения работы программы вызывается функция операционной
системы, она может не удосужиться даже пройти по стеку вызовов. Вам при-
дется самим обеспечивать уничтожение данных, даже если программа вскоре
должна завершить работу. В конце концов, операционная система не дает ни-
каких гарантий того, что данные будут стерты из памяти в самое ближайшее
время. Некоторые операционные системы вообще не утруждают себя очист-
кой освободившейся памяти, когда передают ее следующему приложению.
Даже если вы проделаете все необходимые операции по уничтожению
объектов, ваши усилия могут быть сведены на нет. Некоторые компилято-
ры слишком усердствуют, стараясь все и всегда оптимизировать. Типичная
функция, имеющая отношение к системе безопасности, выполняет несколь-
ко вычислений в локальных переменных и затем пытается уничтожить их,
очистив соответствующие области памяти. В языке C для этого часто ис-
пользуют функцию
memset
. Хорошие компиляторы оптимизируют функцию
memset
, заменяя при трансляции вызов этой функции ее телом, что повышает
скорость работы программы. Но некоторые компиляторы в своем стремлении
к оптимизации заходят чересчур далеко. Они определяют, что уничтожаемая
переменная или массив больше не будут использоваться в программе, и вооб-
ще пропускают функцию
memset
. Это еще больше повышает скорость рабо-
ты программы, однако способно изменить ее поведение самым неожиданным
образом. Нередко код программы выдает данные, случайно обнаруженные
им в оперативной памяти. Если высвобожденная, но не очищенная память
передается какой-нибудь библиотеке, использование последней может при-
вести к утечке данных, которые тут же попадут в руки злоумышленника.
Поэтому не забывайте проверять код, который генерирует ваш компилятор,
и убеждаться в том, что секретные данные действительно удаляются из па-
мяти компьютера.
9.3. Как сохранить секреты
159
Ситуация еще более усложняется в языках наподобие Java. Здесь все объ-
екты находятся в динамической памяти, которая периодически подвергается
очистке посредством сборщика мусора (garbage collector). Это означает, что
метод
Finalize
(аналог деструктора в C++) не вызывается до тех пор, пока
сборщик мусора не обнаружит, что объект больше не используется. Никаких
спецификаций относительно того, как часто запускается сборщик мусора, не
существует. Вполне вероятно, что секретные данные остаются в памяти на
протяжении достаточно долгого времени. Использование обработки исклю-
чений значительно затрудняет очищение памяти вручную. Если программа
выдает исключение, она проходит по стеку вызовов, не давая программи-
сту никакой возможности вставить свой собственный код. Единственное, что
можно было бы сделать в данной ситуации, — это представить
каждую
функ-
цию в виде большого блока
try
. Разумеется, данное решение слишком урод-
ливо и непрактично. Оно также должно было бы применяться на протяжении
абсолютно всей программы, что сделало бы невозможным создание хорошей
библиотеки безопасности для Java. В процессе обработки исключений Java
спокойно проходит по стеку вызовов, выбрасывая ссылки на объекты и не
уничтожая при этом самих объектов. В этом отношении Java действительно
оказывается не на высоте. Самое лучшее решение, которое мы смогли при-
думать на данный момент, — это гарантировать запуск методов
Finalize
по
крайней мере при завершении работы программы. Для этого метод
main
дол-
жен содержать операторы
try-finally
. Код, содержащийся в блоке
finally
,
должен инициировать принудительную очистку памяти, дав указания сбор-
щику мусора, чтобы тот попытался завершить все методы
Finalize
. (См. до-
кументацию к функциям
System.gc()
и
System.runInitialization()
.) Дан-
ный прием тоже не гарантирует, что методы
Finalize
действительно будут
запущены, но это лучшее, что можно сделать.
Чего нам действительно не хватает — так это поддержки от самого языка
программирования. В C++ есть хотя бы теоретическая возможность напи-
сать программу, уничтожающую содержимое всех объектов, которые уже не
нужны. К сожалению, другие особенности этого языка делают его выбор
крайне неудачным для написания систем безопасности. В Java уничтожить
содержимое объекта явно практически невозможно. Было бы хорошо, если
бы мы могли объявлять такие переменные, как “sensitive” (“требующие осо-
бого обращения”), что гарантировало бы уничтожение их содержимого. Еще
лучше, если бы у нас был язык программирования, который бы всегда уни-
чтожал все ненужные данные. Это бы позволило избежать массы ошибок без
существенного снижения производительности.
Секретная информация может очутиться еще в нескольких местах. Все
данные в конце концов попадают в регистры процессора. Большинство язы-
ков программирования не имеют средств для очистки регистров. Впрочем,
160
Достарыңызбен бөлісу: |