

datadog的eBPF安全检测机制分析
source link: https://www.cnxct.com/how-does-datadog-use-ebpf-in-runtime-security/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文目录 [显示]
代码版本为 datadog-Agent 2021年12月9日的 c9ddf6e854276cfc91aabe76415a203e17de5355
工程视角分析
invoke是python的任务管理工具,在这个项目里重度依赖,需要提前安装好。
python管理编译
invoke agent.build
是官方文档介绍中用来编译项目的命令。invoke --list
会显示当前项目所有可使用的命令列表。该命令会读取tasks
目录下python的脚本,读取可执行函数。
本文重点在于eBPF相关功能实现,故只列出eBPF相关命令
cfc4n@vmubuntu:~/project/datadog-agent$ invoke --list
Available tasks:
...
system-probe.build Build the system_probe
system-probe.clang-format Format C code using clang-format
system-probe.clang-tidy Lint C code using clang-tidy
system-probe.generate-cgo-types
system-probe.generate-runtime-files
system-probe.nettop Build and run the `nettop` utility for testing
system-probe.object-files object_files builds the eBPF object files
system-probe.test Run tests on eBPF parts
...
通过阅读源码、查阅资料,确定核心的eBPF源码都在system-probe.build
命令中生成,并编译成.o的eBPF字节码。这个命令,会调用tasks\system_probe.py
中build
函数,再调用build_object_files
来生成所有probe的eBPF字节码。这些probe字节码按照业务类型来划分,可以分为网络
、runtime
两类。
网络probe编译
build_network_ebpf_files
函数会调用clang命令,编译链接pkg/network/ebpf/c/prebuild
目录下4个文件,生成对应的bc
与o
文件
- dns.c
- http.c
- offset-guess.c
- tracer.c
runtime probe编译
合并生成runtime-security.c
generate_runtime_files
函数会调用go命令,合并生成runtime-security.c
文件。
合并的命令是调用了go generate
命令,选择tags参数为linux_bpf
go generate -mod=mod -tags linux_bpf ./pkg/collector/corechecks/ebpf/probe/oom_kill.go
go generate -mod=mod -tags linux_bpf ./pkg/collector/corechecks/ebpf/probe/tcp_queue_length.go
go generate -mod=mod -tags linux_bpf ./pkg/network/http/compile.go
go generate -mod=mod -tags linux_bpf ./pkg/network/tracer/compile.go
go generate -mod=mod -tags linux_bpf ./pkg/network/tracer/connection/kprobe/compile.go
go generate -mod=mod -tags linux_bpf ./pkg/security/probe/compile.go
在这些go文件的头部,有相应的go:generate
指令,调用根目录的pkg/ebpf/include_headers.go
来合并对应文件,并保存到pkg/ebpf/bytecode/build/runtime/
目录下,生成的文件包括如下几个C文件。
- conntrack.c
- http.c
- oom-kill.c
- runtime-security.c
- tcp-queue-length.c
- tracer.c
同时,在pkg/ebpf/bytecode/runtime/
目录下生成相应的.go
文件。
编译链接eBPF字节码
bindata_files.extend(build_security_ebpf_files(ctx, build_dir=build_dir, parallel_build=parallel_build))
system_probe.py
的613行会调用的build_security_ebpf_files
函数,调用外部命令clang编译生产bc文件,并调用llc链接bc文件生产.o的字节码。
clang -D__KERNEL__ -DCONFIG_64BIT -D__BPF_TRACING__ -DKBUILD_MODNAME=\\"ddsysprobe\\" -Wno-unused-value -Wno-pointer-sign -Wno-compare-distinct-pointer-types -Wunused -Wall -Werror -include ./pkg/ebpf/c/asm_goto_workaround.h -O2 -emit-llvm -fno-stack-protector -fno-color-diagnostics -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-jump-tables -I./pkg/ebpf/c -I./pkg/security/ebpf/c -DUSE_SYSCALL_WRAPPER=0 -c \'./pkg/security/ebpf/c/prebuilt/probe.c\' -o \'./pkg/ebpf/bytecode/build/runtime-security.bc
bundle_files
函数会对上面生成的文件一并进行编译链接。
go run github.com/shuLhan/go-bindata/cmd/go-bindata -tags 'ebpf_bindata' -split -pkg bindata -prefix 'pkg/.*/' -modtime 1 -o './pkg/ebpf/bytecode/bindata' './pkg/ebpf/bytecode/build/runtime-security.o' './pkg/ebpf/bytecode/build/runtime-security-syscall-wrapper.o'
probe 模块编译
go build -mod=mod -v -a -tags "kubeapiserver python zk ec2 npm apm consul orchestrator systemd process jetson cri zlib containerd jmx podman clusterchecks netcgo secrets docker gce etcd kubelet linux_bpf" -o ./bin/system-probe/system-probe -gcflags="" -ldflags="-X github.com/DataDog/datadog-agent/pkg/version.Commit=c9ddf6e85 -X github.com/DataDog/datadog-agent/pkg/version.AgentVersion=7.34.0-devel+git.124.c9ddf6e -X github.com/DataDog/datadog-agent/pkg/serializer.AgentPayloadVersion=v5.0.4 -X github.com/DataDog/datadog-agent/pkg/config.ForceDefaultPython=true -X github.com/DataDog/datadog-agent/pkg/config.DefaultPython=3 " github.com/DataDog/datadog-agent/cmd/system-probe
从编译参数可以看出,核心的probe功能都在 cmd/system-probe 这个包里,这个包会独立编译成一个进程,也就是说,我们分析的ebpf使用相关功能都在这个包里。
datadog的probe hook点
在cmd/system-probe
包里,一共5个模块分别是
- NetworkTracerModule ModuleName = "network_tracer"
- OOMKillProbeModule ModuleName = "oom_kill_probe"
- TCPQueueLengthTracerModule ModuleName = "tcp_queue_length_tracer"
- SecurityRuntimeModule ModuleName = "security_runtime"
- ProcessModule ModuleName = "process"
同时,上面编译生产的eBPF字节码文件中,只有6个,分别是
- dns.o / dns-debug.o
- http.o / http-debug.o
- offset-guest.o / offset-guest-debug.o
- runtime-security.o
- runtime-security-syscell-wrapper.o
- tracer.o /tracer-debug.o
从文件名来看,并不能与模块名一一匹配,那么,他们是如何加载的呢?
NetworkTracerModule模块
eBPF字节码加载
网络跟踪模块tracer.NewTracer
初始化时,判断配置中是否启用运行时编译配置,若启用,则调用runtime.Tracer.Compile
进行源码编译,应该是暂时没实现CO-RE,需要每个主机上进行一次编译。
如果没启用运行时编译,则调用了netebpf.ReadOffsetBPFModule
来加载之前编译生成的offset-guess.o
,如果启动了调试模式,则加载offset-guess-debug.o
。
模块管理器初始化
newManager
函数初始化manager.Manager
结构体时,初始化了eBPF maps跟eBPF probe列表。
probe
probe列表覆盖TCP、UDP的链接创建、发送数据、关闭链接等事件(包含入口、出口),支持IPv4\IPv6,并针对linux kernel 4.7以上版本做了更多HOOK点。
从安全场景来看,建模所需数据,也满足网络后门的需求。
// pkg/network/ebpf/probes/probes.go
// InetCskListenStop traces the inet_csk_listen_stop system call (called for both ipv4 and ipv6)
InetCskListenStop ProbeName = "kprobe/inet_csk_listen_stop"
// TCPv6Connect traces the v6 connect() system call
TCPv6Connect ProbeName = "kprobe/tcp_v6_connect"
// TCPv6ConnectReturn traces the return value for the v6 connect() system call
TCPv6ConnectReturn ProbeName = "kretprobe/tcp_v6_connect"
// TCPSendMsg traces the tcp_sendmsg() system call
TCPSendMsg ProbeName = "kprobe/tcp_sendmsg"
// TCPSendMsgPre410 traces the tcp_sendmsg() system call on kernels prior to 4.1.0. This is created because
// we need to load a different kprobe implementation
TCPSendMsgPre410 ProbeName = "kprobe/tcp_sendmsg/pre_4_1_0"
// TCPSendMsgReturn traces the return value for the tcp_sendmsg() system call
// XXX: This is only used for telemetry for now to count the number of errors returned
// by the tcp_sendmsg func (so we can have a # of tcp sent bytes we miscounted)
TCPSendMsgReturn ProbeName = "kretprobe/tcp_sendmsg"
// TCPGetSockOpt traces the tcp_getsockopt() kernel function
// This probe is used for offset guessing only
TCPGetSockOpt ProbeName = "kprobe/tcp_getsockopt"
// SockGetSockOpt traces the sock_common_getsockopt() kernel function
// This probe is used for offset guessing only
SockGetSockOpt ProbeName = "kprobe/sock_common_getsockopt"
// TCPSetState traces the tcp_set_state() kernel function
TCPSetState ProbeName = "kprobe/tcp_set_state"
// TCPCleanupRBuf traces the tcp_cleanup_rbuf() system call
TCPCleanupRBuf ProbeName = "kprobe/tcp_cleanup_rbuf"
// TCPClose traces the tcp_close() system call
TCPClose ProbeName = "kprobe/tcp_close"
// TCPCloseReturn traces the return of tcp_close() system call
TCPCloseReturn ProbeName = "kretprobe/tcp_close"
// We use the following two probes for UDP sends
IPMakeSkb ProbeName = "kprobe/ip_make_skb"
IP6MakeSkb ProbeName = "kprobe/ip6_make_skb"
IP6MakeSkbPre470 ProbeName = "kprobe/ip6_make_skb/pre_4_7_0"
// UDPRecvMsg traces the udp_recvmsg() system call
UDPRecvMsg ProbeName = "kprobe/udp_recvmsg"
// UDPRecvMsgPre410 traces the udp_recvmsg() system call on kernels prior to 4.1.0
UDPRecvMsgPre410 ProbeName = "kprobe/udp_recvmsg/pre_4_1_0"
// UDPRecvMsgReturn traces the return value for the udp_recvmsg() system call
UDPRecvMsgReturn ProbeName = "kretprobe/udp_recvmsg"
// UDPDestroySock traces the udp_destroy_sock() function
UDPDestroySock ProbeName = "kprobe/udp_destroy_sock"
// UDPDestroySockrReturn traces the return of the udp_destroy_sock() system call
UDPDestroySockReturn ProbeName = "kretprobe/udp_destroy_sock"
// TCPRetransmit traces the return value for the tcp_retransmit_skb() system call
TCPRetransmit ProbeName = "kprobe/tcp_retransmit_skb"
TCPRetransmitPre470 ProbeName = "kprobe/tcp_retransmit_skb/pre_4_7_0"
// InetCskAcceptReturn traces the return value for the inet_csk_accept syscall
InetCskAcceptReturn ProbeName = "kretprobe/inet_csk_accept"
// InetBind is the kprobe of the bind() syscall for IPv4
InetBind ProbeName = "kprobe/inet_bind"
// Inet6Bind is the kprobe of the bind() syscall for IPv6
Inet6Bind ProbeName = "kprobe/inet6_bind"
// InetBind is the kretprobe of the bind() syscall for IPv4
InetBindRet ProbeName = "kretprobe/inet_bind"
// Inet6Bind is the kretprobe of the bind() syscall for IPv6
Inet6BindRet ProbeName = "kretprobe/inet6_bind"
// SocketDnsFilter is the socket probe for dns
SocketDnsFilter ProbeName = "socket/dns_filter"
// SockMapFdReturn maps a file descriptor to a kernel sock
SockMapFdReturn ProbeName = "kretprobe/sockfd_lookup_light"
// IPRouteOutputFlow is the kprobe of an ip_route_output_flow call
IPRouteOutputFlow ProbeName = "kprobe/ip_route_output_flow"
// IPRouteOutputFlow is the kretprobe of an ip_route_output_flow call
IPRouteOutputFlowReturn ProbeName = "kretprobe/ip_route_output_flow"
// ConntrackHashInsert is the probe for new conntrack entries
ConntrackHashInsert ProbeName = "kprobe/__nf_conntrack_hash_insert"
// SockFDLookup is the kprobe used for mapping socket FDs to kernel sock structs
SockFDLookup ProbeName = "kprobe/sockfd_lookup_light"
// SockFDLookupRet is the kretprobe used for mapping socket FDs to kernel sock structs
SockFDLookupRet ProbeName = "kretprobe/sockfd_lookup_light"
// DoSendfile is the kprobe used to trace traffic via SENDFILE(2) syscall
DoSendfile ProbeName = "kprobe/do_sendfile"
// DoSendfileRet is the kretprobe used to trace traffic via SENDFILE(2) syscall
DoSendfileRet ProbeName = "kretprobe/do_sendfile"
在初始化的map中,分为两类,一类是BPF_MAP_TYPE_PERF_EVENT_ARRAY
的map,但只有ConnCloseEventMap BPFMapName = "conn_close_event"
这一个。其他的都是别的类型map。
对于ConnCloseEventMap
这个map,使用perfHandlerTCP
函数来处理DataChannel
和LostChannel
两类事件。 这里最终是把数据发送给每个网络客户端。
对于PerfEventArray
类的map数据,使用dumpMapsHandler
来统一处理事件,实现比较粗糙,直接打印出来。
整个模块中,内核态与用户态交互的map列表如下
// pkg/network/ebpf/probes/probes.go
// BPFMapName stores the name of the BPF maps storing statistics and other info
type BPFMapName string
const (
ConnMap BPFMapName = "conn_stats"
TcpStatsMap BPFMapName = "tcp_stats"
ConnCloseEventMap BPFMapName = "conn_close_event"
TracerStatusMap BPFMapName = "tracer_status"
PortBindingsMap BPFMapName = "port_bindings"
UdpPortBindingsMap BPFMapName = "udp_port_bindings"
TelemetryMap BPFMapName = "telemetry"
ConnCloseBatchMap BPFMapName = "conn_close_batch"
ConntrackMap BPFMapName = "conntrack"
ConntrackTelemetryMap BPFMapName = "conntrack_telemetry"
SockFDLookupArgsMap BPFMapName = "sockfd_lookup_args"
DoSendfileArgsMap BPFMapName = "do_sendfile_args"
SockByPidFDMap BPFMapName = "sock_by_pid_fd"
PidFDBySockMap BPFMapName = "pid_fd_by_sock"
)
可以看到,不仅提供了网络的数据,还提供了网络产生的进程ID的数据,安全威胁建模上,可以更好的关联到进程数据了。
probe点
在完成eBPF字节码读取后,开始读取当前模块的probe点信息,当前模块启用的probe点包含TCP的链接创建、UDP的信息发送。在kernel大于4.7版本上,会对^ip6_make_skb$
函数进行hook。并按照配置支持IPv4/IPv6两种IP模式。
datadog-agent使用了自研的DataDog/ebpf类库,基于系统调用封装的ebpf管理模块,实现了setion加载、probe加载处理、过滤,event map读取包等功能。
在加载字节码后,开始启动probe的初始化
- 配置激活的probes
- 修改eBPF常量
- 修改map spec
- 修改program maps
- 加载pinned maps和programs到内核
- 使用验证器加载eBPF program到内核
eBPF maps读取
网络跟踪模块的maps读取分三种,分为TCP链接关闭事件、TCP链接列表map、TCP状态map三种。
在pkg/network/tracer/connection/kprobe/tracer.go
使用newMapIterator
函数对eBPF map进行迭代读取。
在加载eBPF程序到内核后,
- 遍历
PerfMaps
,开协程读取perfbuf事件,使用前面设置的LostHandler
和DataHandler
两个函数来毁掉消费数据。 - 挂载probe点到系统,让系统生产事件,并发送到map。
- 检查probe的选择器
- 更新模块的Map属性路由规则
- 更新模块的program的路由规则
HTTP 接口
在向外提供数据里,除了提供了统计当前链接总数等常规需求外,还提供了debug需求的数据,提供当前模块的eBPF数据大盘数据。
同时,在HTTP接口的服务器启动是,会根据配置选择启动相关metrics的模块,加载相应tracer.o/tracer-debug.o
、http.o/http-debug.o
、dns.o/dns-debug.o
、offset-guess.o/offset-guess-debug.o
、
OOMKillProbe模块
该模块从名字上来看是用来hook linux系统上OOM(Out Of Memory)发生时相关进程名、次数等信息,业务功能上比较简单。
eBPF字节码加载
不支持编译好的.o字节码加载,直接即时编译。
compiledOutput, err := runtime.OomKill.Compile(cfg, nil)
编译过程跟用统一的封装函数,不在赘述。
模块管理器初始化
同理,map跟probe比较少。分别是oom_stats
map 与 kprobe/oom_kill_process
probe。(笔者吐槽一下,这里的map名字是硬编码的字符串,跟网络跟踪模块NetworkTracer不一样,非常不统一,以及搭建分析过程中的各种起码做法,简直无法理解,这代码简直是当代屎山。)
OOM数据是以HTTP接口方式,提供给客户端调用的,HTTP接口调用时,OOMKillProbe.GetAndFlush
函数会读取oom_stats
map里的所有数据,并清空map。将结果返回给HTTP response。
Process 模块
当前模块名字上来看,是用于提供进程数据的接口,但从代码分析上来,并不涉及eBPF probe的HOOK。提供的HTTP接口,也只是根据HTTP request中的PIDS,遍历/proc/{PID}/
目录获取相应的进程数据。
SecurityRuntime模块
模块管理器初始化
system-probe包的runCmd
命令启动时,把包的所有模块都注册启动了。函数的调用顺序是
run(...)
->StartSystemProbe()
->api.StartServer(cfg)
->module.Register(cfg, mux, modules.All)
//module是package的名字
->module.Register(router)
//module是模块名字, module, err := factory.Fn(cfg)
调用每个probe 模块的Register方法,进行模块初始化、模块运行,比如SecurityRuntime模块,在pkg/security/module/module.go
的71行
// Register the runtime security agent module
func (m *Module) Register(_ *module.Router) error {
if err := m.Init(); err != nil {
return err
}
return m.Start()
}
eBPF字节码加载
在Init方法中,对eBPF 的probe模块进行初始化
// initialize the eBPF manager and load the programs and maps in the kernel. At this stage, the probes are not
// running yet.
if err := m.probe.Init(m.statsdClient); err != nil {
return errors.Wrap(err, "failed to init probe")
}
USE_SYSCALL_WRAPPER
在模块初始化第一步,读取/proc/kallsyms
,搜索open
函数的具体syscall函数名,以笔者AMD64 ubuntu 21.04为例,对应的函数名为__x64_sys_open
。
如果该函数不包含SyS_
或者sys_
,则设定useSyscallWrapper
变量为true。
编译或加载
根据配置信息中是否启用运行时编译,决定编译或者加载编译好的.o字节码文件。
若编译,则调用runtime.RuntimeSecurity.Compile
编译,CLANG编译参数的cflags中增加DUSE_SYSCALL_WRAPPER=1
参数。编译过程机制与网络模块一致,略过。
如果是加载预编译,根据useSyscallWrapper
的值,选择runtime-security.o
或者runtime-security-syscall-wrapper.o
相应的版本。
笔者注:关于这参数,网上信息特别少,参见C library system-call wrappers, or the lack thereof和glibc syscall wrapper 内部实现
加载机制跟网络模块一样,走datadog/ebpf-manager
包的Manager.InitWithOptions
统一处理,不再赘述。
datadog/ebpf-manager
基于github.com/cilium/ebpf
包做了封装。
perfMap事件处理
handle的设定,与网络模块一样,在模块的managerOptions
设置时,做了赋值
//pkg/security/probe/probe.go 160行
p.perfMap.PerfMapOptions = manager.PerfMapOptions{
DataHandler: p.reOrderer.HandleEvent,
LostHandler: p.handleLostEvents,
}
p.reOrderer.HandleEvent
对perfMap处理时,增加了metric
的统计信息,之后调用p.handleEvent
来处理。该函数定义在 pkg/security/probe/probe.go
中。
在内核态中,所有probe hook的点产生的时间,都写入到events
这个perfMap中。
普通Map管理
datadog-agent的用户态与内核态数据交互需求上,也实现也基于eBPF map的双向读写。
- concurrent_syscalls
- exec_count_bb
- exec_count_fb
- noisy_processes_fb
- proc_cache
- pid_cache
内核态读写
这些map在内核态会被写入数据,而在用户态会用于查找、删除操作。 比如,进程创建时,触发SEC("kprobe/do_dentry_open")
HOOK点后,内核态调用handle_exec_event
函数会先查询proc_cache
map是否有当前进程对应父进程的数据,之后再把自己进程信息存入proc_cache
map。
在用户态,也可以根据PID信息查找pid_cache
map ,拿到cookie后,再到proc_cache
map查询进程entry信息。
events的数据结构
在SecurityRuntime模块里,eBPF map用在内核态与用户态通讯时,使用统一封装的Event
结构体,在eBPF 的内核态对应struct syscall_cache_t
,包含多个事件类型的union struct。
events解析派发
用户态的go语言中,采用统一封装的大结构体Event
解析内核态发来的数据。event.UnmarshalBinary
读取包header信息,设定事件类型,再根据事件类型交付给Event
的对应属性,对应解码方法解析消息内容。比如Event.Chmod.UnmarshalBinary
来解码字节流。以下是Go中结构体的:
// pkg/security/secl/model/model.go line 118
type Event struct {
ID string `field:"-"`
Type uint64 `field:"-"`
TimestampRaw uint64 `field:"-"`
Timestamp time.Time `field:"timestamp"` // Timestamp of the event
ProcessContext ProcessContext `field:"process" event:"*"`
SpanContext SpanContext `field:"-"`
ContainerContext ContainerContext `field:"container"`
Chmod ChmodEvent `field:"chmod" event:"chmod"` // [7.27] [File] A file’s permissions were changed
Chown ChownEvent `field:"chown" event:"chown"` // [7.27] [File] A file’s owner was changed
Open OpenEvent `field:"open" event:"open"` // [7.27] [File] A file was opened
Mkdir MkdirEvent `field:"mkdir" event:"mkdir"` // [7.27] [File] A directory was created
Rmdir RmdirEvent `field:"rmdir" event:"rmdir"` // [7.27] [File] A directory was removed
Rename RenameEvent `field:"rename" event:"rename"` // [7.27] [File] A file/directory was renamed
Unlink UnlinkEvent `field:"unlink" event:"unlink"` // [7.27] [File] A file was deleted
Utimes UtimesEvent `field:"utimes" event:"utimes"` // [7.27] [File] Change file access/modification times
Link LinkEvent `field:"link" event:"link"` // [7.27] [File] Create a new name/alias for a file
SetXAttr SetXAttrEvent `field:"setxattr" event:"setxattr"` // [7.27] [File] Set exteneded attributes
RemoveXAttr SetXAttrEvent `field:"removexattr" event:"removexattr"` // [7.27] [File] Remove extended attributes
Exec ExecEvent `field:"exec" event:"exec"` // [7.27] [Process] A process was executed or forked
SetUID SetuidEvent `field:"setuid" event:"setuid"` // [7.27] [Process] A process changed its effective uid
SetGID SetgidEvent `field:"setgid" event:"setgid"` // [7.27] [Process] A process changed its effective gid
Capset CapsetEvent `field:"capset" event:"capset"` // [7.27] [Process] A process changed its capacity set
SELinux SELinuxEvent `field:"selinux" event:"selinux"` // [7.30] [Kernel] An SELinux operation was run
BPF BPFEvent `field:"bpf" event:"bpf"` // [7.33] [Kernel] A BPF command was executed
Mount MountEvent `field:"-"`
Umount UmountEvent `field:"-"`
InvalidateDentry InvalidateDentryEvent `field:"-"`
ArgsEnvs ArgsEnvsEvent `field:"-"`
MountReleased MountReleasedEvent `field:"-"`
}
这里实现了多event类型的统一处理,但各个event之间耦合严重,event类型增加减少都需要影响上层代码,从设计模式上来看,问题比较大。并且,大的结构体的实力化也带来较大的内存占用,冗余严重。这里设计很差。
在内核发送来的事件处理上,针对进程事件除了解析之外,还有额外调用ProcessResolver.AddExecEntry
将进程创建数据缓存。便于HTTP接口获取当前主机进程的全量列表。
在handleEvent
处理事件的同时,调用了perfBufferMonitor.CountEvent
对事件的类型、时间、次数、长度、CPU ID做了计数,用于事件完整率对账,同时间接的用于观测当前系统的事件量大小,用作评估系统繁忙的间接依据。
事件威胁风险评估
Event结构体属性修正完后,*Probe.DispatchEvent
将这个事件派发出去,送给规则引擎
做安全模型评估。
规则引擎的实现,在NewRuleSet
中,根据配置信息,针对不同的事件类型选择不同的规则集合
,分别进行风险威胁评估。如果事件类型没有规则,则直接放过。
//
模块的HandleEvent
调用ruleSet.Evaluate
来评估事件。这里在处理多个事件时,采用了对象池的做法来管理内存。ruleSet
规则集合中的eventRuleBuckets
属性是模块的包含所有事件类型
的总规则集合。
每种事件类型对应的bucket
包含多个rules []*Rule
, 评估时,遍历所有rules
进行规则判断。
规则全匹配
如果命中规则,则将命中结果、命中规则信息、事件信息发送给规则的所有监听器(比如告警系统,关联事件处理器等)
规则部分匹配
如果没命中规则,则根据配置的每个字段,进行部分匹配判断,
若命中,则返回true。(但是,笔者看代码中,没发现有判断Evaluate
函数的返回值)
若没命中,则准备丢弃,将事件与规则信息,发送给规则的所有监听器,以及发送到远程服务器,便于做规则调整。
风险检测规则引擎
实现上,是datadog自己做了一套AST语法分析的包,在pkg/security/secl/compiler
下,识别的表达式,也是类似go语法的逻辑判断表达式。
规则配置时,只要根据上面Event
的每个子event
的属性来编写即可。比如
rules := []string{
`open.filename == "/etc/passwd" && process.uid != 0`,
`(open.filename =~ "/sbin/*" || open.filename =~ "/usr/sbin/*") && process.uid != 0 && open.flags & O_CREAT > 0`,
`(open.filename =~ "/var/run/*") && open.flags & O_CREAT > 0 && process.uid != 0`,
`(mkdir.filename =~ "/var/run/*") && process.uid != 0`,
}
规则字符串看上去比较直观,容易理解。
事件结果上报
在规则引擎判断完威胁后,不论结果如何,都仍会将结果异步发送至远程数据中心。
这些上报的数据中,包含完整的内核态发送来的原始数据,以及本地规则引擎命中信息,识别结果等数据。方便远程数据中心做大数据分析、建模、验证、调整检测规则,来提升召回率和准确率。
TCPQueueLength模块
该模块与OOMKillProbe模块类似,只支持本地编译字节码,不支持加载预先编译的.o字节码。 业务功能上偏向于TCP的发送、接收包大小,与安全无关。加载机制上也与其他模块一直,故不再分析。
产品视角总结
datadog-agent支持包含windows、linux、IOT、android等多种系统平台,当然不同平台的支持程度不一样,本文的总结以支持eBPF的Linux为背景,给出这些总结。
业务特性区分不同map
产品中分别使用了*PerfMap
对应BPF_MAP_TYPE_PERF_EVENT_ARRAY
,这类业务对时间顺序有严格要求,事件读取后,会按照CPU时间排序。若对时间顺序无要求,则使用BPF_MAP_TYPE_ARRAY
、BPF_MAP_TYPE_LRU_HASH
、BPF_MAP_TYPE_HASH
等其他类型。
事件类型丰富
多个probe的字节码文件,支持包含系统运行时、网络、内核等事件的感知发送。包含
-
- 所有者变更
-
- UID设置
- GID设置
- cap设置
-
- SELinux命令
- BPF调用
-
- ARGS/ENGS设置
- OOM事件
-
网络事件(IPv4/IPv6)
- TCP链接创建、发送数据、关闭链接
- UDP链接创建、发送数据、关闭链接
- SOCKET链接创建、发送数据、关闭链接
字段属性丰富
比如进程事件,包含
- 进程文件系统属性
- 进程启动参数
- 父进程信息(不全)
其他进程事件不在一一罗列,见pkg/security/secl/model/model.go
内相关事件结构体。
多事件信息合并
在模块内部有部分事件的缓存,用于应对多个事件之间的关联,也可以用作本地规则引擎判断的数据补充。比如先设置环境变量,再执行进程创建命令。
配置识别判断规则远程下发
远程重新加载配置,以更新网络规则、安全事件识别规则。
多重事件风险判断
- 本地判断
- 包含完整匹配
- 远程判断、纠偏
本地判断更快的处理威胁场景,减轻远程数据中心处理压力。远程数据中心离线分析,威胁识别校验,感知新的威胁事件,优化改进识别规则。新规则快速应用到本地,提升检出效率。
事件数据管理
- 灵活注册事件接收器
- 事件数据监控(告警在远程服务器)
- 事件数据统计对账
linux多版本支持
不支持CO-RE,采用本地编译方式,依赖本地编译环境,不适合大型企业的HIDS场景。
以我司为例,内核版本分布相对清晰,版本种类不多,更适合预先编译相应字节码文件,HIDS Agent按照系统版本加载相应的字节码文件。
datadog的system-probe模块使用了eBPF技术研发,实现了网络管理、运行时安全、系统状态感知等几个功能,支持运维、安全两个场景。其中核心模块在网络与运行时安全两个场景。故本文重点分析了这两个场景的技术实现与业务特性。
在上面产品视角分析中已经提到了。是一个功能、数据比较齐全的安全产品,数据满足各种入侵类型的建模,并且有多重安全模型分析机制,具备良好的事件上报、监控告警能力,具备数据统计、对账能力。
该产品以安全数据收集、检测为主,缺少安全防御功能。在已有功能中,没有看到限速限流、资源占用控制等功能。
在工程质量上吐槽点特别多,比如奇葩包的路径、几十种go package的go/C混编译、跨包修改变量、go generate/go build 乱调、混合C/C++/go/python多种语言、自定义N种go build tags、硬编码严重、部分功能耦合严重、扩展性差。这可能就是历经N年,多代程序员累计的shitcode屎山吧。
CFC4N的博客 由 CFC4N 创作,采用 知识共享 署名-非商业性使用-相同方式共享(3.0未本地化版本)许可协议进行许可。基于https://www.cnxct.com上的作品创作。转载请注明转自:datadog的eBPF安全检测机制分析
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK