Екатерина Гайнуллина, Security Vision
Если в первой части мы говорили о шпионаже и наблюдении, то теперь речь пойдёт о действиях. Злоумышленник, получивший доступ к системе, редко ограничивается пассивным сбором информации — ему нужен контроль. А eBPF, как универсальный инъекционный механизм ядра, даёт этот контроль буквально «в корне»: на уровне системных вызовов, сетевого стека, безопасности и подсистем ядра.
Возможности вмешательства велики в своей точности и незаметности. Через eBPF можно превратить Linux-машину в персонализированный брандмауэр, в саботажную платформу, в сетевой ретранслятор — и всё это без одного байта на диске, без единой строчки в cron, без видимого процесса в ps. В этой части разберу, как с помощью eBPF злоумышленник может перехватывать, модифицировать и саботировать поведение системы: от фильтрации пакетов до манипуляции запуском процессов и внедрения теневых политик безопасности. Это не фантазия, не гипотеза — это уже было. И это может быть снова.
Часть 2. Манипуляция – фильтрация и изменение трафика
Наблюдением дело не ограничивается – eBPF позволяет не только подслушивать, но и вмешиваться в работу системы. Одно из самых заманчивых возможностей для атакера – создание скрытного брандмауэра или бэкдора на уровне ядра. Например, eBPF-программы типа XDP (eXpress Data Path) выполняются на самом раннем этапе получения сетевого пакета, ещё до того, как пакет обработается сетевым стеком Linux. Это значит, что вредонос с eBPF может отбросить нежелательные пакеты, перенаправить их или даже подменить, раньше любых iptables и средств мониторинга трафика. Аналогично, программы на уровне tc (Traffic Control) могут фильтровать трафик на этапах входа/выхода сетевого интерфейса.
Рассмотрим простой пример: предположим, злоумышленник хочет блокировать определённый тип трафика – скажем, ICMP Echo Request (ping) – чтобы его заражённый сервер не пинговался (скрывался от простых сканеров), либо чтобы отфильтровать ICMP-трафик мониторинговых систем. Ниже приведён фрагмент eBPF-программы XDP, которая отбрасывает все ICMP-пакеты, проходящие через сетевой интерфейс:
// XDP-программа для блокировки ICMP-пакетов
SEC("xdp")
int block_icmp(struct xdp_md *ctx) {
void data = (void )(unsigned long)ctx->data;
void data_end = (void )(unsigned long)ctx->data_end;
// Проверка наличия Ethernet-заголовка
if (data + sizeof(struct ethhdr) > data_end) {
return XDP_PASS;
}
struct ethhdr *eth = data;
// Интересует только IPv4-пакеты
if (eth->h_proto != bpf_htons(ETH_P_IP)) {
return XDP_PASS;
}
// Проверка наличия IP-заголовка
struct iphdr *ip = data + sizeof(struct ethhdr);
if ((void*)ip + sizeof(*ip) > data_end) {
return XDP_PASS;
}
// Если протокол = ICMP (1) – отбросить пакет
if (ip->protocol == IPPROTO_ICMP) {
return XDP_DROP;
}
// Иначе пропустить пакет дальше
return XDP_PASS;
}
Как это работает: программа типа XDP привязывается к сетевому интерфейсу (например, eth0) и запускается для каждого входящего пакета. В коде мы разбираем Ethernet-заголовок, убеждаемся, что это IPv4-пакет, затем читаем его IP-заголовок. Если поле protocol равно IPPROTO_ICMP (1), функция немедленно возвращает XDP_DROP, указывая ядру отбросить пакет. Для всех остальных пакетов возвращается XDP_PASS, и они продолжают обычную обработку сетевым стеком. Таким образом, пока эта eBPF-программа активна, любые попытки пропинговать машину или другой ICMP-трафик будут тихо блокироваться на самом низком уровне – об этом не узнают ни брандмауэр, ни приложения выше.
Злоумышленник может фильтровать не только ICMP. С небольшими изменениями, eBPF способен блокировать, например, соединения на определённые порты или IP-адреса. Можно реализовать чёрный список адресов прямо в eBPF (через map) и дропать любые пакеты с этих адресов. Либо наоборот – тихо отбросить трафик сканеров портов, делая определённый порт видимым только для «своих». В публично известном бэкдоре BPFDoor, например, использовался фильтр сокетов BPF, который позволял принимать команды на любом порту, просматривая весь входящий трафик и вычленяя специальные «магические» пакеты управления. Такая скрытность достигается именно благодаря тому, что eBPF «подслушивает» пакеты до сетевого стека: BPFDoor не открывал слушающий сокет на фиксированном порту, вместо этого eBPF-программа внутри ядра проверяла каждый входящий пакет на признак команды, и, если команда обнаружена, перенаправляла её в пользовательский процесс-бэкдор. С точки зрения администратора, никаких подозрительных портов в netstat не видно, а команды поступают «из ниоткуда».
Помимо отброса пакетов, eBPF позволяет и изменять трафик на лету. Программа на XDP может переписать содержимое пакета (например, подменить адреса или порты) и отправить его дальше (XDP_TX или XDP_REDIRECT). На уровне tc eBPF-программа может модифицировать или задерживать пакеты. Для атакующего это открывает двери к MITM-атакам внутри ядра: можно внедрять вредоносный код в передаваемые данные, обфусцировать трафик, скрывать своё присутствие. Например, продвинутый вредонос Symbiote комбинировал перехват сетевых функций через LD_PRELOAD и BPF-фильтрацию: он подкладывал свой фильтр при вызове setsockopt() так, что tcpdump и другие снифферы не видели трафик, связанный с малварью.
Важно подчеркнуть, что eBPF-программы сетевого уровня работают очень быстро (близко к скорости обработки на сетевой карте) и с минимальными накладными расходами. Поэтому злоумышленник, используя eBPF, может построить скрытый и эффективный брандмауэр для своих целей. Он может избирательно блокировать пакеты от средств безопасности (например, от SIEM-агентов или сканеров) или наоборот, позволять себе обходить системные политики. Причём классические средства мониторинга могут и не заметить, что пакеты отбрасываются – они просто не доходят до уровня, где обычно логируются.
Но манипуляции не ограничиваются только сетевыми пакетами. Злоумышленник может использовать eBPF и для вмешательства в работу системных вызовов, внедряя саботаж и ограничения прямо на уровне ядра. Для этого используются так называемые kprobes — механизмы, позволяющие перехватывать выполнение любой функции ядра по имени и запускать свою eBPF-программу в момент её вызова. Например, можно вмешаться в работу вызова execve, который отвечает за запуск новых процессов.
Представим сценарий: атакующий хочет временно отключить антивирус или сканер безопасности, чтобы загрузить полезную нагрузку. Вместо того чтобы удалять или модифицировать файлы, он может просто внедрить eBPF-программу, которая будет саботировать вызовы execve() для определённых исполняемых файлов.
Вот пример eBPF-программы, которая блокирует запуск top и ps:
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
char TARGET1[] = "/usr/bin/top";
char TARGET2[] = "/usr/bin/ps";
// Kprobe: срабатывает при вызове sys_execve
SEC("kprobe/sys_execve")
int block_execve(struct pt_regs *ctx) {
const char __user filename = (const char )PT_REGS_PARM1(ctx);
char buf[128];
// Считать имя исполняемого файла из пользовательского пространства
bpf_probe_read_user_str(buf, sizeof(buf), filename);
// Сравнение с целевыми файлами
if (__builtin_memcmp(buf, TARGET1, sizeof(TARGET1)) == 0 ||
__builtin_memcmp(buf, TARGET2, sizeof(TARGET2)) == 0) {
bpf_printk("Blocked execve of: %s\n", buf);
// В идеале, вернуть ошибку — но в kprobe нельзя менять поведение,
// это потребует использовать LSM или kretprobe.
}
return 0;
}
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
char TARGET1[] = "/usr/bin/top";
char TARGET2[] = "/usr/bin/ps";
// Kprobe: срабатывает при вызове sys_execve
SEC("kprobe/sys_execve")
int block_execve(struct pt_regs *ctx) {
const char __user filename = (const char )PT_REGS_PARM1(ctx);
char buf[128];
// Считать имя исполняемого файла из пользовательского пространства
bpf_probe_read_user_str(buf, sizeof(buf), filename);
// Сравнение с целевыми файлами
if (__builtin_memcmp(buf, TARGET1, sizeof(TARGET1)) == 0 ||
__builtin_memcmp(buf, TARGET2, sizeof(TARGET2)) == 0) {
bpf_printk("Blocked execve of: %s\n", buf);
// В идеале, вернуть ошибку — но в kprobe нельзя менять поведение,
// это потребует использовать LSM или kretprobe.
}
return 0;
}
Примечание: Прямое изменение поведения возможно только через LSM-хуки или kretprobe, но даже логирование попыток запуска утилит мониторинга уже даёт атакующему преимущество: он может узнать, когда его пытаются обнаружить — и вовремя уйти в тень.
Такой механизм позволяет реализовать «избирательный саботаж» — например, запрещать запуск только определённых программ, маскируя активность. Или наоборот — позволять запуск только «белого списка», превращая eBPF в ядре в неофициальный механизм политики исполнения (аналог AppArmor, но невидимый).
Вмешательство через LSM-хуки. Начиная с Linux 5.7, eBPF получил возможность подключаться к хукам Linux Security Modules (LSM). Это те самые точки, через которые реализуются политики SELinux, AppArmor, TOMOYO и другие. Хуки вызываются при проверке разрешений на доступ к файлам, процессам, сети и т.д.
Если раньше такие политики реализовывались в виде модулей ядра или патчей, то теперь можно динамически подключать eBPF-программу, которая будет вмешиваться в процесс принятия решений — разрешать или запрещать действия. Причём делать это скрытно и гибко: например, разрешить доступ к закрытому файлу только при определённом UID, или скрыть директорию от всех процессов, кроме одного.
Программа, использующая LSM-хук file_open, срабатывает каждый раз, когда в системе кто-то пытается открыть файл. В eBPF-программе можно считать имя файла из структуры dentry и проверить, не относится ли он к защищённой директории — например, /etc/hidden. Если файл попадает под заданный критерий, происходит дополнительная проверка: сравнивается UID текущего процесса. Только в случае, если UID совпадает с «разрешённым» (например, 1337), доступ к файлу будет разрешён. В противном случае программа возвращает ошибку -EACCES, тем самым запрещая открытие файла. Снаружи это выглядит как обычная ошибка доступа — как будто файл действительно закрыт для пользователя. Ни системные логи, ни команды вроде strace, ни утилиты мониторинга вроде auditd могут не показать факт вмешательства. Для всех остальных процессов этот файл становится как бы «невидимым».
Такой подход позволяет злоумышленнику гибко и незаметно ограничивать доступ к конфиденциальным данным. Можно скрыть конфигурационные файлы, бинарники, скрипты или плагины, связанные с бэкдором. Более того, этот механизм способен заменить или дополнять традиционные политики безопасности — вроде SELinux или AppArmor — но при этом остаётся совершенно неочевидным для администратора. В отличие от официальных LSM-профилей, эти ограничения не настраиваются через конфигурационные файлы, а вшиваются прямо в ядро с помощью eBPF. При этом поведение может динамически меняться: злоумышленник может «открыть» директорию на время и снова «закрыть» её по сигналу — всё это без следов в логах или на диске.
С технической точки зрения, для загрузки eBPF-программ, использующих LSM-хуки, требуются права суперпользователя: в частности, CAP_BPF и CAP_SYS_ADMIN. Это означает, что такой механизм чаще всего применяется на уже скомпрометированной системе, где атакующий получил root-доступ. Однако после загрузки программа может быть зафиксирована (pinned) в виртуальной файловой системе bpffs, что позволит ей переживать перезапуск процессов и даже сохраняться между сессиями, не прибегая к автозагрузке или модификации системных файлов. Кроме того, этот способ вмешательства не требует загрузки модулей ядра: не используется insmod, не появляется новых записей в lsmod, и даже такой антивирус, как rkhunter, может ничего не заметить.
Таким образом, LSM-хуки превращают eBPF в инструмент не только наблюдения или фильтрации, но и теневого управления безопасностью, параллельного официальным механизмам. Это особенно опасно в том случае, если организация полагается на AppArmor или SELinux, но не отслеживает активные eBPF-программы — в таком случае правила могут быть перезаписаны «невидимыми» политиками прямо на уровне ядра.
Заключение
В этой части мы увидели, как eBPF способен трансформироваться из «инструмента наблюдения» в полноценное оружие воздействия. От тихой фильтрации сетевого трафика на уровне XDP до точечных саботажей системных вызовов через kprobe и внедрения невидимых политик безопасности через LSM — злоумышленник получает возможность буквально переписать поведение системы на лету.
Особенно опасна незаметность: большинство таких манипуляций не оставляют следов в логах, не требуют дополнительных процессов, не записываются на диск. Они живут в памяти ядра, под охраной верификатора и CAP_SYS_ADMIN, но при этом могут годами оставаться невидимыми — если никто не знает, что их искать.
Для атакующего это даёт полный контроль: от сетевого трафика до исполнения процессов и доступа к данным. А для защитника — сигнал к действию: если вы не отслеживаете активные eBPF-программы, не проверяете хуки и точки крепления, не аудируете содержимое bpffs — вы слепы.
В следующей части мы поговорим о том, как злоумышленник может обеспечить устойчивость своих eBPF-инструментов в системе: скрываться от аудиторов, обходить EDR, переживать перезагрузки и оставаться вне поля зрения. Потому что настоящее вторжение — это не то, что случается, а то, что остаётся.