Заключение
«Страх рождает гнев. Гнев рождает ненависть. Ненависть влечет…» на темную сторону. Это предсказуемо, и именно поэтому джедаи учатся анализировать свои чувства. Если бы джедаи ограничивались догадками о том, что они чувствуют («Мне плохо!»), то их способность противостоять темной стороне была бы намного ниже.
Атаки на предсказывание и угадывание являются мощными. Легко забыть об удивительной скорости современных компьютеров. Мы можем создать довольно сильную защиту от них, требуя больших пространств поиска, замедляя наши ответы и используя качественную случайность, чтобы повлиять на то, сколько времени займет поиск.
Время также может быть важной частью обороны. Синхронизация часов позволяет нам лучше организовать вывод ключей из оборота и их ротацию, чтобы у поисковиков не было достаточно времени, чтобы исчерпать нашу случайность.
8Распознавание и порча
Как и порча джедая, порча памяти происходит поэтапно. Семя посажено, оно растет, и в конце концов ситх пытается собрать урожай. Начальное изменение может быть всего лишь одним битом, а наградой, которую он получает, часто является возможность запускать код по выбору злоумышленника.
Ввод портит, а неограниченный ввод портит коварно. Ввод портит, потому что он является источником, носителем, средой, сообщением которой является LULZ. Почти все атаки связаны с вводом. Но полезные программы должны обрабатывать входные данные, а интересные программы, те, которые удивляют, восхищают или даже просто служат нам, принимают сложные входные данные. Эти входные данные иногда хитро и коварно спланированы таким образом, чтобы иметь конкретные и пагубные последствия. Чтобы было понятно, это сюрприз для программиста, написавшего код, а не для того, кто создает входные данные.
В этой главе мы рассмотрим порчу памяти, которая часто происходит при синтаксическом анализе ввода; это шаг на пути к уязвимости, но не синоним ее. Память может быть повреждена случайно. Обычно эти ошибки (или космические лучи) приводят к краху или бесполезно странному поведению.
После того как мы рассмотрим порчу и угрозы для средств синтаксического анализа (парсеров), мы обратимся к средствам защиты, включая проверку ввода во многих ее разновидностях, средствам безопасности памяти, которые стремятся ограничить и сдержать повреждение, а затем к надежным защитным шаблонам, включая Recognizer, Single Parser и более безопасный дизайн языка. Шаблон Recognizer (распознаватель) концентрирует весь синтаксический анализ в Recognizer, который передает его результат остальной части кода. Полезно держать эту идею в голове по мере чтения главы. Если совсем коротко, то, когда синтаксический анализ сконцентрирован в одном месте, облегчается оценка результатов. Когда он распределен, становится легко чередовать его с бизнес-логикой или даже забыть, что входные данные не были проверены.
Что такое синтаксический анализ (parsing)?
Синтаксический анализ – это процесс получения входных данных, разделения их на токены и помещения этих токенов в структуру из одного или нескольких объектов в памяти. (Токен – это наименьшая единица, имеющая выраженное значение.) Звучит так просто! Любой, кто когда-либо смотрел на регулярное выражение и задавался вопросом, почему произошло совпадение (или нет), интуитивно начинает понимать, почему синтаксический анализ сложен. Токены могут быть размером с один бит и часто состоят из нескольких символов: числа, операторы, такие как ++ или +=, и имена переменных – все это токены.
Средства синтаксического анализа работают в широком диапазоне сложности ввода, от обработки простого текстового ввода, такого как номер телефона, до синтаксического анализа исходного или машиного кода программы, PDF-файла или веб-страницы. Выходные данные синтаксического анализатора используются непосредственно нашими программами – иногда на этапе валидации, иногда в сочетании с другой информацией, а иногда передаются в другой код. Этап валидации гарантирует, что данные соответствуют бизнес-правилам. Проверка иногда выполняется на необработанных входных данных или смешивается с синтаксическим анализом в рамках так называемого подхода «стрельба из дробовика» (размазанный парсинг), в отличие от целенаправленного подхода.
Мы часто представляем, что синтаксические анализаторы считывают и проверяют входные данные и создают для нас разумный объект, с которым мы можем работать. Анализируя номер телефона, мы можем прочитать ровно десять цифр, что работает нормально, если вы находитесь в Соединенных Штатах и человек, вводящий номер, не включил скобки, тире или пробелы. Таким образом, вы можете прочитать 15 или 20 символов и использовать регулярное выражение, такое как [-()0123456789]+, чтобы проверить это. Конечно, это допускает и 86–75(309), но, возможно, это приемлемый ввод для вашего кода набора номера.
Если вам нужно проанализировать телефонные номера, которые относятся к нескольким странам, вы должны учитывать начальный плюс и длину, которая варьируется в зависимости от кода страны. И по мере того, как вы идете по этому, казалось бы, простому пути, вы получаете регулярные выражения, подобные этому:
^(?:(?:[\+]?(?
[(]?(?[\d]{3})[\-/)]?(?:[]+)?)?(?
[a-z0-9 \-.]{6,})?$
Это урезанная версия рекомендации [Reick, 2008]. Не пропустите включение «a – z» в числовой части! Кажущаяся простой проблема настолько сложна, что для этого созданы целые библиотеки. Libphonenumber от Google документирует сложности в FAQ и в документе «Мифы о телефонных номерах, в которые верят программисты».
Таким образом, мы видим, как даже номинально простые данные могут быстро стать сложными. Одна из сложностей при синтаксическом анализе телефонного номера заключается в том, что формат варьируется в зависимости от данных. То есть телефонный номер, начинающийся с +1 (Северная Америка), скорее всего, будет состоять из десяти цифр, в то время как в некоторых частях Европы стационарные телефоны имеют семь цифр, а мобильные номера – восемь, поэтому содержание данных влияет на поток управления парсером, что быстро приводит к неожиданному поведению. Точно так же распространенные форматы дат, такие как 01.04.04, невозможно проанализировать без контекста. Самый очевидный вопрос: «Что это, американская дата, которая могла быть записана как 1 апреля 1904 года, или британская дата, например 4 января 2004 года?»
В более общем случае синтаксические анализаторы распознают входные данные и создают объект для обработки. Мы вернемся к этому после обсуждения того, что думаем об этих входных данных.
Обычно говорят, что «наши входные данные – это JPG» или «наши входные данные – JSON». Но это не совсем так. Входные данные представляют собой поток битов. Этот поток может поступать по сети или с локального диска. Вы можете надеяться, что это JPG или JSON или даже какой-то другой формат, который не начинается с J, но на самом деле в памяти есть набор битов, которые можно организовать во что-то полезное.
Весь ввод – это биты
Все входные данные являются битами. Или, как мы говорим в шестнадцатеричном формате:
41 6c 6c 20 69 6e 70 75 74 20 69 73 20 62 69 74 73 0a
Возможно, вы предпочитаете бинарную систему? Как известно C-3PO, это язык влагосборников (и всего остального?):
01000011 0011000 10010000…
Это фраза All input is bits, отображаемая в hexdump. Каждое представление несет в себе один и тот же смысл, будучи расшифрованным особым образом. Мы можем рассматривать каждый из них с разных точек зрения. Есть представление на странице, начинающееся со слова «All» или с «41 6c». Мы можем сдвинуть угол обзора и думать о них как о шестнадцатеричном представлении строки. Но они также представляют собой набор символов ASCII, и я мог бы незаметно заменить ноль буквой O. Пока они находятся на странице, вы можете этого не заметить. Если я поменяю шрифт на программный, то нули будут отображаться совсем по-другому (Ø или 0), хотя данные не изменятся. Точно так же Юникод содержит индикатор RLO (текст справа налево), который изменяет порядок отображения без изменения базовых данных.
Ключевым моментом является то, что существует более одного правильного способа интерпретации данных. Они одновременно истинны. (В будущем эти фрагменты будут преобразованы в кривые в PDF-файле, и мы покажем эти кривые чернилами на бумаге или растровыми изображениями для отображения на экране. И верных интерпретаций будет еще больше.)
Весь код – это тоже биты
Небольшое понимание того, как компьютеры выполняют инструкции, может придать вам больше интуитивной осмотрительности в отношении входных данных. Когда в большинстве книг показывают низкоуровневый код, они показывают ассемблерные инструкции, такие как MOV, ADD или JMP. Это мнемоники.
Весь код состоит из битов. Эти ассемблерные инструкции преобразуются в машинные инструкции и хранятся в памяти в виде битов. А это значит, что, если вы можете писать в то место в памяти, где процессор ожидает инструкцию… Ну, это все будут биты.
Как объясняет Бен Кеноби, двоичный код – это то, что дает компьютеру инструкции. Это энергетическое поле, используемое всеми цифровыми вещами. Он окружает их и проникает в них, и он связывает интернет воедино. О, подождите, кажется, он говорил о Силе.
Понимая, что весь код состоит из битов, вы можете начать представлять, что происходит, когда биты «данных» появляются в том месте, где что-то ожидает биты кода. Бинарный язык влагосборников что-то делает при подаче в дегидратор. На самом деле это, вероятно, приводит к сбою сушилки, потому что процессоры немного отличаются. Но если серия битов создана кем-то, кто свободно владеет более чем шестью миллионами форм коммуникации, то, возможно, он сможет создать серию битов, которая сделает что-то неожиданное, и это приводит нас к угрозам при синтаксическом анализе. Легко сказать, что процессор должен просто отслеживать, какие биты к чему. Но, как правило, процессор будет выполнять то, на что указывает указатель выполнения.