Главная » Хабрахабр » [Перевод] Курс MIT «Безопасность компьютерных систем». Лекция 1: «Вступление: модели угроз», часть 3

[Перевод] Курс MIT «Безопасность компьютерных систем». Лекция 1: «Вступление: модели угроз», часть 3

Массачусетский Технологический институт. Курс лекций #6.858. «Безопасность компьютерных систем». Николай Зельдович, Джеймс Микенс. 2014 год

Computer Systems Security — это курс о разработке и внедрении защищенных компьютерных систем. Лекции охватывают модели угроз, атаки, которые ставят под угрозу безопасность, и методы обеспечения безопасности на основе последних научных работ. Темы включают в себя безопасность операционной системы (ОС), возможности, управление потоками информации, языковую безопасность, сетевые протоколы, аппаратную защиту и безопасность в веб-приложениях.

Лекция 1: «Вступление: модели угроз» Часть 1 / Часть 2 / Часть 3

Вы познакомитесь с этим подробно в первой лабораторной работе. Запустим эту программу с помощью отладчика. А сейчас мы постараемся установить точку прерывания в этой функции перенаправления, запустим программу и посмотрим, что у нас получилось.

Теперь отладчик остановлен в начале перенаправления. Итак, я запустил программу, она начала исполнять основную функцию, и перенаправление происходит довольно быстро. Здесь мы будем рассматривать низший уровень, а не уровень исходного кода С. Мы можем увидеть, что здесь происходит, например, мы можем попросить показать нам текущие регистры CPU. Язык С может действительно что-то от нас скрывать, поэтому мы попросим показать нам все регистры. Мы собираемся посмотреть на настоящие инструкции, выполняемые моей машиной, чтобы увидеть, что происходит на самом деле.

И моя программа, что неудивительно, тоже имеет стек. В 32-х битных системах (х86), как вы помните, имеется указатель стека – регистр EBP (stack-frame Base Pointer, указатель на стековый фрейм).

В настоящий момент указатель стека показывает на конкретное расположение памяти ffffd010 (регистр ESP, адрес вершины стека). На х86 стек растёт вниз, это такой стек, как показано на слайде, и мы можем продолжать «запихивать» в него наши данные. Как оно туда попало? Здесь имеется некоторое значение. Один из способов понять это – разобрать код функции перенаправления.

Итак, мы можем разобрать функцию по именам. Переменная Convenience должна иметь целочисленное значение. В первую очередь, она начинает производить какие-то действия с регистром EBP, это не очень интересно. Здесь видно, что делает эта функция. Это, по существу, создаёт пространство для всех переменных параметров, таких, как буфер и целое число, мы видели это в исходном коде С.
Сейчас мы хотим понять работу данной функции. Но затем она вычитает определённое значение из указателя стека. Так что где-то здесь у нас должен находиться обратный адрес. Значение указателя стека, которое мы видели раньше, теперь уже находится посередине стека, а над ним размещены сведения, что делается в буфере, каково целое значение и также находится обратный адрес в основную функцию, которая реализовывается в стеке. Сейчас мы просто стареемся выяснить, где находятся в стеке разные вещи.

Мы можем дать команду напечатать адрес этой переменной буфера.

Теперь выведем на экран адрес целочисленного значения i – он выглядит так: ffffd0ac. Её адрес ffffd02c. Таким образом, integer расположен выше стека, а буфер ниже.

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

Внутри нашего буфера элементы будут располагаться так: [0] внизу, а далее вверх по возрастающей до элемента [128], как я нарисовал на доске. Мы видим, что стек растёт вниз, потому что выше него находятся вещи с более «высокими» адресами.

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

Это отдельный регистр, расположенный после всех переменных, но перед обратным адресом, как показано на этом рисунке. В х86 существует удобная вещь, называемая Convention, которая делает так, чтобы указатель EBP, или регистр, указывающий на нечто, происходящее в работающем стеке, помечался как «сохранённый EBP регистр» (saved EBP).

Изучим, что собой представляет saved EBP. Он сохраняется согласно нескольким инструкциям, размещённым сверху.

В отладчике GDB (GNU Debugger) можно исследовать некоторую переменную Х, например переменную указателя EBP.

Действительно, он расположен выше, чем наша переменная i (регистр edi). Вот его положение в стеке – ffffd0b8. Это отлично.

Если мы напечатаем ebp+4, нам покажет содержимое стека 0x08048E5F. И она имеет некоторое другое значение, которое принимает EBP до того, как будет вызвана функция, а выше находится ещё одно местоположение памяти, которое и будет обратным адресом. Посмотрим, на что это указывает.

Так что вы можете взять этот адрес и попробовать его разобрать. Это то, что вам предстоит проделать в лабораторной работе. Таким образом, GDB действительно помогает выяснить, какая функция содержит этот адрес. Что он из себя представляет и где заканчивается?

Это то, на что указывает обратный адрес. Что такое 5f? Поэтому когда мы возвращаемся из перенаправления, это то самое место, куда мы попадаем и откуда продолжаем выполнение функции. Как вы видите, это инструкция следует сразу после вызова перенаправления <read_req>.

Чтобы подбить итог, мы можем попытаться дизассемблировать наш указатель инструкции. Итак, где мы сейчас находимся? Вводим «disass $eip».

Попробуем запустить функцию get () и и ввести команду «next». Сейчас мы в самом начале перенаправления. А далее печатаем нашу невообразимую величину, которая вызвала остановку программы – ААА…А, чтобы посмотреть, что при этом происходит.

Сейчас мы выясним, что в настоящий момент происходит в памяти и почему потом всё станет плохо. Итак, мы выполнили get (), но программа всё ещё работает.

Я напечатал последовательность символов А. Как вы думаете, ребята, что сейчас происходит? Она разместила эту последовательность в стек памяти, который, если вы помните, содержит внутри себя элементы от [0] до [128]. Что при этом команда get () сделала с памятью? И эта последовательность А принялась заполнять его снизу вверх, вот как я нарисовал, в направлении стрелки.

Но get () не известна длина стека, поэтому она просто продолжает заполнять память нашими данными, перераспределяя их вверх по стеку, возможно, минуя обратный адрес и всё, что расположено выше нашего стека. Но у нас имелся всего один указатель – начало адреса, то есть мы указали, с какого места в буфере нужно начать располагать А. Вот я набираю команду для подсчёта повторов А и получаю значение «180», которое превышает наше значение «128».

Мы можем опять проверить, что происходит с нашим указателем EBP, для этого я набираю $ebp. Это не так уж и хорошо. Получаем адрес 41414141.

Отлично, дальше я набираю «показать расположение обратного адреса $ebp+4» и получаю тот же самый адрес 41414141.

Это показывает, что произойдёт, если программа вернётся сюда после перенаправления, то есть перескочит на регистр с адресом 41414141. Это совсем не хорошо. И она остановится. А там ничего нет! То есть мы получили ошибку сегментации.

Наберём «next» и запустим программу дальше. Так что давайте просто подойдём сюда и посмотрим, что произойдёт.

Снова набираем «nexti». Сейчас мы приближаемся к концу функции и можем переступить ещё через 2 инструкции.

Она как бы «толкает» указатель стека всё время назад к обратному адресу, используя тот же EBP, вот для чего она в основном нужна. Вы видите, что в конце функции имеется инструкция «leave», которая восстанавливает стек туда, где он был. Фактически, это все наши символы А. И теперь стек указывает на обратный адрес, который мы собираемся использовать. И если мы запустим ещё одну инструкцию, процессор перейдёт к этому конкретному адресу 41414141, начнёт там исполнять код и «обрушится», потому что это недопустимый адрес в таблице страниц.

Ещё раз напечатаем содержимое нашего буфера и убедимся, что он полностью заполнен символами «А» в количестве 128 штук. Давайте проверим, что там происходит.

Итак, что-то ещё происходит после того, как произошло переполнения буфера. Если вы помните, всего мы ввели в буфер 180 элементов «А». И если мы имеем только буквенные символы А, без всяких чисел, то в расположение памяти записывается 0, так как букву нельзя представить целым числом. Если вы помните, мы выполнили преобразование А в целочисленное i в регистре integer. Таким образом, GDB думает, что у нас есть прекрасная, завершённая строка из 128 символов А. А 0, как известно, на языке С означает конец строки.

Но это не имеет особого значения, потому что у нас всё ещё имеются все эти А наверху, которые уже повредили стек.

Нужно учесть, что есть ещё и другой код, который будет выполняться после того, как вам удалось переполнить буфер и вызвать повреждение памяти. Отлично, это был действительно важный урок. Так, он должен предусматривать, что при обнаружении не числового значения, в нашем случае это А, мы не сможем перескочить к адресу 41414141. Вы должны убедиться, что этот код не совершает глупостей, например, не пытается конвертировать буквенные символы А в целочисленные величины i. Возможно, это не слишком важно в данном случае, но в других ситуациях вам нужно с осторожностью подходить к типу входных данных, то есть указывать, какого рода данные – числовые или буквенные – должна обрабатывать программа. Таким образом, в некоторых случая вы должны ограничить вводные данные.

Посмотрим на наш регистр. Сейчас мы посмотрим, что произойдёт дальше, и перепрыгнем ещё раз. Если мы сделаем ещё один шаг, мы наконец-то перейдём к нашему несчастному 41414141. Прямо сейчас EIP, вид указателя инструкций, показывает на последний адрес перенаправления <read_req+44>.

Попробуем выполнить ещё одну инструкцию и наконец, получаем сбой программы. Действительно, программа выполняет наше указание, и если мы попросим GDB напечатать текущий набор регистров, то текущий указатель позиции будет представлять собой странное значение.

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

Так в чём же всё-таки заключается наша проблема? Отлично, у меня к вам имеется вопрос.

Аудитория: с этой программой можно делать всё, что хотите!

Хотя, на самом деле, было довольно глупо вводить такое огромное число этих А. Совершенно верно! Давайте посмотрим, сможем ли мы это сделать. Но если бы вы хорошо знали, куда следует поместить эти величины, вы могли бы поместить туда другие значения и перейти по какому-нибудь другому адресу.

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

Мы снова находимся в самом конце перенаправления. Где мы находимся? Давайте посмотрим на наш стек.

Хорошо. Если мы исследуем ESP, то увидим наш повреждённый указатель. Что интересного мы бы могли сделать? Куда мы бы могли отсюда перескочить? В её коде нет ничего, что помогло бы нам перескочить и сделать что-то интересное, но мы всё равно попытаемся. К сожалению, эта программа очень ограничена. Мы можем дизассемблировать основную функцию – disass main. Возможно, нам удастся найти функцию PRINTF, перепрыгнуть туда и заставить её напечатать какое-то значение, или эквивалентную чему-либо величину Х.

Так как насчёт того, чтобы перескочить в эту точку – <+26>, которая устанавливает аргумент для PRINTF, равный %eax в регистре <+22>? А главная функция делает целую кучу вещей – инициацию, переадресацию вызовов, ещё много всего, и затем вызывает PRINTF. Это должно быть достаточно легко сделать при помощи отладчика, можно сделать этот набор esp равным этому значению. Таким образом, мы сможем взять значение в регистре <+26> и «приклеить» его к этому стеку.

Можно проверить ESP ещё раз, и действительно, он имеет это значение.

Мы неправильно настроили все аргументы, потому что прыгнули в середину этой вызывающей последовательности (последовательность команд и данных, необходимая для вызова данной процедуры). Продолжим с помощью команды «C», и мы увидим, что функция распечатала Х равным какой-то ерунде, и я думаю, это случилось из-за содержимого этого стека, которое мы попытались вывести на печать.

Почему это произошло? Да, мы напечатали эту величину, и после этого система дала сбой. Мы изменили обратный адрес, так что когда мы вернулись из перенаправления, мы переходим на этот новый адрес, в ту же самую точку сразу после PRINTF. Мы перепрыгнули к функции PRINTF, а потом что-то пошло не так. Так откуда взялся этот сбой?

Аудитория: из-за возврата главной функции!

Вот что происходит – вот та точка, куда мы прыгнули, в регистре <+26>. Совершенно верно! PRINTF работает и готова к возврату. Она устанавливает некоторые параметры и вызывает PRINTF. Пока всё нормально, потому что эта инструкция вызова «кладёт» обратный адрес в стек для того, что этот адрес использовала функция PRINTF.

Но дело в том, что в этом стеке нет правильного обратного адреса. Главная функция продолжает работать, она готова запустить инструкцию LEAVE, которая не представляет собой ничего интересного, а затем сделать другой «return» в регистре <+39>. Так что, к сожалению, здесь наши псевдоатаки не работают. Поэтому, предположительно, мы возвращаемся к кому-то другому, кто знает расположение памяти выше стека, и прыгаем куда-нибудь ещё. Но затем он «крашится». Здесь запускается какой-то другой код. Это, вероятно, не то, что мы хотели сделать.

Затем вам нужно постараться осторожно поместить в стек что-то ещё, чтобы быть уверенным, что ваша программа «чисто» продолжает выполняться после того, как была взломана, и так, что это вмешательство никто не заметит. Так что если вы действительно хотите быть осторожными, вы должны не только тщательно разместить в стеке обратный адрес, но и выяснить, от кого второй RET получит свой обратный адрес.

Это всё вы попытаетесь проделать в лабораторной работе №1, только более подробно.

В данном случае наша проблема заключается в том, что обратный адрес там расположен вверху, правильно? Есть ещё одна вещь, о которой нам стоит подумать сейчас – об архитектуре стека при переполнении буфера. Но что, если мы перевёрнём стек «вниз готовой»? Буфер продолжает расти и, в конечном счёте, перекрывает обратный адрес. Так что мы могли бы представить себе альтернативный дизайн, где стек начинается снизу и продолжает расти вверх, а не вниз. Вы знаете, некоторые машины имеют стеки, которые растут вверх. Так что если вы переполните такой буфер, вы просто будете продолжать идти вверх по стеку, и в этом случае не случится ничего плохого.

Пусть обратный адрес располагается здесь, внизу стека. Сейчас я нарисую вам, чтобы объяснить, как это выглядит. Если мы делаем переполнение, то оно идёт вверх по этой стрелке. Выше расположены наши переменные, или saved EBP, затем переменные целочисленные integer, и на самом верху буфер от [0] до [128].

Что нам нужно сделать в нашей программе, чтобы осуществить такой вариант работы? Таким образом, переполнение буфера не повлияет на обратный адрес. Расположим слева стек-фрейм, который осуществит такую переадресацию, и переадресуем вызов функции наверх. Правильно, сделать переадресацию! А потом мы начнём переполнять буфер командой get(S). В результате наша схема будет выглядеть так: наверху на стеке расположен обратный адрес, затем saved EBP, и все остальные переменные расположатся сверху нам ним.

В основном, потому что буфер окружён функциями возврата со всех сторон, и в любом случае вы можете что-то переполнить. Итак, работа функции всё ещё проблематична. Тогда в какой момент вы сможете перехватить контроль над выполнением программы? Предположим, что наша машина имеет стек, растущий вверх.

Вам не нужно ждать, когда вернётся редирект. На самом деле, в некоторых случаях это даже проще. На самом дело это проще, потому что команда get(S) переполняет буфер. Возможно, там даже были такие вещи, как превращение А в i. Это изменит обратный адрес, а затем немедленно вернётся обратно и перепрыгнет туда, где вы попытались создать некую конструкцию.

Она вроде бы не содержит интересного кода для прыжка. Что произойдёт, если у нас такая, довольно скучная программа для всяких экспериментов? Всё что вы можете сделать – это напечатать здесь, в PRINTF, другую величину Х.

Давайте попробуем!

Аудитория: если у вас есть дополнительный стек, вы можете поместить произвольный код, который, например, выполняет оболочку программы?

Но здесь имеется некоторая защита от этого, вы узнаете о ней в следующих лекциях. Да-да-да, это действительно разумно, потому что тогда можно поддерживать другие величины «input». И вместо того, чтобы указать его в имеющемся коде, например PRINTF в главной функции, мы могли бы иметь обратный адрес в буфере, так как это просто некое местоположение в буфере. Но в принципе, вы бы могли иметь здесь обратный адрес, который перекрывается на обеих типах машин – со стеками вверх и со стеками вниз. Но вы можете «прыгнуть» туда и считать его исполняемым параметром.

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

И действительно, в системах Unix атакующие часто делают так – они просят операционную систему просто выполнить команду BIN SH, позволяющую вам выбрать тип произвольных команд оболочки, которые затем выполняются. Таким образом вы сможете предоставить код, который хотите запустить, прыгнуть туда и использовать сервер для его запуска. И в вашей лабораторной работе вы попытаетесь сконструировать нечто подобное. В результате эта вещь, этот кусок кода, который вы вставили в буфер, по ряду исторических причин носит название «код оболочки», shell code.

Аудитория: существует ли здесь разделение между кодом и данными?

Современные машины пытаются обеспечить некоторую защиту от такого рода атак, поэтому они часто создают разрешения, связанные с разными областями памяти, и одно из разрешений является исполняемым. Исторически сложилось так, что многие машины не обеспечивали никакого разделения кода и данных, а имели просто плоское адресное пространство памяти: указатель стека указывает туда, указатель кода – сюда, и вы просто выполняете то, на что он указывает. И если ваш указатель инструкции показывает туда, процессор будет на самом деле управлять этими штуками. Таким образом, часть вашего 32-разрядного или 64-разрядного адресного пространства, содержащая код, имеет разрешение на выполнение операций. Но стек и другие области данных вашего адресного пространства обычно не имеют разрешения на исполнение.

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

На самом деле вы видели этот пример раньше, когда мы просто «прыгнули» в середину главной функции. Так как бы вы обошли это препятствие, если бы у вас был неисполняемый стек? Поэтому, даже если бы данный стек был неисполняемым, я бы всё равно смог попасть в середину главной функции. Таким образом, это был способ использования переполнения буфера без необходимости вводить новый собственный код. В данном конкретном случае это довольно скучно, потому что достаточно ввести PRINT X, чтобы обрушить систему.

Это называется атакой «return to lib c» — атака возврата в библиотеку, связанная с переполнением буфера. Но в других ситуациях у вас могут быть другие части кода в вашей программе, позволяющие делать интересные вещи, которые вы действительно хотите выполнить. Это способ обойти меры безопасности. При этом адрес возврата функции в стеке подменяется адресом другой функции, а в последующую часть стека записываются параметры для вызываемой функции. И лучший способ исправить это, вероятно, просто изменить исходный код и убедится, что вы не ввели слишком много getS(), о чём вас как раз предупреждал компилятор. Таким образом, в контексте переполнения буфера нет действительно четкого решение, которое обеспечивает идеальную защиту от этих ошибок, потому что, в конце концов, программист сделал ошибку в написании этого исходного кода.

Так как на практике трудно изменить все программное обеспечение, многие люди пробуют разработать методы для предупреждения подобных ошибок. Но есть более тонкие вещи, о которых компилятор вас не предупреждает, но вы всё равно должны их учесть. В следующих 2-х лекциях мы рассмотрим эти методы защиты. Например, делают стек неисполняемым так, что вы не можете поместить в него код оболочки и должны сделать что-то более сложное, чтобы достичь своей цели. Они не идеальны, но на практике существенно затрудняют жизнь хакеру.
Аудитория: будет ли тест на тему сегодняшней лекции и когда?

Да, если вы посмотрите в расписание, то увидите там 2 теста.

Что нам делать с проблемами механизма переполнения буфера? Итак, подведём итоги. Как мы убедились, если вы полагаете применить политики безопасности в каждой части программного обеспечения, вы неизбежно будете совершать ошибки. Общий ответ должен звучать так – нужно иметь наименьшее количество механизмов. И они позволят противнику обойти ваш механизм, чтобы использовать некоторые недочёты в веб-сервере.

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

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

На сегодня всё, приходите на лекцию в понедельник и не забывайте размещать свои вопросы на сайте.

Продолжение следует…

Полная версия курса доступна здесь.

Вам нравятся наши статьи? Спасибо, что остаётесь с нами. Поддержите нас оформив заказ или порекомендовав знакомым, 30% скидка для пользователей Хабра на уникальный аналог entry-level серверов, который был придуман нами для Вас: Вся правда о VPS (KVM) E5-2650 v4 (6 Cores) 10GB DDR4 240GB SSD 1Gbps от $20 или как правильно делить сервер? Хотите видеть больше интересных материалов? (доступны варианты с RAID1 и RAID10, до 24 ядер и до 40GB DDR4).

класса c применением серверов Dell R730xd Е5-2650 v4 стоимостью 9000 евро за копейки? Dell R730xd в 2 раза дешевле? Только у нас 2 х Intel Dodeca-Core Xeon E5-2650v4 128GB DDR4 6x480GB SSD 1Gbps 100 ТВ от $249 в Нидерландах и США! Читайте о том Как построить инфраструктуру корп.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Прогнозирование продаж недвижимости. Лекция в Яндексе

Успех в проектах по машинному обучению обычно связан не только с умением применять разные библиотеки, но и с пониманием той области, откуда взяты данные. Отличной иллюстрацией этого тезиса стало решение, предложенное командой Алексея Каюченко, Сергея Белова, Александра Дроботова и Алексея ...

Как IaaS приходит в ритейл и производство: кто и зачем перешел на виртуальную инфраструктуру

Есть мнение, что «облако» — это лишь маркетинговое название. Оно часто упоминается к месту и не к месту, а что скрывается под этим понятием люди не из бизнеса и не из ИТ не всегда знают. У себя в блоге мы ...