Building a Small Rust eBPF EDR 5: Optional Network Telemetry
목차
이번 글에서는 rand-guard의 network telemetry를 다룬다. 네트워크 이벤트는 reverse shell, suspicious listener, unusual outbound connection 같은 행위를 이해하는 데 도움이 된다. 하지만 network telemetry는 noise가 많고 오해하기도 쉽다.
그래서 rand-guard는 network collection을 기본값으로 켜지 않는다. 사용자가 명시적으로 켠 경우에만 connect, bind, listen syscall tracepoint를 attach한다.
network는 opt-in이다
기본 설정은 다음과 같다.
[events]
network = false
[network]
enabled = false
hooks = ["connect", "bind", "listen"]
collect_dns = false
collect_payload = false
network event를 수집하려면 두 값을 모두 켜야 한다.
[events]
network = true
[network]
enabled = true
hooks = ["connect", "bind", "listen"]
collect_dns = false
collect_payload = false
이중 flag는 실수로 network telemetry를 켜는 일을 줄이기 위한 장치다. network event는 process/file event보다 민감할 수 있고, first-run noise도 크다.
수집 대상
현재 구현은 crates/ebpf/src/network.rs에 있다.
sys_enter_connectsys_enter_bindsys_enter_listen
connect와 bind는 sockaddr pointer를 받아 address family, port, address를 읽는다. listen은 fd와 backlog를 수집하지만, 현재 socket lifecycle table이 없기 때문에 어떤 address와 port에 bind되었는지 완전히 연결하지 않는다.
이 제한은 중요하다. listen 이벤트만 보고 listener의 port를 완전하게 복원한다고 쓰면 안 된다. full socket lifecycle correlation은 아직 구현하지 않았다.
sockaddr parsing
eBPF program은 sockaddr의 address family를 먼저 읽는다. 현재는 AF_INET과 AF_INET6을 제한적으로 parsing한다.
IPv4에서는 port와 IPv4 address를 읽는다. IPv6에서는 port와 16-byte IPv6 address를 읽는다. 모르는 family에서는 address metadata를 채우지 않고 unknown family로 남긴다.
이 parsing은 bounded read로 제한된다. sockaddr pointer가 null이거나 length가 부족하면 event를 버린다. 불완전한 network event를 그럴듯하게 출력하지 않기 위한 선택이다.
방향성 표현
normalized network event는 대략 세 종류다.
network_connectnetwork_bindnetwork_listen
connect는 outbound direction으로 본다. bind와 listen은 listener 관점의 이벤트다. userspace output에는 family, socket fd, remote/local address, port, backlog 같은 필드가 들어갈 수 있다.
예시는 다음과 같은 형태다.
{"event_type":"network_connect","comm":"nc","family":"ipv4","remote_addr":"127.0.0.1","remote_port":4444}
실제 record에는 timestamp, pid, uid, gid, process enrichment, detection metadata가 더 들어간다.
suspicious port detection
config에는 suspicious network port detection이 있다.
[[detections.network]]
name = "suspicious_outbound_port"
directions = ["outbound"]
ports = [4444, 1337, 31337]
이 detection은 direction과 port를 비교한다. 선택적으로 process_names를 넣으면 특정 process name에 대해서만 match되도록 제한할 수 있다. false positive를 줄이는 데 도움이 된다.
하지만 port만으로 공격을 확정할 수는 없다. 4444는 netcat lab, CTF, local test, debug tunnel에서도 자주 쓰인다. 그래서 이 detection은 “suspicious”라는 이름을 가지지만, 결과는 확정 판정이 아니라 investigation signal이다.
demo scenario
network collection을 켜고 agent를 restart한 뒤, loopback에서 listener와 client를 실행할 수 있다.
nc -l 127.0.0.1 4444
다른 터미널에서 연결한다.
printf 'demo\n' | nc 127.0.0.1 4444
출력에는 network_listen이나 network_connect record가 포함될 수 있다. suspicious port detection이 4444를 포함하고 있으면 detection metadata나 별도 alert record가 나올 수 있다.
이 demo도 공격 실행이 아니다. loopback에서 network telemetry가 어떻게 출력되는지 확인하는 안전한 시나리오다.
구현하지 않은 것들
network telemetry는 특히 과장하기 쉬운 영역이다. 현재 rand-guard가 구현하지 않은 기능을 분명히 적어야 한다.
- DNS collection은 없다.
- payload collection은 없다.
- TLS visibility는 없다.
accept와accept4telemetry는 없다.- socket lifecycle correlation은 없다.
- bind와 listen을 연결하는 socket table enrichment는 없다.
이 제한 때문에 현재 network event만으로 reverse shell을 확정 탐지할 수 없다. shell process가 suspicious port로 outbound connect를 했다는 signal은 의미가 있지만, 그것만으로 attack conclusion을 내릴 수는 없다.
5편 정리
이번 글에서는 network telemetry를 살펴봤다.
- network collection은 기본 비활성화이며 명시적으로 켜야 한다.
- 현재 수집 대상은
connect,bind,listensyscall tracepoint다. - IPv4와 IPv6 sockaddr를 bounded 방식으로 parsing한다.
- suspicious port detection은 investigation signal이다.
- DNS, payload, accept, socket lifecycle correlation은 구현하지 않았다.
다음 글에서는 MVP rule engine을 다룬다. built-in detection과 config 기반 [[rules]]가 어떻게 normalized event를 평가하고 stable alert record를 만드는지 살펴본다.