Как создать легкую песочницу кода C?

Сначала небольшое напоминание (или что-то новое, если вы не знали этого раньше): для любого массива или указателя p и индекса i выражение p[i] точно такое же, как *(p + i).

Теперь, надеюсь, помочь вам понять, что происходит ...

Массив a в вашей программе хранится где-то в памяти, именно там, где это не имеет значения. Чтобы получить место, где хранится a, то есть получить указатель на a, вы используете оператор address-of &, например, &a. Здесь важно учиться, что указатель сам по себе ничего особенного не означает, важен базовый тип указателя . Тип a является int[4], то есть a является массивом из четырех int элементов. Тип выражения &a является указателем на массив из четырех int или int (*)[4]. Скобки важны, потому что тип int *[4] представляет собой массив из четырех указателей на int, что совсем другое.

Теперь вернемся к начальной точке, которая p[i] совпадает с *(p + i). Вместо p у нас есть &a, поэтому наше выражение *(&a + 1) совпадает с (&a)[1].

Теперь это объясняет, что означает *(&a + 1) и что он делает. Теперь давайте немного подумаем о расположении памяти в массиве a. В памяти это выглядит примерно как

+---+---+---+---+
| 0 | 1 | 2 | 3 |
+---+---+---+---+
^
|
&a

Выражение (&a)[1] обрабатывает &a как массив массивов, которым он определенно не является, и получает доступ ко второму элементу в этом массиве, который будет быть вне границ. Технически это, конечно, неопределенное поведение . Давайте немного поработаем с ним и рассмотрим, как будет выглядеть в памяти:

+---+---+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | . | . | . | . |
+---+---+---+---+---+---+---+---+
^               ^
|               |
(&a)[0]         (&a)[1]

Теперь вспомните, что тип a (который совпадает с (&a)[0] и, следовательно, означает, что (&a)[1] также должен быть этого типа) массив из четырех int . Поскольку массивы естественным образом распадаются на указатели на свой первый элемент, выражение (&a)[1] совпадает с &(&a)[1][0], а его тип указывает на int . Поэтому, когда мы используем (&a)[1] в выражении, компилятор дает нам указатель на первый элемент во втором (несуществующем) массиве из &a. И снова мы приходим к уравнению p[i] равно *(p + i): (&a)[1] - это указатель на int , это p в выражении *(p + i), поэтому полное выражение равно *((&a)[1] - 1), и, глядя на схему памяти выше, вычитая единицу int из указателя, данного в (&a)[1], мы получим элемент перед (&a)[1], который является последним элементом в (&a)[0], т.е. такой же, как a[3].

Таким образом, выражение *(*(&a + 1) - 1) совпадает с a[3].

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

36
задан 13 revs 14 June 2009 в 15:15
поделиться

12 ответов

Поскольку стандарт 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 Native Client

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

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

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

Заключение

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

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

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

Заключение

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

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

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

Заключение

Итак, каким бы путем вы ни пошли, вам нужно начинать с чего-то нового, которое поддается проверке и

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

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

Заключение

Итак, каким бы путем вы ни пошли, вам нужно начинать с чего-то нового, которое поддается проверке и

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

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

Заключение

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

16
ответ дан 27 November 2019 в 06:05
поделиться

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

9
ответ дан 27 November 2019 в 06:05
поделиться

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

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

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

5
ответ дан 27 November 2019 в 06:05
поделиться

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5
ответ дан 27 November 2019 в 06:05
поделиться

Я наткнулся на 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, с меньшими рисками. ] Тем не менее, я все еще хочу услышать другие идеи.

4
ответ дан 27 November 2019 в 06:05
поделиться

Совершенно невозможно. Язык просто не работает. В большинстве компиляторов, включая GCC, концепция классов теряется очень рано. Даже если бы это было,

3
ответ дан 27 November 2019 в 06:05
поделиться

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

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

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

3
ответ дан 27 November 2019 в 06:05
поделиться

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

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

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

2
ответ дан 27 November 2019 в 06:05
поделиться

Хорошая идея, но я почти уверен, что то, что вы пытаетесь сделать, невозможно с C или C ++. Если вы отказались от идеи песочницы, она может сработать.

Java уже имеет аналогичную (как в большой библиотеке стороннего кода) систему в Maven2

0
ответ дан 27 November 2019 в 06:05
поделиться

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

0
ответ дан 27 November 2019 в 06:05
поделиться

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

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

0
ответ дан 27 November 2019 в 06:05
поделиться

Liran pointed out codepad.org in a comment above. It isn't suitable because it relies on a very heavy environment (consisting of ptrace, chroot, and an outbound firewall) however I found there a few g++ safety switches which I thought I'd share here:

gcc 4.1.2 flags: -O -fmessage-length=0 -fno-merge-constants -fstrict-aliasing -fstack-protector-all

g++ 4.1.2 flags: -O -std=c++98 -pedantic-errors -Wfatal-errors -Werror -Wall -Wextra -Wno-missing-field-initializers -Wwrite-strings -Wno-deprecated -Wno-unused -Wno-non-virtual-dtor -Wno-variadic-macros -fmessage-length=0 -ftemplate-depth-128 -fno-merge-constants -fno-nonansi-builtins -fno-gnu-keywords -fno-elide-constructors -fstrict-aliasing -fstack-protector-all -Winvalid-pch

The options are explained in the GCC manual

What really caught my eye was the stack-protector flag. I believe it is a merge of this IBM research project (Stack-Smashing Protector) with the official GCC.

The protection is realized by buffer overflow detection and the variable reordering feature to avoid the corruption of pointers. The basic idea of buffer overflow detection comes from StackGuard system.

The novel features are (1) the reordering of local variables to place buffers after pointers to avoid the corruption of pointers that could be used to further corrupt arbitrary memory locations, (2) the copying of pointers in function arguments to an area preceding local variable buffers to prevent the corruption of pointers that could be used to further corrupt arbitrary memory locations, and the (3) omission of instrumentation code from some functions to decrease the performance overhead.

2
ответ дан 27 November 2019 в 06:05
поделиться
Другие вопросы по тегам:

Похожие вопросы: