• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Реверс-инжиниринг eBPF-программы на примере сокет-фильтра и уязвимости CVE-2018-18445

weaver

31 c0 bb ea 1b e6 77 66 b8 88 13 50 ff d3
Забанен
Регистрация
19.12.2018
Сообщения
3 301
Решения
11
Реакции
4 622
Депозит
0.0001
Пожалуйста, обратите внимание, что пользователь заблокирован
Не так давно исследователи из Black Lotus Labs рассматривали несколько образцов 2018 года — elevator.elf и bpf.test. Пускай образцы и старые, но они используют уязвимости в eBPF, что происходит крайне редко: такие случаи можно практически пересчитать по пальцам.

Исследователи достаточно подробно описали общие функции и особенности ВПО, отметили запуск и использование eBPF-программ, но практически не описали сами eBPF-программы. Мне это показалось значительным упущением, ведь крайне редко удается пощупать in the wild использование уязвимостей в eBPF. Основываясь на дате появления образца и его поведении, исследователи предположили, что используется CVE-2018-18445. В этой статье мы научимся анализировать eBPF, достаточно подробно разберем используемые eBPF-программы, а также подтвердим или опровергнем гипотезу об использовании CVE-2018-18445.

Кратко о том, как победить реверс eBPF​

Вдаваться в подробное описание и особенности работы eBPF я не буду: в интернете достаточно материалов (например, цикл статей «BPF для самых маленьких»), отмечу лишь то, что необходимо для понимания этой статьи.

Программа eBPF — это набор (массив) некоторых инструкций. Каждая инструкция — структура типа bpf_insn:

C:
struct bpf_insn {
    __u8    code;       /* opcode */
    __u8    dst_reg:4;  /* dest register */
    __u8    src_reg:4;  /* source register */
    __s16   off;        /* signed offset */
    __s32   imm;        /* signed immediate constant */
};

Структура чем-то напоминает язык ассемблера: есть номер инструкции, используемые регистры, смещение и передаваемое значение.

Загрузка происходит при помощи системного вызова bpf, он же вызов 321 (в таблице системных вызовов Linux) с параметром BPF_PROG_LOAD (5). Поэтому нужно найти его в дизассемблере, в нашем случае — в IDA.

1719432639681.png

Рисунок 1. Загрузка eBPF-программы

Чтобы убедиться в типе eBPF-программы, можно отыскать вызов setsockopt — функцию для настройки сокета с параметром SO_ATTACH_BPF.

1719432669948.png

Рисунок 2. Присоединение фильтра к сокету

Поднявшись по строчкам вызовов функций, можно найти сам массив с eBPF-программой. Однако по умолчанию IDA не умеет работать с eBPF-программами в дизассемблированном коде.

1719432701199.png

Рисунок 3. Стандартное представление eBPF-программы

Поэтому IDA нужно немного помочь. Следует создать структуру данных bpf_insn. Структура после добавления выглядит так.

1719432734950.png

Рисунок 4. Добавленная структура bpf_insn

Поле с регистрами одно, так как они хранятся в одном числе — в его верхних и нижних битах.

Поскольку eBPF-программа — массив структур, то в псевдокоде можно установить тип данных.

1719432780784.png

Рисунок 5. Переопределение типа

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

1719432820518.png

Рисунок 6. Количество инструкций в eBPF-программе


В итоге смотреть уже приятнее.

1719432884453.png

Рисунок 7. Улучшенное представление eBPF-программы

С полями off, imm и regs все просто: это обычные значения, разве что регистры нужно поделить на верхние и нижние биты. Самое сложное — расшифровать флаги из поля code.

Для удобства нужно добавить некоторые флаги в IDA.

1719432912803.png

Рисунок 8. Добавленные константы

Все флаги и дополнительные eBPF-инструкции можно найти в исходниках Linux. Тогда код можно будет просмотреть так.

1719432945654.png

Рисунок 9. Интерпретация флагов

Объединить флаги в одно значение через логическое ИЛИ не получилось из-за разных масок, так что флаги нужно комбинировать вручную.

Для рисунка 9: 0x74 = 0x04 | 0x70 | 0x00 = BPF_ALU | BPF_OP(OP) | BPF_K = BPF_ALU | BPF_RSH | BPF_K

В ядре Linux есть удобные макросы, при помощи которых можно легко определить вызываемую инструкцию. Для этого нужно сравнить полученные флаги и найти подходящую инструкцию; можно также проверить и найти сходства ее остальных параметров. Например, для флагов на рисунке 9:

C:
#define BPF_ALU32_IMM(OP, DST, IMM)             \
  ((struct bpf_insn) {                          \
    .code  = BPF_ALU | BPF_OP(OP) | BPF_K,      \
    .dst_reg = DST,                             \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = IMM })

В этом примере BPF_OP(OP) == BPF_RSH, регистр-приемник под номером 8, а передаваемое значение равно 31, следовательно искомую инструкцию можно представить в виде: BPF_ALU32_IMM(BPF_RSH, BPF_REG_8, 31)

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

Так как eBPF-программ в образцах больше одной, такой способ не подойдет: он муторный и довольно затратный по времени, легко допустить ошибку в процессе. Для оптимизации была написана простая программа, которая и переводит псевдокод в макросы (исходный код представлен по ссылке).

Она преобразует bpf_insn из IDA в человекочитаемый список макросов. Например, в этом случае получится список (он же является программой leak_stack_address из образца bpf.test, но об этом позднее):

1719433038256.png


Если очень хочется протестировать и проверить работу восстановленной программы, то сделать это несложно. Нужна лишь виртуальная машина с Linux и компилятором gcc или clang.
Нужно скомпилировать, загрузить в память и активировать eBPF-программу. В данном случае нужна eBPF-программа с типом
BPF_PROG_TYPE_SOCKET_FILTER (этот тип используется в данных образцах ВПО). Пример кода для загрузки сокет-фильтра можно посмотреть на LWN.

Анализ eBPF-программ, представленных в статье​

В статье описаны два образца ВПО, они оба используют eBPF. Рассмотрим их по очереди.

Для удобства я буду приводить не однотипные снимки экрана с массивами кода eBPF-программ, а только их читаемую версию.

bpf.test​

sha256: 4ad7b6dffc90bddd9beeb5653fad113ad905db81dce0298e376fed15b2246687

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

  • leak_stack_address;
  • check_bpf.

leak_stack_address​

Код программы:

1719433123174.png


Программа — практически полная копия PoC, Pointer Leak via BPF Exploit; она получает указатель из ядерного пространства операционной системы.

Полностью совпадают eBPF-программы, их тип и способ активации. Незначительно различаются лишь сообщения от verifier (псевдокод образца ВПО находится справа):

1719433169092.png

Рисунок 10. Сходства образца с эксплойтом для утечки указателя

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

check_bpf​

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

1719433232326.png

Рисунок 11. Вызов функции check_bpf

Функция check_bpf вернет значение true, если программа успешно загрузится в память.

1719433253493.png

Рисунок 12. Возвращаемое значение функции check_bpf

Вывод консоли в случае успеха (хотя программа и была «убита», она загрузилась в память):

1719433281877.png

В случае неудачи:

1719433300544.png

Если восстановить его к читаемому виду, то получается:

1719433323262.png


Результат очень похож по коду на искомую уязвимость CVE-2018-18445 (новые строки выделены цветом):

1719433350595.png


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

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

elevator.elf​

sha256: 41e45ac439a35fbfffece86469cd29406076ccfcc0e35a6a920aebfc8fdc3622

Внутри есть целых пять eBPF-программ:

  • bpf_1;
  • bpf_2 (aka leak_stack_address_2);
  • bpf_leak_stack_address;
  • bpf_read_kernel;
  • bpf_write_kernel.
Все программы также являются фильтрами для сокетов, поэтому активируются идентично.

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

bpf_1​

Код eBPF-программы:

1719433383913.png


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

1719433406063.png

Рисунок 13. Вызов первой eBPF-программы в elevator

До конца неясно, зачем нужна эта eBPF-программа, ведь она банально не запускается на подходящей версии из-за отсутствия bpf_get_current_comm:

1719433432397.png


А если убрать вызов этой функции, то выведется число, которое задавалось в начале eBPF-программы:

1719433454581.png

bpf_2 (aka leak_stack_address_2)​

Код eBPF-программы:

1719433471148.png


Также непонятно, зачем нужна эта программа — в образце она не используется (на вызов функции нет ссылок). Если исключить кучу мусорных инструкций в ее начале, которые перезаписывают один и тот же регистр (строки 1–11), то получится полная копия eBPF-программы для получения адреса стека (описано далее).

Возможно, она использовалась для тестирования или добавлена для усложнения анализа.

bpf_leak_stack_address​

Код программы:

1719433512415.png


Используется для получения адреса стека.

1719433538866.png

Рисунок 14. Вызов eBPF-программы для получения адреса стека

По своей сути это сильно модифицированная версия PoC, Pointer Leak via BPF Exploit (есть похожие строки, а также есть сходство в смысле программ):

1719433559216.png


Вывод программы:

1719433583750.png


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

bpf_read_kernel​

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

1719433612367.png

Рисунок 15. Чтение из ядерной памяти

Код программы:

1719433635001.png


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

1719433658652.png

Рисунок 16. Чтение произвольного адреса

1719433675202.png

Рисунок 17. Дамп учетных данных

Программы bpf_read_kernel и bpf_write_kernel не удалось протестировать при помощи написания своего PoC, поэтому в пример приведу журналы, полученные при исполнении образца в изолированной среде.
Вывод образца:

1719433702736.png


Сравнение с CVE-2018-18445:

1719433726820.png


Ясно видно, что эта программа — расширенная версия уязвимости CVE-2018-18445, так как у них схожи значительные части кода и сама суть.

bpf_write_kernel​

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

1719433752570.png

Рисунок 18. Запись в ядерную память

Код:

1719433775354.png


Вывод образца:

1719433796238.png

Сравнение с CVE-2018-18445:

1719433813805.png


Сходств немного меньше, но все еще достаточно.

За время исполнения образца происходит около девяти записей в ядерную память. Если судить по перезаписываемым адресам и псевдокоду, то образец перезаписывает структуру cred из своего task_struct.

1719433838289.png

Рисунок 19. Код для повышения привилегий

Образец меняет информацию так, чтобы получить права root (выставляет uid = 0, gid = 0…).

Чтобы подтвердить успех повышения привилегий, он вызывает getuid(), который вернет 0 для вызова от root:

1719433869389.png

Рисунок 20. Проверка повышения привилегий

Выводы​

ВПО интересное, и похоже, что оно действительно эксплуатирует CVE-2018-18445. А для получения ядерного указателя используется другая уязвимость примерно с той же датой появления.

Уязвимость CVE-2018-18445 есть только в относительно старых ядрах Linux. В свежих версиях ядра работает лишь утечка ядерного указателя.

IoC
  • bpf.test
    4ad7b6dffc90bddd9beeb5653fad113ad905db81dce0298e376fed15b2246687
  • elevator.elf
    41e45ac439a35fbfffece86469cd29406076ccfcc0e35a6a920aebfc8fdc3622

Источник: habr.com/ru/companies/pt/articles/823053/
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх