
실험적 유인 브로커
컨테이너화된 네트워크 디코이(허니팟)로, SSH, RDP 및 SMB를 광고하고, eBPF로 모든 인바운드 연결을 관찰하며, 각 세션을 격리된 디코이 컨테이너로 리버스 프록시합니다.
설계는 두 가지 관심사를 분리합니다:
이는 귀하가 소유하거나 모니터링할 권한이 있는 네트워크에서 무단 활동을 탐지하고 연구하기 위한 방어 도구입니다. 해당 권한이 있는 경우에만 배포하십시오.
flowchart TB
A["공격자 / 스캐너"]
subgraph host["디코이 호스트"]
direction TB
NIC["broker eth0<br/>게시됨: 22, 3389, 445"]
subgraph brk["broker 컨테이너"]
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
총 4개의 컨테이너:
디코이는 호스트 또는 외부 세계로의 경로가 없는 내부 Docker 네트워크(decoynet)에 있습니다. 브로커만이 디코이에 도달할 수 있습니다. 공격자가 디코이 내부에서 하는 어떤 것도 호스트 네트워크에 직접 도달할 수 없습니다.
브로커는 호스트에 포트 22, 3389 및 445를 게시하므로 인바운드 패킷은 브로커의 eth0에 도착합니다. 그런 다음 각 패킷에 대해 두 가지 작업이 발생합니다:
broker/bpf/decoy.bpf.c)은 이더넷, IP 및 TCP 헤더를 구문 분석하고 각 새 연결 시도(SYN 설정, ACK 지움)에 대해 conn_event를 링 버퍼에 기록합니다: 소스 IP 및 포트, 대상 포트, TCP 플래그, 포트가 광고된 서비스인지 여부. 패킷은 변경 없이 통과됩니다(TC_ACT_OK).CONNECT와 동등한 작업을 수행한 다음 양방향으로 바이트를 릴레이합니다.advertised_ports eBPF 맵은 시작 시 config.yaml에서 채워지므로 분류기는 프로브가 제공된 포트에 도달했는지 원치 않는 포트에 도달했는지 태그할 수 있습니다. 이를 통해 세 개의 포트만 프록시되더라도 수평 포트 스캔이 보이게 됩니다.
"모든 것이 열려 있다"고 광고하고 임의의 대상 포트를 브로커로 보내려면 분류기를 확장하여 대상 포트를 다시 쓰거나 TPROXY / bpf_sk_assign 리디렉션을 사용하십시오. 현재 버전은 패킷 경로를 그대로 유지하고 관찰에만 제한되며, 이는 더 안전한 기본값입니다.
cyber-decoy/
├── README.md
├── docker-compose.yml # 4-컨테이너 스택
├── docker-compose.override.yml # 로컬 macOS 개발: eBPF cap 없음, 포트 22 재매핑
├── Makefile # build / up / down / bpf 도우미
├── 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 프로세스, 비루트
├── smb_decoy.py # Impacket SimpleSMBServer + 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 후보가 없으며, 이를 포함하면 apt 종료 코드 100으로 arm64에서 빌드가 중단됩니다. eBPF 오브젝트를 컴파일하는 데는 clang 및 libbpf-dev만 있으면 됩니다.
macOS의 Docker Desktop은 호스트 커널이 아닌 LinuxKit VM 내부에서 컨테이너를 실행하므로 TC/TCX eBPF 연결은 일반적으로 작동하지 않습니다. 이는 치명적이지 않습니다. eBPF는 설계상 최선의 노력이므로 브로커는 ebpf disabled: attach failed를 기록하고 리버스 프록시와 세 개의 디코이가 정상적으로 실행되고 로그를 기록합니다. 로컬에서 전체 프록시 경로를 개발하고 테스트한 다음 Linux 호스트에 배포할 때 실제 eBPF 관찰을 얻을 수 있습니다.
docker-compose.override.yml은 자동으로 로드되어 이를 편리하게 만듭니다. eBPF 기능(VM에서 쓸모없음)을 제거하고 macOS의 자체 sshd가 22를 소유하므로 호스트 포트 22를 2022로 다시 매핑합니다.
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
그런 다음 다른 머신(또는 로컬호스트에서 연기 테스트)에서 프로브하십시오:
ssh -p 22 user@DECOY_HOST # SSH 디코이에 도달
nc DECOY_HOST 3389 # RDP 디코이에 도달
nc DECOY_HOST 445 # SMB 디코이에 도달
nc DECOY_HOST 8080 # 광고되지 않음: eBPF가 관찰, 프록시 없음
브로커는 eBPF 프로브 이벤트 및 프록시 세션에 대한 JSON을 발행합니다. 각 디코이는 OpenCanary JSON 이벤트를 발행합니다. 자격 증명이 도착하는 것을 보려면:
docker compose logs -f ssh-decoy | grep 4002
배너만 제공하는 스텁과 달리, 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 데몬이 사용합니다. 실험실의 경우 docker-compose.yml에서 게시된 쪽을 재매핑할 수 있습니다(예: "2022:22"). 스캐너를 거기로 지정하십시오.
세 디코이 모두 OpenCanary(Thinkst)를 실행하며, 각 컨테이너가 정확히 하나의 모듈을 활성화하도록 구성됩니다. 로그는 stdout에 JSON으로 발행되므로 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를 사용하지 않습니다. OpenCanary의 smb 모듈은 로그 감시자일 뿐입니다. 파일을 테일링하고 실제 Samba 서버가 내보낸 smbd_audit 라인을 구문 분석합니다. 즉, Samba + rsyslog + opencanaryd를 supervisord 아래에서 실행해야 하며, 어떤 링크든 조용히 실패할 수 있는 5개 연결 체인입니다.
smb-decoy는 Impacket의 SimpleSMBServer(SMB1/2/3의 순수 Python 구현)를 기반으로 하는 단일 Python 프로세스로 이를 대체합니다. 포트 445에 바인딩하고 읽기 전용 미끼 공유를 제공하며 SMB2/3 협상에 응답하여 nmap -sV가 실제 서비스를 보게 하고, 연결 및 NTLM 인증 시도를 한 줄에 하나의 JSON 객체로 stdout에 로깅합니다. 공격자의 인증 메시지에서 캡처된 사용자 이름, 도메인 및 워크스테이션은 자격 증명 캡처의 보상입니다.
보안 참고: Impacket의 smbserver는 허니팟에 특히 영향을 미치는 중요 경로 탐색 취약점인 CVE-2021-31800을 가지고 있었습니다. 0.9.23에서 수정되었습니다. requirements.txt는 현재 릴리스를 고정하며 그 아래로 다운그레이드되어서는 안 됩니다. 또한 컨테이너는 비루트, 읽기 전용으로 실행되며 NET_BIND_SERVICE를 제외한 모든 기능이 삭제됩니다.
각 디코이는 opencanary.conf를 소유합니다(/etc/opencanaryd/에 설치됨). 유용한 조정:
decoys/ssh/opencanary.conf의 ssh.version. 현재 SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1을 주장합니다. 가장하려는 OS와 일치시키십시오; Windows라고 주장하는 상자에 Ubuntu 배너가 있으면 눈에 띕니다.decoys/smb/smb_decoy.py의 addShare(...) 호출 및 decoys/smb/Dockerfile에서 생성된 미끼 파일. 공유 이름과 파일 이름이 유인물입니다.broker/config.yaml의 backend와 정렬하십시오.다른 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)이 필요합니다. Compose 파일은 이러한 범위의 기능을 요청합니다. 호스트 또는 Docker 버전이 이를 거부하는 경우 대체 방법은 브로커 서비스에서 privileged: true를 사용하는 것입니다. 이는 더 넓은 범위이며 범위 지정 기능이 작동하지 않을 때만 사용해야 합니다.internal 네트워크에 있습니다. 그 상태를 유지하십시오. 모든 디코이 컨테이너를 잠재적으로 손상된 것으로 취급하십시오.bpf_sk_assign을 통해 공격자의 소스 IP를 디코이 내부로 보존하여 브로커 및 디코이 로그를 상관시킬 필요를 없앱니다.MIT. 자세한 내용은 LICENSE를 참조하십시오.
| 컨테이너 | 역할 | 네트워크 |
|---|
broker | 공용 전면 도어: eBPF 관찰 및 리버스 프록시 | edge + decoynet |
ssh-decoy | OpenCanary ssh 모듈 (실제 핸드셰이크, 자격 증명 캡처) | decoynet 전용 |
rdp-decoy | OpenCanary rdp 모듈 (NLA 모방, 사용자 이름 캡처) | decoynet 전용 |
smb-decoy | Impacket SimpleSMBServer (실제 SMB2/3, 인증 캡처) | decoynet 전용 |
| 컨테이너 | OpenCanary 모듈 | 수신 포트 | 실제 수행 작업 |
|---|
ssh-decoy | ssh | 2222 | twisted.conch를 통한 실제 SSH 키 교환. 모든 사용자 이름/암호 쌍을 캡처합니다. |
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) | 아니요 | 예, 자격 증명/파일 |