Наверняка вы так или иначе сталкивались с технологией eBPF в своей повседневной работе, но задумывались ли вы о том, как ее могут использовать злоумышленники в своих атаках? Мы задумались и:
👽разобрали процесс загрузки eBPF-модулей в ядро и структуру загружаемых программ
👽 придумали подход к обнаружению вредоносных eBPF-программ
👽 реализовали PoC для предотвращения загрузки модулей в ядро
👽 написали парсер для структур-инструкций и строк eBPF-программы
Ниже вас ждут некоторые технические подробности нашего ресерча, с полной версией которого можно ознакомиться в нашем блоге.
eBPF: анатомия технологии и программы
За загрузку eBPF-программ в ядро отвечает системный вызов
bpf() с командой BPF_PROG_LOAD (cmd = 5):int bpf(int cmd, union bpf_attr *attr, unsigned int size);
Его мы и будем перехватывать с помощью
kprobe. Второй аргумент содержит в себе структуру bpf_attr, в которой содержится множество полезной информации о загружаемой программе (представлены наиболее полезные элементы):struct {
__u32 prog_type; /* тип eBPF-программы */
__u32 insn_cnt; /* число инструкций загружаемой программы */
__aligned_u64 insns; /* указатель на массив структур-инструкций */
...
char prog_name[BPF_OBJ_NAME_LEN]; /* имя загружаемой программы */
...
}Самая интересная информация хранится по указателю
insns. Внутри лежит массив структур, являющихся инструкциями eBPF-программы: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 */
};Информация из этих структур использована для реализации Prevention и Detection (в kernel mode и user mode).
eBPF: Detection & Prevention (kernel mode)
Полученные структуры перебираются, из них выбираются инструкции
BPF_EMIT_CALL() (code = 133):#define BPF_EMIT_CALL(FUNC)
((struct bpf_insn) {
.code = BPF_JMP | BPF_CALL,
.dst_reg = 0,
.src_reg = 0,
.off = 0,
.imm = ((FUNC) - __bpf_call_base) })
Эти инструкции отвечают за вызов
bpf-helpers. На основании используемых вредоносной программой хэлперов, а также имени загружаемой программы, можно сделать паттерны для детекта:if detectname == "pamspy":
funclimitpatterns = {
14: 1, #bpf_get_current_pid_tgid
1: 1, #bpf_map_lookup_elem
4: 5, #bpf_probe_read
3: 1, #bpf_map_delete_elem
16: 1, #bpf_get_current_comm
132: 1, #bpf_ringbuf_submit
131: 1, #bpf_ringbuf_reserve
}
prognamepatterns = ["handle_", "det_"]
Prevention выполняется при помощи замены первой инструкции анализируемой программы на инструкцию
BPF_EXIT_INSN().eBPF: Detection (user mode)
Если загрузка не была прервана, то можно продолжить обработку на стороне user mode. Для этого мы написали парсер, который переводит некрасивые структуры в более человекочитаемый вид:
BPF_EMIT_CALL(get_current_pid_tgid)
BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_A, -8)
BPF_LDX_MEM(BPF_W, BPF_REG_ARG1, BPF_REG_X, 0)
BPF_JMP_IMM(BPF_JEQ, BPF_REG_ARG1, 0, 18)
BPF_EMIT_CALL(get_current_task)
BPF_MOV64_IMM(BPF_REG_ARG1, 2480)
Также мы научились доставать некоторые константные строки из листинга загружаемых eBPF-программ:
[{'insn_number': 29, 'string': 'sudo\x00'},
{'insn_number': 44, 'string': '/etc/sudoers\x00'}]Всю полученную информацию можно смело собирать в JSON и отправлять в сторону любого стороннего инструмента для дальнейшей корреляции (например, в SIEM).
eBPF: Проблемы и выводы
Главное, что нам удалось выяснить - анализ eBPF-программ возможен до их загрузки, можно даже предотвратить загрузку программы, если она вызывает подозрения. Конечно, не обошлось без сложностей из-за ограничений eBPF, но так или иначе мы смогли их обойти.
Почитать код, в котором мы реализовали описанные подходы, можно на Github.