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

Пространства имен USER обеспечивают функциональность наших любимых инструментов, таких как docker и podman. Мы писали о пространствах имен Linux еще в июне и объясняли их так:

Большинство пространств имен не вызывают споров, например пространство имен UTS, которое позволяет хост-системе скрывать свое имя хоста и время. Другие сложны, но просты — пространства имен NET и NS (mount), как известно, трудно уложить в голове. Наконец, есть это очень особенное, очень любопытное пространство имен USER. Пространство имен USER является особенным, поскольку оно позволяет обычно непривилегированному владельцу работать внутри него как «root». Это основа для того, чтобы такие инструменты, как Docker, не работали как настоящий root, и такие вещи, как контейнеры без рута.

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

Введите дисциплины очереди управления трафиком Linux

В 2019 году мы изучали использование дисциплины очередей (qdisc) управления трафиком Linux для планирования пакетов для одной из наших служб с помощью классовой стратегии qdisc Hierarchy Token Bucket (HTB). Linux Traffic Control — это настраиваемая пользователем система для планирования и фильтрации сетевых пакетов. Дисциплины очереди — это стратегии, в которых пакеты планируются. В частности, мы хотели фильтровать и планировать определенные пакеты от интерфейса, а другие отбрасывать в noqueue qdisc.

noqueue — это особый случай qdisc, так что пакеты должны отбрасываться при планировании. На практике это не так. Linux обрабатывает noqueue таким образом, что пакеты проходят и не отбрасываются (по большей части). В документации так же указано. В нем также говорится, что «невозможно назначить дисциплину очередей noqueue физическим устройствам или классам». Так что же происходит, когда мы назначаем noqueue классу?

Давайте напишем несколько команд оболочки, чтобы показать проблему в действии:

1. $ sudo -i
2. # dev=enp0s5
3. # tc qdisc replace dev $dev root handle 1: htb default 1
4. # tc class add dev $dev parent 1: classid 1:1 htb rate 10mbit
5. # tc qdisc add dev $dev parent 1:1 handle 10: noqueue

  1. Сначала нам нужно войти в систему как root, потому что это дает нам CAP_NET_ADMIN для настройки управления трафиком.
  2. Затем мы назначаем сетевой интерфейс переменной. Их можно найти с ip a. Виртуальные интерфейсы можно найти, вызвав ls /sys/devices/virtual/net. Они будут соответствовать выходным данным из ip a.
  3. Наш интерфейс в настоящее время назначен qdisc pfifo_fast, поэтому мы заменяем его классовым qdisc HTB и назначаем ему дескриптор 1:. Мы можем думать об этом как о корневом узле дерева. «По умолчанию 1» настраивает это таким образом, что неклассифицированный трафик будет направляться непосредственно через этот qdisc, который возвращается к очереди pfifo_fast. (подробнее об этом позже)
  4. Затем мы добавляем класс в наш корневой qdisc 1:назначьте его первому конечному узлу 1 корня 1: 1:1и задайте для него некоторые разумные настройки по умолчанию.
  5. Наконец, мы добавляем noqueue qdisc к нашему первому конечному узлу в иерархии: 1:1. Это фактически означает, что трафик, направляемый сюда, не будет поставлен в очередь.

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

BUG: kernel NULL pointer dereference, address: 0000000000000000
#PF: supervisor instruction fetch in kernel mode
...
Call Trace:
<TASK>
htb_enqueue+0x1c8/0x370
dev_qdisc_enqueue+0x15/0x90
__dev_queue_xmit+0x798/0xd00
...
</TASK>

Мы знаем, что пользователь root отвечает за настройку qdisc на интерфейсах, поэтому, если root может привести к сбою ядра, что с того? Мы просто не применяем noqueue qdisc к идентификатору класса HTB qdisc:

# dev=enp0s5
# tc qdisc replace dev $dev root handle 1: htb default 1
# tc class add dev $dev parent 1: classid 1:2 htb rate 10mbit // A
// B is missing, so anything not filtered into 1:2 will be pfifio_fast

Здесь мы использовали случай HTB по умолчанию, когда мы назначаем идентификатор класса 1: 2 как ограниченный по скорости (A) и неявно не устанавливаем qdisc для другого класса, такого как идентификатор 1: 1 (B). Пакеты, поставленные в очередь на (A), будут отфильтрованы в HTB_DIRECT, а пакеты, поставленные в очередь на (B), будут отфильтрованы в pfifo_fast.

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

Перенесемся в 2022 году: мы продвигаем ужесточение создания пространства имен USER. Мы расширили инфраструктуру Linux LSM с помощью нового хука LSM: userns_create, чтобы использовать eBPF LSM для нашей защиты, и поощряем других делать то же самое. Недавно, прочесывая наш бэклог заявок, мы переосмыслили эту ошибку. Мы спросили себя: «Можем ли мы использовать пространства имен USER, чтобы вызвать ошибку?» и короткий ответ да!

Демонстрация ошибки

Эксплойт может быть выполнен с любым классовым qdisc, который предполагает, что функция struct Qdisc.enqueue не равна NULL (подробнее об этом позже), но в этом случае мы демонстрируем только HTB.

$ unshare -rU –net
$ dev=lo
$ tc qdisc replace dev $dev root handle 1: htb default 1
$ tc class add dev $dev parent 1: classid 1:1 htb rate 10mbit
$ tc qdisc add dev $dev parent 1:1 handle 10: noqueue
$ ping -I $dev -w 1 -c 1 1.1.1.1

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

Почему это работает?

Чтобы немного лучше понять проблему, нам нужно оглянуться на исходную серию патчей, а особенно на этот коммит, который привел к ошибке. До этой серии достижение noqueue на интерфейсах зависело от хака, который устанавливал qdisc устройства в noqueue, если устройство имело tx_queue_len = 0. Коммит d66d6c3152e8 («net: sched: register noqueue qdisc») позволяет обойти это, быть добавлено с tc без необходимости обойти это ограничение.

Ядро проверяет, находимся ли мы в случае отсутствия очереди или нет, просто проверяя, имеет ли qdisc NULL функцию enqueue(). Вспомните, что на практике noqueue не обязательно отбрасывает пакеты? После этой проверки в случае сбоя следующая логика обрабатывает функциональность без очереди. Чтобы не пройти проверку, автору пришлось изменять переназначение с noop_enqueue() на NULL, сделав enqueue = NULL в инициализации, которая вызывается путь после register_qdisc() во время выполнения.

Вот где классные qdisc вступают в игру. Проверка функции постановки в очередь больше не равна NULL. В этом пути вызова теперь установлено значение HTB (в нашем примере), и, таким образом, разрешено ставить структуру skb в очередь путем вызова функции htb_enqueue(). Оказавшись там, HTB выполняет поиск для извлечения qdisc, назначенного конечному узлу, и в конечном итоге пытается поставить в очередь struct skb для выбранного qdisc, что в конечном итоге достигает этой функции:

включить/net/sch_generic.h

static inline int qdisc_enqueue(struct sk_buff *skb, struct Qdisc *sch,
				struct sk_buff **to_free)
{
	qdisc_calculate_pkt_len(skb, sch);
	return sch->enqueue(skb, sch, to_free); // sch->enqueue == NULL
}

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

Решения

У нас было несколько решений, от лучшего до худшего:

  1. Следуйте документации tc-noqueue и не позволяйте noqueue назначаться классовому qdisc.
  2. Вместо проверки на NULL проверьте структуру noqueue_qdisc_ops и сбросьте noqueue, чтобы вернуться к noop_enqueue.
  3. Для каждого классового qdisc проверьте наличие NULL и резервного варианта.

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

Причина, по которой мы не следовали этому подходу, заключается в том, что нам нужно сначала ответить на следующие вопросы:

Почему бы не разрешить noqueue для классовых qdisc?

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

Что изменится, если мы разрешим noqueue для qdisc?

На этот вопрос сложнее ответить, потому что нам нужно определить, каким должно быть это поведение. В настоящее время, когда noqueue применяется в качестве корневого qdisc для интерфейса, путь, по существу, позволяет обрабатывать пакеты. Заявка на откат для классов — это другое дело. У каждого из них могут быть свои собственные правила отката, и как мы узнаем, какой откат правильный? Иногда в HTB используется резервный вариант с HTB_DIRECT, иногда с pfifo_fast. А как насчет других классов? Возможно, вместо этого нам следует вернуться к поведению noqueue по умолчанию, как для корневых qdisc?

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

Выводы

Прежде всего, примените этот патч как можно скорее. И рассмотрите возможность усиления пространств имен USER в ваших системах, установив sysctl -w kernel.unprivileged_userns_clone=0который позволяет только root создавать пространства имен USER в ядрах Debian, sysctl -w user.max_user_namespaces=[number] для иерархии процессов или рассмотрите возможность переноса этих двух исправлений: security_create_user_ns() и реализация SELinux (теперь в Linux 6.1.x), позволяющая защитить ваши системы с помощью eBPF или SELinux. Если вы уверены, что не используете пространства имен USER, и в крайних случаях вы можете отключить эту функцию с помощью CONFIG_USERNS=n. Это всего лишь один из многих примеров, когда пространства имен используются для проведения атаки, и, несомненно, в будущем появятся новые с разной степенью серьезности.

Особая благодарность Игнату Корчагину и Якубу Ситницки за обзоры кода и помощь в демонстрации ошибки на практике.