![](https://habrastorage.org/getpro/habr/upload_files/fb8/aa3/5b8/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-программы](https://habrastorage.org/getpro/habr/upload_files/063/b53/f08/063b53f08e6b86f615720105ef05ac3d.png)
Чтобы убедиться в типе eBPF-программы, можно отыскать вызов setsockopt — функцию для настройки сокета с параметром SO_ATTACH_BPF.
![Рисунок 2. Присоединение фильтра к сокету Рисунок 2. Присоединение фильтра к сокету](https://habrastorage.org/getpro/habr/upload_files/fed/aa7/d8f/fedaa7d8f81560f9eecf23b91b22cdb8.png)
Поднявшись по строчкам вызовов функций, можно найти сам массив с eBPF-программой. Однако по умолчанию IDA не умеет работать с eBPF-программами в дизассемблированном коде.
![Рисунок 3. Стандартное представление eBPF-программы Рисунок 3. Стандартное представление eBPF-программы](https://habrastorage.org/getpro/habr/upload_files/6ec/64e/aaa/6ec64eaaac1f30c8057896e5701e5714.png)
Поэтому IDA нужно немного помочь. Следует создать структуру данных bpf_insn
. Структура после добавления выглядит так.
![Рисунок 4. Добавленная структура bpf_insn Рисунок 4. Добавленная структура bpf_insn](https://habrastorage.org/getpro/habr/upload_files/336/3b3/944/3363b3944552bde0c93f128c329abb32.png)
bpf_insn
Поле с регистрами одно, так как они хранятся в одном числе — в его верхних и нижних битах.
Поскольку eBPF-программа — массив структур, то в псевдокоде можно установить тип данных.
![Рисунок 5. Переопределение типа Рисунок 5. Переопределение типа](https://habrastorage.org/getpro/habr/upload_files/fb9/ff4/1be/fb9ff41bee75b3120343ab4ff6f9c462.png)
Количество инструкций можно подобрать или посмотреть в соседних функциях.
![Рисунок 6. Количество инструкций в eBPF-программе Рисунок 6. Количество инструкций в eBPF-программе](https://habrastorage.org/getpro/habr/upload_files/8dc/63f/a76/8dc63fa763e36eda365ca9671b00e4ab.png)
В итоге смотреть уже приятнее.
![Рисунок 7. Улучшенное представление eBPF-программы Рисунок 7. Улучшенное представление eBPF-программы](https://habrastorage.org/getpro/habr/upload_files/ff5/6cd/e54/ff56cde5438155a0fb332fa3e38e50f1.png)
С полями off
, imm
и regs
все просто: это обычные значения, разве что регистры нужно поделить на верхние и нижние биты. Самое сложное — расшифровать флаги из поля code
.
Для удобства нужно добавить некоторые флаги в IDA.
![Рисунок 8. Добавленные константы Рисунок 8. Добавленные константы](https://habrastorage.org/getpro/habr/upload_files/8fe/2a9/11b/8fe2a911b140020a383c23e153634430.png)
Все флаги и дополнительные eBPF-инструкции можно найти в исходниках Linux. Тогда код можно будет просмотреть так.
![Рисунок 9. Интерпретация флагов Рисунок 9. Интерпретация флагов](https://habrastorage.org/getpro/habr/upload_files/7dd/e5a/d45/7dde5ad45e4f8c04d5886db0eb1ce8c4.png)
Объединить флаги в одно значение через логическое ИЛИ
не получилось из-за разных масок, так что флаги нужно комбинировать вручную.
Для рисунка 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
, но об этом позднее):
![](https://habrastorage.org/getpro/habr/upload_files/ab3/dce/ac3/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
Код программы:
![](https://habrastorage.org/getpro/habr/upload_files/f0f/248/111/f0f2481114ccca9b4ebb519dff40b39d.png)
Программа — практически полная копия PoC, Pointer Leak via BPF Exploit; она получает указатель из ядерного пространства операционной системы.
Полностью совпадают eBPF-программы, их тип и способ активации. Незначительно различаются лишь сообщения от verifier (псевдокод образца ВПО находится справа):
![Рисунок 10. Сходства образца с эксплойтом для утечки указателя Рисунок 10. Сходства образца с эксплойтом для утечки указателя](https://habrastorage.org/getpro/habr/upload_files/de9/cd7/211/de9cd721193d593e6ff5e35ab26bb6b8.png)
Эта уязвимость не CVE-2018-18445, но она была опубликована примерно в то же время и является ключевой для работы основного образца.
check_bpf
Программа нужна только для проверки загрузки eBPF-программы в память. Иными словами, для проверки того, что verifier разрешил загрузку и что сам эксплойт работает в системе.
![Рисунок 11. Вызов функции check_bpf Рисунок 11. Вызов функции check_bpf](https://habrastorage.org/getpro/habr/upload_files/4bd/0b0/fc5/4bd0b0fc5311140fff63b5eb7d21e820.png)
check_bpf
Функция check_bpf
вернет значение true
, если программа успешно загрузится в память.
![Рисунок 12. Возвращаемое значение функции check_bpf Рисунок 12. Возвращаемое значение функции check_bpf](https://habrastorage.org/getpro/habr/upload_files/f7a/dbe/006/f7adbe006ff1482b153b049732c2c287.png)
check_bpf
Вывод консоли в случае успеха (хотя программа и была «убита», она загрузилась в память):
![](https://habrastorage.org/getpro/habr/upload_files/b64/d00/d4c/b64d00d4c72e21da008b21e2de239307.png)
В случае неудачи:
![](https://habrastorage.org/getpro/habr/upload_files/9ee/60c/fa4/9ee60cfa417477b5e0557b3b6cd2a320.png)
Если восстановить его к читаемому виду, то получается:
![](https://habrastorage.org/getpro/habr/upload_files/444/7c8/0ff/4447c80ff1051171bb3fe0cbaf4a8e1d.png)
Результат очень похож по коду на искомую уязвимость CVE-2018-18445 (новые строки выделены цветом):
![](https://habrastorage.org/getpro/habr/upload_files/750/b0f/ccb/750b0fccb136c5673ea1583ee3f9cd89.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-программы:
![](https://habrastorage.org/getpro/habr/upload_files/0b3/217/ce3/0b3217ce3418a4176dac4a7cd0bb38e5.png)
Вызывается только при наличии переменной окружения TEST_ADDR
.
![Рисунок 13. Вызов первой eBPF-программы в elevator Рисунок 13. Вызов первой eBPF-программы в elevator](https://habrastorage.org/getpro/habr/upload_files/db5/7c3/d4f/db57c3d4f201ac6b1a637fb1b256081e.png)
До конца неясно, зачем нужна эта eBPF-программа, ведь она банально не запускается на подходящей версии из-за отсутствия bpf_get_current_comm
:
![](https://habrastorage.org/getpro/habr/upload_files/ef3/18b/896/ef318b896ab66a3f9a290cc4008e9ec1.png)
А если убрать вызов этой функции, то выведется число, которое задавалось в начале eBPF-программы:
![](https://habrastorage.org/getpro/habr/upload_files/60a/64f/eb4/60a64feb4b43c64c4a43542bc16ffc32.png)
bpf_2 (aka leak_stack_address_2)
Код eBPF-программы:
![](https://habrastorage.org/getpro/habr/upload_files/777/5ca/8c5/7775ca8c5dd92438be0fdf0bf7c8997b.png)
Также непонятно, зачем нужна эта программа — в образце она не используется (на вызов функции нет ссылок). Если исключить кучу мусорных инструкций в ее начале, которые перезаписывают один и тот же регистр (строки 1–11), то получится полная копия eBPF-программы для получения адреса стека (описано далее).
Возможно, она использовалась для тестирования или добавлена для усложнения анализа.
bpf_leak_stack_address
Код программы:
![](https://habrastorage.org/getpro/habr/upload_files/403/d69/081/403d690819a4c2f6047a2f8511247907.png)
Используется для получения адреса стека.
![Рисунок 14. Вызов eBPF-программы для получения адреса стека Рисунок 14. Вызов eBPF-программы для получения адреса стека](https://habrastorage.org/getpro/habr/upload_files/77f/6f1/8ac/77f6f18acb7bfdaafff08cbd6255f7d3.png)
По своей сути это сильно модифицированная версия PoC, Pointer Leak via BPF Exploit (есть похожие строки, а также есть сходство в смысле программ):
![](https://habrastorage.org/getpro/habr/upload_files/60e/02f/8f2/60e02f8f2fa7ada3f9b09cbe65162b17.png)
Вывод программы:
![](https://habrastorage.org/getpro/habr/upload_files/00f/3df/118/00f3df118ab07ad5dd30ff2e95c7c608.png)
Думаю, можно с уверенностью назвать эту программу ключевой: адрес стека используется во многих частях образца, в том числе для вызовов следующих двух программ.
bpf_read_kernel
Исходя из контекста и кода самой программы, она нужна для чтения 8 байтов информации из ядерной памяти.
![Рисунок 15. Чтение из ядерной памяти Рисунок 15. Чтение из ядерной памяти](https://habrastorage.org/getpro/habr/upload_files/72b/632/df4/72b632df456459a22f7a3f4f70bf248f.png)
Код программы:
![](https://habrastorage.org/getpro/habr/upload_files/2de/62d/db0/2de62ddb0907f0275c0a0b7200446976.png)
За время исполнения образца происходит множество чтений из ядерной памяти. С помощью этой программы образец получает все необходимые адреса, в том числе для повышения привилегий. Кроме того, образец может прочитать произвольно заданные адреса или сдампить учетные данные.
![Рисунок 16. Чтение произвольного адреса Рисунок 16. Чтение произвольного адреса](https://habrastorage.org/getpro/habr/upload_files/597/1ed/1c7/5971ed1c733522b6263842544c020d0f.png)
![Рисунок 17. Дамп учетных данных Рисунок 17. Дамп учетных данных](https://habrastorage.org/getpro/habr/upload_files/2e8/161/a35/2e8161a3549979fd135b2837ddd8ff6d.png)
Программы
bpf_read_kernel
иbpf_write_kernel
не удалось протестировать при помощи написания своего PoC, поэтому в пример приведу журналы, полученные при исполнении образца в изолированной среде.
Вывод образца:
![](https://habrastorage.org/getpro/habr/upload_files/5e4/381/b81/5e4381b81890719d1dc8c8fb8fcfe52a.png)
Сравнение с CVE-2018-18445:
![](https://habrastorage.org/getpro/habr/upload_files/255/e02/36f/255e0236f701ea36cc3cab251162d4ee.png)
Ясно видно, что эта программа — расширенная версия уязвимости CVE-2018-18445, так как у них схожи значительные части кода и сама суть.
bpf_write_kernel
Учитывая контекст и код самой программы, становится понятно, что она нужна для записи 8 байтов информации в ядерную память.
![Рисунок 18. Запись в ядерную память Рисунок 18. Запись в ядерную память](https://habrastorage.org/getpro/habr/upload_files/dc1/d9d/868/dc1d9d8680c72e5a1ef73a1325028f07.png)
Код:
![](https://habrastorage.org/getpro/habr/upload_files/7cb/7f5/610/7cb7f5610a135293ff143c38169fc506.png)
Вывод образца:
![](https://habrastorage.org/getpro/habr/upload_files/81e/68b/052/81e68b052aa685c5b895aeabfed8d4db.png)
Сравнение с CVE-2018-18445:
![](https://habrastorage.org/getpro/habr/upload_files/340/d14/6ad/340d146ad13e20602b8324756cb541b7.png)
Сходств немного меньше, но все еще достаточно.
За время исполнения образца происходит около девяти записей в ядерную память. Если судить по перезаписываемым адресам и псевдокоду, то образец перезаписывает структуру cred из своего task_struct.
![Рисунок 19. Код для повышения привилегий Рисунок 19. Код для повышения привилегий](https://habrastorage.org/getpro/habr/upload_files/78e/f9a/dc8/78ef9adc87ef73eb880406f6d7cdb14c.png)
Образец меняет информацию так, чтобы получить права root (выставляет uid
= 0, gid
= 0…).
Чтобы подтвердить успех повышения привилегий, он вызывает getuid(), который вернет 0 для вызова от root:
![Рисунок 20. Проверка повышения привилегий Рисунок 20. Проверка повышения привилегий](https://habrastorage.org/getpro/habr/upload_files/9ab/522/46e/9ab52246e30e0110db2b0447e29ce157.png)
Выводы
ВПО интересное, и похоже, что оно действительно эксплуатирует CVE-2018-18445. А для получения ядерного указателя используется другая уязвимость примерно с той же датой появления.
Уязвимость CVE-2018-18445 есть только в относительно старых ядрах Linux. В свежих версиях ядра работает лишь утечка ядерного указателя.
IoC
bpf.test
4ad7b6dffc90bddd9beeb5653fad113ad905db81dce0298e376fed15b2246687elevator.elf
41e45ac439a35fbfffece86469cd29406076ccfcc0e35a6a920aebfc8fdc3622