В этом блоге объясняются технические подробности эксплойта, использующего функцию Linux eBPF для повышения локальных привилегий. Из-за характера ошибок, которые являются довольно тонким неправильным поведением критически важной для безопасности функции, называемой «верификатор», сначала необходимо дать некоторые пояснения относительно внутренней работы eBPF. Может быть полезно иметь исходный код ядра ( в частности, kernel / bpf / verifyier.c ) в качестве ссылки вместе с этими пояснениями. Эта ошибка была назначена CVE-2020-8835 и была исправлена
Как работает eBPF
eBPF
Начиная с версии 3.15, ядро Linux поддерживает общую функцию трассировки, называемую «расширенные фильтры пакетов Беркли», или сокращенно eBPF. Эта функция позволяет пользователям запускать программы eBPF, которые написаны в виде набора команд, подобного сборке, непосредственно в пространстве ядра и могут использоваться для отслеживания определенных функций ядра. Эта функция также может быть использована для фильтрации сетевых пакетов.
Все функции BPF доступны через системный вызов BPF, который поддерживает различные команды. Страница man для
В текущей реализации все команды bpf () требуют, чтобы вызывающая сторона имела возможность CAP_SYS_ADMIN.
Это неверно Начиная с Linux 4.4, любой пользователь может загружать программы eBPF и запускать их, подключая их к собственному сокету.
Программы eBPF
eBPF использует набор команд, который очень похож на (очень) ограниченное подмножество стандартной сборки x86. Существует 10 регистров (плюс один указатель стека), и мы можем выполнять с ними все основные операции копирования и арифметики, включая побитовые операции и сдвиги. Например:
BPF_MOV64_IMM(BPF_REG_3, 1)
устанавливает регистр 3 в значение 1 и
BPF_ALU64_REG(BPF_ARSH, BPF_REG_7, BPF_REG_5)
арифметически сдвигает регистр 7 вправо на содержимое регистра 5. Обратите внимание, что суффикс _REGобозначает регистр как второй операнд, тогда как _IMM-инструкции принимают непосредственное значение.
Есть также инструкции по переходу с ветвлением:
BPF_JMP_IMM(BPF_JNE, BPF_REG_3, 0, 3)
перепрыгивает через следующие три инструкции, если регистр 3 не равен 0.
Для каждой mov, aluи jmpинструкции, есть также соответствующая 32-битная версия , которая работает только на нижних 32 бит регистров (результаты нулевые расширены в случае необходимости).
Наконец, есть загрузка памяти и хранилища:
BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_9, 24)
загружает 64-битное значение из [reg9+24]в reg3, и
BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_6, 0)
сохраняет содержимое регистра 6 в [reg8+0].
Для выполнения программы eBPF ее сначала необходимо загрузить с помощью BPF_PROG_LOADкоманды. Это возвращает файловый дескриптор, соответствующий программе. Этот дескриптор файла может быть присоединен к сокету. Затем программа выполняется для каждого пакета, который проходит через этот сокет.
Карты eBPF
Для хранения данных или связи с программами пользовательского пространства (или друг с другом) программы eBPF могут использовать функцию, называемую картами. Все карты представляют собой сопоставления ключ-значение некоторого вида. Существуют разные типы карт, например, очереди и стеки, однако для этого эксплойта используется только один arraymap. Как видно из названия, этот тип карты представляет собой просто непрерывный массив записей. Это определяется тремя параметрами:
- key_sizeэто размер в байтах каждого индекса, используемого для доступа к элементу. Эксплойт всегда использует key_size=sizeof(int)=4.
- value_sizeразмер каждого элемента массива. Этот размер может быть произвольным в разумных пределах.
- max_entriesдлина массива. Мы всегда будем устанавливать value_sizeстолько, сколько нам нужно, и устанавливать max_entriesв 1.
Одним из преимуществ установки значения max_entries1 вместо наличия небольшого, value_sizeно большего количества записей является то, что мы можем затем получить указатель на одно значение внутри программы eBPF, так что arraymapв действительности это всего лишь один фрагмент памяти. Это будет очень удобно позже.
Подобно программам, карты создаются BPF_MAP_CREATEкомандой bpfsyscall и идентифицируются дескриптором файла.
Эксплойт будет использовать три карты:
- inmapэто небольшая карта, содержащая все параметры, которые необходимо использовать для эксплойта (например, смещение за пределами считывания для выполнения). Обратите внимание, что хотя будет несколько параметров, все они
будут храниться в одной записи массива большего размера.
- outmapэто очень маленькая карта, содержащая любые выходные данные программы эксплойта (для операций чтения вне пределов (OOB) - значение чтения).
- explmapэто карта большего размера, которая будет использоваться для самой эксплойты.
JIT-компилятор
По соображениям производительности все программы eBPF JIT компилируются в машинный код при загрузке (кроме случаев, когда CONFIG_BPF_JIT_ALWAYS_ONэто отключено).
Процесс компиляции JIT довольно прост, поскольку очень легко найти инструкции x86, соответствующие большинству инструкций eBPF. Скомпилированная программа работает в пространстве ядра без дополнительной песочницы.
контрольник
Очевидно, что выполнение произвольных JIT-скомпилированных инструкций eBPF будет тривиально разрешать произвольный доступ к памяти, поскольку инструкции загрузки и сохранения преобразуются в косвенные movs. Поэтому ядро запускает верификатор для каждой программы, чтобы убедиться, что доступ к памяти OOB не может быть выполнен, а также что утечка указателей ядра невозможна. Верификатор обеспечивает примерно следующее (некоторые из них применяются только к программам, загруженным непривилегированными процессами):
- Арифметика или сравнение указателей не могут быть выполнены, за исключением сложения или вычитания указателя и скалярного значения ( скалярное значение - это любое значение, которое не является производным от значения указателя).
- Невозможно выполнить арифметику указателей, которая выходит за границы известных безопасных областей памяти (т. Е. Карт).
- Значения указателя не могут быть возвращены с карт, а также не могут быть сохранены на картах, где они будут читаемы из пространства пользователя.
- Ни одна инструкция сама по себе не достижима, что означает, что программа не может содержать никаких циклов.
Для этого верификатор должен отслеживать - для каждой программной инструкции - какие регистры содержат указатели, а какие - скалярные значения. Кроме того, верификатор должен выполнить вычисления диапазона, чтобы указатели никогда не могли покинуть свои соответствующие области памяти. Он также должен выполнять отслеживание диапазона для скалярных значений, потому что без знания нижних и верхних границ было бы невозможно определить, приведет ли добавление регистра, содержащего скалярное значение к регистру, содержащему указатель, к указателю вне границ.
Для отслеживания диапазона возможных значений каждого регистра верификатор отслеживает три отдельные границы:
1- umin и umaxотслеживайте минимальное и максимальное значение, которое может содержать регистр, когда интерпретируется как целое число без знака.
2- smin и smaxотслеживать минимальное и максимальное значение, которое может содержать регистр, когда интерпретируется как целое число со знаком.
3- var_off содержит информацию об определенных битах, которые, как известно, равны 0 или 1. Типом var_offявляется структура, известная как tnum, которая является сокращением от «отслеживаемого числа» или «числа с тремя состояниями». А tnumимеет два поля. Одно поле named valueимеет все установленные биты, которые, как известно, равны 1 в рассматриваемом регистре. В другом поле named maskвсе биты установлены там, где соответствующий бит в регистре неизвестен. Например, если valueбинарный010и maskявляется двоичным 100, тогда регистр может содержать двоичный 010или двоичный код 110.
Чтобы понять, почему 1 и 2 необходимы, рассмотрим регистр, границы со знаком которого равны -1 и 0. Интерпретируемые как целое число без знака, эти значения могут находиться в диапазоне от 0 до 2ˆ64-1, который имеет то же представление, что и знак -1. С другой стороны, диапазон без знака от 2 63 до 2 63 включает в себя как самые маленькие, так и самые большие возможные значения со знаком.
Все эти границы регулярно используются для обновления друг друга. Например, если umaxниже 2 63, то sminустанавливается в 0 (если раньше оно было отрицательным), так как все такие числа положительны. Аналогично, если var_offуказывает, что все, кроме последних трех битов, равны 0, тогда umaxможно безопасно установить значение 7.
Верификатор проверяет каждый возможный путь выполнения, то есть в каждой ветви оба результата проверяются отдельно. Для двух ветвей условного перехода можно узнать некоторую дополнительную информацию. Например, рассмотрим:
BPF_JMP_IMM(BPF_JGE, BPF_REG_5, 8, 3)
который принимает ветвь, если регистр 5 больше или равен 8 в сравнении без знака. При анализе ложной ветви верификатор может установить umaxрегистр от 5 до 7, поскольку любое более высокое значение без знака привело бы к переходу на другую ветвь.
АЛУ Санитария
В ответ на большое количество уязвимостей в системе безопасности, которые были вызваны ошибками в верификаторе, была введена функция «ALU sanitation». Идея состоит в том, чтобы дополнить проверки статического диапазона верификатора проверками во время выполнения фактических значений, обрабатываемых программой. Напомним, что единственные допустимые вычисления с указателями - сложение или вычитание скаляра. Для каждой арифметической операции, которая включает указатель и скалярный регистр (в отличие от непосредственного), alu_limitопределяется как максимальное абсолютное значение, которое можно безопасно добавлять к указателю или вычитать из него без превышения допустимого диапазона.
Арифметика указателей, где знак скалярного операнда неизвестен, не допускается. В остальной части этого подраздела предположим, что каждый скаляр рассматривается как положительный; отрицательный случай аналогичен.
Перед каждой арифметической инструкцией, имеющей alu_limit, добавляется следующая последовательность инструкций. Обратите внимание, что off_regэто регистр, содержащий скалярное значение, и BPF_REG_AXявляется вспомогательным регистром.
BPF_MOV32_IMM(BPF_REG_AX, aux->alu_limit - 1) BPF_ALU64_REG(BPF_SUB, BPF_REG_AX, off_reg) BPF_ALU64_REG(BPF_OR, BPF_REG_AX, off_reg) BPF_ALU64_IMM(BPF_NEG, BPF_REG_AX, 0) BPF_ALU64_IMM(BPF_ARSH, BPF_REG_AX, 63) BPF_ALU64_REG(BPF_AND, off_reg, BPF_REG_AX)
Если скаляр превысит alu_limit, то первое вычитание будет отрицательным, так что будет установлен самый левый бит BPF_REG_AX. Точно так же, если скаляр, который должен быть положительным, фактически отрицателен, BPF_ORинструкция установит самый левый бит BPF_REG_AX. Отрицание, сопровождаемое арифметическим сдвигом, будет затем заполняться BPF_REG_AXвсеми нулями, так что оно BPF_ANDбудет приводить off_regк нулю, заменяя нарушающий скаляр. С другой стороны, если скаляр попадает в соответствующий диапазон 0 <= off_reg <= alu_limit, арифметическое смещение заполнится BPF_REG_AXвсеми 1, так что BPF_ANDрегистр скаляра останется неизменным.
Чтобы убедиться, что установка в регистр 0 сама по себе не вводит новое условие выхода за пределы, верификатор будет отслеживать дополнительную «умозрительную» ветвь, где операнд обрабатывается как 0.
Использование верификатора
Отслеживание дальности действия
Как уже упоминалось, условия перехода также существуют в 32-битном варианте. Однако, поскольку все границы отслеживаются исключительно для полных 64-битных регистров, не существует простого способа использовать 32-битные ветви для обновления границ регистров, как это можно сделать для 64-битных ветвей, как описано выше. Поскольку это приводило к неправильно отклоненным программам, для каждого 32-разрядного перехода переходов добавлялся дополнительный вызов функции в попытке максимизировать информацию о границах, которую можно получить. Идея состоит в том, чтобы использовать uminи umaxсузить последние 32 бита var_off. Код выглядит следующим образом kernel/bpf/verifier.c:
Чтобы понять этот код, мы должны сначала объяснить назначение нескольких функций, используемых здесь.
- tnum_rangeэто функция, которая генерирует tnumсоответствующие возможные значения в данном диапазоне целых чисел без знака.
- tnum_castсоздает новый tnumна основе самых младших битов из существующих tnum. Здесь он используется для возврата младших 32 битов reg->var_off.
- tnum_lshift/ tnum_rshiftвыполнять смены на tnumс. Здесь они используются вместе, чтобы очистить младшие 32 бита a tnum.
- tnum_intersectпринимает два tnumаргумента, относящихся к одному значению, и возвращает единственный, tnumкоторый синтезирует все знания, переданные аргументами.
- tnum_orвозвращаетtnum указывает на наше знание побитового ИЛИ двух операндов, где два аргумента указывают на наше знание двух операндов.
Теперь рассмотрит регистр с umin_value = 1и umax_value = 2ˆ32+1, а в var_offпротивном случае неограниченный. Тогда reg->umin_value & maskи reg->umax_value & maskоба будут tnum_rangeравны 1. Таким образом, результирующий будет указывать, что самый младший бит известен как 1, а все остальные биты известны как 0. tnum_intersectСохранит эту информацию и пометит все младшие 32 бита как известные в своих выходных данных. Наконец, tnum_orбудет вновь введена неопределенность в старшие 32 бита, но поскольку hi32указывает, что младшие 32 бита являются известными нулями, младшие 32 бита из этого значения tnum_intersectбудут сохранены. Это означает, что после завершения этой функции нижняя половина var_offбудет помечена как известный двоичный файл 00...01.
Однако этот вывод не обоснован. Просто потому , что uminи umaxкак конец в двоичной системе 00...01не означает , что каждое значение между также делает. Например, истинное значение регистра могло бы легко быть 2. Как мы увидим, это первоначальное неверное предположение, сделанное верификатором, может быть использовано для того, чтобы нарушить дальнейшие предположения.
Вызывая ошибку
Чтобы фактически вызвать описанную ошибку, нам нужен регистр, который удовлетворяет следующим условиям:
- Во время выполнения фактическое значение в регистре равно 2.
- Для регистра umin_valустановлено значение 1 и umax_valзначение 2ˆ32 + 1.
- Выполнен условный переход с 32-битным сравнением в этом регистре.
Мы не можем напрямую загрузить значение 2 в регистр с помощью a mov, так как тогда верификатор узнает об этом umin_val=umax_val=2. Тем не менее, есть простой обходной путь. Если мы загружаем регистр из карты (мы будем использовать нашу входную карту inmap), то верификатор не будет иметь информации о его значении, поскольку значения карты могут быть изменены во время выполнения.
Чтобы установить umin_valи umax_valмы можем использовать логику перехода ветви верификатора:
BPF_JMP_IMM(BPF_JGE, BPF_REG_2, 1, 1)
BPF_RAW_INSN(BPF_JMP | BPF_EXIT, 0, 0, 0, 0)
Условный переход приведет к двум ветвям. В той, где берется ветвь, верификатор знает это BPF_REG_2 >= 1, в то время как другая ветвь заканчивается инструкцией выхода и отбрасывается. Таким образом, для всех дальнейших инструкций umin_valрегистра 2 будет 1.
Аналогично, другой условный переход может быть использован для установки umax_valна 2ˆ32+1. Однако здесь нам нужно сравнить с регистром, потому что поддерживаются только 32-битные непосредственные значения. После этого мы umin_valи umax_valу становили как хотели.
Теперь любой условный 32-битный переход может быть использован для запуска ошибки:
BPF_JMP32_IMM(BPF_JNE, BPF_REG_2, 5, 1),
BPF_RAW_INSN(BPF_JMP | BPF_EXIT, 0, 0, 0, 0),
Теперь верификатор считает, что последние 32 бита регистра 2 являются двоичными, 00...01хотя в действительности они являются двоичными 00...10. После двух дополнительных инструкций:
BPF_ALU64_IMM(BPF_AND, BPF_REG_2, 2),
BPF_ALU64_IMM(BPF_RSH, BPF_REG_2, 1),
Теперь верификатор предполагает, что регистр 2 должен быть 0, потому что ANDинструкция обязательно должна приводить к 0, если второй-последний бит регистра 2 был 0, но на самом деле это так (2&2)>>1 = 1. Это очень полезный примитив, поскольку теперь мы можем умножить регистр 2 на любое число, чтобы создать произвольное значение, которое верификатор считает равным 0.
В обход АЛУ Санитария
Хотя перепроверка всей арифметики с указателями во время выполнения кажется хорошей идеей, реализация серьезно отсутствует. Основная проблема, как представляется, заключается в том, что значения на самом деле не ограничены диапазоном, в котором статическая часть верификатора предполагает их нахождение, а скорее более широким пределом, который в данный момент кажется безопасным , но не обязательно так в отношении в будущем. Чтобы понять это различие, рассмотрим следующий пример:
- Регистр 1 содержит указатель p на начало 4000 байтов безопасной памяти. Предположим, что тип *pимеет размер всего один байт. Это означает, что добавление 3999 к pсовершенно нормально, но добавление 4000 оставило бы границы.
- Регистр 2 содержит скаляр a, который статический верификатор считает равным 1000.
- Теперь добавьте регистр 2 в регистр 1. Статический верификатор не имеет проблем с этим, так как p+1000все еще находится в пределах области памяти. Поскольку это арифметика с указателем, дезинфицирующее средство ALU установит значение alu_limit. Однако, несмотря на то, что верификатор считает, что aон не больше 1000, ограничение не установлено равным 1000, а скорее 3999, поскольку это самое большое значение, которое все еще считается безопасным.
- Добавить регистр 2, чтобы зарегистрировать 1 снова. На этот раз статическая часть верификатора считает, что pуказывает на позицию 1000 в блоке памяти 4000 байт. alu_limitТеперь будет установлен в 2999, потому что это самое большое юридическое значение , что еще можно добавить.
Теперь рассмотрим, что произойдет, если мы использовали ошибку в статическом анализе, чтобы обмануть верификатора, заставив его поверить в то a = 1000, что в действительности мы установили a = 2500. Значение будет по-прежнему проходить обе проверки во время выполнения без принудительной установки на 0, поскольку 2500 меньше, чем 3999 и 2999. Однако в общей сложности мы добавили 5000 к указателю p, что привело к адресу, выходящему за пределы безопасного диапазона памяти. Хотя каждый из alu_limitних достаточно низок в отдельности, в сочетании они недостаточны. Второе вычисление пределов только улавливает ошибки, которые статический анализ допускает во втором добавлении, предполагая, что оно было полностью правильным относительно первого.
Достижение OOB Читать и писать
Теперь для получения доступа к памяти OOB достаточно просто объединить части. Сначала мы используем JMP32ошибку, чтобы установить регистр в 1, тогда как верификатор считает, что он должен быть 0. Предположим, у нас есть указатель pна карту размером 8000 байт (эксплойт использует немного другие значения). В целях эксплойта наиболее удобно получить OOB-доступ до начала карты, а не после ее окончания, поэтому первым шагом является добавление большого значения, например 6000, для pтого, чтобы мы могли «отразить» обход санитарного контроля ALU. описано выше.
Затем мы производим регистр, который имеет фактическое значение 6000, в то время как верификатор считает, что он содержит 0. Мы делаем это путем умножения нашего исходного «поддельного» регистра на 6000. Теперь мы можем вычесть это из p. Это работает, потому что alu_limitбудет установлено 6000, наибольшая юридическая сумма, которую мы можем вычесть. Тем не менее, статическая часть верификатора все еще будет предполагать, что она pуказывает на позицию 6000 на нашей карте. Таким образом, теперь мы можем вычесть любое значение до 6000 p, в то время как pв действительности указывает на начало карты.
Теперь у нас есть указатель до 6000 байтов до начала содержимого карты. Поскольку верификатор по-прежнему полагает, что этот указатель находится внутри границ карты, он теперь позволяет нам читать и писать по этому адресу. Для удобства мы можем даже сделать операцию и смещение (а для записи - записанное значение) зависимыми от параметров в нашей входной карте inmap. Для чтения OOB значение read записывается в outmap. Поскольку карты могут быть прочитаны и изменены из пользовательского пространства через bpfсистемный вызов, теперь мы можем загрузить эту программу только один раз, а затем повторно запустить ее с различными параметрами для произвольного гаджета чтения / записи OOB.
Наращивание привилегий
Утечка KASLR
Для нас удобно, чтобы содержимое карты не сохранялось в куче кучи, а помещалось в конец большей структуры, называемой struct bpf_map(определяется в include/linux/bpf.h). Эта структура содержит несколько полезных членов:
Особенно полезным является указатель, opsкоторый указывает на таблицу функций, в зависимости от типа карты. В этом случае, поскольку карта является arraymap, она указывает на array_map_ops. Поскольку это постоянная структура в фиксированном месте rodata(это даже экспортированный символ), чтение этого может использоваться для прямого обхода KASLR.
Произвольное чтение
Также в структуре полезна структура указателя btf *btf, которая указывает на дополнительную структуру, содержащую отладочную информацию. Этот указатель обычно не используется (и имеет значение NULL), что делает его хорошей целью перезаписи, не путая вещи слишком много.
Оказывается, к этому указателю можно легко получить доступ через bpf_map_get_info_by_fd-function, который, в свою очередь, вызывается BPF_OBJ_GET_INFO_BY_FDкомандой bpfсистемного вызова, если предоставляется файловый дескриптор карты. Эта функция эффективно выполняет следующее (полная функция находится в kernel/bpf/syscall.c):
Это означает, что если мы используем примитив OOB-write для set map->btfto someaddr - offsetof(struct btf, id), то BPF_OBJ_GET_INFO_BY_FDвозвращается *someaddrв info.btf_id. Поскольку поле id struct btf- это u32, этот примитив можно использовать для чтения 4 байтов за раз с произвольного адреса.
Нахождение структур процесса
Чтобы найти credи filesструктуры процесса, мы сначала ищем init_pid_nsпространство имен процесса по умолчанию (если он выполняется в контейнере, нам сначала нужно найти соответствующее пространство имен). Это может быть не самый быстрый способ, но это работает.
Есть два способа найти init_pid_ns:
- Если мы знаем смещение между array_map_opsи init_pid_ns, мы можем просто добавить его к адресу array_map_ops, который мы уже знаем. Это смещение не зависит от KASLR, но оно не стабильно при обновлении ядра.
- Вместо этого мы можем найти init_pid_nsв ksymtabи kstrtabсегментах. Для этого мы сначала находим начало с kstrtabпомощью итеративного поиска, начинающегося с array_map_ops. Затем мы находим строку с нулевым символом в конце «init_pid_ns» kstrtab, снова с помощью простого итеративного поиска. Последняя итерация ksymtabзавершает поиск записи символа, которая ссылается на kstrtabзапись, а также содержит относительный адрес init_pid_ns.
Как только init_pid_nsоно найдено, его основополагающее дерево можно повторить, чтобы найти указатель на taskструктуру, соответствующую pid процесса эксплойта. Фактически, это именно тот механизм, который само ядро использует для поиска taskпо pid.
Используя структуру tasks, credможно найти как структуру, содержащую права пользователя, так и массив дескрипторов открытых файлов. Последний может быть проиндексирован дескриптором загруженного файла explmapдля получения соответствующего файла структуры карты. В свою очередь, private_dataэтой структуры указывает на структуру bpf_map. Это означает, что теперь мы также знаем адрес содержимого explmap.
Произвольная запись
Обратите внимание, что при перезаписи opsс использованием OOB-записи мы можем управлять RIP для любой функции, на которую у нас есть указатель. Тем не менее, первый аргумент в RDI всегда будет установлен на bpf_map-struct, что исключает множество существующих функций. Таким образом, кажется естественным перезаписать определенные элементы array_map_opsв различных операциях карты, которые могут по крайней мере правильно обрабатывать первый аргумент.
Для этого мы сначала загружаем полную копию array_map_opsтаблицы в данные explmapиспользования произвольного примитива чтения, а также bpfсистемного вызова с BPF_MAP_UPDATE_ELEM. Единственная модификация этой копии - то, что map_push_elemэлемент, обычно не используемый в arraymaps, перезаписывается map_get_next_keyоперацией.
arraymapРеализация map_get_next_keyвыглядит следующим образом (с kernel/bpf/arraymap.c):
Если мы контролируем next_keyи key, то *next = index + 1;можно использовать как произвольный примитив записи при условии, что index < array->map.max_entries. Если этот параметр map->max_entriesможет быть установлен 0xffffffff, эта проверка будет всегда проходить (за исключением того index=0xffffffff, что мы и будем использовать, но это нормально, так как *nextвсе еще установлено 0=index+1).
Поскольку мы уже получили указатель на данные explmap, мы можем теперь перезаписать, explmap->array_map_opsчтобы указать на нашу измененную таблицу операций.
Обратите внимание, что подпись map_push_elem, которую мы перезаписали map_get_next_key, является:
Однако map_push_elemвызывается BPF_MAP_UPDATE_ELEMкомандой, только если карта имеет тип BPF_MAP_TYPE_STACKили BPF_MAP_TYPE_QUEUE. Но если это так, мы можем напрямую контролировать flagsаргумент, который будет интерпретироваться как next_key, а также valueаргумент.
Чтобы запустить произвольный гаджет записи, мы должны выполнить следующие записи OOB в последовательности:
- Установите opsнаш поддельный vtable внутри explmapбуфера.
- Установите explmap->spin_lock_off0, чтобы пройти некоторые дополнительные проверки.
- Установите, explmap->max_entriesчтобы 0xffffffffпройти проверку array_map_get_next_key.
- Установите, explmap->map_typeчтобы BPF_MAP_TYPE_STACKиметь возможность достичь map_push_elem.
Произвольная 32-битная запись теперь может быть запущена путем передачи соответствующих аргументов BPF_MAP_UPDATE_ELEM. После того, как все записи выполнены, поля должны быть сброшены до их первоначальных значений, чтобы предотвратить сбои при очистке.
Получение привилегий root
Получение root теперь тривиально: нам просто нужно установить uid значение 0 в credструктуре процесса. Теперь мы можем выполнить произвольные привилегированные команды или перейти в интерактивную оболочку.
Источник:
Ссылка скрыта от гостей
. Вот короткая видео демонстрация эксплойта в действии: Как работает eBPF
eBPF
Начиная с версии 3.15, ядро Linux поддерживает общую функцию трассировки, называемую «расширенные фильтры пакетов Беркли», или сокращенно eBPF. Эта функция позволяет пользователям запускать программы eBPF, которые написаны в виде набора команд, подобного сборке, непосредственно в пространстве ядра и могут использоваться для отслеживания определенных функций ядра. Эта функция также может быть использована для фильтрации сетевых пакетов.
Все функции BPF доступны через системный вызов BPF, который поддерживает различные команды. Страница man для
Ссылка скрыта от гостей
гласит:В текущей реализации все команды bpf () требуют, чтобы вызывающая сторона имела возможность CAP_SYS_ADMIN.
Это неверно Начиная с Linux 4.4, любой пользователь может загружать программы eBPF и запускать их, подключая их к собственному сокету.
Программы eBPF
eBPF использует набор команд, который очень похож на (очень) ограниченное подмножество стандартной сборки x86. Существует 10 регистров (плюс один указатель стека), и мы можем выполнять с ними все основные операции копирования и арифметики, включая побитовые операции и сдвиги. Например:
BPF_MOV64_IMM(BPF_REG_3, 1)
устанавливает регистр 3 в значение 1 и
BPF_ALU64_REG(BPF_ARSH, BPF_REG_7, BPF_REG_5)
арифметически сдвигает регистр 7 вправо на содержимое регистра 5. Обратите внимание, что суффикс _REGобозначает регистр как второй операнд, тогда как _IMM-инструкции принимают непосредственное значение.
Есть также инструкции по переходу с ветвлением:
BPF_JMP_IMM(BPF_JNE, BPF_REG_3, 0, 3)
перепрыгивает через следующие три инструкции, если регистр 3 не равен 0.
Для каждой mov, aluи jmpинструкции, есть также соответствующая 32-битная версия , которая работает только на нижних 32 бит регистров (результаты нулевые расширены в случае необходимости).
Наконец, есть загрузка памяти и хранилища:
BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_9, 24)
загружает 64-битное значение из [reg9+24]в reg3, и
BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_6, 0)
сохраняет содержимое регистра 6 в [reg8+0].
Для выполнения программы eBPF ее сначала необходимо загрузить с помощью BPF_PROG_LOADкоманды. Это возвращает файловый дескриптор, соответствующий программе. Этот дескриптор файла может быть присоединен к сокету. Затем программа выполняется для каждого пакета, который проходит через этот сокет.
Карты eBPF
Для хранения данных или связи с программами пользовательского пространства (или друг с другом) программы eBPF могут использовать функцию, называемую картами. Все карты представляют собой сопоставления ключ-значение некоторого вида. Существуют разные типы карт, например, очереди и стеки, однако для этого эксплойта используется только один arraymap. Как видно из названия, этот тип карты представляет собой просто непрерывный массив записей. Это определяется тремя параметрами:
- key_sizeэто размер в байтах каждого индекса, используемого для доступа к элементу. Эксплойт всегда использует key_size=sizeof(int)=4.
- value_sizeразмер каждого элемента массива. Этот размер может быть произвольным в разумных пределах.
- max_entriesдлина массива. Мы всегда будем устанавливать value_sizeстолько, сколько нам нужно, и устанавливать max_entriesв 1.
Одним из преимуществ установки значения max_entries1 вместо наличия небольшого, value_sizeно большего количества записей является то, что мы можем затем получить указатель на одно значение внутри программы eBPF, так что arraymapв действительности это всего лишь один фрагмент памяти. Это будет очень удобно позже.
Подобно программам, карты создаются BPF_MAP_CREATEкомандой bpfsyscall и идентифицируются дескриптором файла.
Эксплойт будет использовать три карты:
- inmapэто небольшая карта, содержащая все параметры, которые необходимо использовать для эксплойта (например, смещение за пределами считывания для выполнения). Обратите внимание, что хотя будет несколько параметров, все они
будут храниться в одной записи массива большего размера.
- outmapэто очень маленькая карта, содержащая любые выходные данные программы эксплойта (для операций чтения вне пределов (OOB) - значение чтения).
- explmapэто карта большего размера, которая будет использоваться для самой эксплойты.
JIT-компилятор
По соображениям производительности все программы eBPF JIT компилируются в машинный код при загрузке (кроме случаев, когда CONFIG_BPF_JIT_ALWAYS_ONэто отключено).
Процесс компиляции JIT довольно прост, поскольку очень легко найти инструкции x86, соответствующие большинству инструкций eBPF. Скомпилированная программа работает в пространстве ядра без дополнительной песочницы.
контрольник
Очевидно, что выполнение произвольных JIT-скомпилированных инструкций eBPF будет тривиально разрешать произвольный доступ к памяти, поскольку инструкции загрузки и сохранения преобразуются в косвенные movs. Поэтому ядро запускает верификатор для каждой программы, чтобы убедиться, что доступ к памяти OOB не может быть выполнен, а также что утечка указателей ядра невозможна. Верификатор обеспечивает примерно следующее (некоторые из них применяются только к программам, загруженным непривилегированными процессами):
- Арифметика или сравнение указателей не могут быть выполнены, за исключением сложения или вычитания указателя и скалярного значения ( скалярное значение - это любое значение, которое не является производным от значения указателя).
- Невозможно выполнить арифметику указателей, которая выходит за границы известных безопасных областей памяти (т. Е. Карт).
- Значения указателя не могут быть возвращены с карт, а также не могут быть сохранены на картах, где они будут читаемы из пространства пользователя.
- Ни одна инструкция сама по себе не достижима, что означает, что программа не может содержать никаких циклов.
Для этого верификатор должен отслеживать - для каждой программной инструкции - какие регистры содержат указатели, а какие - скалярные значения. Кроме того, верификатор должен выполнить вычисления диапазона, чтобы указатели никогда не могли покинуть свои соответствующие области памяти. Он также должен выполнять отслеживание диапазона для скалярных значений, потому что без знания нижних и верхних границ было бы невозможно определить, приведет ли добавление регистра, содержащего скалярное значение к регистру, содержащему указатель, к указателю вне границ.
Для отслеживания диапазона возможных значений каждого регистра верификатор отслеживает три отдельные границы:
1- umin и umaxотслеживайте минимальное и максимальное значение, которое может содержать регистр, когда интерпретируется как целое число без знака.
2- smin и smaxотслеживать минимальное и максимальное значение, которое может содержать регистр, когда интерпретируется как целое число со знаком.
3- var_off содержит информацию об определенных битах, которые, как известно, равны 0 или 1. Типом var_offявляется структура, известная как tnum, которая является сокращением от «отслеживаемого числа» или «числа с тремя состояниями». А tnumимеет два поля. Одно поле named valueимеет все установленные биты, которые, как известно, равны 1 в рассматриваемом регистре. В другом поле named maskвсе биты установлены там, где соответствующий бит в регистре неизвестен. Например, если valueбинарный010и maskявляется двоичным 100, тогда регистр может содержать двоичный 010или двоичный код 110.
Чтобы понять, почему 1 и 2 необходимы, рассмотрим регистр, границы со знаком которого равны -1 и 0. Интерпретируемые как целое число без знака, эти значения могут находиться в диапазоне от 0 до 2ˆ64-1, который имеет то же представление, что и знак -1. С другой стороны, диапазон без знака от 2 63 до 2 63 включает в себя как самые маленькие, так и самые большие возможные значения со знаком.
Все эти границы регулярно используются для обновления друг друга. Например, если umaxниже 2 63, то sminустанавливается в 0 (если раньше оно было отрицательным), так как все такие числа положительны. Аналогично, если var_offуказывает, что все, кроме последних трех битов, равны 0, тогда umaxможно безопасно установить значение 7.
Верификатор проверяет каждый возможный путь выполнения, то есть в каждой ветви оба результата проверяются отдельно. Для двух ветвей условного перехода можно узнать некоторую дополнительную информацию. Например, рассмотрим:
BPF_JMP_IMM(BPF_JGE, BPF_REG_5, 8, 3)
который принимает ветвь, если регистр 5 больше или равен 8 в сравнении без знака. При анализе ложной ветви верификатор может установить umaxрегистр от 5 до 7, поскольку любое более высокое значение без знака привело бы к переходу на другую ветвь.
АЛУ Санитария
В ответ на большое количество уязвимостей в системе безопасности, которые были вызваны ошибками в верификаторе, была введена функция «ALU sanitation». Идея состоит в том, чтобы дополнить проверки статического диапазона верификатора проверками во время выполнения фактических значений, обрабатываемых программой. Напомним, что единственные допустимые вычисления с указателями - сложение или вычитание скаляра. Для каждой арифметической операции, которая включает указатель и скалярный регистр (в отличие от непосредственного), alu_limitопределяется как максимальное абсолютное значение, которое можно безопасно добавлять к указателю или вычитать из него без превышения допустимого диапазона.
Арифметика указателей, где знак скалярного операнда неизвестен, не допускается. В остальной части этого подраздела предположим, что каждый скаляр рассматривается как положительный; отрицательный случай аналогичен.
Перед каждой арифметической инструкцией, имеющей alu_limit, добавляется следующая последовательность инструкций. Обратите внимание, что off_regэто регистр, содержащий скалярное значение, и BPF_REG_AXявляется вспомогательным регистром.
BPF_MOV32_IMM(BPF_REG_AX, aux->alu_limit - 1) BPF_ALU64_REG(BPF_SUB, BPF_REG_AX, off_reg) BPF_ALU64_REG(BPF_OR, BPF_REG_AX, off_reg) BPF_ALU64_IMM(BPF_NEG, BPF_REG_AX, 0) BPF_ALU64_IMM(BPF_ARSH, BPF_REG_AX, 63) BPF_ALU64_REG(BPF_AND, off_reg, BPF_REG_AX)
Если скаляр превысит alu_limit, то первое вычитание будет отрицательным, так что будет установлен самый левый бит BPF_REG_AX. Точно так же, если скаляр, который должен быть положительным, фактически отрицателен, BPF_ORинструкция установит самый левый бит BPF_REG_AX. Отрицание, сопровождаемое арифметическим сдвигом, будет затем заполняться BPF_REG_AXвсеми нулями, так что оно BPF_ANDбудет приводить off_regк нулю, заменяя нарушающий скаляр. С другой стороны, если скаляр попадает в соответствующий диапазон 0 <= off_reg <= alu_limit, арифметическое смещение заполнится BPF_REG_AXвсеми 1, так что BPF_ANDрегистр скаляра останется неизменным.
Чтобы убедиться, что установка в регистр 0 сама по себе не вводит новое условие выхода за пределы, верификатор будет отслеживать дополнительную «умозрительную» ветвь, где операнд обрабатывается как 0.
Использование верификатора
Отслеживание дальности действия
Как уже упоминалось, условия перехода также существуют в 32-битном варианте. Однако, поскольку все границы отслеживаются исключительно для полных 64-битных регистров, не существует простого способа использовать 32-битные ветви для обновления границ регистров, как это можно сделать для 64-битных ветвей, как описано выше. Поскольку это приводило к неправильно отклоненным программам, для каждого 32-разрядного перехода переходов добавлялся дополнительный вызов функции в попытке максимизировать информацию о границах, которую можно получить. Идея состоит в том, чтобы использовать uminи umaxсузить последние 32 бита var_off. Код выглядит следующим образом kernel/bpf/verifier.c:
Чтобы понять этот код, мы должны сначала объяснить назначение нескольких функций, используемых здесь.
- tnum_rangeэто функция, которая генерирует tnumсоответствующие возможные значения в данном диапазоне целых чисел без знака.
- tnum_castсоздает новый tnumна основе самых младших битов из существующих tnum. Здесь он используется для возврата младших 32 битов reg->var_off.
- tnum_lshift/ tnum_rshiftвыполнять смены на tnumс. Здесь они используются вместе, чтобы очистить младшие 32 бита a tnum.
- tnum_intersectпринимает два tnumаргумента, относящихся к одному значению, и возвращает единственный, tnumкоторый синтезирует все знания, переданные аргументами.
- tnum_orвозвращаетtnum указывает на наше знание побитового ИЛИ двух операндов, где два аргумента указывают на наше знание двух операндов.
Теперь рассмотрит регистр с umin_value = 1и umax_value = 2ˆ32+1, а в var_offпротивном случае неограниченный. Тогда reg->umin_value & maskи reg->umax_value & maskоба будут tnum_rangeравны 1. Таким образом, результирующий будет указывать, что самый младший бит известен как 1, а все остальные биты известны как 0. tnum_intersectСохранит эту информацию и пометит все младшие 32 бита как известные в своих выходных данных. Наконец, tnum_orбудет вновь введена неопределенность в старшие 32 бита, но поскольку hi32указывает, что младшие 32 бита являются известными нулями, младшие 32 бита из этого значения tnum_intersectбудут сохранены. Это означает, что после завершения этой функции нижняя половина var_offбудет помечена как известный двоичный файл 00...01.
Однако этот вывод не обоснован. Просто потому , что uminи umaxкак конец в двоичной системе 00...01не означает , что каждое значение между также делает. Например, истинное значение регистра могло бы легко быть 2. Как мы увидим, это первоначальное неверное предположение, сделанное верификатором, может быть использовано для того, чтобы нарушить дальнейшие предположения.
Вызывая ошибку
Чтобы фактически вызвать описанную ошибку, нам нужен регистр, который удовлетворяет следующим условиям:
- Во время выполнения фактическое значение в регистре равно 2.
- Для регистра umin_valустановлено значение 1 и umax_valзначение 2ˆ32 + 1.
- Выполнен условный переход с 32-битным сравнением в этом регистре.
Мы не можем напрямую загрузить значение 2 в регистр с помощью a mov, так как тогда верификатор узнает об этом umin_val=umax_val=2. Тем не менее, есть простой обходной путь. Если мы загружаем регистр из карты (мы будем использовать нашу входную карту inmap), то верификатор не будет иметь информации о его значении, поскольку значения карты могут быть изменены во время выполнения.
Чтобы установить umin_valи umax_valмы можем использовать логику перехода ветви верификатора:
BPF_JMP_IMM(BPF_JGE, BPF_REG_2, 1, 1)
BPF_RAW_INSN(BPF_JMP | BPF_EXIT, 0, 0, 0, 0)
Условный переход приведет к двум ветвям. В той, где берется ветвь, верификатор знает это BPF_REG_2 >= 1, в то время как другая ветвь заканчивается инструкцией выхода и отбрасывается. Таким образом, для всех дальнейших инструкций umin_valрегистра 2 будет 1.
Аналогично, другой условный переход может быть использован для установки umax_valна 2ˆ32+1. Однако здесь нам нужно сравнить с регистром, потому что поддерживаются только 32-битные непосредственные значения. После этого мы umin_valи umax_valу становили как хотели.
Теперь любой условный 32-битный переход может быть использован для запуска ошибки:
BPF_JMP32_IMM(BPF_JNE, BPF_REG_2, 5, 1),
BPF_RAW_INSN(BPF_JMP | BPF_EXIT, 0, 0, 0, 0),
Теперь верификатор считает, что последние 32 бита регистра 2 являются двоичными, 00...01хотя в действительности они являются двоичными 00...10. После двух дополнительных инструкций:
BPF_ALU64_IMM(BPF_AND, BPF_REG_2, 2),
BPF_ALU64_IMM(BPF_RSH, BPF_REG_2, 1),
Теперь верификатор предполагает, что регистр 2 должен быть 0, потому что ANDинструкция обязательно должна приводить к 0, если второй-последний бит регистра 2 был 0, но на самом деле это так (2&2)>>1 = 1. Это очень полезный примитив, поскольку теперь мы можем умножить регистр 2 на любое число, чтобы создать произвольное значение, которое верификатор считает равным 0.
В обход АЛУ Санитария
Хотя перепроверка всей арифметики с указателями во время выполнения кажется хорошей идеей, реализация серьезно отсутствует. Основная проблема, как представляется, заключается в том, что значения на самом деле не ограничены диапазоном, в котором статическая часть верификатора предполагает их нахождение, а скорее более широким пределом, который в данный момент кажется безопасным , но не обязательно так в отношении в будущем. Чтобы понять это различие, рассмотрим следующий пример:
- Регистр 1 содержит указатель p на начало 4000 байтов безопасной памяти. Предположим, что тип *pимеет размер всего один байт. Это означает, что добавление 3999 к pсовершенно нормально, но добавление 4000 оставило бы границы.
- Регистр 2 содержит скаляр a, который статический верификатор считает равным 1000.
- Теперь добавьте регистр 2 в регистр 1. Статический верификатор не имеет проблем с этим, так как p+1000все еще находится в пределах области памяти. Поскольку это арифметика с указателем, дезинфицирующее средство ALU установит значение alu_limit. Однако, несмотря на то, что верификатор считает, что aон не больше 1000, ограничение не установлено равным 1000, а скорее 3999, поскольку это самое большое значение, которое все еще считается безопасным.
- Добавить регистр 2, чтобы зарегистрировать 1 снова. На этот раз статическая часть верификатора считает, что pуказывает на позицию 1000 в блоке памяти 4000 байт. alu_limitТеперь будет установлен в 2999, потому что это самое большое юридическое значение , что еще можно добавить.
Теперь рассмотрим, что произойдет, если мы использовали ошибку в статическом анализе, чтобы обмануть верификатора, заставив его поверить в то a = 1000, что в действительности мы установили a = 2500. Значение будет по-прежнему проходить обе проверки во время выполнения без принудительной установки на 0, поскольку 2500 меньше, чем 3999 и 2999. Однако в общей сложности мы добавили 5000 к указателю p, что привело к адресу, выходящему за пределы безопасного диапазона памяти. Хотя каждый из alu_limitних достаточно низок в отдельности, в сочетании они недостаточны. Второе вычисление пределов только улавливает ошибки, которые статический анализ допускает во втором добавлении, предполагая, что оно было полностью правильным относительно первого.
Достижение OOB Читать и писать
Теперь для получения доступа к памяти OOB достаточно просто объединить части. Сначала мы используем JMP32ошибку, чтобы установить регистр в 1, тогда как верификатор считает, что он должен быть 0. Предположим, у нас есть указатель pна карту размером 8000 байт (эксплойт использует немного другие значения). В целях эксплойта наиболее удобно получить OOB-доступ до начала карты, а не после ее окончания, поэтому первым шагом является добавление большого значения, например 6000, для pтого, чтобы мы могли «отразить» обход санитарного контроля ALU. описано выше.
Затем мы производим регистр, который имеет фактическое значение 6000, в то время как верификатор считает, что он содержит 0. Мы делаем это путем умножения нашего исходного «поддельного» регистра на 6000. Теперь мы можем вычесть это из p. Это работает, потому что alu_limitбудет установлено 6000, наибольшая юридическая сумма, которую мы можем вычесть. Тем не менее, статическая часть верификатора все еще будет предполагать, что она pуказывает на позицию 6000 на нашей карте. Таким образом, теперь мы можем вычесть любое значение до 6000 p, в то время как pв действительности указывает на начало карты.
Теперь у нас есть указатель до 6000 байтов до начала содержимого карты. Поскольку верификатор по-прежнему полагает, что этот указатель находится внутри границ карты, он теперь позволяет нам читать и писать по этому адресу. Для удобства мы можем даже сделать операцию и смещение (а для записи - записанное значение) зависимыми от параметров в нашей входной карте inmap. Для чтения OOB значение read записывается в outmap. Поскольку карты могут быть прочитаны и изменены из пользовательского пространства через bpfсистемный вызов, теперь мы можем загрузить эту программу только один раз, а затем повторно запустить ее с различными параметрами для произвольного гаджета чтения / записи OOB.
Наращивание привилегий
Утечка KASLR
Для нас удобно, чтобы содержимое карты не сохранялось в куче кучи, а помещалось в конец большей структуры, называемой struct bpf_map(определяется в include/linux/bpf.h). Эта структура содержит несколько полезных членов:
Особенно полезным является указатель, opsкоторый указывает на таблицу функций, в зависимости от типа карты. В этом случае, поскольку карта является arraymap, она указывает на array_map_ops. Поскольку это постоянная структура в фиксированном месте rodata(это даже экспортированный символ), чтение этого может использоваться для прямого обхода KASLR.
Произвольное чтение
Также в структуре полезна структура указателя btf *btf, которая указывает на дополнительную структуру, содержащую отладочную информацию. Этот указатель обычно не используется (и имеет значение NULL), что делает его хорошей целью перезаписи, не путая вещи слишком много.
Оказывается, к этому указателю можно легко получить доступ через bpf_map_get_info_by_fd-function, который, в свою очередь, вызывается BPF_OBJ_GET_INFO_BY_FDкомандой bpfсистемного вызова, если предоставляется файловый дескриптор карты. Эта функция эффективно выполняет следующее (полная функция находится в kernel/bpf/syscall.c):
Это означает, что если мы используем примитив OOB-write для set map->btfto someaddr - offsetof(struct btf, id), то BPF_OBJ_GET_INFO_BY_FDвозвращается *someaddrв info.btf_id. Поскольку поле id struct btf- это u32, этот примитив можно использовать для чтения 4 байтов за раз с произвольного адреса.
Нахождение структур процесса
Чтобы найти credи filesструктуры процесса, мы сначала ищем init_pid_nsпространство имен процесса по умолчанию (если он выполняется в контейнере, нам сначала нужно найти соответствующее пространство имен). Это может быть не самый быстрый способ, но это работает.
Есть два способа найти init_pid_ns:
- Если мы знаем смещение между array_map_opsи init_pid_ns, мы можем просто добавить его к адресу array_map_ops, который мы уже знаем. Это смещение не зависит от KASLR, но оно не стабильно при обновлении ядра.
- Вместо этого мы можем найти init_pid_nsв ksymtabи kstrtabсегментах. Для этого мы сначала находим начало с kstrtabпомощью итеративного поиска, начинающегося с array_map_ops. Затем мы находим строку с нулевым символом в конце «init_pid_ns» kstrtab, снова с помощью простого итеративного поиска. Последняя итерация ksymtabзавершает поиск записи символа, которая ссылается на kstrtabзапись, а также содержит относительный адрес init_pid_ns.
Как только init_pid_nsоно найдено, его основополагающее дерево можно повторить, чтобы найти указатель на taskструктуру, соответствующую pid процесса эксплойта. Фактически, это именно тот механизм, который само ядро использует для поиска taskпо pid.
Используя структуру tasks, credможно найти как структуру, содержащую права пользователя, так и массив дескрипторов открытых файлов. Последний может быть проиндексирован дескриптором загруженного файла explmapдля получения соответствующего файла структуры карты. В свою очередь, private_dataэтой структуры указывает на структуру bpf_map. Это означает, что теперь мы также знаем адрес содержимого explmap.
Произвольная запись
Обратите внимание, что при перезаписи opsс использованием OOB-записи мы можем управлять RIP для любой функции, на которую у нас есть указатель. Тем не менее, первый аргумент в RDI всегда будет установлен на bpf_map-struct, что исключает множество существующих функций. Таким образом, кажется естественным перезаписать определенные элементы array_map_opsв различных операциях карты, которые могут по крайней мере правильно обрабатывать первый аргумент.
Для этого мы сначала загружаем полную копию array_map_opsтаблицы в данные explmapиспользования произвольного примитива чтения, а также bpfсистемного вызова с BPF_MAP_UPDATE_ELEM. Единственная модификация этой копии - то, что map_push_elemэлемент, обычно не используемый в arraymaps, перезаписывается map_get_next_keyоперацией.
arraymapРеализация map_get_next_keyвыглядит следующим образом (с kernel/bpf/arraymap.c):
Если мы контролируем next_keyи key, то *next = index + 1;можно использовать как произвольный примитив записи при условии, что index < array->map.max_entries. Если этот параметр map->max_entriesможет быть установлен 0xffffffff, эта проверка будет всегда проходить (за исключением того index=0xffffffff, что мы и будем использовать, но это нормально, так как *nextвсе еще установлено 0=index+1).
Поскольку мы уже получили указатель на данные explmap, мы можем теперь перезаписать, explmap->array_map_opsчтобы указать на нашу измененную таблицу операций.
Обратите внимание, что подпись map_push_elem, которую мы перезаписали map_get_next_key, является:
Однако map_push_elemвызывается BPF_MAP_UPDATE_ELEMкомандой, только если карта имеет тип BPF_MAP_TYPE_STACKили BPF_MAP_TYPE_QUEUE. Но если это так, мы можем напрямую контролировать flagsаргумент, который будет интерпретироваться как next_key, а также valueаргумент.
Чтобы запустить произвольный гаджет записи, мы должны выполнить следующие записи OOB в последовательности:
- Установите opsнаш поддельный vtable внутри explmapбуфера.
- Установите explmap->spin_lock_off0, чтобы пройти некоторые дополнительные проверки.
- Установите, explmap->max_entriesчтобы 0xffffffffпройти проверку array_map_get_next_key.
- Установите, explmap->map_typeчтобы BPF_MAP_TYPE_STACKиметь возможность достичь map_push_elem.
Произвольная 32-битная запись теперь может быть запущена путем передачи соответствующих аргументов BPF_MAP_UPDATE_ELEM. После того, как все записи выполнены, поля должны быть сброшены до их первоначальных значений, чтобы предотвратить сбои при очистке.
Получение привилегий root
Получение root теперь тривиально: нам просто нужно установить uid значение 0 в credструктуре процесса. Теперь мы можем выполнить произвольные привилегированные команды или перейти в интерактивную оболочку.
Источник:
Ссылка скрыта от гостей