Исполнение произвольного кода

Исполнение произвольного кода — уязвимость в программной системе, когда злонамеренный пользователь (хакер) может заставить её исполнять любой машинный код, какой захочет. Если можно заставить машину исполнять произвольный код по сети, это иногда называется удалённое исполнение кода. Это самая опасная из программных уязвимостей — вредоносный код внедряется через внешне безобидные действия (например, загрузку файла данных), и может делать что угодно в пределах привилегий программы: извлекать информацию из памяти программы, пересылать её по сети, читать, писать и модифицировать файлы…

Аналогичная уязвимость для веба называется межсайтовый скриптинг — но поскольку JavaScript сам по себе ограничен в возможностях, то и урона от него обычно меньше, в основном утечки данных. Аналогичная уязвимость для SQL называется внедрение SQL-кода, и чревата она всем тем, что может SQL — утечками, подменами и потерями данных.

Причины

Основные причины исполнения произвольного машинного кода:

Порча памяти
Пример: переполнением буфера перезаписываем указатель возврата (в x86 стек растёт вниз, и указатель после стекового фрейма). Как только программе потребуется закончить подпрограмму, процессор извлекает из стека этот адрес и переходит туда — на хакерскую подпрограмму.
Чтобы была больше вероятность перейти на вредоносную подпрограмму, используется техника heap spraying — в динамической памяти создаётся много небольших объектов, и каждый содержит подпрограмму[1].
Спутанные типы в интерпретаторах с динамической типизацией
Подтип порчи памяти: функция ожидает, например, строковый тип, а ей дают целочисленную переменную, и она это никак не проверяет. Дав интерпретатору специальным образом подобранные данные, можно испортить память[1].
Автоматическая сериализация + излишнее доверие
Часто в составе программы или системных библиотеках встречаются уязвимые классы, исполняющие код по ошибке, или системные, действительно способные исполнить что угодно. В некоторых языках (Java) принято передавать сложные объектные структуры через стандартную сериализацию — и если взломщик передаст команду создать подобный объект, можно будет исполнить произвольный код. Подобную уязвимость имели WebSphere и Jenkins[2].
Случайное исполнение кода в операциях над EXE и DLL, которые этого исполнения не подразумевают
Два примера: 1. В Windows загрузка иконки из DLL происходит через функцию LoadLibrary, которая, в числе прочего, исполняет и код инициализации DLL — таким образом, достаточно просмотреть Проводником каталог с «отравленным» DLL, чтобы исполнить зловредный код. Решено ещё в 2000-е через дополнительный флаг LOAD_LIBRARY_AS_DATAFILE, не исполняющий кода. 2. В Unix-подобных ОС программа ldd, проверяющая зависимости программы, является несложным скриптом, исполняющим ELF-файл с определёнными настройками системы[3]. Не решено — просто запрещается запускать ldd на непроверенных файлах.

Защита

Запрет на модификацию сегментов кода, и на исполнение сегментов данных
В этом случае программа просто не сможет ничего записать в ту память, которую можно исполнять. Экстремальный вариант — гарвардская архитектура, распространённая в ограниченных машинах, в ней код и данные расположены в разных адресных пространствах. Недостаток: существуют самомодифицирующийся код, собственные загрузчики (сжатие исполняемых файлов, защита от копирования…), JIT-компиляция.
Работа программ с правами, которые не позволят нанести большой вред
Поскольку работа в ограниченном аккаунте довольно неудобна, Microsoft реализовал контроль учётных записей пользователей, не дающий программам работать с системными данными. Впоследствии Linux сделал аналогичное: большинство работы, даже в администраторских аккаунтах, идёт с пониженными правами, а команды, требующие полных прав, выполняются через sudo. В браузерах, как программах, принимающих на себя самые массовые хакерские атаки, есть работа вкладок в ограниченных процессах — впервые это реализовано в Google Chrome, а впоследствии — и в Firefox Quantum. Недостаток: сложность системы раздачи прав; в любом случае будут программы с широкими правами, и именно они станут основной мишенью хакеров.
Рандомизация адресного пространства программы (ASLR)
Из-за непостоянных адресов усложняется сборка действующего эксплойта. Недостаток: замедленная загрузка.
Расширенные самопроверки в библиотеках
Используются в критичном ПО, особо подверженном хакерским атакам (например, в сетевом). Например, в системе исполнения Java доступ к массиву или объекту всегда проверяется, из-за чего порча памяти затруднена. Недостаток: сниженная производительность.
Ограничения на стиль кода
Так, жёсткий стандарт кода MISRA C, принятый в автоиндустрии, призван снизить ошибкоопасность.
Использование ограниченных виртуальных машин и скриптовых языков, а не машинного кода
Если нужна программируемость, но недопустимы вредоносные программы, используют сильно ограниченный язык. Так работают Java, JavaScript, многие из смартфонных ОС, включая Android. Эта схема решает и другие вопросы — даёт независимость от архитектуры процессора, упрощает программирование, отказ программы не приводит к отказу всей системы. Недостаток: крайняя сложность системы и вытекающая из неё затруднённая проверка безопасности, низкая производительность.
Превращение сетевого ПО в многофункциональные «комбайны»
Как можно меньше сетевой работы отдаётся внешнему ПО: логично, что в браузере сильнее дисциплина программирования, чем в настольном просмотрщике PDF. К тому же браузер Chrome или Firefox исполнит код просмотра PDF в ограниченном процессе, а просмотрщик — в обычном.

Примечания

  1. 1 2 Understanding type confusion vulnerabilities: CVE-2015-0336 | Microsoft Security Blog. Дата обращения: 18 июня 2023. Архивировано 18 июня 2023 года.
  2. What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common? This Vulnerability. Дата обращения: 31 августа 2022. Архивировано 31 августа 2022 года.
  3. ldd arbitrary code execution. Дата обращения: 31 августа 2022. Архивировано 6 сентября 2022 года.