SEC·X Секреты и атаки Глава 61 из 65
Учебный полигон
Математика шифров из прошлой главы стойкая, а системы на ней всё равно взламывают — почти всегда через ошибку в коде или в человеке. Войдём на учебный полигон: уязвимое приложение, которое можно атаковать и чинить, не выходя за его стены. Переполнение буфера, которым в 1988-м прошёл червь Морриса; SQL-инъекция и мальчик по имени «Bobby Tables»; чужой скрипт на вашей странице; сервер, который сказал лишнее, — Heartbleed. После каждой атаки ставим заплатку и проверяем, что атака больше не проходит.
Секреты и атаки
- 59 Шифры
- 60 Открытый ключ
- 61 Безопасность вы здесь
Опирается на: 60 · Секрет на виду у всех 43 · Анатомия этой страницы 45 · Архивариус
Что вы унесёте из главы
- видеть в своём коде места, где чужие данные становятся командой, и закрывать их: параметры запроса вместо склейки строк, проверка границ, экранирование вывода
- понимать, как устроены переполнение буфера, SQL-инъекция, XSS и утечка памяти вроде Heartbleed — и почему заплатка от каждой держится
- знать правила безопасной работы: наименьшие привилегии, защита в несколько слоёв, ответственное раскрытие и закон
В прошлой главе мы собрали криптографию, которую нельзя сломать в лоб: чтобы разложить ключ RSA на множители, не хватит и возраста Вселенной. И всё же системы, построенные на этой математике, взламывают каждый день. Пароли утекают из банка, потому что программист склеил SQL-запрос из строк. Сайт с безупречным TLS отдаёт чужие пароли, потому что одна функция не проверила длину. Шифр в обоих случаях цел. Замок держит, а дверь вынимают вместе с косяком — или уговаривают хозяина открыть самому. Эта глава — о косяках.
Учиться атаке, никому не навредив, можно только на полигоне. Наш полигон — сама глава: уязвимая функция в живой ячейке, учебная форма входа в виджете, крошечная база данных, которая живёт секунду в памяти песочницы и исчезает. Вы ломаете эти мишени и тут же чините, а тесты задач проверяют, что после заплатки атака, только что прошедшая, больше не проходит. Чужих систем мы не трогаем: этого требует не одна вежливость, но и закон — о нём в конце главы.
Полигон и модель угроз
Прежде чем строить защиту, отвечают на три вопроса: что мы защищаем, от кого и чего это стоит. Ответы вместе называют моделью угроз. Без неё разговор о безопасности превращается в размахивание руками: «надо шифровать всё» — а зачем, от кого и что будет, если не шифровать? От школьника, который балуется в соседней вкладке, спасает одно; от того, кто неделями изучает ваш сервер за деньги, — совсем другое; а если вы сами продиктовали пароль по телефону, не спасёт ничего из перечисленного.
Слабое место, через которое нарушитель добивается того, чего вы не предусмотрели, называют уязвимостью, а готовый способ ею воспользоваться — эксплойтом. Знать об ошибке в программе и уметь превратить её в атаку — далеко не одно и то же. На полигоне мы учимся и тому и другому, чтобы потом замечать такие ошибки в своём коде раньше, чем их заметит кто-то другой.
Если сжать главу до одной фразы: почти всякая атака — это данные, которые притворились командой. Строка из формы входа оказывается куском SQL, буквы из сети ложатся на адрес возврата и решают, куда прыгнет программа, текст комментария исполняется как скрипт на чужой странице. Поэтому и защита везде одна по духу: провести явную границу, за которой данные не могут сыграть роль кода.
Под полигоном лежит песочница, в которой выполняются все ячейки курса. Она запускает ваш код без доступа к сети, под учётной записью без прав, в памяти, которую стирает после каждого запуска. Уязвимую программу в такой ячейке можно писать и запускать спокойно: вредить ей некуда и нечем. Из каких слоёв сложена сама песочница, мы разберём ближе к концу, когда дойдём до наименьших привилегий.
Ночь 2 ноября 1988 года
Начнём с переполнения буфера: оно старше остальных атак этой главы и нагляднее всего показывает, как данные становятся кодом. Для него понадобится C из главы 33, где Python под микроскопом оказался программой на C, а запись за край массива тихо портила соседнюю память.
За краем буфера
Разницу между Python и C глава 33 свела к одной строке. Python перед каждой записью по индексу проверяет границу и на выходе за край бросает IndexError. C не проверяет ничего: запись за край массива — неопределённое поведение, и в памяти молча меняется то, что лежало рядом. Если рядом свои же данные, выходит тихая порча. Если рядом адрес, по которому программа вернётся из функции, то тот, кто пишет за край, выбирает, куда она прыгнет. Вот уязвимая учебная программа. Она принимает «имя пользователя» и кладёт его в структуру, где сразу за именем лежит поле balance — остаток на счёте.
Короткое имя проходит без происшествий: остаток как был, сто. Но поле name — восемь байт, а strcpy копирует столько, сколько прислали. Тринадцать латинских букв (тринадцать байт и завершающий ноль) в восемь не помещаются, и лишние пять ложатся прямо на balance. Счёт никто не трогал, а остаток стал огромным числом, собранным из кодов буквы «A»: данные из stdin переписали переменную, к которой не имели никакого отношения.
Переполнение буфера — запись за границу буфера, которая портит то, что лежит дальше. Пока дальше соседняя переменная, дело кончается неверным числом. Но в главе 33 мы видели план кадра стека: сразу за локальными переменными лежат сохранённый указатель кадра и адрес возврата. Если прислать имя подлиннее и подобрать байты, на адрес возврата ляжет число, выбранное атакующим, и ret прыгнет туда, куда он хочет. Так червь Морриса и входил через fingerd. Доводить эту атаку до конца мы не станем. За краем структуры программа падает по-разному на разных машинах (в песочнице — по сигналу), а конкретные адреса зависят от процессора и ключей компилятора. Хватит и увиденного: лишние данные затирают соседнюю память, и что именно затереть, решает тот, кто их прислал.
Заплатка очевидна и скучна, потому её так часто и забывают: копировать не больше, чем влезает, и оставлять место под завершающий ноль. В C для этого есть snprintf, он сам обрежет лишнее.
Теперь длинное имя обрезается до семи букв, а остаток держится на ста при любой длине входа: лишним байтам больше некуда лечь. В задаче bounds-check вы почините похожую программу сами — там в массив на сто чисел пытаются записать двести.
Реальные системы одной проверкой не ограничиваются. Поверх неё стоят ещё три слоя — на случай, если проверку всё-таки забыли. Канарейка на стеке: компилятор кладёт перед адресом возврата случайное число и проверяет его перед выходом; переполнение затрёт канарейку, несовпадение заметят, программу остановят. Неисполняемая память (право «исполнять как код» в таблице страниц из главы 38, его называют битом NX): страницы со стеком и кучей помечены «нельзя исполнять», так что данные, которые туда положили, не запустишь как код. Случайное расположение (ASLR): адреса библиотек и стека при каждом запуске разные, и атакующему некуда прицелиться — вы уже видели в главе 33, что адреса в C-программе меняются от запуска к запуску именно поэтому. Ни один слой не идеален, но вместе они превращают «тихо переписал адрес возврата» в «скорее всего, упал». К такой защите в несколько слоёв мы ещё вернёмся.
Мальчик по имени Bobby Tables
Переполнение буфера живёт в языках без проверки границ. Но та же беда — данные, ставшие командой, — случается и там, где никаких буферов не видно. В главе 45 мы спрашивали базу на SQL и мимоходом дали зарок: значения от посетителя передавать через ? и никогда не вклеивать в текст запроса. Теперь нарушим его нарочно. В комиксе xkcd №327 (2007 год) из школы звонят матери: база данных потеряла записи об учениках за весь год, потому что сына она назвала Robert'); DROP TABLE Students;--. С тех пор такие имена зовут «Bobby Tables».
Вот учебная форма входа. База — крошечная таблица пользователей, она живёт в памяти песочницы один запуск. Функция проверки склеивает SQL-запрос из строк — так её часто пишут новички.
Первые две строки ведут себя как надо: с верным паролем — user, с неверным — None. А третья входит без пароля вообще, да ещё и как первый попавшийся пользователь. Разгадка — в напечатанном запросе: имя ' OR 1=1 -- вклеилось в текст, и условие стало читаться так: name = '' OR 1=1 --' AND pw = '…'. Одинарная кавычка закрыла пустое имя, OR 1=1 сделало условие истинным для каждой строки, а два дефиса в SQL начинают комментарий, так что проверку пароля база даже не увидела. Это SQL-инъекция: присланная строка перестала быть данными и стала частью команды. Имя «Bobby Tables» устроено так же, только вместо OR 1=1 там DROP TABLE.
Заплатка — зарок из главы 45. Текст запроса с ? на месте значений передаём базе отдельно от самих значений. Вопросительные знаки отмечают места, куда база сама подставит данные как содержимое ячейки; частью команды они уже не станут. Тот же запрос, те же атаки — и ни одна не проходит.
Все четыре атаки вернули None: база искала пользователя с именем ' OR 1=1 --, не нашла и дала от ворот поворот. Даже «Bobby Tables» с его DROP TABLE безвреден — последняя строка показывает, что таблица цела. Защищает здесь не хитрая проверка и не чёрный список опасных слов (его всегда можно обойти), а стена между командой и данными, проведённая один раз и навсегда. В полигоне ниже следите за текстом запроса: на склейке атака переписывает саму команду, а на параметрах команда не меняется ни на букву и присланная строка уходит в базу отдельно, как значение.
В задаче sqli-fix вы почините уязвимый вход сами. Тесты проверят две вещи: обычный вход по-прежнему работает, в том числе у человека с кавычкой в имени (d'Anthès), а ни одна инъекция не пускает внутрь и не роняет таблицу. Так проверяют любую заплатку: функциональность цела, атака не проходит.
Чужой скрипт на вашей странице
SQL-инъекция вклеивает данные в команду для базы. Если вклеить их в страницу, которую увидит другой человек, получится межсайтовый скриптинг. В главе 43 мы разобрали, что страница — это текст с тегами: браузер строит из него дерево DOM и исполняет всё, что попало в тег <script>. Представьте форум. Вы пишете комментарий, сервер сохраняет его и показывает каждому, кто откроет страницу. А если вместо текста прислать тег <script>?
В первом случае присланный тег попадёт в дерево страницы как обычная разметка. Браузер каждого читателя форума попытается загрузить картинку по несуществующему адресу x, загрузка не удастся, и сработает обработчик onerror — маленький скрипт, который выполнится от имени этого читателя, на этом сайте. Он может прочитать куки, отправить их на чужой сервер, нажать кнопки за посетителя. Это межсайтовый скриптинг (XSS). Во втором случае html.escape заменил < на <, > на > — и браузер покажет текст тега буквами вместо того, чтобы его выполнить. Принцип тот же, что с SQL: где данные входят в другой язык (здесь в HTML), их экранируют, и спецсимволы остаются буквами.
Защита стоит в два слоя. Первый — экранирование вывода, как выше. Второй мы встречали в главе 43: флаг HttpOnly у куки сессии. Он вовсе запрещает скриптам страницы читать это куки, так что даже прорвавшийся скрипт не утащит ключ сессии через document.cookie. Если первый слой забыли, второй уменьшает ущерб.
Как XSS выглядит на деле, в 2005 году показал Сэми Камкар. Его червь для MySpace, спрятанный в профиле, переползал от профиля к профилю и дописывал строчку «but most of all, samy is my hero»; меньше чем за сутки он добавил автора в друзья более чем к миллиону пользователей. Данных червь не крал и ничего не стирал, но MySpace пришлось на время закрыть сайт, чтобы его вычистить. Автор признал себя виновным во взломе и получил испытательный срок, общественные работы и запрет выходить в интернет. MySpace дыру закрыл, а XSS годами держится в списках самых частых уязвимостей рядом с инъекцией.
Слабый пароль
Третий путь червя Морриса был самым примитивным и самым надёжным: подбор пароля по короткому списку расхожих слов. С тех пор мало что изменилось. В утечках чаще всего по-прежнему встречается 123456, а рядом с ним в первых строках списков — 123456789, qwerty и password. Зачем перебирать все комбинации, если миллионы людей выбирают одни и те же сто? Пусть даже защита устроена правильно: пароли, как мы договорились в прошлой главе, хранятся солёными медленными хешами, и по украденной базе пароль сразу не прочитать — его приходится угадывать. Сколько угадываний на это уйдёт?
Шестизначный цифровой пароль падает мгновенно: миллион вариантов при десяти миллиардах проверок в секунду. Восемь строчных букв с цифрами держатся уже минуты. Решает длина: каждая добавленная буква умножает число вариантов на размер алфавита. Фраза из четырёх простых слов (знаменитый пример из xkcd — «correct horse battery staple») побуквенным перебором не берётся вовсе, а запомнить её легче, чем Tr0ub32. Поэтому длинная парольная фраза надёжнее короткого частокола из спецсимволов.
Но весь этот расчёт — про перебор вслепую. Живой атакующий начинает со словаря: утёкшие пароли, имена, даты, клавиатурные ряды. Против словаря длина не помогает, если сама фраза расхожая. Даже случайные слова перебирают целиком, а не по буквам: если атакующий знает, что пароль — четыре слова из списка в 2048 слов (так считал и xkcd), вариантов уже не $10^{49}$, а $2048^4 \approx 1{,}8 \cdot 10^{13}$, и на десяти миллиардах проверок в секунду это полчаса. Тут выручают медленный хеш и ещё пара слов: каждое умножает перебор на 2048. Поэтому к защите паролей добавляют то, что от их стойкости не зависит: ограничение числа попыток входа, второй фактор (код из приложения), проверку нового пароля по списку уже утёкших. На стороне хранения работает собранное в главе 60: соль, чтобы одинаковые пароли давали разные хеши, и медленный хеш, чтобы каждое угадывание дорого обходилось атакующему. В задаче password-strength вы соберёте оценщик, который сначала заглядывает в словарь, а уже потом считает перебор, — и увидите, как длинный, но расхожий пароль проваливается первым.
Когда сервер говорит лишнее
До сих пор атакующий заставлял систему выполнить лишнее. Но её можно заставить и сказать лишнее — отдать кусок памяти, который не предназначался никому. Самый известный такой случай — Heartbleed, 2014 год.
Heartbleed — зеркальное отражение переполнения: вместо лишней записи здесь лишнее чтение за краем буфера, и прочитанное уходит наружу. Корень тот же — доверие к числу, которое прислал другой. Соберём учебную модель: «память сервера» — строка, где рядом с безобидным ответом лежит секрет. Клиент шлёт слово и длину; уязвимый сервер отдаёт срез запрошенной длины, безопасный сверяет длину с тем, что прислали на деле, и на раздутую просьбу не отвечает вовсе.
Просьба с верной длиной возвращает «птицу». Та же просьба с раздутой длиной вытаскивает секрет, лежавший в памяти сразу за словом: сервер досказал то, о чём его не спрашивали. Починенный сервер видит, что длина больше присланного слова, и не отвечает вовсе (None): так и сделали в версии 1.0.1g. На симуляторе ниже следите за моментом, когда длина переходит за край слова: с этого байта сервер уже отдаёт чужое.
Звонок из «службы поддержки»
Все дыры до сих пор сидели в коде. Самая широкая — в человеке, который этот код обслуживает. Обмануть человека часто дешевле, чем искать уязвимость: не нужно ни переполнения, ни инъекции, хватит убедительного письма. Такой обман называют социальной инженерией, а её частный случай — подложное письмо или страницу, выманивающие пароль, — фишингом.
15 июля 2020 года в Twitter взломали сразу десятки самых заметных аккаунтов — Обамы, Маска, Гейтса, крупных компаний — и разом разослали от их имени обещание удвоить любой присланный биткойн. Техническую защиту аккаунтов при этом никто не ломал. Нападавшие позвонили сотрудникам Twitter, представились своей же службой поддержки и выманили у них вход во внутреннюю панель управления, из которой можно было войти в любой аккаунт. Атака затронула 130 аккаунтов, за пару часов мошенники собрали с доверчивых людей больше ста тысяч долларов в биткойнах, и для этого не понадобилось ни строчки эксплойта. Главным организатором оказался семнадцатилетний подросток.
Алгоритм здесь не поможет: атакуют человека. Помогают правила и привычки. Служба поддержки никогда не просит пароль или код из приложения. Письмо со словом «срочно» и ссылкой проверяют по адресу отправителя, а не по кнопке в самом письме. Доступ к важному дают по правилу «двух пар глаз». Всё это скучно и на программирование не похоже, поэтому через человека и входят чаще всего. Система надёжна настолько, насколько надёжно её самое слабое звено, а им нередко оказывается усталый человек в конце смены.
Наименьшие привилегии и слои
Идеальной защиты мы так и не нашли: проверку можно забыть, человека — обмануть, а в чужой библиотеке найдётся дыра. Раз взлом рано или поздно случится, спрашивать надо иначе: сколько нарушитель сможет сделать, когда войдёт? На этот вопрос отвечает правило, которое Джером Зальцер сформулировал в 1974 году, а через год вместе с Майклом Шрёдером включил в свой знаменитый список принципов защиты. Это принцип наименьших привилегий: каждая программа и каждый пользователь работают с тем минимумом прав, которого хватает для их задачи, и ни с чем сверх того. Тогда захваченная часть системы навредит лишь в пределах своих куцых прав, и вся машина не пострадает.
На этом принципе держится полигон, в котором вы запускали ячейки этой главы. Вокруг чужого кода там стоит несколько независимых стен: если одна не удержит, за ней следующая. Такой подход называют защитой в несколько слоёв, а окружение, которое так строят вокруг недоверенного кода, — песочницей. Ниже — стены вокруг вашей ячейки и атаки, которые останавливает каждая.
Слои описаны на уровне идей, без настроек: про чужую защиту полезно понимать, зачем нужен каждый слой, а как его обойти, мы не обсуждаем. Идея же переносится на любую систему. Код, которому вы не доверяете (а чужому коду вы не доверяете по определению), держите там, где у него нет ни сети, ни доступа к вашим файлам, ни лишних прав, ни времени и памяти сверх отмеренного. Тогда даже вредная программа сможет сделать лишь то, на что хватит этих крох, — почти ничего.
Отравленный колодец: закладки
До сих пор мы чинили свой код и учили своих людей. Но программа состоит не только из вашего кода: под ней компилятор, библиотеки, инструменты сборки, десятки чужих пакетов. Что, если отравлен сам колодец, из которого пьют все?
Этот вопрос уже звучал в главе 52. Кен Томпсон в тьюринговской лекции 1983 года показал, что закладку можно встроить в компилятор так, что она будет вставлять себя в каждую собранную программу и воспроизводиться даже в компиляторе, собранном из чистых исходников. Закладка — намеренно оставленный тайный ход; уязвимость же появляется по ошибке. Вывод Томпсона жёсткий: проверить исходники недостаточно, если не доверяешь всей цепочке инструментов, которой их собирали.
В марте 2024 года это перестало быть мысленным опытом. Инженер Андрес Фройнд, разбираясь, почему вход по SSH на его тестовой машине стал занимать чуть больше процессорного времени — доли секунды, которые он всё-таки заметил, — раскопал закладку в xz, крошечной и вездесущей библиотеке сжатия. Атака на цепочку поставок готовилась годами. Человек под именем «Jia Tan» с 2021 года помогал развивать проект, заслужил доверие, стал сопровождающим и затем спрятал закладку по частям. Саму нагрузку он положил в репозиторий под видом двоичных тестовых файлов, а изменённый сценарий сборки, который её доставал и встраивал в библиотеку, был только в выпускных архивах. Читая исходники в репозитории, закладку было не заметить. Закладка целилась в SSH и открыла бы тайный вход на огромное число серверов, попади заражённые версии в стабильные выпуски.
Защита от отравленного колодца — та же, о которой мы говорили в главе 52: двойная компиляция независимыми компиляторами и воспроизводимые сборки, когда любой может собрать те же байты из тех же исходников и сверить. К ним добавляется то, что сработало в случае xz: открытый код, который может прочитать кто угодно, и внимательный человек, доверяющий своему удивлению. История xz — ещё и про социальную инженерию, только растянутую на годы: доверие к новому сопровождающему копили терпеливо, а в Twitter хватило нескольких звонков.
Закон, этика, ответственное раскрытие
Всё, что вы делали в этой главе, законно, потому что происходило на учебном полигоне курса. За его стенами правила другие. Проверять на прочность чужую систему без явного разрешения владельца — почти везде преступление, даже если вы ничего не сломали и хотели как лучше. В США за это судят по закону о компьютерном мошенничестве — по нему судили и Морриса. В законах многих стран есть и отдельные статьи: о неправомерном доступе к компьютерной информации, о создании и распространении вредоносных программ, о вмешательстве в работу критической инфраструктуры. «Я просто посмотрел, уязвимо ли» — не оправдание: вход без разрешения и есть нарушение.
Два правила, которые стоит запомнить твёрже любого приёма из этой главы. Первое: работайте только со своими системами или с теми, где владелец дал явное письменное разрешение (учебный полигон, собственный сервер, программа поиска уязвимостей с опубликованными правилами). Второе: нашли дыру в чужом — сообщите владельцу, а не используйте и не публикуйте.
Для второго правила есть принятый порядок — ответственное раскрытие, его ещё называют согласованным. Нашедший уязвимость сначала тихо сообщает о ней владельцу и даёт время на заплатку — в индустрии прижился срок около 90 дней (столько даёт, например, Project Zero в Google), — и только потом рассказывает публично. Так раскрывали и истории, которые вы встречали в курсе: разработчиков языков предупредили до доклада об атаке на хеш-таблицы; Heartbleed чинили в день огласки, потому что о нём знали заранее. Крупные компании платят за найденные дыры (программы bug bounty), а чтобы нашедшему было куда писать, у сайта заводят файл /.well-known/security.txt с контактом службы безопасности. Этичный исследователь работает с разрешения, бьёт по учебным мишеням и своим стендам, а найденное отдаёт тому, кто может починить.
Глава с первой строки была про оборону: как не написать дыру и как устроена защита. Теперь вы узнаёте в своём коде места, где чужие данные норовят стать командой, и знаете, чем закрыть эти косяки: параметрами, проверкой границ, экранированием, наименьшими привилегиями.
Полигон: задачи
Три мишени: две уязвимые заготовки, которые проходят простые проверки и валятся на атаке, и оценщик паролей, который предстоит собрать с нуля. Чинить надо так, чтобы полезное работало, а атака перестала; тесты проверяют и то и другое.
Учебный сайт пускает пользователя внутрь функцией login(con, name, pw): она возвращает роль пользователя ('user', 'admin'…), если в таблице users(name, pw, role) есть строка с таким именем и паролем, иначе None. Соединение con с базой SQLite вам передают готовым. Заготовка собирает запрос склейкой строк и уязвима для SQL-инъекции. Почините её так, чтобы обычный вход работал (в том числе для пользователя с кавычкой в имени, вроде d'Anthès), а ни одна инъекция — ' OR 1=1 --, '; DROP TABLE users; -- и подобные — не пускала внутрь и не портила таблицу. Таблицу после атак тесты проверяют на целость.
Не чините склейку чёрными списками и заменой кавычек — их обходят. Передайте имя и пароль как параметры запроса: на их месте в тексте поставьте ?, а сами значения отдайте вторым аргументом кортежем.
con.execute("SELECT role FROM users WHERE name = ? AND pw = ?", (name, pw)). Теперь база подставит значения как данные, и кавычка в имени тоже перестанет что-либо ломать.
Вопросительные знаки отмечают места, куда база подставит значения уже после разбора команды, поэтому присланный текст не может стать ни новым условием, ни новой командой. Один приём закрывает и обход проверки, и порчу таблицы, и кавычку в обычном имени, которая ломала запрос, — все три теста, на которых падала заготовка. Так пишут запросы всюду, где в них попадают чужие данные.
Программа на C читает число k, затем k целых чисел, и печатает их сумму. Числа она складывает в массив на 100 элементов. Заготовка не проверяет, что k влезает в массив: при k больше ста она пишет за край — переполнение буфера. Почините: если k меньше нуля или больше 100, программа должна напечатать ровно too many и завершиться, не трогая память за краем; иначе — как раньше, сумму. Текст программы верните строкой в переменной SOURCE. Тесты пришлют и обычные наборы (в том числе ровно 100 чисел — это ещё не слишком много), и k = 200.
Границу надо проверить до того, как писать в массив, — сразу после чтения k. Массив на 100 элементов вмещает индексы от 0 до 99, то есть ровно 100 чисел; 100 — ещё можно, 101 — уже нет.
Добавьте сразу после scanf("%d", &k): if (k < 0 || k > 100) { printf("too many\n"); return 0; }. Тогда до цикла с записью в buf дело дойдёт, только когда k точно помещается.
Всё решает одна строка проверки, поставленная до записи. Пока её нет, при k = 200 цикл пишет 200 чисел в массив на 100: лишнее ложится на соседнюю память, и программа либо печатает чушь, либо падает: неопределённое поведение, знакомое по главе 33. Проверка границы превращает «тихо испортил память» в явное too many. Так выглядит защита от переполнения, записанная руками там, где язык её за вас не сделает.
Соберите attempts(password, common) — сколько попыток нужно атакующему, чтобы угадать пароль. Атакующий действует как в жизни: сначала перебирает словарь common — список самых частых паролей по порядку. Если пароль есть в словаре, ответ — его номер попытки (позиция в списке, считая с единицы: первый пароль угадывается за 1 попытку). Если пароля в словаре нет, атакующий сначала впустую перебирает весь словарь, а потом берётся за полный перебор; ответ — len(common) плюс A ** len(password), где A — размер алфавита: 26 за наличие строчных латинских букв, ещё 26 за заглавные, ещё 10 за цифры, ещё 33 за любой иной символ. Так короткий цифровой пароль и длинная, но расхожая фраза оба окажутся слабыми — каждый по своей причине.
Размер алфавита: четыре независимые проверки, каждая добавляет своё число, если в пароле есть хоть один символ такого класса. «Любой иной символ» — это not c.isalnum() (пробел, знаки препинания).
Словарь: if password in common: return common.index(password) + 1. Иначе — len(common) + alphabet(password) ** len(password). Возведение в степень в Python даёт точное большое целое, округлять не нужно.
Словарь проверяется первым нарочно: он ловит именно те пароли, что люди выбирают чаще всего, сколько бы символов в них ни было. Пароль 123456 из словаря угадывается за считанные попытки; восьмибуквенный password — тоже, хотя перебором его искали бы $26^8$ раз. Полный перебор включается, только когда словарь исчерпан, и тогда решает длина: каждый символ умножает число вариантов на размер алфавита. Поэтому длинная парольная фраза, которой нет в словарях, перебору практически недоступна, а короткий частокол из спецсимволов доступен вполне.
Куда дальше
Подведём счёт всей части. Мы научили машину хранить секреты (главы 59–60) и защищать их от нападения. Но каждую защиту этой главы — проверку границ, экранирование, параметры, слои прав, правила для людей — придумал и записал человек. Машина исполняла наши инструкции до последней запятой, быстро и бездумно. Так было всю книгу: считать, сортировать, прокладывать маршруты, шифровать, узнавать инъекцию в строке мы учили её точными рецептами.
А может ли машина вывести правило сама, из примеров, — такое, которое мы не сумели бы записать? Начнём с вопроса попроще и азартнее: можно ли научить машину играть, то есть выбирать ход, когда по другую сторону доски сидит противник, который тоже думает и хочет вашего проигрыша? С него начинается последняя часть книги и следующая глава — турнир ботов.