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

fb8aa35b8f60bc1c7374fbab4fcab9eb.png

Привет! Меня зовут Евгений Биричевский, в Positive Technologies я работаю в отделе обнаружения вредоносного ПО экспертного центра безопасности (PT ESC). Я занимаюсь исследованием различных вредоносных техник и образцов ВПО, написанием статических и динамических правил обнаружения, а также разработкой различных модулей для DRAKVUF.

Не так давно исследователи из 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:

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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

#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, но об этом позднее):

ab3dceac3ca391796687bda0d3384e39.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

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

f0f2481114ccca9b4ebb519dff40b39d.png

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

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

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

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

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

check_bpf

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

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

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

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

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

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

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

b64d00d4c72e21da008b21e2de239307.png

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

9ee60cfa417477b5e0557b3b6cd2a320.png

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

4447c80ff1051171bb3fe0cbaf4a8e1d.png

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

750b0fccb136c5673ea1583ee3f9cd89.png

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

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

elevator.elf

sha256: 41e45ac439a35fbfffece86469cd29406076ccfcc0e35a6a920aebfc8fdc3622

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

Все программы также являются фильтрами для сокетов, поэтому активируются идентично.

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

bpf_1

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

0b3217ce3418a4176dac4a7cd0bb38e5.png

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

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

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

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

ef318b896ab66a3f9a290cc4008e9ec1.png

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

60a64feb4b43c64c4a43542bc16ffc32.png

bpf_2 (aka leak_stack_address_2)

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

7775ca8c5dd92438be0fdf0bf7c8997b.png

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

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

bpf_leak_stack_address

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

403d690819a4c2f6047a2f8511247907.png

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

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

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

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

60e02f8f2fa7ada3f9b09cbe65162b17.png

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

00f3df118ab07ad5dd30ff2e95c7c608.png

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

bpf_read_kernel

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

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

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

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

2de62ddb0907f0275c0a0b7200446976.png

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

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

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

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

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

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

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

5e4381b81890719d1dc8c8fb8fcfe52a.png

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

255e0236f701ea36cc3cab251162d4ee.png

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

bpf_write_kernel

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

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

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

Код:

7cb7f5610a135293ff143c38169fc506.png

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

81e68b052aa685c5b895aeabfed8d4db.png

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

340d146ad13e20602b8324756cb541b7.png

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

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

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

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

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

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

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

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

Выводы

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

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

IoC

© Habrahabr.ru