Защита систем. Чему «Звездные войны» учат инженера ПО — страница 51 из 68

перечисленные в разделе «Защита». Это может быть сделано умно. Вам становится доступен не только переход от гаджета к гаджету, чтобы выполнять команды по своему выбору, но и создание ветвящегося кода! (Это полезно, если вам нужно подстроить параметры, чтобы обойти защиту памяти.) На самом деле, было показано, что небольшие гаджеты, используемые в возвратно-ориентированном программировании, являются тьюринг-полным языком.

• Использование после освобождения происходит при наличии двух указателей на один и тот же участок памяти. Один из указателей удаляется при освобождении объекта, но код забывает очистить другой. В зависимости от того, как злоумышленник контролирует забытый указатель, он может быть использован для чтения, записи или выполнения. Один из типичных потоков выглядит следующим образом:

– злоумышленник заставляет ОС освободить исходный объект;

– злоумышленник заставляет ОС заполнить пространство исходного объекта данными, которые он контролирует;

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

(Существуют проблемы с освобождением после использования и без множественных указателей, которые я опускаю из соображений экономии места.)

• Конвертация типов и продвижение. Если у вас есть такой код на C:

char char1, char2, char3;

char3 = char1 + char2;

тогда значение char3 может быть больше, чем максимум для типа char. Таким образом, переменная char3 получит тип, поддерживающий бóльшие значения, и он может перетащить за собой char1 и char2! Эксплуатация этих проблем довольно тонкая, но самый простой случай заключается в том, что эти значения используются в тестах, контролирующих поток выполнения.

Если вы действительно решите узнать больше об этих техниках, имейте в виду: дорога на темную сторону приносит много страданий на пути к могуществу.

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

Организация памяти

Я упомянул «стек», и пришло время более четко объяснить, что это такое. Локальные переменные выполняемой программы хранятся в стеке, они подлежат вставке в стек и выталкиванию из него и обрабатывают сочетание кода и данных. (Представьте себе стопку тарелок, возможно, на пружине, в кафетерии. Вы можете ставить тарелки в стопку или вынимать одну из них, но только из верхней части стопки.)

Вы ожидаете здесь шестнадцатеричных дампов, не так ли? Что-то вроде того, что выглядит вот так и заставляет глаза стекленеть [Aleph1, 1996]?


Dump of assembler code for function main:

0x8000490

: pushl %ebp

0x8000491 : movl %esp,%ebp

0x8000493 : subl $0x4,%esp

0x8000496 : movl $0x0,0xfffffffc(%ebp)


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

• Строки и другие переменные, содержащие данные, предоставленные пользователем, часто попадают в стек.

• Если вы скопируете больше данных, чем ожидалось, вы можете разбить стек этими данными, предоставленными пользователем.

• Некоторые пользователи являются злоумышленниками.

• Указатель инструкций указывает на конец стека, который может быть разбит.

• Код состоит из битов. Данные – это биты. Они хранятся одинаково в памяти, и процессор не может их различить.

• Процессор делает то, что ему говорит указатель инструкций.

Таким образом, решение состоит в том, чтобы прекратить помещать неограниченные данные в стек. Большинство современных языков управляют памятью за вас снисходительным образом. Язык C будет обрабатывать память именно так, как вы ему скажете. В точности как вы скажете. В ТОЧНОСТИ. Вы должны понимать, что вы говорите C, C++ и другим языкам, которые позволяют управлять памятью вручную.

В дополнение к стеку, другим важным типом памяти является куча. Куча – это место, где находятся постоянные переменные и структуры данных, а памятью необходимо управлять с помощью семейства вызовов alloc. (Вы также не можете записывать неограниченные данные в кучу.) Современные языки справятся с памятью за вас. Они отслеживают память, которую выделили для того, чтобы сборщик мусора мог пройти, приостановить ваш код в наименее удобное время, выпить чашку кофе Java и очистить вашу неиспользуемую память, оставляя небольшие кусочки здесь и там, чтобы предназначенные для выделения блоки были фрагментированы. Шучу! Java – не единственный язык с раздражающей сборкой мусора. Что еще более важно для наших целей, куча является удобным местом для злоумышленника, чтобы сбрасывать код или другие ресурсы, которые он хотел бы иметь доступными при запуске эксплойта. Это часто достигается с помощью «распыления кучи» (heap spraying). Затем доступ к памяти осуществляется кодом злоумышленника.

Защита

Важно, хотя и недостаточно, сказать, что защита от проблем с синтаксическим анализом – это крайняя осторожность. Важно, так как при синтаксическом анализе возникает очень много проблем с безопасностью, и недостаточно, потому что с этим ничего не сделаешь. Сообщество LangSec – это академическое движение. Они рассматривают «эпидемию незащищенности в интернете как следствие программирования ad hoc (ситуационно) обработки ввода данных на всех уровнях…». А также приводят убедительные доводы в пользу того, что общая стоимость формальной спецификации часто себя оправдывает. Но им не нужно платить за изменение вашего программного обеспечения или программного обеспечения ваших конкурентов или экосистемы.

Этот раздел посвящен набору средств защиты, в том числе тому, как думать об устойчивости, защитном распознавании и методах валидации. После этого мы рассмотрим наиболее сильные шаблоны из LangSec, а закончим обсуждением безопасности памяти, потому что очень часто повреждающим эффектом плохого ввода является порча памяти, приводящая к выполнению кода.

Принцип устойчивости

Раннее выражение принципа устойчивости Постеля звучало так: «Будьте либеральны в том, что вы принимаете, и консервативны в том, что вы посылаете». В 2012 году группа исследователей, занимающихся вопросами безопасности языка, «пропатчила» этот принцип. Его положения приведены ниже.

• Будьте уверены в том, что вы принимаете.

• Обрабатывайте допустимые или ожидаемые входные данные как формальные языки, принимайте их с соответствующей вычислительной мощностью и создавайте Recognizer на основе их грамматики.

• Уменьшайте вычислительную сложность синтаксического анализа. (Последнее перефразировано; все взято из [Sassaman, 2012].)

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

Даже если у вас нет контроля над форматами, которые вы должны принять, вы можете быть консервативны в том, что отправляете. Не усложняйте синтаксический анализ выходных данных. Выдача относительных имен путей, escape-последовательностей, которые можно было бы упростить, или выражений, предназначенных для вычисления принимающей стороной, где вы могли бы этого избежать, означает, что синтаксические анализаторы вынуждены анализировать такие конструкции. Будьте осторожны в том, что вы отправляете, и это поможет нам избежать ползучего возрастания сложности.

Валидация входа

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

Поскольку все данные являются битами и могут использоваться бесконечным количеством различных способов, проверка не может быть «полной» без указания формата или контракта, по которому была выполнена проверка. В примере с внедрением кода SQL подготовленные операторы помещают код и данные в тщательно разделенные переменные.

Валидацию лучше всего выполнять перед записью в строго типизированную переменную, такую как Signed32bitInt, email_address, URL и т. п. Строковый тип, который может быть использован для любого из них, затрудняет формирование конкретного контракта [Poll, 2018; Arce, 2014]. Можно создать более конкретные типы, такие как unsafe_path или filesystem_path_canonicalized_from_user. Обратите внимание на название: unsafe подразумевает, что мы не выполняли никаких проверок, и filesystem_path_canonicalized позволяет легко отследить, что они выполняются, прежде чем присваивать им какие-либо данные, и не подразумевает ничего другого. Называя переменную безопасной, можно легко впасть в опасный оптимизм.

Возникает важный вопрос: где происходит проверка безопасности? Проверка безопасности должна выполняться, когда данные больше не могут быть изменены недоверенными сторонами. В веб-контексте это означает, что сервер проверяет, что ему отправляется. Даже если у вас есть список дат в выпадающем меню, если кто-то изменит HTML-код с помощью редактора исходного кода браузера или HTTP-вызов с помощью прокси-сервера, он может вставить произвольные или даже неправильно сформированные входные данные. Аналогично, вызовы ядра должны копировать данные в память, предназначенную только для ядра, перед их проверкой.

Это не значит, что вы не можете поставить «вежливую проверку» в браузере, что-то, что проверяет ввод, скорее всего, будет правильно проанализировано на сервере. Это хорошо по соображениям удобства использования, и вы можете беспокоиться только о том, что это либо нарушит принцип единого синтаксического анализатора, либо раскроет вашу процедуру валидации. Не беспокойтесь о раскрытии процедуры валидации: если ваша безопасность зависит от неясности, у вас проблемы. Вспомните раздел о прозрачности в главе 7 «Предсказуемость и случайность».