secdev02/cyber-decoy

GitHub: secdev02/cyber-decoy

一个基于 eBPF 和 Docker 的实验性网络蜜罐系统,对外模拟 SSH/RDP/SMB 服务,在不改动数据包的前提下观察并记录所有入站探测行为和攻击交互。

Stars: 3 | Forks: 0

# cyber-decoy 一个容器化的网络诱饵(honeypot),对外宣告提供 **SSH**、**RDP** 和 **SMB** 服务,使用 **eBPF** 观察每一个入站连接,并将每个会话反向代理到一个隔离的诱饵容器中。 该设计分离了两个关注点: 1. **观察。** 附加到 broker 接口的 eBPF TC 分类器会记录每一个入站 TCP SYN,包括对诱饵未提供服务端口的扫描。这提供了对探测活动的完整可见性。 2. **交互。** broker 中的用户空间反向代理在宣告的端口上接受连接,并为该服务打开一个到诱饵容器的匹配连接,在双向传输字节流并记录完整的会话。 ## 架构 ``` flowchart TB A["Attacker / Scanner"] subgraph host["Decoy Host"] direction TB NIC["broker eth0
published: 22, 3389, 445"] subgraph brk["broker container"] direction TB E["eBPF TC classifier
logs every SYN
sees true source IP"] P["reverse proxy
CONNECT to backend"] L["structured JSON logs"] end subgraph dec["decoynet (internal, no host route)"] direction LR S["ssh-decoy
OpenCanary ssh
port 2222"] D["rdp-decoy
OpenCanary rdp
port 3389"] M["smb-decoy
Impacket SMB server
port 445"] end end A --> NIC NIC --> E NIC --> P E --> L P --> S P --> D P --> M ``` 总共有四个容器: | 容器 | 角色 | 网络 | |-------------|-------------------------------------------------------------|----------------| | `broker` | 公开门户:eBPF 观察与反向代理 | edge + decoynet | | `ssh-decoy` | OpenCanary `ssh` 模块(真实握手,捕获凭据) | 仅 decoynet | | `rdp-decoy` | OpenCanary `rdp` 模块(NLA 模拟,捕获用户名) | 仅 decoynet | | `smb-decoy` | Impacket SimpleSMBServer(真实 SMB2/3,捕获认证) | 仅 decoynet | 这些诱饵位于一个 `internal` Docker 网络(`decoynet`)中,没有通往宿主机或外部世界的路由。只有 broker 可以访问它们。攻击者在诱饵内部所做的任何事情都无法直接触及宿主机网络。 ## eBPF 路由的工作原理 broker 向宿主机发布端口 22、3389 和 445,因此入站数据包会到达 broker 的 `eth0`。然后每个数据包会经历两件事情: - **eBPF TC ingress 程序**(`broker/bpf/decoy.bpf.c`)解析 Ethernet、IP 和 TCP 头,并针对每个新的连接尝试(设置了 SYN,清除了 ACK),将一个 `conn_event` 写入环形缓冲区:源 IP 和端口、目标端口、TCP flags,以及该端口是否为已宣告的服务。数据包原封不动地通过(`TC_ACT_OK`)。 - **用户空间代理**在匹配的监听器上接受连接,并对该服务的诱饵后端执行相当于 `CONNECT` 的操作,然后在两个方向上转发字节流。 `advertised_ports` eBPF map 在启动时从 `config.yaml` 填充,因此分类器可以标记探测是命中了已服务的端口还是未经请求的端口。这使得水平端口扫描变得可见,即使只代理了三个端口。 如果你想宣告“一切皆已开放”并将任意目标端口汇集到 broker,请扩展分类器以重写目标端口,或使用 TPROXY / `bpf_sk_assign` 重定向。当前版本保持数据包路径不变,仅限于观察,这是更安全的默认设置。 ## 仓库布局 ``` cyber-decoy/ ├── README.md ├── docker-compose.yml # 4-container stack ├── docker-compose.override.yml # local macOS dev: no eBPF caps, port 22 remap ├── Makefile # build / up / down / bpf helpers ├── LICENSE ├── scripts/ │ └── setup.sh # host preflight checks ├── broker/ │ ├── Dockerfile # compiles eBPF object + Go binary │ ├── config.yaml # advertised services (configurable) │ ├── go.mod │ ├── main.go # entrypoint │ ├── bpf/ │ │ └── decoy.bpf.c # eBPF TC classifier │ └── internal/ │ ├── config/config.go # config loader │ ├── proxy/proxy.go # TCP reverse proxy │ └── bpf/loader.go # loads + attaches eBPF, streams events └── decoys/ # all three run OpenCanary ├── ssh/ │ ├── Dockerfile │ └── opencanary.conf # ssh module, port 2222 ├── rdp/ │ ├── Dockerfile │ └── opencanary.conf # rdp module, port 3389 └── smb/ ├── Dockerfile # single Python process, non-root ├── smb_decoy.py # Impacket SimpleSMBServer + JSON logging └── requirements.txt # impacket (pinned) ``` ## 环境要求 - Linux 宿主机,具有 **6.6 或更新的内核**,以支持 TCX eBPF 附加路径。在较旧的内核上,代理仍然会运行;只有 eBPF 观察会被跳过(broker 会记录一条警告并继续运行)。 - 带有 Compose 插件的 Docker Engine(如果你使用捆绑的 `docker-compose.override.yml`,则需 **v2.24+**,因为它依赖于 `!reset` / `!override` 标签)。 - 一个已挂载的 BPF 文件系统:`sudo mount -t bpf bpf /sys/fs/bpf`。 ### 架构 broker 镜像会检测其构建架构,并将匹配的 `__TARGET_ARCH_*` 宏传递给 clang,因此它可以在 `x86_64` 和 `aarch64`(Apple Silicon、Graviton)上构建。请注意,`gcc-multilib` 特意 **没有** 被安装:它是一个仅限 x86 的包,没有 arm64 的候选版本,包含它会因 apt 退出代码 100 而破坏 arm64 上的构建。编译 eBPF 对象只需要 `clang` 和 `libbpf-dev`。 ### 在 macOS 上开发 macOS 上的 Docker Desktop 在 LinuxKit 虚拟机内运行容器,而不是在你的宿主机内核上运行,因此 TC/TCX eBPF 挂载通常在 macOS 上 **不起作用**。这并不是致命的:eBPF 在设计上是尽力而为的,因此 broker 会记录 `ebpf disabled: attach failed`,反向代理和所有三个诱饵都会正常运行并记录日志。你可以在本地开发和测试整个代理路径,然后在你部署到 Linux 宿主机时获得真正的 eBPF 观察。 `docker-compose.override.yml` 会被自动加载并使这变得轻松:它会移除 eBPF capabilities(在虚拟机中无用),并将宿主机端口 22 重新映射到 2022,因为 Mac 自身的 sshd 占用了 22 端口。 ``` docker compose up --build # local dev, override applied docker compose -f docker-compose.yml up -d # real deployment, override bypassed ``` 首先运行预检: ``` ./scripts/setup.sh ``` ## 快速开始 ``` # 1. 构建全部四个镜像(在 broker 镜像内编译 eBPF 对象) make build # 2. 启动 stack make up # 3. 观察发生的情况 make logs ``` 然后从另一台机器(或 localhost 进行冒烟测试)探测它: ``` ssh -p 22 user@DECOY_HOST # hits the SSH decoy nc DECOY_HOST 3389 # hits the RDP decoy nc DECOY_HOST 445 # hits the SMB decoy nc DECOY_HOST 8080 # unadvertised: observed by eBPF, no proxy ``` broker 为 eBPF 探测事件和代理会话发出 JSON;每个诱饵发出 OpenCanary JSON 事件。要查看捕获的凭据: ``` docker compose logs -f ssh-decoy | grep 4002 ``` 与仅包含 banner 的存根不同,`ssh -p 22 user@DECOY_HOST` 现在会完成真实的密钥交换并提示输入密码。每一次尝试都会被捕获。验证服务指纹在版本检测下是否依然成立: ``` nmap -sV -p 22,3389,445 DECOY_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 daemon 占用。对于测试环境,你可以在 `docker-compose.yml` 中重新映射发布的端口,例如 `"2022:22"`,并将你的扫描器指向那里。 ## 诱饵后端 所有三个诱饵都运行 [OpenCanary](https://github.com/thinkst/opencanary) (Thinkst),其配置使得每个容器只启用一个模块。日志以 JSON 格式输出到 stdout,因此 `docker compose logs` 和任何 SIEM 发送器无需额外配置即可工作。 | 容器 | OpenCanary 模块 | 监听 | 它的实际作用 | |-------------|-------------------|---------|-----------------------| | `ssh-decoy` | `ssh` | 2222 | 通过 twisted.conch 进行真实的 SSH 密钥交换。捕获每一对用户名/密码。 | | `rdp-decoy` | `rdp` | 3389 | 模拟启用 NLA 的服务器,始终返回登录失败,并提取 `mstshash` 用户名。 | | `smb-decoy` | Impacket | 445 | 纯 Python 实现的 SMB2/3 服务器。展示诱饵共享,并以 JSON 格式记录连接和 NTLM 认证尝试。 | ### 事件类型 OpenCanary 用一个数字 `logtype` 标记每个事件。你将在这里看到的包括: | logtype | `opencanary/logger.py` 中的常量 | 含义 | |---------|------------------------------------|---------| | 1000 | `LOG_BASE_BOOT` | Daemon 启动 | | 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 连接 / 登录尝试 | 一个被捕获的 SSH 凭据如下所示: ``` {"dst_port": 2222, "logtype": 4002, "node_id": "decoy-ssh", "src_host": "10.0.0.66", "src_port": 42958, "logdata": {"USERNAME": "admin", "PASSWORD": "Passw0rd123"}} ``` ### 重要提示:诱饵无法看到攻击者的 IP 这是 broker 架构的直接后果,也是阅读这些日志时需要理解的最重要的一点。 broker 终止攻击者的 TCP 连接,并打开一个到诱饵的 **新** 连接。所以从 OpenCanary 的角度来看,客户端就是 broker。诱饵事件中的每一个 `src_host` 都将是 broker 在 `decoynet` 上的地址,而不是真实的源地址。 真实的源 IP 仍然会被捕获,只是在不同的位置: | 层级 | 知道真实的源 IP 吗? | 知道尝试了什么吗? | |-------|---------------------------|---------------------------| | eBPF 分类器(`probe observed`) | 是 | 否,仅有 SYN 元数据 | | Broker 代理(`session opened`) | 是 | 否,仅有字节计数 | | OpenCanary 诱饵(`logtype 4002`) | **否** | 是,凭据/文件 | 因此,溯源需要将 broker 日志与诱饵日志进行关联,通过时间戳和服务进行连接。broker 为每个会话记录 `remote`(真实的攻击者地址)和 `backend`,这就是使连接成为可能的原因: ``` docker compose logs broker | grep 'session opened' # who docker compose logs ssh-decoy | grep '"logtype": 4002' # what they tried ``` 如果你需要在诱饵内部获取真实的 IP,可选的方案是发送 PROXY protocol(诱饵不解析它,因此这意味着需要对它们打补丁),或者将用户空间代理替换为保留原始源地址的透明重定向(TPROXY 或 eBPF `bpf_sk_assign`)。这两项都已列在路线图中。在此之前,请将 broker 视为“是谁”的真相来源,而将诱饵视为“做了什么”的真相来源。 ### SMB 诱饵(Impacket,而不是 Samba) 与 SSH 和 RDP 不同,这个诱饵不使用 OpenCanary。OpenCanary 的 smb 模块仅仅是一个日志监视器:它会追踪一个文件并解析由真实 Samba 服务器发出的 `smbd_audit` 行,这意味着要在 supervisord 下运行 Samba 加上 rsyslog 以及 opencanaryd,这是一个由五个环节组成的链条,其中任何一个环节都可能悄无声息地发生故障。 `smb-decoy` 用一个基于 [Impacket](https://github.com/fortra/impacket) 的 `SimpleSMBServer` 构建的单个 Python 进程取代了所有这些。 它是 SMB1/2/3 的纯 Python 实现。它绑定 445 端口,展示只读的诱饵共享,响应 SMB2/3 协商(因此 `nmap -sV` 会看到一个真实的服务),并以每行一个 JSON 对象的形式在 stdout 上记录连接和 NTLM 认证尝试。从攻击者的认证消息中捕获的用户名、域和工作站就是凭据捕获的成果。 安全提示:Impacket 的 smbserver 曾携带过一个严重的路径遍历漏洞, [CVE-2021-31800](https://checkmarx.com/blog/cve-2021-31800-how-we-used-impacket-to-hack-itself/), 它特别影响 honeypot。它在 0.9.23 版本中被修复了。`requirements.txt` 固定了一个当前的发行版本,绝不能降级到该版本以下。该容器也以非 root 用户、只读方式运行,并移除了除 NET_BIND_SERVICE 之外的所有 capabilities。 ### 配置诱饵 每个诱饵都拥有一个 `opencanary.conf`(安装到 `/etc/opencanaryd/`)。一些有用的调节项: - **SSH banner**:`decoys/ssh/opencanary.conf` 中的 `ssh.version`。目前它声明为 `SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1`。让它与你伪装的操作系统相匹配;一台声称是 Windows 的机器上却带有 Ubuntu 的 banner 是一个破绽。 - **SMB 共享名**:`decoys/smb/smb_decoy.py` 中的 `addShare(...)` 调用,以及在 `decoys/smb/Dockerfile` 中创建的诱饵文件。共享名和文件名就是诱饵。 - **端口**:保持这些与 `broker/config.yaml` 中的 `backend` 对齐。 要启用另一个 OpenCanary 模块(可用的有 ftp、telnet、mysql、vnc、redis 等),请设置 `.enabled` 和 `.port`,添加一个诱饵容器,并在 `broker/config.yaml` 中添加匹配的服务。 ### SSH 主机密钥持久化 `ssh-decoy` 在 `/var/lib/opencanary`(`ssh.key_path`)挂载了一个命名卷,因此生成的 SSH 主机密钥在重启后依然保留。如果没有它,OpenCanary 每次启动时都会生成一个新密钥,而不断变化的指纹明显的破绽。 ## SMB 诱饵故障排除 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` 是 broker 的地址,而不是真实的攻击者。通过时间戳与 broker 日志进行关联。 ## 安全注意事项 - **Capabilities。** broker 需要 `NET_ADMIN`(以及在较新内核上的 `BPF` / `PERFMON`)来加载和附加 eBPF 程序。Compose 文件请求了这些范围受限的 capabilities。如果你的宿主机或 Docker 版本拒绝了它们,备选方案是在 broker 服务上使用 `privileged: true`,这权限范围更广,应仅在受限的 capabilities 不起作用时使用。 - **隔离。** 诱饵位于一个没有宿主机路由的 `internal` 网络上。保持这种状态不变。将每个诱饵容器视为可能已被攻破。 - **影响范围。** 将整个堆栈运行在与生产环境隔离的宿主机上。诱饵就是诱饵;假设攻击者会与其进行交互。 - **法律。** 仅在你拥有或被授权防御的基础设施上进行监控和欺骗。 ## 路线图设想 - 通过 TPROXY 或 `bpf_sk_assign` 将攻击者的源 IP 保留到诱饵中,从而无需关联 broker 和诱饵日志。 - 通过 eBPF 目标重写或 TPROXY 实现全端口汇集。 - 将会话捕获为每个连接对应的 PCAP。 - 将事件发送到 SIEM(JSON 日志已经为此进行了结构化)。 - broker 中的速率限制和连接配额。 ## 许可证 MIT。详见 [LICENSE](LICENSE)。
标签:ADCS攻击, Docker镜像, EVTX分析, NIDS, 安全, 容器化, 插件系统, 日志审计, 版权保护, 蜜罐, 证书利用, 请求拦截, 超时处理, 逆向代理, 逆向工具