Ошибки сегментации и безопасность, или как «упавшая» программа может привести к взлому системы
Содержание:
1. Модель памяти процесса и атаки на переполнение буфера, стек (Вы читаете данный раздел);
2. Куча;
3. Средства защиты, способы их обхода и слова-канарейки;
4. «Канарейки» в glibc и NX-бит;
5. Коммерческая сторона и конкуренты;
Многие пользователи ОС Linux (и не только ее) наверняка хотя бы раз в жизни сталкивались с «падением» того или иного приложения. Сам факт аварийного завершения программы уже неприятен - ведь он может привести к потере важных данных, вызвать необходимость заново повторять уже проделанную работу и так далее.
Однако последствия могут быть еще более печальными - наличие программ, в некоторых ситуациях завершающихся аварийно, может быть серьезной угрозой безопасности всей системы. «Падающая» программа - это с достаточно большой вероятностью брешь в безопасности операционной системы.
Давайте разберемся почему.
Одной из основных причин аварийного завершения работы программ вот уже несколько десятилетий является некорректная работа с памятью, приводящая, как правило, к ошибке сегментации (segmentation fault) - попытке обращения к несуществующему адресу памяти.
Для понимания причин возникновения таких проблем и исходящих от них угроз для безопасности системы, необходимо рассмотреть организацию памяти любого процесса в Linux на основных аппаратных архитектурах.
Память любой программы представляется как линейный массив байтов. В начало этого массива помещается код программы, за ним следуют данные - глобальные переменные (как инициализированные, так и не имеющие начального значения) и так называемая «куча» («Heap»), из которой в процессе работы программы выделяется память под динамические переменные. Куча «растет» в сторону увеличения адресов. Ближе к концу массива располагается стек, который растет в сторону уменьшения адресов (навстречу куче).
Расположение кучи, стека и ряда других объектов (например, загруженных разделяемых библиотек) в памяти процесса можно увидеть с помощью файловой системы proofs. Вся эта информация содержится в файле /proc/ /maps, где - идентификатор процесса.
В стек записываются локальные переменные функций и вспомогательная информация, используемая при их вызове - в частности адрес возврата и аргументы. Информация, относящаяся к вызову одной функции, формирует так называемый фрейм. Чтобы определить границы фреймов, в каждый новый фрейм помещается указатель на предыдущий.
Структуру стека при вызове некоторой функции можно представить диаграммой, представленной на рис. выше. Адреса на этой диаграмме увеличиваются снизу вверх, а сам стек растет «сверху вниз» (то есть адрес предыдущего фрейма больше адреса текущего).
Если в области локальных переменных есть переменная-буфер, в которую последовательно записываются некоторое данные, то буфер будет «расти» снизу вверх. И если записать в этот буфер слишком много информации, то она может затереть значения, располагающиеся в более высоких адресах.
В частности, могут оказаться перезаписанными указатель на предыдущий фрейм и значение адреса, по которому необходимо производить возврат из функции. В результате вместо реального адреса система получит некоторое значение, с большой вероятностью не имеющее никакого смысла. Попытка передать управление на этот адрес приведет, скорее всего, либо к ошибке сегментации, либо к ошибке некорректной инструкции (Illegal instruction).
Однако если окажется, что по «новому адресу» находится некоторый код, который может быть корректно выполнен, то работа приложения продолжится - правда совсем не так, как ожидали пользователи или разработчики. Именно на этом и основаны многие атаки, суть которых заключается в использовании переполнения буфера данных для подмены адреса возврата из функции. В случае успешной атаки старый адрес подменяется новым, по которому располагается некоторая вредоносная функция.
В принципе, после выполнения своих вредоносных действий (например, отсылки ваших персональных данных злоумышленнику) эта функция может передать выполнение обратно на настоящий адрес и приложение даже имеет шанс продолжить работу (конечно, если в результате переполнения не возникло других повреждений данных в памяти, критичных для работы программы).
Порядок расположения переменных в стеке в момент вызова функции вычислить нетрудно, поскольку он определяется соглашениями вызова функций, используемыми в компиляторе. Также нетрудно просчитать что, как и куда надо записать, чтобы переполнить буфер «с умом», перезаписав нужные переменные нужными значениями.
1.
2. Куча;
3. Средства защиты, способы их обхода и слова-канарейки;
4. «Канарейки» в glibc и NX-бит;
5. Коммерческая сторона и конкуренты;
Многие пользователи ОС Linux (и не только ее) наверняка хотя бы раз в жизни сталкивались с «падением» того или иного приложения. Сам факт аварийного завершения программы уже неприятен - ведь он может привести к потере важных данных, вызвать необходимость заново повторять уже проделанную работу и так далее.
Однако последствия могут быть еще более печальными - наличие программ, в некоторых ситуациях завершающихся аварийно, может быть серьезной угрозой безопасности всей системы. «Падающая» программа - это с достаточно большой вероятностью брешь в безопасности операционной системы.
Давайте разберемся почему.
Модель памяти процесса и атаки на переполнение буфера
Одной из основных причин аварийного завершения работы программ вот уже несколько десятилетий является некорректная работа с памятью, приводящая, как правило, к ошибке сегментации (segmentation fault) - попытке обращения к несуществующему адресу памяти.
Для понимания причин возникновения таких проблем и исходящих от них угроз для безопасности системы, необходимо рассмотреть организацию памяти любого процесса в Linux на основных аппаратных архитектурах.
Память любой программы представляется как линейный массив байтов. В начало этого массива помещается код программы, за ним следуют данные - глобальные переменные (как инициализированные, так и не имеющие начального значения) и так называемая «куча» («Heap»), из которой в процессе работы программы выделяется память под динамические переменные. Куча «растет» в сторону увеличения адресов. Ближе к концу массива располагается стек, который растет в сторону уменьшения адресов (навстречу куче).
Расположение кучи, стека и ряда других объектов (например, загруженных разделяемых библиотек) в памяти процесса можно увидеть с помощью файловой системы proofs. Вся эта информация содержится в файле /proc/
Стек
В стек записываются локальные переменные функций и вспомогательная информация, используемая при их вызове - в частности адрес возврата и аргументы. Информация, относящаяся к вызову одной функции, формирует так называемый фрейм. Чтобы определить границы фреймов, в каждый новый фрейм помещается указатель на предыдущий.
Структуру стека при вызове некоторой функции можно представить диаграммой, представленной на рис. выше. Адреса на этой диаграмме увеличиваются снизу вверх, а сам стек растет «сверху вниз» (то есть адрес предыдущего фрейма больше адреса текущего).
Если в области локальных переменных есть переменная-буфер, в которую последовательно записываются некоторое данные, то буфер будет «расти» снизу вверх. И если записать в этот буфер слишком много информации, то она может затереть значения, располагающиеся в более высоких адресах.
В частности, могут оказаться перезаписанными указатель на предыдущий фрейм и значение адреса, по которому необходимо производить возврат из функции. В результате вместо реального адреса система получит некоторое значение, с большой вероятностью не имеющее никакого смысла. Попытка передать управление на этот адрес приведет, скорее всего, либо к ошибке сегментации, либо к ошибке некорректной инструкции (Illegal instruction).
Однако если окажется, что по «новому адресу» находится некоторый код, который может быть корректно выполнен, то работа приложения продолжится - правда совсем не так, как ожидали пользователи или разработчики. Именно на этом и основаны многие атаки, суть которых заключается в использовании переполнения буфера данных для подмены адреса возврата из функции. В случае успешной атаки старый адрес подменяется новым, по которому располагается некоторая вредоносная функция.
В принципе, после выполнения своих вредоносных действий (например, отсылки ваших персональных данных злоумышленнику) эта функция может передать выполнение обратно на настоящий адрес и приложение даже имеет шанс продолжить работу (конечно, если в результате переполнения не возникло других повреждений данных в памяти, критичных для работы программы).
Порядок расположения переменных в стеке в момент вызова функции вычислить нетрудно, поскольку он определяется соглашениями вызова функций, используемыми в компиляторе. Также нетрудно просчитать что, как и куда надо записать, чтобы переполнить буфер «с умом», перезаписав нужные переменные нужными значениями.