Простая настройка ipfw

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

Кроме того, здесь я расскажу об одной полезной для web-разработчика (и не только) дополнительной возможности FireWall.

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

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

Это всё теория. Главное для нас то, что FireWall — это не программа, а подсистема ядра, что он может блокировать трафик и что его можно гибко настраивать.
Подготовка

Прежде всего, вам надо загрузить модуль FireWall в ядро.

Для тестирования, вы можете сделать это «руками» командой:

kldload ipfw

ВНИМАНИЕ! После того, как вы загрузите FireWall вступят в действие настройки по умолчанию и вы будете полностью изолированны от внешнего мира! Поэтому не торопитесь с выполнением этой команды, не прочитав, как настраивать FireWall, или не сохранив инструкции на своём локальном диске.

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

ipfw_load="YES"

в файл /boot/loader.conf.

Строго говоря, /boot/loader.conf можно и не трогать, модуль подгрузится и если в /etc/rc.conf присутствует firewall_enable=»YES» (см. ниже).
Написание правил

Я не буду здесь пересказывать man 8 ipfw. Он есть в каждой системе, а в сети он есть (хотя и немного устаревший) переведённый на русский язык. Я приведу готовые примеры.

Пример 1: Можно всё

Чтобы разрешить весь трафик, достаточно одного правила

00100 pass all from any to any

Смысл этого правила понятен и без чтения документации: «Пропускай все пакеты от кого угодно к кому угодно». Это правило пригодится вам во время отладки. Неминуемо у вас будут получаться конфигурации в которых вы будете полностью отрезаны от мира. Это правило — идеальное временное решение.

Первое число это номер правила. Каждое правило пронумеровано и проверяются они в порядке возрастания номеров.

Пример 2: Паранойя

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

Для пользователя эти правила тоже очень жёсткие, они позволяют ему пользоваться далеко не всеми ресурсами сети.

Давайте рассмотрим эти правила по порядку.

00100 allow all from any to any via lo0
00101 deny all from any to 127.0.0.0/8
00102 deny all from 127.0.0.0/8 to any
 
00500 check-state
 
00501 deny all from any to any frag
00502 deny tcp from any to any established
 
00600 allow tcp from any to any  80 out via bge0 setup keep-state
00601 allow tcp from any to any 110 out via bge0 setup keep-state
00602 allow tcp from any to any 143 out via bge0 setup keep-state
00603 allow tcp from any to any  25 out via bge0 setup keep-state
 
00700 allow tcp from any to XX.XX.XX.XX 53 out via bge0 setup keep-state
00701 allow udp from any to XX.XX.XX.XX 53 out via bge0 keep-state
00702 allow tcp from any to YY.YY.YY.YY 53 out via bge0 setup keep-state
00703 allow udp from any to YY.YY.YY.YY 53 out via bge0 keep-state

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

Правило 500 проверяет виртуальные правила (о них чуть ниже). Если виртуальные правила не сработали, то поиск подходящего правила продолжается.

Правила 501 и 502 запрещают все соединения, которые уже созданы.

Правила 600-603 создают виртуальные правила (keep-state), пропускающие (allow) пакеты для соединений, которые инициированы (setup) вами (out via bge0).

Кстати, имя интерфейса bge0 вам следует изменить на имя вашего интерфейса.

Причём правила 600-603 позволяют вам работать только с WWW (порт 80) и e-mail (порты 25, 110, 143). Если вам нужно работать и по другим протоколам, то добавьте правил по аналогии.

Правила 700-703 аналогичны предыдущим, они позволяют вам работать с DNS, причём только с легальными DNS вашего провайдера. Замените XX.XX.XX.XX и YY.YY.YY.YY на адреса ваших DNS. Добавьте или удалите правила по необходимости.

Пакеты, которые не удовлетворяют ни одному правилу, будут блокированы FireWall.

Таким образом мы получили набор правил, который не пропускает входящие пакеты кроме тех, которые пришли в ответ на ваш запрос. Кроме того, вы можете работать только с ограниченным кругом Интернет-сервисов. То есть если у вас заведётся какая-то программа (разве такое возможно под FreeBSD?!), желающая «выйти в интернет» у неё это скорее всего не получится. Кроме того, вы не сможете воспользоваться никакими DNS, кроме тех, которым доверяете. Это сразу резко сужает круг возможных атак.
Пример 3: Разумный компромисс

Параноидальная конфигурация хороша, но уж очень параноидальна. Сам я пользуюсь более либеральными настройками:

00100 pass all from any to any via lo0
00101 deny all from any to 127.0.0.0/8
00102 deny all from 127.0.0.0/8 to any
00500 check-state
00502 deny all from any to any frag
00501 deny tcp from any to any established
00600 allow tcp from any to any out via bge0 setup keep-state
00601 allow udp from any to any out via bge0 keep-state
# allow in to 80
00700 allow tcp from any to any 80 in via bge0 setup keep-state
# ping
02000 allow icmp from any to any out icmptype 8
02001 allow icmp from any to any in icmptype 0

Здесь разрешён весь трафик инициированный мной (на любые порты).

Правило 700 разрешает всем обращаться к Web-серверу, размещённому на моей машине. Вы можете добавить аналогичные правила для других сервисов. На пример:

00701 allow tcp from AA.AA.AA.AA to any 22 in via bge0 setup keep-state

Это правило разрешит обращаться к вашему sshd с машины с IP-адресом AA.AA.AA.AA.

Наконец, правила 2000 и 2001 разрешают вам «пинговать» других. Вас по-прежнему никто «пинговать» не сможет.

Чтобы ping на вас работал, поправьте эти правила вот так:

02000 allow icmp from any to any icmptype 8
02001 allow icmp from any to any icmptype 0

Или напишите просто:

02000 allow icmp from any to any

Это не опасно.

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

Итак, вы выбрали правило, поправили его под свои конкретные нужды и хотите его загрузить. Для загрузки правил есть программа, которая называется так же, как и модель ядра — ipfw. Существует два способа загрузки.

Первый способ — загрузить правила из файла:

ipfw /полный/путь/к/файлу

Перед загрузкой не забудьте удалить существующие правила:

ipfw -q -f flush

Второй способ более громоздок, но он более общепринятый. Для загрузки пишется скрипт, который загружает правила по одному. Традиционно, скрипт имеет примерно такую организацию:

#!/bin/sh
ipfw -q -f flush       # чистим
# устанавливаем переменные
oif="bge0"             # мой интерфейс
cmd="ipfw -q add "     # для краткости
ks="keep-state"        # для краткости
# теперь загружаем, используя краткие "макросы"
$cmd 00500 check-state
$cmd 00502 deny all from any to any frag
$cmd 00501 deny tcp from any to any established
$cmd 00600 allow tcp from any to any out via $oif setup $ks
$cmd 00601 allow udp from any to any out via $oif $ks

Чтобы при старте системы всё загружалось автоматически, вам надо дописать в файл /etc/rc.conf строчки:

firewall_enable="YES"
firewall_script="/etc/rc.ipfw"

Упомянутый здесь файл /etc/rc.ipfw может называться и по другому (это имя выбрал я сам). Это просто файл-скрипт, загружающий правила.
Диагностика

Для диагностики есть две команды:

ipfw list

показывает все правила, а

ipfw show

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

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

65535 deny ip from any to any

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

Наконец, ipfw предоставляет возможность, которая никак не связана с безопасностью, но может быть очень полезной при отладке web-приложений. Ipfw может эмитировать торможение сети.

Не вдаваясь в детали (которые всегда можно прочитать в man), приведу сразу пример правил. Загрузить их можно вот такими тремя командами:

ipfw pipe 1 config bw 50Kbit/s queue 20
ipfw queue 1 config pipe 1 weight 50 queue 20
ipfw 00099 add queue 1 tcp from any 9090 to any

Первой командой мы создали трубу (pipe) с скоростью пропускания 50Kbit/s и длиной очереди 20 пакетов. Второй командой мы создали очередь (queue), которая работает с нашей трубой. И третьей командой мы создали правило, которое отправляет в нашу очередь все пакеты, которые идут с порта 9090. То есть даже если вы будете соединяться сами с собой через порт 9090, то ширина канала будет ограничена 50Kbit/s.

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

Я лишь добавлю, что очерёдность выполнения этих трёх команд не важна. А чтобы удалить трубы и очереди, есть две специальные команды:

ipfw -f pipe flush
ipfw -f queue flush

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

Основные команды консоли восстановления Windows

В один прекрасный день компьютер отказывается загружать систему …

В один прекрасный день компьютер отказывается загружать систему и выводит на экран тарабарщину на голубом фоне. Неизвестно откуда появляется сообщение о том, что система не может быть запущена, так как потерян или испорчен какой-то файл. Прежде всего вы пытаетесь перезагрузиться, однако этот способ не всегда срабатывает. В такой ситуации восстановить работоспособность системы можно с помощью нового инструмента, входящего в состав Windows 2000 Professional — Консоли восстановления (Recovery Console — RC).

Установка консоли восстановления

Для начала нужно запустить RC с установочного CD-ROM Windows 2000 Pro (в том случае, конечно, если система поддерживает загрузку с CD-ROM) или с установочных дискет. Можно установить RC как один из вариантов загрузки. Установив компакт-диск, выбираем меню Start, затем Run и вводим в командную строку:

{x}:\i386\winnt32 /cmdcons

где вместо х подставляется буква устройства чтения компакт-дисков. После запуска появится диалоговое окно с вопросом о том, нужно ли устанавливать RC. Выбираем Yes. Для установки консоли понадобится до 7Mбайт дискового пространства. По завершении установки система предложит перезагрузиться. После этого новая опция загрузки — <Консоль восстановления> — к вашим услугам.

Для запуска RC с установочных дискет или напрямую с CD-ROM придется загружать систему именно с них. В текстовой части программы установки Windows 2000 Pro появляется возможность выбора между инсталляцией системы (нажав Enter) или ее восстановлением (нажав R). Выбираем вариант восстановления. Далее программа установки предлагает выбрать режим восстановления с помощью консоли восстановления (нажав С) или диска аварийного восстановления emergency repair disk (снова нажав R).

Если система отказывается загружаться, я рекомендую выбирать R, т.е. восстановление с помощью диска аварийного восстановления, даже если самого диска нет. В этом случае автоматически проверяется наличие и целостность всех важнейших системных файлов. Я использовал эту опцию для восстановления Windows 2000 после того, как привел ее в нерабочее состояние установкой Windows NT 4.0. Если этот метод не работает, придется перезагрузиться и запустить RC.

Использование RC

Вне зависимости от того, как запускается RC — с CD-ROM, установочных дисков или из загрузчика — после запуска придется зарегистрироваться. В системах с двойной загрузкой нужно будет выбрать, в какой из установок это сделать (например, C:\winnt, D:\winnt), и ввести пароль администратора. После регистрации появится приглашение на ввод команд. По команде help выдается перечень всех команд, доступных в консоли восстановления. Большинство из них знакомы пользователям командной строки Windows 2000 Pro и DOS, однако информацию по каждой конкретной команде можно получить , набрав

help <command>

В этом примере приведена строка для выдачи информации о команде attrib и результат ее исполнения:

C:\WINNT>help attrib

Команда показывает или изменяет атрибуты файлов:

+ — Назначить атрибут.
— Снять атрибут.
R — Атрибут файла только для чтения.
S — Атрибут системного файла.
H — Атрибут скрытого файла.
C — Атрибут сжатого файла.

Приведенный результат практически полностью совпадает с результатом выполнения команды

help attrib

из командной строки Windows 2000 Pro. В RC, однако, отсутствует возможность установки или снятия атрибута архивного файла, вместо нее предоставляется возможность работы с атрибутом сжатия. В RC также отсутствуют переключатели /S и /D, изменяющие атрибуты подкаталогов и папок. Большинство команд RC предоставляют сокращенные возможности по сравнению с их вариантами в Windows 2000 Pro. Некоторые команды, однако, существуют только в RC.

Batch. Команда запуска на исполнение файла пакетной обработки. Определены два аргумента: имя запускаемого пакетного файла и файла, в который система перенаправляет выходные данные.

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

disable cdrom

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

Diskpart. С помощью этой команды можно разбить жесткий диск на разделы. Все параметры ей можно передавать из командной строки (введите help diskpart для получения списка аргументов команды). Однако проще работать с ней в диалоговом режиме

diskpart

На текстовом экране появится список разделов жесткого диска. Cтрелками вверх и вниз выбираем необходимый раздел или свободное пространство. После выбора раздела клавишей D удаляем его. Для создания нового раздела в свободном пространстве нажимаем клавишу С.

Система попросит задать размер нового раздела. Завершив все настройки, нажимаем Esc для возврата в RC. Не забудьте отформатировать вновь созданные разделы командой Format.

Enable. Команда разрешает запуск при загрузке системы ранее отключенных служб и драйверов. Она использует два аргумента — наименование службы и ее флаг запуска (если не указать флаг запуска, команда Enable выведет перечень допустимых флагов). Например

enable cdrom SERVICE_SYSTEM_START

разрешает запуск службы cdrom и устанавливает флаг ее запуска при загрузке системы.

Exit. Команда завершает работу RC и перезагружает компьютер.

Expand. Команда распаковывает архивные файлы (.cab), используемые программой установки Windows 2000 Pro. В консоли восстановления она работает так же, как и ее аналог в Windows 2000 Pro, однако использует слегка измененные аргументы. Команда help expand даст их подробное описание.

Fixboot. Команда позволит восстановить систему в случае небрежной установки NT или инсталляции Windows 9x поверх установленной Windows 2000 Pro — в такой ситуации загрузить Windows 2000 Pro обычно не удается. Команда Fixboot перезаписывает загрузочный сектор жесткого диска, что делает диск загружаемым. Понимает единственный аргумент — букву устройства, на которое производится запись. Например

fixboot c:

произведет запись нового загрузочного сектора на диск С.

Fixmbr. С помощью этой команды можно попытаться восстановить главную загрузочную запись (Master Boot Record) загрузочного раздела. С ее помощью можно попытаться заставить систему загружаться в тех случаях, когда она отказывается это делать. У нее определен только один аргумент — имя устройства, на которое устанавливается новая MBR. Если имя в команде не указано, Fixmbr запишет новую MBR в устройство по умолчанию (обычно диск С).

Format. Команда позволяет переформатировать дефектный диск или заново отформатировать раздел, созданный командой Diskpart. Ее аргументами могут быть только: буква устройства, /Q (quick format) — параметр быстрого форматирования и /FS:(file system) — тип файловой системы, с указанием одного из параметров FAT, FAT32, или NTFS. Например команда

format g: /Q /FS:FAT32

применит быстрое форматирование к устройству G и создаст на нем файловую систему FAT32. Если при запуске команды тип файловой системы не указан, по умолчанию задается NTFS, используемая только системами Windows 2000. Это может вызвать проблемы при настройке окружения на работу двух операционных систем одновременно. (Прим. переводчика: установка Windows NT 4.0 с сервисным пакетом SP4 позволяет NT работать с NTFS 5.0, созданной Windows 2000).

Listsvc. Команда выводит на экран перечень всех используемых в системе служб и драйверов с указанием их флагов запуска. Если система не может загрузиться из-за проблем с каким-либо драйвером, Listsvc может помочь. Выпишите наименования всех служб, появившихся на «голубом экране смерти» и загрузите RC. Запустив Listsvc, найдите в выданном ею списке выписанные службы. Командой Disable отключите подозрительные драйверы и покиньте RC. Если система после этого не запустится, загружайте RC и пробуйте снова.

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

Map. Команда выводит список всех устройств жесткого диска, поставленных им в соответствие букв, форматов файловой системы, размеров устройств и физических устройств NT, соответствующих каждому диску. Например, для диска С: команда Map может вывести подобную информацию

C: FAT162047MB
\Device\Harddisk0\Partition1

Аргументом этой команды может быть arc. В этом случае команда покажет разметку физического устройства в формате Advanced RISC Computing (ARC). Именно в таком виде эта информация обычно представлена в файле boot.ini.

Systemroot. Команда делает текущим корневой системный каталог Windows 2000 Pro (обычно C:\winnt).

Команды консоли восстановления могут помочь «оживить» систему. Когда исчерпаны все средства, RC подскажет верное решение.

VPN на основе FreeBSD и vtun

В сети так много сайтов, объясняющих каждому, как престижно иметь в своем распоряжении VPN. Но ни один из них не публикует детального описания, как этого добиться. Побродив несколько дней по просторам интернета, но так и не найдя нормального описания процесса развертывания VPN, решил написать об этом сам.

Я буду использовать vtun, написаный Максимом Краснянским на основе пакета VPPP. Вы можете спросить, почему именно vtun. Ведь можно было использовать что-либо вроде PPP поверх SSH, IPSEC или GRE. Возможно, в ближайшее время я напишу о работе с IPSEC или OpenVPN. Главным достоинством vtun является простота в установке и настройке. Он поддерживает разнообразные типы туннелей IP, Ethernet, PPP, SLIP. В качестве туннеля можно использовать даже pipe. Для шифрования используется OpenSSl. Доступны алгоритмы blowfish с ключом в 128 бит или MD5 с ключом той же длины. Компрессия потока производится с помощью библиотек LZO или zlib. Следует отметить, что zlib работает только с tcp туннелями. Поддерживаются следующие операционные системы: Linux, Solaris, FreeBSD, NetBSD, OpenBSD и другие BSD клоны. В принципе, vtun должен работать на любой платформе, для которой есть универсальный драйвер tun/tap. Устройство tun используется для туннелирования IP фреймов, а tap для Ethernet фреймов. С помощью tun/tap пользовательские программы получают возможность самостоятельно обрабатывать IP пакеты. Для некоторых операционных систем необходимо перекомпилировать ядро с поддержкой tun/tap устройств. Vtun работает на основе клиент-серверной модели. Соответственно, для создания туннеля на одном из хостов демон vtun должен быть запущен как сервер, а на другом в роли клиента.

После запуска демон, выполняющий роль сервера, по умолчанию начинает слушать порт 5000. Если между клиентом и сервером находится брандмауэр, необходимо разрешить прохождение пакетов, адресованных на порт 5000. При попытке подсоединиться на этот порт происходит аутентификация клиента на основе пароля, записанного в конфигурационном файле /usr/local/etc/vtund.conf. Затем с помощью функции fork запускается еще один демон vtun, которому передается клиентское соединение. Новый демон будет существовать до тех пор, пока соединение не будет разорвано. В тоже время родительский демон продолжает ждать новых соединений. Это значит, что единственный демон может обслуживать множество одновременных подключений.

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

Давайте представим, что у нас есть филиал, магазин и офис, использующие адреса из пространства частных сетей. Необходимо эти подразделения соединить с помощью VPN. Для этих целей мы будем использовать реальные ip адреса, выданные нам провайдером из сети 80.80.20.0. Для соединения сетей нам понадобятся три компьютера. На каждом из них будет по три сетевых интерфейса. Более подробно это показано в приведенной ниже таблице.

имя машины внутренняя подсеть внутренний интерфейс внешний интерфейс виртуальный интерфейс tun
офис vpn_office 192.168.30.0 192.168.30.251 ed0 80.80.20.2 ed1 192.168.0.2
филиал vpn_filial 192.168.20.0 192.168.20.251 ed0 80.80.20.1 xl0 192.168.0.1
магазин vpn_shop 192.168.40.0 192.168.40.251 wb0 80.80.20.3 fxp0 192.168.0.3

Схема соединения наших сетей выглядит так :

net_schema

А пока берем исходник библиотеки LZO. Если не удалось скачать, то берем дистрибутив здесь. Распаковываем и собираем.

# tar zxvf lzo-1.08.tar.gz
# cd lzo-1.08
# ./configure
# make
# make check
# make test
# make install

Скачиваем vtun. Конфигурируем его c указанием использовать библиотеку lzo.

# tar zxvf vtun-2.5.tar.gz
# cd vtun
# ./configure --with-lzo-headers=/usr/local/include/ --with-lzo-lib=/usr/local/lib

Случается, что команда ./configure завершается с ошибкой. Вероятнее всего это значит, что система не смогла обнаружить библиотеку lzo. Если Вам не удастся самостоятельно избавиться от этой ошибки, выполните команду:

# ./configure --disable-lzo

А затем, как положено, выполняем компиляцию и установку.

# make
# make install

Повторяем эти действия на каждой из трех машин. Если все прошло гладко, значит пришло время заняться конфигурированием каждой машины. В нашем случае машина vpn_office будет выполнять роль сервера, соответственно, vpn_filial и vpn_shop станут клиентами. Конфигурационный файл vtun находится в директории /usr/local/etc/vtund.conf.

Давайте посмотрим, из чего состоит конфигурационный файл хоста vpn_office.

options {
port 5000;                         # номер порта, используемого для соединения с клиентами
ifconfig /sbin/ifconfig;         # путь к программе ifconfig
route /sbin/route;              # путь к программе route
}
 
default {
compress lzo:9;               # если не удалось включить поддержку lzo, то  параметр должен принять значение compress no;
speed 0;                          # ограничения скорости соединения нет
}
 
# настройки, определенные в блоках options и default, относятся ко всем
# остальным блокам
 
filial {{                        # описываем клиента филиал
    pass secret;          # пароль соединения - слово secret
    type tun;               # тип IP туннеля tun
    proto udp;             # используется протокол UDP
    encr yes;              # включить шифрование
    keepalive yes;       # постоянно поддерживать соединение
 
# секция up описывает действия, выполняемые при удачном соединении
# конфигурируем виртуальный интерфейс с адресом 192.168.0.2
# и привязываем его к другому  виртуальному интерфейсу 192.168.0.1
# настраиваем маршрутизацию для сети 192.168.20.0/24 через интерфейс с адресом 192.168.0.1
 
up {
ifconfig "%% 192.168.0.2 192.168.0.1 netmask 255.255.255.255 mtu 1450 up";
route "add -net 192.168.20.0/24 192.168.0.1";
};
 
# секция down описывает действия, выполняемые при разрыве соединения
# удаляем виртуальный интерфейс tun
# разрушаем маршрутизацию для сети 192.168.20.0/24
 
down {
ifconfig "%% down";
route "delete 192.168.20.0";
};
}
 
shop {                      # описываем клиента магазин
    pass secret;         # пароль соединения - слово secret
    type tun;;             # тип IP туннеля
    proto udp;            # используется протокол UDP
    encr yes;             # включить шифрование
    keepalive yes;      # постоянно поддерживать соединение
 
# конфигурируем виртуальный интерфейс с адресом 192.168.0.2
# и привязываем его к другому виртуальному интерфейсу 192.168.0.3
# настраиваем маршрутизацию для сети 192.168.40.0/24 через интерфейс с адресом 192.168.0.3
 
up {
ifconfig "%% 192.168.0.2 192.168.0.3 netmask 255.255.255.255 mtu 1450 up";
route "add -net 192.168.40.0/24 192.168.0.3";
};
 
# описываем действия, выполняемые при разрыве соединения
# удаляем виртуальный интерфейс tun
# разрушаем маршрутизацию для сети 192.168.40.0/24
 
down {
ifconfig "%% down";
route "delete 192.168.40.0";
};
}

А вот это мы запишем в файл конфигурации хоста vpn_shop.

options {
port 5000;
ifconfig /sbin/ifconfig;
route /sbin/route;
}
 
default {
compress lzo:9;
speed 0;
}
 
shop {
    pass secret;
    type tun;
    proto udp;
    encr yes;
    keepalive yes;
 
up {
ifconfig "%% 192.168.0.3 192.168.0.2 netmask 255.255.255.255 mtu 1450 up";
route "add -net 192.168.30.0/24 192.168.0.2";
};
 
down {
ifconfig "%% down";
route "delete 192.168.30.0";
};
}

Ну и наконец, конфигурация хоста vpn_filial.

options {
port 5000;
ifconfig /sbin/ifconfig;
route /sbin/route;
}
 
default {
 
compress lzo:9;
speed 0;
}
 
filial {
    pass secret;
    type tun;
    proto udp;
    encr yes;
    keepalive yes;
 
up {
ifconfig "%% 192.168.0.1 192.168.0.2 netmask 255.255.255.255 mtu 1450 up";
route "add -net 192.168.30.0/24 192.168.0.2";
};
 
down {
ifconfig "%% down";
route "delete 192.168.30.0";
};
}

В связи с тем, что в файлах vtund.conf находится пароль соединения, доступ к ним должен иметь только пользователь root. После всех этих манипуляций можно запускать vtun. На машине vpn_office запускаем демон в режиме сервера.

vpn_office# vtund -s

На другой консоли смотрим на сообщения об ошибках.

vpn_office# tail -f /var/log/messages

Если ошибок не появилось, значит все у нас хорошо. Соответственно, на хостах vpn_shop и vpn_filial запускаем демоны в режиме клиента.

vpn_shop# vtund -p shop 80.80.20.2
vpn_filial# vtund -p filial 80.80.20.2

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

vpn_office# ifconfig -u
 
ed0: flags=8843 mtu 1500
	inet 192.168.30.251 netmask 0xffffff00 broadcast 192.168.30.255
	inet6 fe80::280:48ff:fedf:66f7%ed0 prefixlen 64 scopeid 0x1
	ether 00:80:48:df:66:f7
ed1: flags=8843 mtu 1500
	inet 80.80.20.2 netmask 0xffffff00 broadcast 80.80.20.255
	inet6 fe80::240:95ff:fe45:9ce2%ed1 prefixlen 64 scopeid 0x2
	ether 00:40:95:45:9c:e2
lo0: flags=8049 mtu 16384
	inet6 ::1 prefixlen 128
	inet6 fe80::1%lo0 prefixlen 64 scopeid 0x5
	inet 127.0.0.1 netmask 0xff000000
tun0: flags=8051 mtu 1450
	inet6 fe80::280:48ff:fedf:66f7%tun0 prefixlen 64 scopeid 0x8
	inet 192.168.0.2 --> 192.168.0.3 netmask 0xffffffff
	Opened by PID 1143
tun1: flags=8051 mtu 1450
	inet6 fe80::280:48ff:fedf:66f7%tun1 prefixlen 64 scopeid 0x9
	inet 192.168.0.2 --> 192.168.0.1 netmask 0xffffffff
	Opened by PID 1150
 
vpn_shop# ifconfig -u
wb0: flags=8843 mtu 1500
	inet 192.168.40.251 netmask 0xffffff00 broadcast 192.168.40.255
	inet6 fe80::280:48ff:feb6:435f%wb0 prefixlen 64 scopeid 0x1
	ether 00:80:48:b6:43:5f
	media: Ethernet autoselect (100baseTX)
	status: active
fxp0: flags=8843 mtu 1500
	inet 80.80.20.3 netmask 0xffffff00 broadcast 80.80.20.255
	inet6 fe80::202:b3ff:fe65:f47%fxp0 prefixlen 64 scopeid 0x2
	ether 00:02:b3:65:0f:47
	media: Ethernet autoselect (100baseTX)
	status: active
lo0: flags=8049 mtu 16384
	inet6 ::1 prefixlen 128
	inet6 fe80::1%lo0 prefixlen 64 scopeid 0x5
	inet 127.0.0.1 netmask 0xff000000
tun0: flags=8051 mtu 1450
	inet6 fe80::280:48ff:feb6:435f%tun0 prefixlen 64 scopeid 0x8
	inet 192.168.0.3 --> 192.168.0.2 netmask 0xffffffff
	Opened by PID 1101
 
vpn_filial#  ifconfig -u
ed0: flags=8843 mtu 1500
	inet 192.168.20.251 netmask 0xffffff00 broadcast 192.168.20.255
	inet6 fe80::280:48ff:fec7:c79b%ed0 prefixlen 64 scopeid 0x1
	ether 00:80:48:c7:c7:9b
xl0: flags=8843 mtu 1500
	options=3
	inet 80.80.20.1 netmask 0xffffff00 broadcast 80.80.20.255
	inet6 fe80::202:2eff:fef1:1726%xl0 prefixlen 64 scopeid 0x2
	ether 00:02:2e:f1:17:26
	media: Ethernet autoselect (100baseTX)
	status: active
lo0: flags=8049 mtu 16384
	inet6 ::1 prefixlen 128
	inet6 fe80::1%lo0 prefixlen 64 scopeid 0x4
	inet 127.0.0.1 netmask 0xff000000
tun0: flags=8051 mtu 1450
	inet6 fe80::280:48ff:fec7:c79b%tun0 prefixlen 64 scopeid 0x8
	inet 192.168.0.1 --> 192.168.0.2 netmask 0xffffffff
	Opened by PID 20966

Теперь можно попробовать, как работает наша VPN. Выполним команду ping на хостах vpn_filial и vpn_shop.

vpn_filial#  ping 192.168.30.251
PING 192.168.30.251 (192.168.30.251): 56 data bytes
64 bytes from 192.168.30.251: icmp_seq=0 ttl=64 time=5.788 ms
64 bytes from 192.168.30.251: icmp_seq=1 ttl=64 time=5.724 ms
64 bytes from 192.168.30.251: icmp_seq=2 ttl=64 time=5.683 ms
64 bytes from 192.168.30.251: icmp_seq=3 ttl=64 time=5.685 ms
 
--- 192.168.30.251 ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max/stddev = 5.683/5.720/5.788/0.043 ms 
 
vpn_shop# ping 192.168.30.251
PING 192.168.30.251 (192.168.30.251): 56 data bytes
64 bytes from 192.168.30.251: icmp_seq=0 ttl=64 time=6.092 ms
64 bytes from 192.168.30.251: icmp_seq=1 ttl=64 time=5.785 ms
64 bytes from 192.168.30.251: icmp_seq=2 ttl=64 time=5.851 ms
64 bytes from 192.168.30.251: icmp_seq=3 ttl=64 time=5.826 ms
 
--- 192.168.30.251 ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max/stddev = 5.785/5.888/6.092/0.120 ms

Судя по всему, туннель работает как положено. Теперь давайте проверим, как работает шифрование. Нужно проверить, что и в каком виде передается по интерфейсам tun0 — 192.168.0.2 и fxp0 — 80.80.20.3. Давайте начнем прослушивание этих интерфейсов. В тоже время с машины vpn_shop начинаем пинговать интерфейс 192.168.40.251.

vpn_office# tcpdump -i tun0 -lenx
13:33:14.573619 AF 2 84: 192.168.0.2 > 192.168.40.251: icmp: echo request
			 4500 0054 0cc3 0000 4001 c398 c0a8 0002
			 c0a8 28fb 0800 edcc c904 0000 ede7 cc3d
			 9505 0700 0809 0a0b 0c0d 0e0f 1011 1213
			 1415 1617 1819 1a1b 1c1d 1e1f 2021 2223
			 2425 2627 2829 2a2b 2c2d 2e2f 3031 3233
			 3435 3637
13:33:14.573665 AF 2 84: 192.168.40.251 > 192.168.0.2: icmp: echo reply
			 4500 0054 1b3f 0000 4001 b51c c0a8 28fb
			 c0a8 0002 0000 f5cc c904 0000 ede7 cc3d
			 9505 0700 0809 0a0b 0c0d 0e0f 1011 1213
			 1415 1617 1819 1a1b 1c1d 1e1f 2021 2223
			 2425 2627 2829 2a2b 2c2d 2e2f 3031 3233
			 3435 3637
13:33:15.583143 AF 2 84: 192.168.0.2 > 192.168.40.251: icmp: echo request
			 4500 0054 0cc6 0000 4001 c395 c0a8 0002
			 c0a8 28fb 0800 42a6 c904 0100 eee7 cc3d
			 3e2c 0700 0809 0a0b 0c0d 0e0f 1011 1213
			 1415 1617 1819 1a1b 1c1d 1e1f 2021 2223
			 2425 2627 2829 2a2b 2c2d 2e2f 3031 3233
			 3435 3637
13:33:15.583194 AF 2 84: 192.168.40.251 > 192.168.0.2: icmp: echo reply
			 4500 0054 1b43 0000 4001 b518 c0a8 28fb
			 c0a8 0002 0000 4aa6 c904 0100 eee7 cc3d
			 3e2c 0700 0809 0a0b 0c0d 0e0f 1011 1213
			 1415 1617 1819 1a1b 1c1d 1e1f 2021 2223
			 2425 2627 2829 2a2b 2c2d 2e2f 3031 3233
			 3435 3637

На предыдущем листинге явно видно содержимое тестовых icmp пакетов. А теперь посмотрите, в каком виде эти пакеты путешествуют по небезопасной сети 80.80.20.0/24.

vpn_office# tcpdump -i fxp0 -lenx
13:33:14.573441 0:40:95:45:9c:e2 0:2:b3:65:f:47 0800 140: 80.80.20.2.5000 > 80.80.20.3.1035: udp 98
			 4500 007e 0cc4 0000 4011 a506 5050 1402
			 5050 1403 1388 040b 006a f9e2 0060 7db0
			 f6ef dd81 4638 917a 5a80 7f48 87d7 7bc9
			 459f 97f0 b95a 95cf 87b1 29ce b2d7 8f50
			 228e 6b8f eafb 1f5d ae9d 7518 2085 2da9
			 8c85
13:33:14.574798 0:2:b3:65:f:47 0:40:95:45:9c:e2 0800 140: 80.80.20.3.1035 > 80.80.20.2.5000: udp 98
			 4500 007e 1b40 0000 4011 968a 5050 1403
			 5050 1402 040b 1388 006a 998c 0060 7db0
			 f6ef dd81 4638 5390 c84e 886e 466d ffcd
			 df10 9010 5995 fcdd b315 92fb 6a1d 8f50
			 228e 6b8f eafb 1f5d ae9d 7518 2085 2da9
			 8c85
13:33:15.582910 0:40:95:45:9c:e2 0:2:b3:65:f:47 0800 140: 80.80.20.2.5000 > 80.80.20.3.1035: udp 98
			 4500 007e 0cc7 0000 4011 a503 5050 1402
			 5050 1403 1388 040b 006a 28fd 0060 7db0
			 f6ef dd81 4638 3048 4e92 e692 1c3d 5fa3
			 c2a6 bc50 8fa5 79d3 c0c2 6537 c74b 1e84
			 b95e c8f8 6048 3d3c 4f33 32a4 25a2 2da9
			 8c85
13:33:15.584332 0:2:b3:65:f:47 0:40:95:45:9c:e2 0800 140: 80.80.20.3.1035 > 80.80.20.2.5000: udp 98
			 4500 007e 1b44 0000 4011 9686 5050 1403
			 5050 1402 040b 1388 006a cd92 0060 7db0
			 f6ef dd81 4638 f41d cb55 f37d 1229 dbb6
			 14f7 14d1 08e3 a204 5045 74a0 7807 1e84
			 b95e c8f8 6048 3d3c 4f33 32a4 25a2 2da9
			 8c85

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

Установка терминального сервера Windows 2003 Server

Установка сервера терминалов

Заходим Administrator’ом. Если всё сделано правильно — перед нами окно «Manage Your Server» и никаких (!) сообщений об активации.

Список «ролей» (теперь это так называется) нашего сервера пуст. Добавляем роль нашему серверу. Компьютер симулирует поиск чего-то там по локальной сети, и предлагает нам выбрать: между той ролью, которая нужна нам по ЕГО мнению и той, которая нужна по ВАШЕМУ мнению. Поскольку в искусственный интеллект и прочие сказки мы с вами уже не верим, да и своё мнение нам как-то дороже, то выбираем: Сustom.

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

Перегружаясь, удивляемся, как быстро загружается Windows 2003 Server по сравнению с Windows 2000.

Заходим под Administrator’ом, и говорим установке сервера терминалов Finish. Заметьте: в окне Managing your computer появилась роль: Terminal Server. Однако ниже сказано, что: «Поскольку сервера терминальных лицензий не найдено, то сервер терминалов будет выдавать временные лицензии на соединение, каждая из которых закончит своё существование через 120 дней».

Такая ситуация нас никак не устраивает, поэтому мы устанавливаем сервер терминальных лицензий. Для этого заходим в Пуск -> Панель управления -> Установка и удаление программ -> Компоненты Windows. Ставим галочку напротив пункта Terminal Server Licensing. Hажимаем Next. Затем Finish.

Теперь у нас установлен сервер терминальных лицензий. Hо его ещё надо активировать.

Если есть корпоративный прокси, его следует прописать в Панель управления -> Hастройки интернет.

Заходим в Администрирование -> Terminal Server Licensing. Видим, что найденный на нашем компьютере сервер находится в состоянии Not activated.

Щелкаем правой кнопкой, говорим Activate server. Выбираем тип подключения Automatic. Вводим свои личные данные (имя, фамилию, организацию, страну — строго те, которые были введены при установке Windows). Следующую страничку (E-Mail, адрес) я оставил пустой. Hажимаем Next, и ждём.

Активация должна пройти успешно. Становится непонятным, какой смысл тогда Microsoft закладывала в эту активацию? Зачем она нужна кроме сбора статистики? После успешной активации вам будет предложено добавить лицензии. Что ж, продолжим.

Запустится Client Access License (CAL) Activation Wizard, который первым делом снова полезет в Microsoft. После чего спросит тип лицензии, которую желаете установить. Я выбрал Enterprise Agreement, и следующим этапом у меня спросили магическое
число. Как оказалось, это магическое число прекрасно ищется в любом поисковике по запросу Enrollment Number. Я выбрал первое попавшееся: 6565792.

Теперь нужно указать продукт — Windows 2003 Server. Тип лицензии — per Device. Количество — 999 (9999 у меня почему-то не прошло). Лицензия инсталлировалась отлично. Закрываем окно Terminal Server Licensing.

Hастраиваем сервер

Из папки Администрирование вытаскиваем на рабочий стол ярлычки Computer Management и Terminal Server Manager. Это не обязательно, но так намного удобнее.

Заходим в Computer Management, создаём группу/группы пользователей 1С.

Заходим в Администрирование -> Terminal Services Configuration. В настройках Server Setting снимаем ограничение «Restrict each user to one session». В списке Connections выбираем соединения и настраиваем их Propetries:

Закладка Client Settings:

  • Заменяем настройки пользователей в отношении дисков и принтеров собственными: подключать всё (то есть верхняя галочка должна быть снята, а три следующие должны быть активными и установленными);
  • Ограничить глубину цвета до 16 бит;
  • Запретить переадресацию LPT, COM, Audio портов.

Закладка Permissions:

  • Добавляем созданные группы пользователей 1С и ставим им права: Guest Access + User Access.

Закладка Remote Control:

  • Use remote control with following setting:
  • [ ] Require user’s permission (снимаем галочку)
  • (o) Interact with the session

Закрываем Terminal Services Configuration. Далее делаем следующее:

  • Заходим в My computer;
  • Говорим Propetries на диске C:;
  • Говорим Advanced…;
  • Удаляем права, позволяющие группе Users создавать папки и файлы;
  • Так секьюрнее.

Заходим с Панель управления -> Система. Закладка «Автоматическое обновление». Отключаем всё автоматическое обновление напрочь.

Заходим в Администрирование -> Local Security Settings -> Account Policies -> Password Polisy. Устанавливаем «Maximum password age» = 0 — так удобнее.

Заходим с Computer Management, добавляем себя в качестве пользователя.
Hе забываем:
— Password never expires;
— Добавить себя в группы пользователей 1С;
— Снять галочку «Require user’s permission» на закладке «Remote control».

Пробуем подключиться к серверу

Пробуем подключиться с другого компьютера к серверу терминалов. Для этого нам нужно установить на компьютер клиент. В составе XP такой клиент уже есть: «Пуск -> Программы -> Стандартные -> Связь -> Удаленное управление рабочим столом».
Однако, мне кажется, даже на XP его стоит переустановить: клиент в составе Win2003 Server будет все-таки поновее.

Он находится в папке: C:WINDOWSSYSTEM32CLIENTSTSCLIENT. Установите его на всех машинах, с которых хотите работать на сервере терминалов.

После запуска клиента нажмите кнопку «Options>>». Для пользователей 1С лучше применять следующие настройки:

Закладка «Общие»:
— заполнить поля компьютер, имя пользователя, пароль
— домен (если сеть с доменами — имя домена, если без доменов — имя сервера)

Закладка «Экран»:
— рабочий стол — во весь экран
— цветовая палитра — 16 бит

Локальные ресурсы:
— звук — не проигрывать
— клавиши — только в полноэкранном режиме
— автоматически подключаться к дискам и принтерам — (!!!) это обязательно

Закладка «Дополнительно»:
— скорость — модем 28.8 — должна остаться одна галочка — кэширование графики

Теперь эти настройки можно сохранить в файл с расширением RDP. Обратите внимание на галочку «Сохранять пароль» на закладке «Общие». Для операционных систем Win2000 и WinXP эта галочка доступна. Для других — нет. К сожалению, пользователи Win9x лишены возможности сохранять пароль в RDP файле — поэтому настройте ваших пользователей Win9x, что кроме пароля на 1С им придётся набивать пароль на Windows.

Как поступить с этим паролем — дело ваше. Можно назначить всем один пароль, можно сгенерировать свой пароль для каждого пользователя. Однако, вам крайне желательно знать пароль каждого пользователя — это пригодится для удалённого присоединения и администрирования сессий.

Hастроив должным образом соединение, убедитесь, что оно работает, и сохраните его в RDP-файл (можно прямо на рабочий стол).

Опасный код на C

Перевод цикла статей David Chisnall «Writing Insecure C, Part 1», «Writing Insecure C, Part 2», «Writing Insecure C, Part 3».

Использование языка программирования C часто приводит к написанию очень опасного кода. Но это не совсем справедливое обвинение; такие проекты, как OpenBSD показывают, что возможно писать безопасный код на C. Проблема C та же, что и в ассемблере — язык открывает вам все возможности архитектуры, но и кое-что ещё. Он дает все возможности для написания безопасного кода, но не делает эти вещи сам.

В этой статье рассмотрены стандартные примеры ошибок в коде C и то, как их и избежать.

Проверка ошибок

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

В частности, код на Java часто засорен блоками try…catch, которые ничего не делают, кроме отбрасывания ошибок, но даже в этом случае механизм исключений преследует цель: это заставляет программиста помнить о том, что он небезопасно обрабатывает условия ошибок.

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

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

Стандартный пример функции, которая почти никогда не завершается с ошибкой — malloc(), наряду со связанными с ней функциями, вроде calloc(). В спецификации C указано, что malloc должна вернуть NULL в случае, если памяти недостаточно, чтобы удовлетворить запрос. Linux не полностью следует этому правилу: он возвращает NULL, если в системе нет достаточного виртуального пространства адресов, чтобы выполнить выделение памяти, но в случае недостатка памяти, Linux по-прежнему выделяет пространство адресов, а затем возвращает ошибку, если вы пытаетесь использовать эту память. Как бы то ни было, исходя из того, что у вас есть подходящая реализация C, необходимо проверять значения, которые возвращает malloc.

В большинстве случаев, вы не сможете сделать ничего разумного, если malloc завершится с ошибкой. Даже код, восстанавливающий ошибку, обычно нуждается в выделении памяти. Вы можете попробовать выделять эту память, когда программа запускается (не забудьте проверить, что вы можете получать к ней доступ). В качестве альтернативы, вы можете использовать что-нибудь вроде этого макроса:

#define MALLOC(x,y) do { y = malloc(x); if (!y) abort(1); } while(0)

Он будет тестировать каждое выделение памяти, и прерывать программу в случае ошибки. Вы можете заменить вызов abort на ваш код, обрабатывающий ошибку, но будьте осторожны. Одна из недавних уязвимостей в OpenSSH была вызвана вызовом кода, восстанавливающего ошибку в ситуации, когда программа была в неопределенном состоянии. Зачастую, прерывание выполнения безопаснее.

Точно также важна проверка возвращаемых значений других функций.

Начальные значения

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

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

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

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

int a = 42;

А затем вы решаете, что вам необходимо условие для инициализации, поэтому вы копируете и вставляете его в выражение if:

if ({некоторое условие})
 
{
 
   int a = 42;
 
}

Ой, вы же забыли объявить переменную за пределами блока if, поэтому вы делаете это позже, и даете ей значение по умолчанию:

int a = 0;
 
if ({некоторое условие})
 
{
 
   int a = 42;
 
}

Теперь у вас есть код, который компилируется и запускается. Большинство времени (когда не встречается условие), он будет работать. Но код внутри фигурных скобок будет «холостым». Он определяет новую переменную под названием a и присваивает ей 42. Как только блок закончится, эта переменная выйдет из области видимости и останется старая a, до сих пор со значением 0.

Более незаметный вариант может возникнуть при опечатке в инициализации, когда вы выполняете что-то вроде этого:

int value = value + 12;

когда на самом деле ожидаете выполнение этого:

int value = othervalue + 12;

Как только компилятор распарсит int value, переменная станет валидной и будет находиться в области видимости. Вы можете считать её значение, добавить 12 к нему и присвоить результат ей же. К несчастью для вас, value, которую вы считываете, не определена. Теперь вам придется присвоить value неопределенное значение. Если вы считываете неосторожно, вы можете подумать, что проинициализировали её, хотя вы этого и не сделали. Компилятор с оптимизацией удалит +12, поскольку неопределенность плюс 12 есть неопределенность, и это будет эквивалентно следующему:

int value;

Если в вашем компиляторе есть выдача предупреждений о том, что не было проинициализировано, он должен выявить эти проблемы. К сожалению, выключение этих предупреждений происходит довольно часто, потому как в C нет способа указать выходные параметры. Вот почему следующие вещи встречаются относительно часто:

int a;
 
call_some_function(&amp;a);

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

Проблемы целых

Если вы используете высокоуровневые языки программирования, поддержка числовых переменных в C может показаться мучительно примитивной. То же самое может быть, если раньше вы писали на ассемблере, так как C также не открывает программисту условных кодов современных ЦПУ.

В высокоуровневых языках, числа обычно условно-точные, причем точность определена так, как вам надо. В C есть набор целых типов. Наиболее часто используемый — это int, который должен соответствовать машинному слову. На компьютерах, на которых я учил C, оно было длиной в 16 бит. Теперь он обычно размером в 32 бита, даже на архитектурах, где машинное слово имеет длину 64 бита, так как большое количество написанного кода подразумевает, что он всегда имеет длину 32 бита.

Одна из наиболее частых причин странного поведения — попытка хранить значение указателя в int. На обычных 32-битных платформах, этот метод работает хорошо. На некоторых 64-битных он работает, а на некоторых — нет. Стандарт C99 определяет новый целый тип, intptr_t, который гарантированно имеет достаточный размер, чтобы хранить указатель. Если вы планируете хранить указатель в int, вы всегда должны использовать intptr_t.

Указатель — один из других слегка сбивающих с толку моментов. C определяет два типа указателей: один указывает на код, другой указывает на данные. Указатель на функцию не имеет гарантировано такой же размер, что и указатель, указывающий на данные. На множестве встроенных платформ, часто используются 16-битные указатели для кода и 32-битные для данных. Преобразование типа в void* указателя на функцию приведет к тому, что некоторые данные будут отброшены.

Другим основным видом целого в C является char, short и long. В C99 также определен long long. Ни один из них не имеет фиксированного размера, но все они имеют минимальные размеры (технически у них есть минимальный набор значений, который они могут хранить, не предоставляя гарантий по внутреннему формату). Short должен быть, по крайней мере, 16 бит, long как минимум 32, а lon long как минимум 64. Если вам необходимо минимальное количество точности, выберите один из них, и вы сможете получить дополнительное пространство к тому, что запросили, в зависимости от архитектуры.

Я не упомянул char ещё и потому, что они детально отличаются от других типов. Все другие основные целые типы данных являются знаковыми, пока не будут объявлены, как беззнаковые. Это не всегда так в случае с char. К сожалению, в случае с char, знаковая она, или нет, полностью зависит от реализации. Если вы используете char, всегда явным образом объявляйте их как знаковые или нет, так как в противном случае, вы можете быть удивлены позднее.

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

 a = b + c;

Так как вы храните результат в a, вы можете предположить, что сложение будет выполнено, каким бы типом не была a. По факту, оно будет выполнено с типом b и c. Это имеет смысл тогда, когда вы думаете, что лучше иметь значение (b + c) в зависимости от чего-то, чем b и c сами по себе. Вы можете думать, что тип этого выражения будет типом b. Стандарт C99 определяет набор правил для определения типа, но в основном он будет представлять собой тот тип, который больше, b или c. Например, если a — char, а b — int, тип выражения будет int. Поразительно часто встречается ошибка, когда делают что-то вроде такого:

long long a;
 
long b;// = чему-то
 
long c;// = чему-то
 
a = b * c;

a как минимум 64 бита, b и c как минимум 32 бита. Так как у вас есть только гарантия того, что b и c по 32 бита, вы не должны помещать нечто большее в них, даже если ваш компилятор или платформа имеет long в 64 бита. Когда вы умножаете их, вы получаете результат, подходящий для 64-битного int и вы храните его в чем-то, что подходит для 64-битного int. Звучит удобно? Так и есть, кроме того факта, что результат искажается до 32 бит прямо перед присваиванием. Вот правильный способ сделать это:

a = (long long)b * c;

Это расширяет b до 64 бит (или больше, в зависимости от реализации). Расширение типа гарантирует, что c имеет тип такого же размера, что и b, так что она также расширена. Тогда умножение происходит как умножение двух long long с 32 или более первыми нулями, а результат (long long) хранится в переменной типа long long.

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

Более неуловимая ошибка возникает от переполнения int. Она особенна часто возникает при использовании malloc, так как стандартный шаблон написания — malloc(i * sizeof(x)). Если у взломщика есть влияние на i, он может попытаться выполнить это переполнение. Для очень больших значений i, это даст результат гораздо меньший, чем i, что приведет к проблеме. Вызов malloc будет успешным, но когда вы попытаетесь использовать массив, полученный в результате, только первые несколько значений будут валидными. Взломщик может вынудить вас переписать другие данные.

Простым путем избегания этого вида уязвимости может быть использование calloc() вместо malloc() (конечно, в надежде, что реализация calloc в вашей системе производит проверку границ сама, а не просто делает malloc() и memset() для количество*размер байт).

realloc() — ещё одна проблема. Нет стандартного пути сделать это с ней, поэтому вам надо делать это самому. К счастью, OpenSSH включает в себя xrealloc(), который является версией realloc() с проверкой границ. Она включает несколько других проверок, но если вам не нужны все из них, вы можете реализовать упрощенную версию:

void * xrealloc(void *ptr, size_t nmemb, size_t size)
 
{
 
    void *new_ptr;
 
    size_t new_size = nmemb * size;
 
    if (SIZE_T_MAX / nmemb &lt; size)
 
            return NULL;
 
        return realloc(ptr, new_size);
 
}

Этот тест довольно просто. SIZE_T_MAX — это максимальное значение, которое может иметь size_t. Когда вы делите на указанное количество элементов, вы получаете максимальный размер, который может быть без переполнения. Если этот размер меньше, чем требуемое пространство, возникает переполнение, поэтому вы возвращаете NULL.

realloc возвращает NULL в случае ошибки, так что вам всегда следует проверять возвращаемое значение realloc на валидность. К сожалению, это является наиболее частой причиной утечек памяти (которые, в свою очередь, являются причиной атак DDoS). Если realloc() возвращает NULL, исходный указатель по-прежнему является валидным. Часто разработчики забывают этот принцип и просто делают что-то вроде этого:

ptr = realloc(ptr, newsize);

Когда realloc() возвращает NULL, вы теряете вашу ссылку на предыдущее выделение памяти. FreeBSD предоставляет удобную функцию, reallocf(), которая эквивалентна следующей:

void *reallocf(void* ptr, size_t size)
 
{
 
    void *newptr = realloc(ptr, size);
 
    if (NULL == newptr)
 
    {
 
        free(ptr);
 
    }
 
    return newptr;
 
}

Если у вас нет кода для обработки случая, когда realloc() завершается с ошибкой, вам необходимо использовать что-то вроде неё.

Проблемы с памятью

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

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

Что ещё хуже, это не работает для всех динамически выделяемых данных. Рассмотрим что-нибудь, вроде sprintf(). Этот аналог printf() пишет в буфер вместо стандартного вывода. Проблема sprintf в том, что вызывающей функции необходимо знать размер буфера, что не всегда просто — на самом деле, это требует реализации большинства кода sprintf в вызывающей функции.

По факту, почти невозможно использовать sprintf безопасно. Вам необходимо указать длину каждого отдельного элемента строки формата (вот почему sprintf была создана). Это может привести к завершению процесса (что может также вызвать проблемы безопасности, и мы вернемся к этому позднее), поэтому некоторые реализации libc включают в себя asprintf().

Функция asprintf() — это то же самое, что и sprintf, кроме того, что она выделяет свой собственный буфер с помощью malloc. Это позволяет избежать досрочного завершения процесса или переполнения. К несчастью, она приносит новую проблему: когда вызывающая функция должна освобождать указатели, которые возвращает вызываемая функция?

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

typedef struct _RefCountedBuffer
 
{
 
    void *buffer;
 
    int refcount;
 
    void (free*)(struct _RefCountedBuffer*);
 
} *RefCountedBuffer;

Когда вы возвращаете указатель, вы создаете одну из этих структур и устанавливаете refcount в 1. Когда вы получаете его, вы всегда вызываете функцию, которая увеличивает refcount на единицу и вызывает соответствующую функцию free(), если она достигнет нуля.

Эта методика близка для программистов Objective-C, потому как OpenStep реализует схожий механизм работы. GNUstep продвинулся дальше, предоставляя макросы ASSIGN() и DESTROY(). Эти макросы делают ошибки при работе с памятью более редкими, и мы можем сделать нечто похожее на обычном C.

Прежде всего, нам надо определить функции retain() и release():

RefCountedBuffer retain(RefCountedBuffer *buf)
 
{
 
    buffer->refcount++;
 
    return buffer;
 
}
 
void release(RefCountedBuffer *buf)
 
{
 
    buf->refcount--;
 
    if (buf->refcount == 0)
 
        buf->free(buf);
 
}

Учтите, что это упрощенные версии тех функций, что вам могут действительно потребоваться. Наиболее распространенная проблема — они не являются потоко-безопасными. Операторы ++ и — обычно компилируются в последовательность команд загрузить, добавить (или вычесть), сохранить. Если два потока, например, одновременно аккумулируют, они оба должны сначала загрузить эти значения, прежде чем сохранить их, и одна аккумуляция будет потеряна. Вы можете обойти эту проблему, используя специальный ассемблер для ЦПУ или встроенные средства GCC для элементарных операций.

Определив эти функции, вы можете определить макросы SET и FREE следующим образом:

#define SET(var, val) do { 
 
RefCountedBuffer __tmp = retain(val); 
 
if (NULL != var) release(var) var = __tmp; } while(0)

Учтите, что вы удерживаете новое значение перед освобождением старого, что может привести к проблемам в случае, если новое и старое значения равны. Соответствующий макрос FREE() довольно прост:

#define FREE(var) do { release(var) var = NULL; } while(0)

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

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

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

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

У разработчиков сервера Dovecot IMAP есть превосходное решение. В дополнение к обычному контролю стека, они делают отдельный стек данных и аналог asprintf, которая использует его. Вызывающая функция сначала вызывает функцию, которая выделяет фрейм в новом стеке данных. Затем она вызывает аналог asprintf(), которая выделяет пространство в стеке результатов. Этот подход работает правильно, пока вызывающая функция достает верхний фрейм стека данных.

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

Вам ничего не мешает иметь несколько независимых областей памяти. Вы можете использовать mmap() на /dev/zero, чтобы выделить где-либо непрерывный BLOB памяти и использовать его по своему желанию. Одной из возможных идей может быть выделение стека данных, который работает на такой же скорости, как и контрольный стек. Используйте этот метод для всех массивов, которые вы будете по-другому выделять в стеке. В отличие от контрольного стека, это может расти вверх в памяти. Вы можете сделать его переместимым, постоянно адресуя его через глобальный указатель к началу. Например, с помощью подобного макроса:

#define NEW_ARRAY(type, name, elements) 
 
__attribute__((cleanup(popDataStack)))    
 
type *name = dataStackCalloc(sizeof(type), elements)

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

Как бы то ни было, вы до сих пор можете выйти за пределы массива. Вы не разрушите возвращаемый адрес, но можете переписать некоторые данные или могут возникнуть другие проблемы. Вы можете избежать выхода за пределы стека данных, вызвав функцию mprotect() на последней странице стека, чтобы удалить все права доступа. У большинства реализаций malloc() есть режим отладки, который вставляет этот вид защитной страницы после каждого выделения памяти. Вы можете сделать это, создав свой стек данных, в котором будут чередоваться доступные и недоступные страницы, с каждым выделенным массивом, так что он завершится недоступной страницей, но эта система достаточно ресурсоемка. Никакая нормальная операционная система не выделит реальную память для защитных страниц, но вы потеряете много пространства в промежутках между началами страниц и началами массивов и будете использовать большое количество адресного пространства.

Буферы и строки

Строки в C — вечная причина проблем. Когда C был создан, было две концепции о том, как лучше всего реализовывать строки — известные сейчас как строки C и строки Pascal, в соответствии с языками, которые сделали эти идеи популярными. Такие языки, как Lisp использовали третью реализацию: строки являлись связанным списком символов (Erlang до сих пор использует эту модель).

Строки в стиле Lisp имеют очевидный недостаток. Каждому символу требуется один байт для хранения символа и 4 или 8 байт для хранения адреса следующего, — до девяти байт уходит на хранение одного байта данных. Эта структура далека от идеальной, но делает разделение и конкатенацию строк очень простой.

Более совершенные модели представляют строки, как связанный список массивов символов, позволяя легко их объединять.

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

Большинство «классических» строковых функций практически невозможно использовать безопасно (по этой причине линковщик OpenBSD легко выдает предупреждение, когда вы используете одну из них). Каноническим примером «плохой» функции является strcat(), которая принимает два указателя на строки C. Функция проходит по первой строке, пока не найдет null; она записывает туда байты из второй строки пока не дойдет до null во второй строке. Вызывающая функция должна быть уверена, что существует достаточно места в первой строке, чтобы сохранить вторую.

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

В OpenBSD представлена strlcat, которая похожа на strncat, но возвращает сумму входных строк. Если результат работы функции больше третьего аргумента, имело место искажение. Эта функция находится в каждой ОС семейства BSD (включая Darwin/OS X), но её нет в glibc, так как, согласно главному разработчику glibc, является «бесполезным хламом BSD». К счастью, лицензия BSD позволяет вам копировать эту функцию из libc BSD в ваш код без каких-либо проблем.

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

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

int *a = alloca(sizeof(int) * n);
 
int a[n];

Разница в поведении, если существует недостаточно пространства для роста стека до n целых значений. Первая строка будет при этом NULL, но вы сможете проверить это, что упрощает отладку — достаточно просто обратиться к началу массива. Во второй строке, если не будет хватать размера стека, он будет указывать… куда-то. А куда именно, будет зависеть от реализации. Вот почему, если вы используете массивы C99 с переменной длиной, невозможно протестировать программу на переполнение стека. В большинстве случаев, это не является проблемой. Небольшие выделения памяти более безопасны для работы, но если взломщик сможет увеличить размер n, у вас может получиться массив, указывающий никуда.

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

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

Когда все идет не так

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

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

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

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

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

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

Отказ от привилегий

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

Веб-сервер нуждается в запуске в качестве root, так как ему нужно быть привязанным к 80 порту и иметь доступ к файлам в директории public_html каждого пользователя. Тем не менее, как только он привязан к 80 порту, ему не нужно более работать как root, и он может отказаться от прав root. Хотя ему по-прежнему нужен способ получать доступ к директории public_html каждого пользователя. Одно решение — заставить каждого пользователя сделать его файлы доступными для группы веб-сервера. Другим может быть выполнение fork() процесса-потомка для каждого пользователя, который работает в качестве этого пользователя и имеет доступ к файлам в директории пользователя.

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

Пользователь с правами root может легко избежать chroot, создав новое устройство для жесткого диска и смонтировав его внутрь chroot (или даже получая доступ к нему напрямую). Вот почему важно сбросить привилегии root сразу же после входа в chroot.

Ещё легче использовать chroot(), если он встроен в приложение. Команда chroot, которая запускает процесс в окружении chroot, также доступна, но этот подход имеет две проблемы. Первая — он вызывает chroot до запуска процесса, поэтому программа и все необходимые библиотеки должны быть внутри директории chroot. Вторая заключается в том, что он должен выполняться в качестве root, поэтому ему нужно что-то внутри chroot для возможности сброса привилегий. Обычным решением является поместить команду su внутрь chroot. Хотя, когда вы поместите так много кода внутрь chroot, он станет выглядеть, как и внешнее окружение.

Разгоняя ядро

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

Наиболее яркий пример был недавно обнаружен в большинстве фреймворков, перехватывающих системные вызовы. Все они работали примерно следующим образом:

1. Пользовательский процесс запрашивает системный вызов.

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

3. Ядро обрабатывает вызов.

К сожалению, этот подход имеет немного неожиданное поведение. Многие системные вызовы принимают указатели в качестве аргументов. Обычно ядро отображается в адресное пространство каждого процесса (но в режиме привилегий помечается только как для чтения), поэтому обработчик системного вызова в ядре может получить доступ к тому, на что указывают эти указатели дешево (без копирования). Даже если он не может получить к ним доступ напрямую на платформах, где ядро имеет полностью отдельное адресное пространство, обычно оно так же может получать доступ к адресному пространству процесса дешево. Если вы производите системный вызов с указателем, вот добавочный шаг 2a:

2a. Другой поток изменяет данные, на которые указывает аргумент-указатель.

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

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

Заключение

Писать безопасный код на C сложно, но это возможно. Безопасность таких систем, как OpenBSD доказывает, что это можно сделать. Язык не делает написание безопасного кода простым, но в некоторых случаях этот факт является полезным. Чтобы избежать проблем, программист должен основываться на хорошем коде, а не на возможностях языка.

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

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