Привет! Меня зовут Евгений Биричевский, в 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.
Чтобы убедиться в типе eBPF-программы, можно отыскать вызов setsockopt — функцию для настройки сокета с параметром SO_ATTACH_BPF.
Поднявшись по строчкам вызовов функций, можно найти сам массив с eBPF-программой. Однако по умолчанию IDA не умеет работать с eBPF-программами в дизассемблированном коде.
Поэтому IDA нужно немного помочь. Следует создать структуру данных bpf_insn
. Структура после добавления выглядит так.
Поле с регистрами одно, так как они хранятся в одном числе — в его верхних и нижних битах.
Поскольку eBPF-программа — массив структур, то в псевдокоде можно установить тип данных.
Количество инструкций можно подобрать или посмотреть в соседних функциях.
В итоге смотреть уже приятнее.
С полями off
, imm
и regs
все просто: это обычные значения, разве что регистры нужно поделить на верхние и нижние биты. Самое сложное — расшифровать флаги из поля code
.
Для удобства нужно добавить некоторые флаги в IDA.
Все флаги и дополнительные eBPF-инструкции можно найти в исходниках Linux. Тогда код можно будет просмотреть так.
Объединить флаги в одно значение через логическое ИЛИ
не получилось из-за разных масок, так что флаги нужно комбинировать вручную.
Для рисунка 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
, но об этом позднее):
Если очень хочется протестировать и проверить работу восстановленной программы, то сделать это несложно. Нужна лишь виртуальная машина с 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
Код программы:
Программа — практически полная копия PoC, Pointer Leak via BPF Exploit; она получает указатель из ядерного пространства операционной системы.
Полностью совпадают eBPF-программы, их тип и способ активации. Незначительно различаются лишь сообщения от verifier (псевдокод образца ВПО находится справа):
Эта уязвимость не CVE-2018-18445, но она была опубликована примерно в то же время и является ключевой для работы основного образца.
check_bpf
Программа нужна только для проверки загрузки eBPF-программы в память. Иными словами, для проверки того, что verifier разрешил загрузку и что сам эксплойт работает в системе.
Функция check_bpf
вернет значение true
, если программа успешно загрузится в память.
Вывод консоли в случае успеха (хотя программа и была «убита», она загрузилась в память):
В случае неудачи:
Если восстановить его к читаемому виду, то получается:
Результат очень похож по коду на искомую уязвимость CVE-2018-18445 (новые строки выделены цветом):
Программа из ВПО отличается только инструкциями с 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-программы:
Вызывается только при наличии переменной окружения TEST_ADDR
.
До конца неясно, зачем нужна эта eBPF-программа, ведь она банально не запускается на подходящей версии из-за отсутствия bpf_get_current_comm
:
А если убрать вызов этой функции, то выведется число, которое задавалось в начале eBPF-программы:
bpf_2 (aka leak_stack_address_2)
Код eBPF-программы:
Также непонятно, зачем нужна эта программа — в образце она не используется (на вызов функции нет ссылок). Если исключить кучу мусорных инструкций в ее начале, которые перезаписывают один и тот же регистр (строки 1–11), то получится полная копия eBPF-программы для получения адреса стека (описано далее).
Возможно, она использовалась для тестирования или добавлена для усложнения анализа.
bpf_leak_stack_address
Код программы:
Используется для получения адреса стека.
По своей сути это сильно модифицированная версия PoC, Pointer Leak via BPF Exploit (есть похожие строки, а также есть сходство в смысле программ):
Вывод программы:
Думаю, можно с уверенностью назвать эту программу ключевой: адрес стека используется во многих частях образца, в том числе для вызовов следующих двух программ.
bpf_read_kernel
Исходя из контекста и кода самой программы, она нужна для чтения 8 байтов информации из ядерной памяти.
Код программы:
За время исполнения образца происходит множество чтений из ядерной памяти. С помощью этой программы образец получает все необходимые адреса, в том числе для повышения привилегий. Кроме того, образец может прочитать произвольно заданные адреса или сдампить учетные данные.
Программы
bpf_read_kernel
иbpf_write_kernel
не удалось протестировать при помощи написания своего PoC, поэтому в пример приведу журналы, полученные при исполнении образца в изолированной среде.
Вывод образца:
Сравнение с CVE-2018-18445:
Ясно видно, что эта программа — расширенная версия уязвимости CVE-2018-18445, так как у них схожи значительные части кода и сама суть.
bpf_write_kernel
Учитывая контекст и код самой программы, становится понятно, что она нужна для записи 8 байтов информации в ядерную память.
Код:
Вывод образца:
Сравнение с CVE-2018-18445:
Сходств немного меньше, но все еще достаточно.
За время исполнения образца происходит около девяти записей в ядерную память. Если судить по перезаписываемым адресам и псевдокоду, то образец перезаписывает структуру cred из своего task_struct.
Образец меняет информацию так, чтобы получить права root (выставляет uid
= 0, gid
= 0…).
Чтобы подтвердить успех повышения привилегий, он вызывает getuid(), который вернет 0 для вызова от root:
Выводы
ВПО интересное, и похоже, что оно действительно эксплуатирует CVE-2018-18445. А для получения ядерного указателя используется другая уязвимость примерно с той же датой появления.
Уязвимость CVE-2018-18445 есть только в относительно старых ядрах Linux. В свежих версиях ядра работает лишь утечка ядерного указателя.
IoC
bpf.test
4ad7b6dffc90bddd9beeb5653fad113ad905db81dce0298e376fed15b2246687elevator.elf
41e45ac439a35fbfffece86469cd29406076ccfcc0e35a6a920aebfc8fdc3622