Как создать легкую песочницу с кодом C? - PullRequest
36 голосов
/ 11 июня 2009

Я хотел бы создать препроцессор / компилятор C, который позволяет собирать функции из локальных и онлайн-источников. то есть:

#fetch MP3FileBuilder http://scripts.com/MP3Builder.gz
#fetch IpodDeviceReader http://apple.com/modules/MP3Builder.gz

void mymodule_main() {
  MP3FileBuilder(&some_data);
}

Это легкая часть.

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

ТРЕБОВАНИЯ

  • Мне нужно было бы установить квоты на доступ к данным и ресурсам, включая время процессора.
  • Я заблокирую прямой доступ к стандартным библиотекам
  • Я хочу остановить вредоносный код, который создает бесконечную рекурсию
  • Я хочу ограничить статическое и динамическое распределение определенными пределами
  • Я хочу перехватить все исключения, которые может вызвать модуль (например, делить на 0).
  • Модули могут взаимодействовать с другими модулями только через основные интерфейсы
  • Модули могут взаимодействовать с системой (ввод-вывод и т. Д.) Только через основные интерфейсы
  • Модули должны разрешать битовые операции, математические операции, массивы, перечисления, циклы и ветвления.
  • Модули не могут использовать ASM
  • Я хочу ограничить доступ указателя и массива к памяти, зарезервированной для модуля (через пользовательский safe_malloc ())
  • Должен поддерживать ANSI C или подмножество (см. Ниже)
  • Система должна быть легкой и кроссплатформенной (включая встроенные системы).
  • Система должна быть совместима с GPL или LGPL.

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

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

Теоретическим вводом для такого компилятора / препроцессора будет отдельный файл ANSI C (или безопасное подмножество) с функцией module_main, директивами препроцессора NO или NO, без ASM, он будет разрешать циклы, ветвления, функции вызовы, математические указатели (ограниченные диапазоном, выделенным модулю), сдвиг битов, битовые поля, приведение, перечисления, массивы, целые числа, числа с плавающей запятой, строки и математические вычисления. Все остальное необязательно.

ПРИМЕР ОСУЩЕСТВЛЕНИЯ

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

buffer* transcodeToAVI_main( &in_buffer ) {
    int buffer[1000000000]; // allocation exceeding quota
    while(true) {} // infinite loop
    return buffer;
}

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

buffer* transcodeToAVI_main( &in_buffer ) {
    try {
        core_funcStart(__FILE__,__FUNC__); // tell core we're executing this function
        buffer = core_newArray(1000000000, __FILE__, __FUNC__); // memory allocation from quota
        while(true) {
           core_checkLoop(__FILE__, __FUNC__, __LINE__) && break; // break loop on recursion limit
        } 
        core_moduleEnd(__FILE__,__FUNC__);
    } catch {
        core_exceptionHandler(__FILE__, __FUNC__);
    }
    return buffer;
}

Я понимаю, что выполнение этих проверок влияет на производительность модуля, но я подозреваю, что он все равно будет превосходить языки высокого уровня или языки VM для задач, которые он должен решать. Я не пытаюсь помешать модулям совершать опасные действия напрямую, я просто пытаюсь заставить эти опасные вещи происходить контролируемым образом (например, через обратную связь с пользователем). т.е.: «Модуль X превысил выделение памяти, продолжить или прервать?».

UPDATE

Лучшее, что у меня есть, - это использование собственного компилятора (например, взломанного TCC) с проверкой границ и некоторой пользовательской функцией и циклическим кодом для перехвата рекурсий. Я все еще хотел бы услышать мысли о том, что еще мне нужно проверить или какие решения есть. Я полагаю, что удаление ASM и проверка указателей перед использованием решает многие проблемы, высказанные в предыдущих ответах ниже. Я добавил награду, чтобы получить еще больше отзывов от SO сообщества.

Для награды, которую я ищу:

  • Сведения о потенциальных подвигах против теоретической системы, определенной выше
  • Возможные оптимизации проверки указателей при каждом доступе
  • Экспериментальные реализации концепций с открытым исходным кодом (например, Google Native Client)
  • Решения, поддерживающие широкий спектр ОС и устройств (без ОС / аппаратных решений)
  • Решения, которые поддерживают большинство операций C или даже C ++ (если это возможно)

Дополнительный кредит за метод, который может работать с GCC (например, препроцессор или маленький патч GCC).

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

Мое понимание C может быть классифицировано как «промежуточное», мое понимание аппаратного обеспечения ПК, возможно, на шаг ниже «продвинутого». Попробуйте тренировать свои ответы для этого уровня, если можете. Поскольку я не эксперт по Си, я буду в основном опираться на голоса, отданные за ответ, а также на то, насколько близко ответ соответствует моим требованиям. Вы можете помочь, предоставив достаточные доказательства своих претензий (респонденты) и проголосовав (все остальные). Я назначу ответ, как только обратный отсчет достигнет 6 часов.

Наконец, я считаю, что решение этой проблемы было бы важным шагом к поддержанию актуальности C в мире, который становится все более сетевым и параноидальным. По мере того, как другие языки сокращают разрыв с точки зрения производительности и вычислительной мощности, все труднее будет оправдывать дополнительный риск разработки C (как это сейчас происходит с ASM). Я полагаю, что ваши ответы будут иметь гораздо большую актуальность, чем набирание нескольких SO-баллов, поэтому, пожалуйста, предоставьте все, что можете, даже если срок действия награды истек.

Ответы [ 13 ]

16 голосов
/ 14 июня 2009

Поскольку стандарт C слишком широк, чтобы его можно было разрешить, вам нужно будет пойти другим путем: указать минимальное подмножество C, которое вам нужно, и попытаться реализовать это. Даже ANSI C уже слишком сложен и допускает нежелательное поведение.

Наиболее проблематичным аспектом C являются указатели: для языка C требуется арифмитика указателей, и они не проверяются. Например:

char a[100];
printf("%p %p\n", a[10], 10[a]);

оба напечатают один и тот же адрес. С a[10] == 10[a] == *(10 + a) == *(a + 10).

Все эти обращения к указателю не могут быть проверены во время компиляции. Это та же сложность, что и у компилятора запрашивать «все ошибки в программе», которые потребовали бы решения проблемы остановки.

Поскольку вы хотите, чтобы эта функция могла выполняться в одном и том же процессе (возможно, в другом потоке), вы делите память между вашим приложением и «безопасным» модулем, поскольку в этом весь смысл наличия потока: делитесь данными для более быстрого доступ. Однако это также означает, что оба потока могут читать и записывать одну и ту же память.

И поскольку вы не можете доказать время компиляции, когда появляются указатели, вы должны сделать это во время выполнения. Это означает, что код типа «a [10]» должен быть переведен во что-то вроде «get_byte (a + 10)», и в этот момент я больше не буду называть его C.

Собственный клиент Google

Так что, если это правда, как тогда Google это делает? Что ж, в отличие от требований здесь (кроссплатформенность (включая встроенные системы)), Google концентрируется на x86, который помимо страниц с защитой страниц также сегментирует регистры. Это позволяет ему создать «песочницу», в которой другой поток не разделяет ту же самую память тем же способом: «песочница» ограничена сегментацией, изменяя только свой диапазон памяти. Кроме того:

  • список безопасных сборочных конструкций x86 собран
  • gcc изменен, чтобы испустить эти безопасные конструкции
  • этот список составлен так, чтобы его можно было проверить.
  • после загрузки модуля эта проверка выполняется

Так что это зависит от платформы и не является «простым» решением, хотя оно и работает. Подробнее читайте в их исследовательской работе .

Заключение

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

9 голосов
/ 12 июня 2009

Я думаю, вы многому научились бы, прочитав о некоторых проблемах и вариантах реализации, которые Google сделал при разработке Native Client , системы для выполнения кода x86 (мы надеемся, безопасно) в браузере. Возможно, вам придется выполнить некоторую переписывание исходного кода или компиляцию исходного кода в , чтобы сделать код безопасным, если это не так, но вы должны иметь возможность полагаться на изолированную программную среду NaCL, чтобы перехватить ваш сгенерированный код сборки, если он пытается сделать что-нибудь слишком в стиле фанк.

5 голосов
/ 11 июня 2009

Если бы я собирался это сделать, я бы исследовал один из двух подходов:

  • Используйте CERN CINT для запуска изолированного кода в интерпретаторе и ознакомления с ограничениями, которые разрешает интерпретатор. Это, вероятно, не даст ужасно хороших результатов.
  • Используйте LLVM , чтобы создать промежуточное представление кода C ++, а затем посмотрите, возможно ли запустить этот байт-код в изолированной виртуальной машине в стиле Java.

Однако я согласен с другими, что это, вероятно, ужасно сложный проект. Посмотрите на проблемы, которые возникли у веб-браузеров с глючными или зависшими плагинами, дестабилизирующими весь браузер. Или посмотрите примечания к выпуску проекта Wireshark ; кажется, что почти каждый выпуск содержит исправления безопасности для проблем в одном из его протокольных анализаторов, которые затем затрагивают всю программу. Если бы песочница на C / C ++ была выполнима, я бы ожидал, что эти проекты уже привязаны к одному.

5 голосов
/ 12 июня 2009

Это не тривиально, но это не так сложно.

Вы можете запустить двоичный код в песочнице. Каждая операционная система делает это весь день.

Им придется использовать вашу стандартную библиотеку (против общей библиотеки C). Ваша стандартная библиотека будет применять любые элементы управления, которые вы хотите наложить.

Далее вам нужно убедиться, что они не могут создавать «исполняемый код» во время выполнения. То есть стек не является исполняемым, он не может выделять исполняемую память и т. Д. Это означает, что будет выполняться только код, сгенерированный компилятором (ВАШ компилятор).

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

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

Хотите наложить ограничения памяти? Поставьте чек на malloc. Хотите ограничить размер выделенного стека? Ограничить сегмент стека.

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

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

4 голосов
/ 11 июня 2009

Я наткнулся на Tiny C Compiler (TCC) . Это может быть то, что мне нужно:

*  SMALL! You can compile and execute C code everywhere, for example on rescue disks (about 100KB for x86 TCC executable, including C preprocessor, C compiler, assembler and linker).
* FAST! tcc generates x86 code. No byte code overhead. Compile, assemble and link several times faster than GCC.
* UNLIMITED! Any C dynamic library can be used directly. TCC is heading torward full ISOC99 compliance. TCC can of course compile itself.
* SAFE! tcc includes an optional memory and bound checker. Bound checked code can be mixed freely with standard code.
* Compile and execute C source directly. No linking or assembly necessary. Full C preprocessor and GNU-like assembler included.
* C script supported : just add '#!/usr/local/bin/tcc -run' at the first line of your C source, and execute it directly from the command line.
* With libtcc, you can use TCC as a backend for dynamic code generation.

Это очень маленькая программа, которая делает взлом на нее жизнеспособным вариантом (взломать GCC? Не в этой жизни!). Я подозреваю, что это станет отличной основой для сборки моего собственного ограниченного компилятора. Я уберу поддержку языковых функций, которые я не могу сделать безопасными, и перенесу или заменю выделение памяти и обработку циклов.

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

libtcc также является отличной возможностью, поскольку я могу управлять компиляцией кода внутри.

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

Тем не менее, я хочу услышать другие идеи.

3 голосов
/ 14 июня 2009

Я не очень тщательно исследовал это, но парни, работающие над Chromium (он же Google Chrome), уже почти так работают над песочницей, что, возможно, стоит посмотреть.

http://dev.chromium.org/developers/design-documents/sandbox/Sandbox-FAQ

Это открытый исходный код, поэтому его можно использовать.

3 голосов
/ 11 июня 2009

Совершенно невозможно. Язык просто не работает таким образом. Концепция классов теряется очень рано в большинстве компиляторов, включая GCC. Даже если бы это было так, не было бы способа связать каждое выделение памяти с живым объектом, не говоря уже о «модуле».

2 голосов
/ 17 июня 2009

Лиран указал на codepad.org в комментарии выше. Он не подходит, потому что он опирается на очень тяжелую среду (состоящую из ptrace, chroot и исходящего брандмауэра), однако я нашел там несколько переключателей безопасности g ++, о которых я думал, что поделюсь здесь:

gcc 4.1.2 флаги: -O -fmessage-length = 0 -fno-merge-constants -fstrict-aliasing -fstack-protector-all

г ++ 4.1.2 флаги: -O -std = c ++ 98 -pedantic-errors -Wfatal-errors -Werror -Wall -Wextra -Wno-missing-field-initializer -Wwrite-strings -Wno-deprecated -Wno-unused -Wno-non- virtual-dtor -Wno-variadic-macros -fmessage-length = 0 -ftemplate-глубина-128 -fno-слияние-константы -fno-nonansi-builtins -fno-gnu-Keywords -FNO-Elide-конструкторы -Fstrict-псевдонимы - fstack-protector-all -Winvalid-pch

Опции описаны в руководстве GCC

Что действительно привлекло мое внимание, так это флаг защиты стека. Я считаю, что это слияние этого исследовательского проекта IBM ( Stack-Smashing Protector ) с официальным GCC.

Защита обеспечивается за счет обнаружения переполнения буфера и функции переупорядочения переменных, чтобы избежать повреждения указателей. Основная идея обнаружения переполнения буфера исходит от системы StackGuard .

Новые функции: (1) переупорядочение локальных переменных для размещения буферов после указателей во избежание повреждения указателей, которые могут быть использованы для дальнейшего повреждения произвольных областей памяти, (2) копирование указателей в аргументах функции в область предшествующие буферы локальных переменных для предотвращения повреждения указателей, которые могут быть использованы для дальнейшего повреждения произвольных областей памяти, и (3) пропуск инструментария в некоторых функциях для снижения производительности.

2 голосов
/ 12 июня 2009

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

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

Лучший способ сделать это - запустить код в другом процессе (ipc не так уж и плох) и перехватывать системные вызовы, такие как Ptrace, в linux http://linux.die.net/man/2/ptrace

1 голос
/ 12 марта 2017

8 лет спустя, и я обнаружил новую платформу, которая отвечает всем моим первоначальным требованиям. Веб-сборка позволяет безопасно запускать подмножество C / C ++ внутри браузера и поставляется с аналогичными ограничениями безопасности, соответствующими моим требованиям, такими как ограничение доступа к памяти и предотвращение небезопасных операций в ОС и родительском процессе. Он реализован в Firefox 52, и есть многообещающие признаки того, что другие браузеры будут поддерживать его в будущем.

...