
Экспериментальный брокер приманок
Контейнеризированная сетевая приманка (honeypot), которая рекламирует SSH, RDP и SMB, наблюдает за каждым входящим соединением с помощью eBPF и обратным прокси-сервером перенаправляет каждый сеанс в изолированный контейнер-приманку.
Архитектура разделяет две задачи:
Это защитный инструмент для обнаружения и изучения несанкционированной активности в сетях, которыми вы владеете или на мониторинг которых уполномочены. Развёртывайте его только там, где у вас есть такие полномочия.
flowchart TB
A["Атакующий / Сканер"]
subgraph host["Хост-приманка"]
direction TB
NIC["eth0 брокера<br/>опубликованные: 22, 3389, 445"]
subgraph brk["контейнер брокера"]
direction TB
E["Классификатор eBPF TC<br/>регистрирует каждый SYN<br/>видит реальный IP-источник"]
P["обратный прокси<br/>CONNECT к бэкенду"]
L["структурированные JSON-логи"]
end
subgraph dec["decoynet (внутренняя, без маршрута на хост)"]
direction LR
S["ssh-decoy<br/>OpenCanary ssh<br/>порт 2222"]
D["rdp-decoy<br/>OpenCanary rdp<br/>порт 3389"]
M["smb-decoy<br/>Impacket SMB сервер<br/>порт 445"]
end
end
A --> NIC
NIC --> E
NIC --> P
E --> L
P --> S
P --> D
P --> M
Всего четыре контейнера:
Приманки живут во внутренней сети Docker (decoynet) без маршрута к хосту или внешнему миру. Только брокер может до них добраться. Ничто из того, что атакующий сделает внутри приманки, не достигнет сети хоста напрямую.
Брокер публикует порты 22, 3389 и 445 на хосте, поэтому входящие пакеты поступают на eth0 брокера. Затем с каждым пакетом происходит две вещи:
broker/bpf/decoy.bpf.c) разбирает заголовки Ethernet, IP и TCP, и для каждой новой попытки соединения (SYN установлен, ACK сброшен) записывает conn_event в кольцевой буфер: IP-источник и порт, порт назначения, флаги TCP и является ли порт рекламируемым сервисом. Пакет пропускается без изменений (TC_ACT_OK).CONNECT к бэкенду-приманке для этого сервиса, затем передаёт байты в обоих направлениях.Карта eBPF advertised_ports заполняется при запуске из config.yaml, поэтому классификатор может пометить, попал ли зонд на обслуживаемый порт или на нерекламируемый. Это делает видимыми горизонтальные сканирования портов, даже если проксируются только три порта.
Если вы хотите рекламировать «всё открыто» и направлять произвольные порты назначения в брокер, расширьте классификатор для перезаписи порта назначения или используйте перенаправление TPROXY / bpf_sk_assign. Текущая версия не изменяет путь пакета и ограничивается наблюдением, что является более безопасным поведением по умолчанию.
cyber-decoy/
├── README.md
├── docker-compose.yml # Стек из 4 контейнеров
├── docker-compose.override.yml # Локальная разработка на macOS: без eBPF-возможностей, переназначение порта 22
├── Makefile # Сборка / запуск / остановка / помощь с eBPF
├── LICENSE
├── scripts/
│ └── setup.sh # Предварительные проверки хоста
├── broker/
│ ├── Dockerfile # Компилирует eBPF-объект + Go-бинарник
│ ├── config.yaml # Рекламируемые сервисы (настраивается)
│ ├── go.mod
│ ├── main.go # Точка входа
│ ├── bpf/
│ │ └── decoy.bpf.c # Классификатор eBPF TC
│ └── internal/
│ ├── config/config.go # Загрузчик конфигурации
│ ├── proxy/proxy.go # TCP-обратный прокси
│ └── bpf/loader.go # Загружает и прикрепляет eBPF, передаёт события
└── decoys/ # Все три запускают OpenCanary
├── ssh/
│ ├── Dockerfile
│ └── opencanary.conf # Модуль ssh, порт 2222
├── rdp/
│ ├── Dockerfile
│ └── opencanary.conf # Модуль rdp, порт 3389
└── smb/
├── Dockerfile # Один Python-процесс, не root
├── smb_decoy.py # SimpleSMBServer из Impacket + JSON-логирование
└── requirements.txt # impacket (зафиксирован)
docker-compose.override.yml, который полагается на теги !reset / !override).sudo mount -t bpf bpf /sys/fs/bpf.Образ брокера определяет архитектуру сборки и передаёт соответствующий макрос __TARGET_ARCH_* компилятору clang, поэтому он собирается как на x86_64, так и на aarch64 (Apple Silicon, Graviton). Обратите внимание, что gcc-multilib намеренно не установлен: это пакет только для x86 без кандидата для arm64, и его включение ломает сборку на arm64 с кодом выхода apt 100. Для компиляции eBPF-объекта нужны только clang и libbpf-dev.
Docker Desktop на macOS запускает контейнеры внутри виртуальной машины LinuxKit, а не в вашем хост-ядре, поэтому прикрепление TC/TCX eBPF обычно не будет работать там. Это не критично: eBPF по замыслу работает по принципу «best effort», поэтому брокер регистрирует ebpf disabled: attach failed, а обратный прокси и все три приманки работают и регистрируются нормально. Вы можете разрабатывать и тестировать весь путь прокси локально, а затем получить реальное eBPF-наблюдение при развёртывании на хосте Linux.
docker-compose.override.yml загружается автоматически и делает это удобным: он удаляет возможности eBPF (бесполезные в виртуальной машине) и переназначает хост-порт 22 на 2022, поскольку на Mac собственный sshd занимает порт 22.
docker compose up --build # локальная разработка, override применён
docker compose -f docker-compose.yml up -d # реальное развёртывание, override обойдён
Сначала выполните предварительную проверку:
./scripts/setup.sh
# 1. Сборка всех четырёх образов (компилирует eBPF-объект внутри образа брокера)
make build
# 2. Запуск стека
make up
# 3. Просмотр логов
make logs
Затем протестируйте с другой машины (или localhost для быстрой проверки):
ssh -p 22 user@HOST_ПРИМАНКИ # попадает в SSH-приманку
nc HOST_ПРИМАНКИ 3389 # попадает в RDP-приманку
nc HOST_ПРИМАНКИ 445 # попадает в SMB-приманку
nc HOST_ПРИМАНКИ 8080 # нерекламируемый: наблюдается eBPF, без прокси
Брокер выводит JSON для событий eBPF-зондов и проксированных сеансов; каждая приманка выводит события JSON OpenCanary. Чтобы наблюдать за поступающими учётными данными:
docker compose logs -f ssh-decoy | grep 4002
В отличие от заглушки с баннером, ssh -p 22 user@HOST_ПРИМАНКИ теперь выполняет реальный обмен ключами и запрашивает пароль. Каждая попытка захватывается. Проверьте, что отпечаток сервиса выдерживает обнаружение версий:
nmap -sV -p 22,3389,445 HOST_ПРИМАНКИ
Остановка:
make down
Сервисы определяются в broker/config.yaml. Каждая запись может быть независимо включена/отключена и переназначена:
services:
- name: ssh
enabled: true
listen_port: 22
backend: ssh-decoy:2222
Чтобы добавить сервис, добавьте сюда запись, опубликуйте порт в docker-compose.yml и добавьте контейнер-приманку. Чтобы отключить сервис, установите enabled: false (и по желанию удалите его опубликованный порт).
Обратите внимание, что хост-порт 22 обычно занят реальным SSH-демоном хоста. Для лабораторной работы вы можете переназначить публикуемую сторону в docker-compose.yml, например "2022:22", и направить туда свой сканер.
Все три приманки запускают OpenCanary (Thinkst), настроенные так, чтобы каждый контейнер включал ровно один модуль. Логи выводятся в виде JSON на stdout, поэтому docker compose logs и любой отправитель SIEM работают без дополнительной настройки.
OpenCanary отмечает каждое событие числовым logtype. Вот какие вы увидите:
Захваченные SSH-учётные данные выглядят так:
{"dst_port": 2222, "logtype": 4002, "node_id": "decoy-ssh",
"src_host": "10.0.0.66", "src_port": 42958,
"logdata": {"USERNAME": "admin", "PASSWORD": "Passw0rd123"}}
Это прямое следствие архитектуры брокера и самое важное, что нужно понять при чтении этих логов.
Брокер завершает TCP-соединение атакующего и открывает новое соединение с приманкой. Поэтому с точки зрения OpenCanary клиентом является брокер. Каждый src_host в событии приманки будет адресом брокера в сети decoynet, а не реальным источником.
Настоящий IP-источник всё равно захватывается, но в другом месте:
Таким образом, для атрибуции требуется коррелировать логи брокера с логами приманок, объединяя по временной метке и сервису. Брокер регистрирует remote (настоящий адрес атакующего) и backend для каждого сеанса, что и позволяет выполнить объединение:
docker compose logs broker | grep 'session opened' # кто
docker compose logs ssh-decoy | grep '"logtype": 4002' # что они пытались
Если вам нужен реальный IP внутри самой приманки, варианты: отправить протокол PROXY (приманки его не разбирают, поэтому потребуется их модификация) или заменить пользовательский прокси на прозрачное перенаправление (TPROXY или eBPF bpf_sk_assign), которое сохраняет исходный адрес. Оба варианта перечислены в разделе «План развития». Пока что считайте брокера источником истины для «кто», а приманку — источником истины для «что».
В отличие от SSH и RDP, эта приманка не использует OpenCanary. Модуль smb в OpenCanary — это только наблюдатель логов: он следит за файлом и разбирает строки smbd_audit, генерируемые реальным сервером Samba, что означало запуск Samba + rsyslog + opencanaryd под supervisord — пятизвенная цепочка, где любое звено могло беззвучно отказать.
smb-decoy заменяет всё это одним Python-процессом на основе SimpleSMBServer из Impacket — чистая реализация SMB1/2/3 на Python. Он слушает порт 445, предоставляет приманки-расшаренные папки только для чтения, отвечает на согласование SMB2/3 (поэтому nmap -sV видит реальный сервис) и регистрирует соединения и попытки NTLM-аутентификации в виде одного JSON-объекта на строку в stdout. Захваченные имя пользователя, домен и рабочая станция из сообщения аутентификации атакующего — это основная цель захвата учётных данных.
Замечание по безопасности: smbserver из Impacket содержал критическую уязвимость path traversal, CVE-2021-31800, которая особенно затрагивала honeypot. Она была исправлена в версии 0.9.23. Файл requirements.txt фиксирует текущий релиз, и его нельзя понижать ниже этой версии. Контейнер также работает от имени не-root, с файловой системой только для чтения и со всеми возможностями, кроме NET_BIND_SERVICE.
Каждая приманка имеет собственный opencanary.conf (устанавливается в /etc/opencanaryd/). Полезные параметры:
ssh.version в decoys/ssh/opencanary.conf. В настоящее время он выставляет SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1. Сделайте его соответствующим ОС, которую вы имитируете; баннер Ubuntu на машине, претендующей на Windows, — это разоблачение.addShare(...) в decoys/smb/smb_decoy.py, а также файлы-приманки, создаваемые в decoys/smb/Dockerfile. Имена ресурсов и файлов — это приманка.backend в broker/config.yaml.Чтобы включить другой модуль OpenCanary (ftp, telnet, mysql, vnc, redis и другие доступны), установите <module>.enabled и <module>.port, добавьте контейнер-приманку и добавьте соответствующий сервис в broker/config.yaml.
ssh-decoy монтирует именованный том в /var/lib/opencanary (ssh.key_path), поэтому сгенерированный ключ хоста переживает перезапуски. Без него OpenCanary генерирует новый ключ при каждом запуске, и изменяющийся отпечаток — очевидный признак.
SMB-приманка теперь представляет собой один процесс, поэтому диагностика проста.
docker compose logs -f smb-decoy
Каждая строка — JSON. Вы должны увидеть одно событие smb_decoy_start при запуске, затем события smb_connect, smb_auth_attempt и smb_tree_connect по мере взаимодействия клиентов. Проверьте с хоста любым SMB-клиентом:
# macOS Finder: Переход > Подключиться к серверу
open 'smb://guest@localhost/HR-Payroll'
# или с Linux
smbclient -L //localhost -p 445 -N
Типичные проблемы:
smb_decoy_start, и контейнер завершает работу: проверьте, что requirements.txt установился чисто. Impacket требует Python 3.8+; образ использует 3.12.smb_auth_attempt: некоторые клиенты перечисляют ресурсы анонимно, без аутентификации. Это всё равно даёт smb_connect и smb_tree_connect. Принудительно выполните аутентификацию, подключив ресурс с именем пользователя.src_host — это адрес брокера, а не реального атакующего. Коррелируйте с логами брокера по временной метке.NET_ADMIN (и BPF / PERFMON на новых ядрах) для загрузки и прикрепления программы eBPF. Файл Compose запрашивает эти ограниченные возможности. Если ваш хост или версия Docker их отвергает, запасной вариант — privileged: true для сервиса брокера, что шире и должно использоваться только тогда, когда ограниченные возможности не работают.bpf_sk_assign, исключая необходимость корреляции логов брокера и приманок.MIT. См. LICENSE.
| Контейнер | Роль | Сеть |
|---|
broker | Публичный вход: eBPF-наблюдение + обратный прокси | edge + decoynet |
ssh-decoy | Модуль ssh OpenCanary (реальное рукопожатие, захват учётных данных) | только decoynet |
rdp-decoy | Модуль rdp OpenCanary (имитация NLA, захват имён пользователей) | только decoynet |
smb-decoy | SimpleSMBServer из Impacket (реальный SMB2/3, захват аутентификации) | только decoynet |
| Контейнер | Модуль OpenCanary | Слушает | Что он на самом деле делает |
|---|
ssh-decoy | ssh | 2222 | Реальный обмен ключами SSH через twisted.conch. Захватывает каждую пару имя пользователя/пароль. |
rdp-decoy | rdp | 3389 | Имитирует сервер с NLA, всегда возвращает ошибку входа, извлекает имя пользователя mstshash. |
smb-decoy | Impacket | 445 | Чисто Python SMB2/3 сервер. Представляет приманки-расшаренные папки и регистрирует соединения и попытки NTLM-аутентификации в виде JSON. |
| logtype | Константа в opencanary/logger.py | Значение |
|---|
| 1000 | LOG_BASE_BOOT | Запуск демона |
| 4000 | LOG_SSH_NEW_CONNECTION | Открыто SSH-соединение |
| 4001 | LOG_SSH_REMOTE_VERSION_SENT | Клиент отправил строку версии |
| 4002 | LOG_SSH_LOGIN_ATTEMPT | Попытка входа по SSH (включает USERNAME и PASSWORD) |
| 5000 | LOG_SMB_FILE_OPEN | Открыт файл SMB (включает USER, SHARENAME, FILENAME) |
| 14001 | LOG_RDP | Соединение RDP / попытка входа |
| Уровень | Знает реальный IP-источник? | Знает, что было предпринято? |
|---|
Классификатор eBPF (probe observed) | Да | Нет, только метаданные SYN |
Прокси брокера (session opened) | Да | Нет, только количество байт |
Приманка OpenCanary (logtype 4002) | Нет | Да, учётные данные/файлы |