From 81307783f4b0a450195d135c555f61d3cb3d510d Mon Sep 17 00:00:00 2001 From: Amit Schendel Date: Mon, 12 Aug 2024 16:30:41 +0000 Subject: [PATCH 1/4] Adding initial fixes Signed-off-by: Amit Schendel --- pkg/containerwatcher/v1/container_watcher.go | 21 ++ .../v1/container_watcher_private.go | 11 + pkg/containerwatcher/v1/ssh.go | 60 +++++ pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.bpf.c | 135 ++++++++++ pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.h | 31 +++ pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.go | 146 +++++++++++ pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.o | Bin 0 -> 20088 bytes pkg/ebpf/gadgets/ssh/tracer/tracer.go | 202 +++++++++++++++ pkg/ebpf/gadgets/ssh/types/types.go | 36 +++ pkg/ebpf/include/sockets-map.h | 243 ++++++++++++++++++ pkg/ebpf/include/types.h | 62 ++++- pkg/ruleengine/v1/r1011_ld_preload_hook.go | 5 + pkg/rulemanager/rule_manager_interface.go | 3 + pkg/rulemanager/rule_manager_mock.go | 5 + pkg/rulemanager/v1/rule_manager.go | 12 + pkg/utils/events.go | 1 + 16 files changed, 972 insertions(+), 1 deletion(-) create mode 100644 pkg/containerwatcher/v1/ssh.go create mode 100644 pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.bpf.c create mode 100644 pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.h create mode 100644 pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.go create mode 100644 pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.o create mode 100644 pkg/ebpf/gadgets/ssh/tracer/tracer.go create mode 100644 pkg/ebpf/gadgets/ssh/types/types.go create mode 100644 pkg/ebpf/include/sockets-map.h diff --git a/pkg/containerwatcher/v1/container_watcher.go b/pkg/containerwatcher/v1/container_watcher.go index 00683a3d..5cbaab0a 100644 --- a/pkg/containerwatcher/v1/container_watcher.go +++ b/pkg/containerwatcher/v1/container_watcher.go @@ -33,6 +33,8 @@ import ( tracerhardlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/hardlink/types" tracerandomx "github.com/kubescape/node-agent/pkg/ebpf/gadgets/randomx/tracer" tracerandomxtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/randomx/types" + tracerssh "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/tracer" + tracersshtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" tracersymlink "github.com/kubescape/node-agent/pkg/ebpf/gadgets/symlink/tracer" tracersymlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/symlink/types" "github.com/kubescape/node-agent/pkg/malwaremanager" @@ -55,6 +57,7 @@ const ( randomxTraceName = "trace_randomx" symlinkTraceName = "trace_symlink" hardlinkTraceName = "trace_hardlink" + sshTraceName = "trace_ssh" capabilitiesWorkerPoolSize = 1 execWorkerPoolSize = 2 openWorkerPoolSize = 8 @@ -63,6 +66,7 @@ const ( randomxWorkerPoolSize = 1 symlinkWorkerPoolSize = 1 hardlinkWorkerPoolSize = 1 + sshWorkerPoolSize = 1 ) type IGContainerWatcher struct { @@ -98,6 +102,7 @@ type IGContainerWatcher struct { randomxTracer *tracerandomx.Tracer symlinkTracer *tracersymlink.Tracer hardlinkTracer *tracerhardlink.Tracer + sshTracer *tracerssh.Tracer kubeIPInstance operators.OperatorInstance kubeNameInstance operators.OperatorInstance @@ -110,6 +115,7 @@ type IGContainerWatcher struct { randomxWorkerPool *ants.PoolWithFunc symlinkWorkerPool *ants.PoolWithFunc hardlinkWorkerPool *ants.PoolWithFunc + sshdWorkerPool *ants.PoolWithFunc capabilitiesWorkerChan chan *tracercapabilitiestype.Event execWorkerChan chan *tracerexectype.Event @@ -119,6 +125,7 @@ type IGContainerWatcher struct { randomxWorkerChan chan *tracerandomxtype.Event symlinkWorkerChan chan *tracersymlinktype.Event hardlinkWorkerChan chan *tracerhardlinktype.Event + sshWorkerChan chan *tracersshtype.Event preRunningContainersIDs mapset.Set[string] @@ -293,6 +300,18 @@ func CreateIGContainerWatcher(cfg config.Config, applicationProfileManager appli if err != nil { return nil, fmt.Errorf("creating hardlink worker pool: %w", err) } + // Create a ssh worker pool + sshWorkerPool, err := ants.NewPoolWithFunc(sshWorkerPoolSize, func(i interface{}) { + event := i.(tracersshtype.Event) + if event.K8s.ContainerName == "" { + return + } + metrics.ReportEvent(utils.SSHEventType) + ruleManager.ReportSSHEvent(event) + }) + if err != nil { + return nil, fmt.Errorf("creating ssh worker pool: %w", err) + } return &IGContainerWatcher{ // Configuration @@ -323,6 +342,7 @@ func CreateIGContainerWatcher(cfg config.Config, applicationProfileManager appli randomxWorkerPool: randomxWorkerPool, symlinkWorkerPool: symlinkWorkerPool, hardlinkWorkerPool: hardlinkWorkerPool, + sshdWorkerPool: sshWorkerPool, metrics: metrics, preRunningContainersIDs: preRunningContainers, @@ -335,6 +355,7 @@ func CreateIGContainerWatcher(cfg config.Config, applicationProfileManager appli randomxWorkerChan: make(chan *tracerandomxtype.Event, 5000), symlinkWorkerChan: make(chan *tracersymlinktype.Event, 1000), hardlinkWorkerChan: make(chan *tracerhardlinktype.Event, 1000), + sshWorkerChan: make(chan *tracersshtype.Event, 1000), // cache ruleBindingPodNotify: ruleBindingPodNotify, diff --git a/pkg/containerwatcher/v1/container_watcher_private.go b/pkg/containerwatcher/v1/container_watcher_private.go index b7e44377..3e0d1c3b 100644 --- a/pkg/containerwatcher/v1/container_watcher_private.go +++ b/pkg/containerwatcher/v1/container_watcher_private.go @@ -257,6 +257,11 @@ func (ch *IGContainerWatcher) startTracers() error { logger.L().Error("error starting hardlink tracing", helpers.Error(err)) return err } + + if err := ch.startSshTracing(); err != nil { + logger.L().Error("error starting ssh tracing", helpers.Error(err)) + return err + } } return nil @@ -322,6 +327,12 @@ func (ch *IGContainerWatcher) stopTracers() error { logger.L().Error("error stopping hardlink tracing", helpers.Error(err)) errs = errors.Join(errs, err) } + + // Stop ssh tracer + if err := ch.stopSshTracing(); err != nil { + logger.L().Error("error stopping ssh tracing", helpers.Error(err)) + errs = errors.Join(errs, err) + } } return errs diff --git a/pkg/containerwatcher/v1/ssh.go b/pkg/containerwatcher/v1/ssh.go new file mode 100644 index 00000000..49a3f7b8 --- /dev/null +++ b/pkg/containerwatcher/v1/ssh.go @@ -0,0 +1,60 @@ +package containerwatcher + +import ( + "fmt" + + tracersshlink "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/tracer" + tracersshtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" + + "github.com/inspektor-gadget/inspektor-gadget/pkg/types" + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" +) + +func (ch *IGContainerWatcher) sshEventCallback(event *tracersshtype.Event) { + if event.Type == types.DEBUG { + return + } + + if isDroppedEvent(event.Type, event.Message) { + logger.L().Ctx(ch.ctx).Warning("ssh tracer got drop events - we may miss some realtime data", helpers.Interface("event", event), helpers.String("error", event.Message)) + return + } + + ch.sshWorkerChan <- event +} + +func (ch *IGContainerWatcher) startSshTracing() error { + if err := ch.tracerCollection.AddTracer(sshTraceName, ch.containerSelector); err != nil { + return fmt.Errorf("adding tracer: %w", err) + } + + // Get mount namespace map to filter by containers + sshMountnsmap, err := ch.tracerCollection.TracerMountNsMap(sshTraceName) + if err != nil { + return fmt.Errorf("getting sshMountnsmap: %w", err) + } + + tracerSsh, err := tracersshlink.NewTracer(&tracersshlink.Config{MountnsMap: sshMountnsmap}, ch.containerCollection, ch.sshEventCallback) + if err != nil { + return fmt.Errorf("creating tracer: %w", err) + } + go func() { + for event := range ch.sshWorkerChan { + _ = ch.sshdWorkerPool.Invoke(*event) + } + }() + + ch.sshTracer = tracerSsh + + return nil +} + +func (ch *IGContainerWatcher) stopSshTracing() error { + // Stop ssh tracer + if err := ch.tracerCollection.RemoveTracer(sshTraceName); err != nil { + return fmt.Errorf("removing tracer: %w", err) + } + ch.sshTracer.Stop() + return nil +} diff --git a/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.bpf.c b/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.bpf.c new file mode 100644 index 00000000..6eaefe30 --- /dev/null +++ b/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.bpf.c @@ -0,0 +1,135 @@ +//#include "../../../../include/amd64/vmlinux.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "ssh.h" + +#include "../../../../include/macros.h" +#include "../../../../include/buffer.h" +#define GADGET_TYPE_NETWORKING +#include "../../../../include/sockets-map.h" + + +// Events map. +struct { + __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); + __uint(key_size, sizeof(__u32)); + __uint(value_size, sizeof(__u32)); +} events SEC(".maps"); + +// Empty event map. +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); + __uint(max_entries, 1); + __type(key, __u32); + __type(value, struct event); +} empty_event SEC(".maps"); + +// we need this to make sure the compiler doesn't remove our struct. +const struct event *unusedevent __attribute__((unused)); + +const volatile bool gadget_filter_by_mntns = false; + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, gadget_mntns_id); + __type(value, __u32); + __uint(max_entries, 1024); +} gadget_mntns_filter_map SEC(".maps"); + +// gadget_should_discard_mntns_id returns true if events generated from the given mntns_id should +// not be taken into consideration. +static __always_inline bool gadget_should_discard_mntns_id(gadget_mntns_id mntns_id) +{ + return gadget_filter_by_mntns && + !bpf_map_lookup_elem(&gadget_mntns_filter_map, &mntns_id); +} + +SEC("socket") +int ssh_detector(struct __sk_buff *skb) { + // Check if it's an IP packet + if (skb->protocol != bpf_htons(ETH_P_IP)) + return 0; + + // Define the offset for IP header + int ip_offset = ETH_HLEN; + + // Read IP header + struct iphdr iph; + if (bpf_skb_load_bytes(skb, ip_offset, &iph, sizeof(iph)) < 0) + return 0; + + // Check if it's a TCP packet + if (iph.protocol != IPPROTO_TCP) + return 0; + + // Calculate TCP header offset + int tcp_offset = ip_offset + (iph.ihl * 4); + + // Read TCP header + struct tcphdr tcph; + if (bpf_skb_load_bytes(skb, tcp_offset, &tcph, sizeof(tcph)) < 0) + return 0; + + // Calculate payload offset + int payload_offset = tcp_offset + (tcph.doff * 4); + + // Read the first 4 bytes of the payload + char payload[SSH_SIG_LEN]; + if (bpf_skb_load_bytes(skb, payload_offset, payload, SSH_SIG_LEN) < 0) + return 0; + + // Check for SSH signature using memcmp + if (__builtin_memcmp(payload, SSH_SIGNATURE, SSH_SIG_LEN) == 0) { + bpf_printk("SSH packet detected\n"); + struct event *event; + __u32 zero = 0; + event = bpf_map_lookup_elem(&empty_event, &zero); + if (!event) { + return 0; + } + + // Enrich event with process metadata + struct sockets_value *skb_val = gadget_socket_lookup(skb); + if (skb_val != NULL) { + __u64 mntns_id = skb_val->mntns; + if (gadget_should_discard_mntns_id(mntns_id)) { + bpf_printk("Discarding event from mntns_id %llu\n", mntns_id); + return 0; + } + + event->netns = skb->cb[0]; // cb[0] initialized by dispatcher.bpf.c + event->mntns_id = skb_val->mntns; + event->pid = skb_val->pid_tgid >> 32; + event->uid = (__u32)(skb_val->uid_gid); + event->gid = (__u32)(skb_val->uid_gid >> 32); + __builtin_memcpy(&event->comm, skb_val->task, sizeof(event->comm)); + + event->src_ip = iph.saddr; + event->dst_ip = iph.daddr; + event->src_port = bpf_ntohs(tcph.source); + event->dst_port = bpf_ntohs(tcph.dest); + + event->timestamp = bpf_ktime_get_boot_ns(); + + } + + bpf_perf_event_output(skb, &events, BPF_F_CURRENT_CPU, &event, sizeof(struct event)); + } + + return 0; +} + + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; \ No newline at end of file diff --git a/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.h b/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.h new file mode 100644 index 00000000..7c075692 --- /dev/null +++ b/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.h @@ -0,0 +1,31 @@ +#pragma once + +#include "../../../../include/types.h" + +#ifndef TASK_COMM_LEN +#define TASK_COMM_LEN 16 +#endif +#define INVALID_UID ((uid_t)-1) +// Defined in include/uapi/linux/magic.h +#define OVERLAYFS_SUPER_MAGIC 0x794c7630 +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif +#define SSH_SIGNATURE "SSH-" +#define SSH_SIG_LEN 4 +#define ETH_P_IP 0x0800 /* Internet Protocol packet */ +#define ETH_HLEN 14 + +struct event { + __u32 netns; + gadget_timestamp timestamp; + gadget_mntns_id mntns_id; + __u32 pid; + __u32 uid; + __u32 gid; + __u16 dst_port; + __u16 src_port; + __u32 dst_ip; + __u32 src_ip; + __u8 comm[TASK_COMM_LEN]; +}; diff --git a/pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.go b/pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.go new file mode 100644 index 00000000..03e048aa --- /dev/null +++ b/pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.go @@ -0,0 +1,146 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 + +package tracer + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +type sshEvent struct { + Netns uint32 + _ [4]byte + Timestamp uint64 + MntnsId uint64 + Pid uint32 + Uid uint32 + Gid uint32 + DstPort uint16 + SrcPort uint16 + DstIp uint32 + SrcIp uint32 + Comm [16]uint8 +} + +// loadSsh returns the embedded CollectionSpec for ssh. +func loadSsh() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_SshBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load ssh: %w", err) + } + + return spec, err +} + +// loadSshObjects loads ssh and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *sshObjects +// *sshPrograms +// *sshMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadSshObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadSsh() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// sshSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type sshSpecs struct { + sshProgramSpecs + sshMapSpecs +} + +// sshSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type sshProgramSpecs struct { + SshDetector *ebpf.ProgramSpec `ebpf:"ssh_detector"` +} + +// sshMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type sshMapSpecs struct { + EmptyEvent *ebpf.MapSpec `ebpf:"empty_event"` + Events *ebpf.MapSpec `ebpf:"events"` + GadgetHeap *ebpf.MapSpec `ebpf:"gadget_heap"` + GadgetMntnsFilterMap *ebpf.MapSpec `ebpf:"gadget_mntns_filter_map"` + GadgetSockets *ebpf.MapSpec `ebpf:"gadget_sockets"` +} + +// sshObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadSshObjects or ebpf.CollectionSpec.LoadAndAssign. +type sshObjects struct { + sshPrograms + sshMaps +} + +func (o *sshObjects) Close() error { + return _SshClose( + &o.sshPrograms, + &o.sshMaps, + ) +} + +// sshMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadSshObjects or ebpf.CollectionSpec.LoadAndAssign. +type sshMaps struct { + EmptyEvent *ebpf.Map `ebpf:"empty_event"` + Events *ebpf.Map `ebpf:"events"` + GadgetHeap *ebpf.Map `ebpf:"gadget_heap"` + GadgetMntnsFilterMap *ebpf.Map `ebpf:"gadget_mntns_filter_map"` + GadgetSockets *ebpf.Map `ebpf:"gadget_sockets"` +} + +func (m *sshMaps) Close() error { + return _SshClose( + m.EmptyEvent, + m.Events, + m.GadgetHeap, + m.GadgetMntnsFilterMap, + m.GadgetSockets, + ) +} + +// sshPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadSshObjects or ebpf.CollectionSpec.LoadAndAssign. +type sshPrograms struct { + SshDetector *ebpf.Program `ebpf:"ssh_detector"` +} + +func (p *sshPrograms) Close() error { + return _SshClose( + p.SshDetector, + ) +} + +func _SshClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed ssh_bpfel.o +var _SshBytes []byte diff --git a/pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.o b/pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.o new file mode 100644 index 0000000000000000000000000000000000000000..e0afa5f2d170abe93a52caec099012d27196700e GIT binary patch literal 20088 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 literal 0 HcmV?d00001 diff --git a/pkg/ebpf/gadgets/ssh/tracer/tracer.go b/pkg/ebpf/gadgets/ssh/tracer/tracer.go new file mode 100644 index 00000000..67f5e005 --- /dev/null +++ b/pkg/ebpf/gadgets/ssh/tracer/tracer.go @@ -0,0 +1,202 @@ +package tracer + +import ( + "encoding/binary" + "errors" + "fmt" + "net" + "os" + "syscall" + "unsafe" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/perf" + gadgetcontext "github.com/inspektor-gadget/inspektor-gadget/pkg/gadget-context" + "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets" + "github.com/inspektor-gadget/inspektor-gadget/pkg/rawsock" + eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" + "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" + "golang.org/x/sys/unix" +) + +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -no-global-types -target bpfel -cc clang -cflags "-g -O2 -Wall" -type event ssh bpf/ssh.bpf.c -- -I./bpf/ + +type Config struct { + MountnsMap *ebpf.Map +} + +type Tracer struct { + config *Config + enricher gadgets.DataEnricherByMntNs + netEnricher gadgets.DataEnricherByNetNs + eventCallback func(*types.Event) + + objs sshObjects + + file int + reader *perf.Reader +} + +func NewTracer(config *Config, enricher gadgets.DataEnricherByMntNs, + eventCallback func(*types.Event), + netEnricher gadgets.DataEnricherByNetNs, +) (*Tracer, error) { + t := &Tracer{ + config: config, + enricher: enricher, + netEnricher: netEnricher, + eventCallback: eventCallback, + } + + if err := t.install(); err != nil { + t.close() + return nil, err + } + + go t.run() + + return t, nil +} + +func (t *Tracer) Stop() { + t.close() +} + +func (t *Tracer) close() { + if t.file != 0 { + syscall.Close(t.file) + t.file = 0 + } + + if t.reader != nil { + t.reader.Close() + } + + t.objs.Close() +} + +func (t *Tracer) install() error { + spec, err := loadSsh() + if err != nil { + return fmt.Errorf("loading ebpf program: %w", err) + } + + if err := gadgets.LoadeBPFSpec(t.config.MountnsMap, spec, nil, &t.objs); err != nil { + return fmt.Errorf("loading ebpf spec: %w", err) + } + + // Open raw socket + rawSockFd, err := rawsock.OpenRawSock(1) + if err != nil { + logger.L().Error("Error opening raw socket", helpers.Error(err)) + } + + // Attach BPF program to raw socket + if err := syscall.SetsockoptInt(rawSockFd, syscall.SOL_SOCKET, unix.SO_ATTACH_BPF, t.objs.SshDetector.FD()); err != nil { + logger.L().Error("Error attaching BPF program to raw socket", helpers.Error(err)) + } + + // Store the file for later cleanup + t.file = rawSockFd + + t.reader, err = perf.NewReader(t.objs.Events, gadgets.PerfBufferPages*os.Getpagesize()) + if err != nil { + return fmt.Errorf("creating perf ring buffer: %w", err) + } + + return nil +} + +func (t *Tracer) run() { + for { + record, err := t.reader.Read() + if err != nil { + if errors.Is(err, perf.ErrClosed) { + return + } + + msg := fmt.Sprintf("Error reading perf ring buffer: %s", err) + t.eventCallback(types.Base(eventtypes.Err(msg))) + return + } + + if record.LostSamples > 0 { + msg := fmt.Sprintf("lost %d samples", record.LostSamples) + t.eventCallback(types.Base(eventtypes.Warn(msg))) + continue + } + + bpfEvent := (*sshEvent)(unsafe.Pointer(&record.RawSample[0])) + + srcIP := make(net.IP, 4) + dstIP := make(net.IP, 4) + binary.BigEndian.PutUint32(srcIP, bpfEvent.SrcIp) + binary.BigEndian.PutUint32(dstIP, bpfEvent.DstIp) + + event := types.Event{ + Event: eventtypes.Event{ + Type: eventtypes.NORMAL, + Timestamp: gadgets.WallTimeFromBootTime(bpfEvent.Timestamp), + }, + WithMountNsID: eventtypes.WithMountNsID{MountNsID: bpfEvent.MntnsId}, + WithNetNsID: eventtypes.WithNetNsID{NetNsID: uint64(bpfEvent.Netns)}, + SrcIP: srcIP.String(), + DstIP: dstIP.String(), + SrcPort: bpfEvent.SrcPort, + DstPort: bpfEvent.DstPort, + Pid: bpfEvent.Pid, + Uid: bpfEvent.Uid, + Gid: bpfEvent.Gid, + Comm: gadgets.FromCString(bpfEvent.Comm[:]), + } + + if t.enricher != nil { + t.enricher.EnrichByMntNs(&event.CommonData, event.MountNsID) + t.netEnricher.EnrichByNetNs(&event.CommonData, event.NetNsID) + } + + logger.L().Info("SSH event", helpers.Interface("event", event)) + + t.eventCallback(&event) + } +} + +func (t *Tracer) Run(gadgetCtx gadgets.GadgetContext) error { + defer t.close() + if err := t.install(); err != nil { + return fmt.Errorf("installing tracer: %w", err) + } + + go t.run() + gadgetcontext.WaitForTimeoutOrDone(gadgetCtx) + + return nil +} + +func (t *Tracer) SetMountNsMap(mountnsMap *ebpf.Map) { + t.config.MountnsMap = mountnsMap +} + +func (t *Tracer) SetEventHandler(handler any) { + nh, ok := handler.(func(ev *types.Event)) + if !ok { + panic("event handler invalid") + } + t.eventCallback = nh +} + +type GadgetDesc struct{} + +func (g *GadgetDesc) NewInstance() (gadgets.Gadget, error) { + tracer := &Tracer{ + config: &Config{}, + } + return tracer, nil +} + +// // Helper function to convert host byte order to network byte order +// func htons(host uint16) uint16 { +// return (host&0xff)<<8 | (host&0xff00)>>8 +// } diff --git a/pkg/ebpf/gadgets/ssh/types/types.go b/pkg/ebpf/gadgets/ssh/types/types.go new file mode 100644 index 00000000..4cea4e68 --- /dev/null +++ b/pkg/ebpf/gadgets/ssh/types/types.go @@ -0,0 +1,36 @@ +package types + +import ( + "github.com/inspektor-gadget/inspektor-gadget/pkg/columns" + eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" +) + +type Event struct { + eventtypes.Event + eventtypes.WithMountNsID + eventtypes.WithNetNsID + + Pid uint32 `json:"pid,omitempty" column:"pid,template:pid"` + PPid uint32 `json:"ppid,omitempty" column:"ppid,template:ppid"` + Uid uint32 `json:"uid,omitempty" column:"uid,template:uid"` + Gid uint32 `json:"gid,omitempty" column:"gid,template:gid"` + UpperLayer bool `json:"upperlayer,omitempty" column:"upperlayer,template:upperlayer"` + Comm string `json:"comm,omitempty" column:"comm,template:comm"` + ExePath string `json:"exe_path,omitempty" column:"exe_path,template:exe_path"` + SrcPort uint16 `json:"src_port,omitempty" column:"src_port,template:src_port"` + DstPort uint16 `json:"dst_port,omitempty" column:"dst_port,template:dst_port"` + SrcIP string `json:"src_ip,omitempty" column:"src_ip,template:src_ip"` + DstIP string `json:"dst_ip,omitempty" column:"dst_ip,template:dst_ip"` +} + +func GetColumns() *columns.Columns[Event] { + sshColumns := columns.MustCreateColumns[Event]() + + return sshColumns +} + +func Base(ev eventtypes.Event) *Event { + return &Event{ + Event: ev, + } +} diff --git a/pkg/ebpf/include/sockets-map.h b/pkg/ebpf/include/sockets-map.h new file mode 100644 index 00000000..5ed51d87 --- /dev/null +++ b/pkg/ebpf/include/sockets-map.h @@ -0,0 +1,243 @@ +/* SPDX-License-Identifier: (GPL-2.0 WITH Linux-syscall-note) OR Apache-2.0 */ + +#ifndef SOCKETS_MAP_H +#define SOCKETS_MAP_H + +// The include below requires to include either +// or before. We can't include both because they +// are incompatible. Let the gadget choose which one to include. +#if !defined(__VMLINUX_H__) && !defined(_LINUX_TYPES_H) +#error "Include or before including this file." +#endif + +// Necessary for the SEC() definition +#include + +// This file is shared between the networking and tracing programs. +// Therefore, avoid includes that are specific to one of these types of programs. +// For example, don't include nor here. +// Redefine the constants we need but namespaced (SE_) so we don't pollute gadgets. + +#define SE_PACKET_HOST 0 +#define SE_ETH_HLEN 14 +#define SE_ETH_P_IP 0x0800 /* Internet Protocol packet */ +#define SE_ETH_P_IPV6 0x86DD /* IPv6 over bluebook */ +#define SE_AF_INET 2 /* Internet IP Protocol */ +#define SE_AF_INET6 10 /* IP version 6 */ + +#define SE_IPV6_HLEN 40 +#define SE_IPV6_NEXTHDR_OFFSET 6 // offsetof(struct ipv6hdr, nexthdr) + +#define SE_TCPHDR_DEST_OFFSET 2 // offsetof(struct tcphdr, dest); +#define SE_TCPHDR_SOURCE_OFFSET 0 // offsetof(struct tcphdr, source); +#define SE_UDPHDR_DEST_OFFSET 2 // offsetof(struct udphdr, dest); +#define SE_UDPHDR_SOURCE_OFFSET 0 // offsetof(struct udphdr, source); + +#define SE_NEXTHDR_HOP 0 /* Hop-by-hop option header. */ +#define SE_NEXTHDR_TCP 6 /* TCP segment. */ +#define SE_NEXTHDR_UDP 17 /* UDP message. */ +#define SE_NEXTHDR_ROUTING 43 /* Routing header. */ +#define SE_NEXTHDR_FRAGMENT 44 /* Fragmentation/reassembly header. */ +#define SE_NEXTHDR_AUTH 51 /* Authentication header. */ +#define SE_NEXTHDR_NONE 59 /* No next header */ +#define SE_NEXTHDR_DEST 60 /* Destination options header. */ + +#define SE_TASK_COMM_LEN 16 + +struct sockets_key { + __u32 netns; + __u16 family; + + // proto is IPPROTO_TCP(6) or IPPROTO_UDP(17) + __u8 proto; + __u16 port; +}; + +struct sockets_value { + __u64 mntns; + __u64 pid_tgid; + __u64 uid_gid; + char task[SE_TASK_COMM_LEN]; + __u64 sock; + __u64 deletion_timestamp; + char ipv6only; +}; + +#define MAX_SOCKETS 16384 +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, MAX_SOCKETS); + __type(key, struct sockets_key); + __type(value, struct sockets_value); +} gadget_sockets SEC(".maps"); + +#ifdef GADGET_TYPE_NETWORKING +static __always_inline struct sockets_value * +gadget_socket_lookup(const struct __sk_buff *skb) +{ + struct sockets_value *ret; + struct sockets_key key = { + 0, + }; + int l4_off; + __u16 h_proto; + int i; + long err; + + key.netns = skb->cb[0]; // cb[0] initialized by dispatcher.bpf.c + err = bpf_skb_load_bytes(skb, offsetof(struct ethhdr, h_proto), + &h_proto, sizeof(h_proto)); + if (err < 0) + return 0; + + switch (h_proto) { + case bpf_htons(SE_ETH_P_IP): + key.family = SE_AF_INET; + err = bpf_skb_load_bytes( + skb, SE_ETH_HLEN + offsetof(struct iphdr, protocol), + &key.proto, sizeof(key.proto)); + if (err < 0) + return 0; + + // An IPv4 header doesn't have a fixed size. The IHL field of a packet + // represents the size of the IP header in 32-bit words, so we need to + // multiply this value by 4 to get the header size in bytes. + __u8 ihl_byte; + err = bpf_skb_load_bytes(skb, SE_ETH_HLEN, &ihl_byte, + sizeof(ihl_byte)); + if (err < 0) + return 0; + struct iphdr *iph = (struct iphdr *)&ihl_byte; + __u8 ip_header_len = iph->ihl * 4; + l4_off = SE_ETH_HLEN + ip_header_len; + break; + + case bpf_htons(SE_ETH_P_IPV6): + key.family = SE_AF_INET6; + err = bpf_skb_load_bytes( + skb, SE_ETH_HLEN + SE_IPV6_NEXTHDR_OFFSET, + &key.proto, sizeof(key.proto)); + if (err < 0) + return 0; + l4_off = SE_ETH_HLEN + SE_IPV6_HLEN; + +// Parse IPv6 extension headers +// Up to 6 extension headers can be chained. See ipv6_ext_hdr(). +#pragma unroll + for (i = 0; i < 6; i++) { + __u8 nextproto; + __u8 off; + + // TCP or UDP found + if (key.proto == SE_NEXTHDR_TCP || + key.proto == SE_NEXTHDR_UDP) + break; + + err = bpf_skb_load_bytes(skb, l4_off, &nextproto, + sizeof(nextproto)); + if (err < 0) + return 0; + + // Unfortunately, each extension header has a different way to calculate the header length. + // Support the ones defined in ipv6_ext_hdr(). See ipv6_skip_exthdr(). + switch (key.proto) { + case SE_NEXTHDR_FRAGMENT: + // No hdrlen in the fragment header + l4_off += 8; + break; + case SE_NEXTHDR_AUTH: + // See ipv6_authlen() + err = bpf_skb_load_bytes(skb, l4_off + 1, &off, + sizeof(off)); + if (err < 0) + return 0; + l4_off += 4 * (off + 2); + break; + case SE_NEXTHDR_HOP: + case SE_NEXTHDR_ROUTING: + case SE_NEXTHDR_DEST: + // See ipv6_optlen() + err = bpf_skb_load_bytes(skb, l4_off + 1, &off, + sizeof(off)); + if (err < 0) + return 0; + l4_off += 8 * (off + 1); + break; + case SE_NEXTHDR_NONE: + // Nothing more in the packet. Not even TCP or UDP. + return 0; + default: + // Unknown header + return 0; + } + key.proto = nextproto; + } + break; + + default: + return 0; + } + + int off = l4_off; + switch (key.proto) { + case IPPROTO_TCP: + if (skb->pkt_type == SE_PACKET_HOST) + off += SE_TCPHDR_DEST_OFFSET; + else + off += SE_TCPHDR_SOURCE_OFFSET; + break; + case IPPROTO_UDP: + if (skb->pkt_type == SE_PACKET_HOST) + off += SE_UDPHDR_DEST_OFFSET; + else + off += SE_UDPHDR_SOURCE_OFFSET; + break; + default: + return 0; + } + + err = bpf_skb_load_bytes(skb, off, &key.port, sizeof(key.port)); + if (err < 0) + return 0; + key.port = bpf_ntohs(key.port); + + ret = bpf_map_lookup_elem(&gadget_sockets, &key); + if (ret) + return ret; + + // If a native socket was not found, try to find a dual-stack socket. + if (key.family == SE_AF_INET) { + key.family = SE_AF_INET6; + ret = bpf_map_lookup_elem(&gadget_sockets, &key); + if (ret && ret->ipv6only == 0) + return ret; + } + + return 0; +} +#endif + +#ifdef GADGET_TYPE_TRACING +static __always_inline struct sockets_value * +gadget_socket_lookup(const struct sock *sk, __u32 netns) +{ + struct sockets_key key = { + 0, + }; + key.netns = netns; + key.family = BPF_CORE_READ(sk, __sk_common.skc_family); + key.proto = BPF_CORE_READ_BITFIELD_PROBED(sk, sk_protocol); + if (key.proto != IPPROTO_TCP && key.proto != IPPROTO_UDP) + return 0; + + BPF_CORE_READ_INTO(&key.port, sk, __sk_common.skc_dport); + struct inet_sock *sockp = (struct inet_sock *)sk; + BPF_CORE_READ_INTO(&key.port, sockp, inet_sport); + // inet_sock.inet_sport is in network byte order + key.port = bpf_ntohs(key.port); + + return bpf_map_lookup_elem(&gadget_sockets, &key); +} +#endif + +#endif \ No newline at end of file diff --git a/pkg/ebpf/include/types.h b/pkg/ebpf/include/types.h index 78eb4ab7..cc49b63d 100644 --- a/pkg/ebpf/include/types.h +++ b/pkg/ebpf/include/types.h @@ -1,11 +1,71 @@ #pragma once -// This file contains inspektor gadget types that are needed untill we move to containerized build. +/* SPDX-License-Identifier: Apache-2.0 */ + +#ifndef __TYPES_H +#define __TYPES_H + +// Keep these types aligned with definitions in pkg/gadgets/run/tracer/tracer.go. + +// union defining either an IPv4 or IPv6 address +union gadget_ip_addr_t { + __u8 v6[16]; + __u32 v4; +}; + +// struct defining either an IPv4 or IPv6 L3 endpoint +struct gadget_l3endpoint_t { + union gadget_ip_addr_t addr_raw; + __u8 version; // 4 or 6 +}; + +// struct defining an L4 endpoint +struct gadget_l4endpoint_t { + union gadget_ip_addr_t addr_raw; + __u16 port; // L4 port in host byte order + __u16 proto; // IP protocol number + __u8 version; // 4 or 6 +}; // Inode id of a mount namespace. It's used to enrich the event in user space typedef __u64 gadget_mntns_id; +// Inode id of a network namespace. It's used to enrich the event in user space +typedef __u32 gadget_netns_id; + // gadget_timestamp is a type that represents the nanoseconds since the system boot. Gadgets can use // this type to provide a timestamp. The value contained must be the one returned by // bpf_ktime_get_boot_ns() and it's automatically converted by Inspektor Gadget to a human friendly // time. typedef __u64 gadget_timestamp; + +// gadget_signal is used to represent a unix signal. A field is automatically added that contains the name +// as string. +typedef __u32 gadget_signal; + +// gadget_errno is used to represent a unix errno. A field is automatically added that contains the name +// as string. +typedef __u32 gadget_errno; + +// gadget_uid is used to represent a uid. A field is automatically added that contains the corresponding user +// name on the host system +typedef __u32 gadget_uid; + +// gadget_gid is used to represent a uid. A field is automatically added that contains the corresponding group +// name on the host system +typedef __u32 gadget_gid; + +// gadget_syscall is used to represent a unix syscall. A field is automatically added that contains the name +// as string. +typedef __u64 gadget_syscall; + +typedef __u32 gadget_kernel_stack; + +// typedefs used for metrics +typedef __u32 gadget_counter__u32; +typedef __u64 gadget_counter__u64; +typedef __u32 gadget_gauge__u32; +typedef __u64 gadget_gauge__u64; +typedef __u32 gadget_histogram_slot__u32; +typedef __u64 gadget_histogram_slot__u64; + +#endif /* __TYPES_H */ diff --git a/pkg/ruleengine/v1/r1011_ld_preload_hook.go b/pkg/ruleengine/v1/r1011_ld_preload_hook.go index 912bef21..1a529d04 100644 --- a/pkg/ruleengine/v1/r1011_ld_preload_hook.go +++ b/pkg/ruleengine/v1/r1011_ld_preload_hook.go @@ -68,6 +68,11 @@ func (rule *R1011LdPreloadHook) handleExecEvent(execEvent *tracerexectype.Event, return nil } + // Check if the process is a MATLAB process and ignore it. + if execEvent.GetContainer() == "matlab" { + return nil + } + envVars, err := utils.GetProcessEnv(int(execEvent.Pid)) if err != nil { logger.L().Debug("Failed to get process environment variables", helpers.Error(err)) diff --git a/pkg/rulemanager/rule_manager_interface.go b/pkg/rulemanager/rule_manager_interface.go index 520fb383..52ae18c6 100644 --- a/pkg/rulemanager/rule_manager_interface.go +++ b/pkg/rulemanager/rule_manager_interface.go @@ -3,6 +3,7 @@ package rulemanager import ( tracerhardlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/hardlink/types" tracerrandomxtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/randomx/types" + tracersshtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" tracersymlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/symlink/types" containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection" @@ -11,6 +12,7 @@ import ( tracerexectype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/exec/types" tracernetworktype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/network/types" traceropentype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/open/types" + v1 "k8s.io/api/core/v1" ) @@ -25,6 +27,7 @@ type RuleManagerClient interface { ReportRandomxEvent(event tracerrandomxtype.Event) ReportSymlinkEvent(event tracersymlinktype.Event) ReportHardlinkEvent(event tracerhardlinktype.Event) + ReportSSHEvent(event tracersshtype.Event) HasApplicableRuleBindings(namespace, name string) bool HasFinalApplicationProfile(pod *v1.Pod) bool IsContainerMonitored(k8sContainerID string) bool diff --git a/pkg/rulemanager/rule_manager_mock.go b/pkg/rulemanager/rule_manager_mock.go index b8edc6b9..08d0cbd1 100644 --- a/pkg/rulemanager/rule_manager_mock.go +++ b/pkg/rulemanager/rule_manager_mock.go @@ -3,6 +3,7 @@ package rulemanager import ( tracerhardlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/hardlink/types" tracerrandomxtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/randomx/types" + tracersshtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" tracersymlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/symlink/types" containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection" @@ -63,6 +64,10 @@ func (r *RuleManagerMock) ReportHardlinkEvent(_ tracerhardlinktype.Event) { // noop } +func (r *RuleManagerMock) ReportSSHEvent(_ tracersshtype.Event) { + // noop +} + func (r *RuleManagerMock) HasApplicableRuleBindings(_, _ string) bool { return false } diff --git a/pkg/rulemanager/v1/rule_manager.go b/pkg/rulemanager/v1/rule_manager.go index 848bd250..92c3b3f4 100644 --- a/pkg/rulemanager/v1/rule_manager.go +++ b/pkg/rulemanager/v1/rule_manager.go @@ -28,6 +28,7 @@ import ( tracerhardlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/hardlink/types" tracerrandomxtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/randomx/types" + tracersshtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" tracersymlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/symlink/types" ruleenginetypes "github.com/kubescape/node-agent/pkg/ruleengine/types" @@ -423,6 +424,17 @@ func (rm *RuleManager) ReportHardlinkEvent(event tracerhardlinktype.Event) { rm.processEvent(utils.HardlinkEventType, &event, rules) } +func (rm *RuleManager) ReportSSHEvent(event tracersshtype.Event) { + if event.GetNamespace() == "" || event.GetPod() == "" { + logger.L().Error("RuleManager - failed to get namespace and pod name from ReportSSHEvent event") + return + } + + // list ssh rules + rules := rm.ruleBindingCache.ListRulesForPod(event.GetNamespace(), event.GetPod()) + rm.processEvent(utils.SSHEventType, &event, rules) +} + func (rm *RuleManager) processEvent(eventType utils.EventType, event interface{}, rules []ruleengine.RuleEvaluator) { for _, rule := range rules { if rule == nil { diff --git a/pkg/utils/events.go b/pkg/utils/events.go index 8198f837..5af8e2e0 100644 --- a/pkg/utils/events.go +++ b/pkg/utils/events.go @@ -12,5 +12,6 @@ const ( RandomXEventType SymlinkEventType HardlinkEventType + SSHEventType AllEventType ) From a95a7024f76a3d3fe1c16770ebbcbe02c040fc49 Mon Sep 17 00:00:00 2001 From: Amit Schendel Date: Tue, 13 Aug 2024 11:29:13 +0000 Subject: [PATCH 2/4] Adding final ssh code Signed-off-by: Amit Schendel --- .../v1/container_watcher_private.go | 1 + pkg/containerwatcher/v1/ssh.go | 36 ++- pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.bpf.c | 37 +--- pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.h | 2 + pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.go | 19 +- pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.o | Bin 20088 -> 18344 bytes pkg/ebpf/gadgets/ssh/tracer/tracer.go | 205 +++++------------- pkg/ebpf/gadgets/ssh/types/types.go | 19 +- .../v1/r1003_malicious_ssh_connection.go | 166 ++++---------- 9 files changed, 148 insertions(+), 337 deletions(-) diff --git a/pkg/containerwatcher/v1/container_watcher_private.go b/pkg/containerwatcher/v1/container_watcher_private.go index 3e0d1c3b..bee90137 100644 --- a/pkg/containerwatcher/v1/container_watcher_private.go +++ b/pkg/containerwatcher/v1/container_watcher_private.go @@ -258,6 +258,7 @@ func (ch *IGContainerWatcher) startTracers() error { return err } + // NOTE: SSH tracing relies on the network tracer, so it must be started after the network tracer. if err := ch.startSshTracing(); err != nil { logger.L().Error("error starting ssh tracing", helpers.Error(err)) return err diff --git a/pkg/containerwatcher/v1/ssh.go b/pkg/containerwatcher/v1/ssh.go index 49a3f7b8..a2976e50 100644 --- a/pkg/containerwatcher/v1/ssh.go +++ b/pkg/containerwatcher/v1/ssh.go @@ -3,9 +3,10 @@ package containerwatcher import ( "fmt" - tracersshlink "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/tracer" + tracerssh "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/tracer" tracersshtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" + "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection/networktracer" "github.com/inspektor-gadget/inspektor-gadget/pkg/types" "github.com/kubescape/go-logger" "github.com/kubescape/go-logger/helpers" @@ -21,6 +22,9 @@ func (ch *IGContainerWatcher) sshEventCallback(event *tracersshtype.Event) { return } + ch.containerCollection.EnrichByMntNs(&event.CommonData, event.MountNsID) + ch.containerCollection.EnrichByNetNs(&event.CommonData, event.NetNsID) + ch.sshWorkerChan <- event } @@ -29,13 +33,7 @@ func (ch *IGContainerWatcher) startSshTracing() error { return fmt.Errorf("adding tracer: %w", err) } - // Get mount namespace map to filter by containers - sshMountnsmap, err := ch.tracerCollection.TracerMountNsMap(sshTraceName) - if err != nil { - return fmt.Errorf("getting sshMountnsmap: %w", err) - } - - tracerSsh, err := tracersshlink.NewTracer(&tracersshlink.Config{MountnsMap: sshMountnsmap}, ch.containerCollection, ch.sshEventCallback) + tracerSsh, err := tracerssh.NewTracer() if err != nil { return fmt.Errorf("creating tracer: %w", err) } @@ -45,8 +43,28 @@ func (ch *IGContainerWatcher) startSshTracing() error { } }() + tracerSsh.SetSocketEnricherMap(ch.socketEnricher.SocketsMap()) + tracerSsh.SetEventHandler(ch.sshEventCallback) + + err = tracerSsh.RunWorkaround() + if err != nil { + return fmt.Errorf("running workaround: %w", err) + } + ch.sshTracer = tracerSsh + config := &networktracer.ConnectToContainerCollectionConfig[tracersshtype.Event]{ + Tracer: ch.sshTracer, + Resolver: ch.containerCollection, + Selector: ch.containerSelector, + Base: tracersshtype.Base, + } + + _, err = networktracer.ConnectToContainerCollection(config) + if err != nil { + return fmt.Errorf("creating tracer: %w", err) + } + return nil } @@ -55,6 +73,6 @@ func (ch *IGContainerWatcher) stopSshTracing() error { if err := ch.tracerCollection.RemoveTracer(sshTraceName); err != nil { return fmt.Errorf("removing tracer: %w", err) } - ch.sshTracer.Stop() + ch.sshTracer.Close() return nil } diff --git a/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.bpf.c b/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.bpf.c index 6eaefe30..8fc637d5 100644 --- a/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.bpf.c +++ b/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.bpf.c @@ -1,5 +1,3 @@ -//#include "../../../../include/amd64/vmlinux.h" - #include #include #include @@ -11,15 +9,15 @@ #include #include -#include - -#include "ssh.h" #include "../../../../include/macros.h" #include "../../../../include/buffer.h" + #define GADGET_TYPE_NETWORKING #include "../../../../include/sockets-map.h" +#include "ssh.h" + // Events map. struct { @@ -39,23 +37,6 @@ struct { // we need this to make sure the compiler doesn't remove our struct. const struct event *unusedevent __attribute__((unused)); -const volatile bool gadget_filter_by_mntns = false; - -struct { - __uint(type, BPF_MAP_TYPE_HASH); - __type(key, gadget_mntns_id); - __type(value, __u32); - __uint(max_entries, 1024); -} gadget_mntns_filter_map SEC(".maps"); - -// gadget_should_discard_mntns_id returns true if events generated from the given mntns_id should -// not be taken into consideration. -static __always_inline bool gadget_should_discard_mntns_id(gadget_mntns_id mntns_id) -{ - return gadget_filter_by_mntns && - !bpf_map_lookup_elem(&gadget_mntns_filter_map, &mntns_id); -} - SEC("socket") int ssh_detector(struct __sk_buff *skb) { // Check if it's an IP packet @@ -92,7 +73,6 @@ int ssh_detector(struct __sk_buff *skb) { // Check for SSH signature using memcmp if (__builtin_memcmp(payload, SSH_SIGNATURE, SSH_SIG_LEN) == 0) { - bpf_printk("SSH packet detected\n"); struct event *event; __u32 zero = 0; event = bpf_map_lookup_elem(&empty_event, &zero); @@ -103,12 +83,6 @@ int ssh_detector(struct __sk_buff *skb) { // Enrich event with process metadata struct sockets_value *skb_val = gadget_socket_lookup(skb); if (skb_val != NULL) { - __u64 mntns_id = skb_val->mntns; - if (gadget_should_discard_mntns_id(mntns_id)) { - bpf_printk("Discarding event from mntns_id %llu\n", mntns_id); - return 0; - } - event->netns = skb->cb[0]; // cb[0] initialized by dispatcher.bpf.c event->mntns_id = skb_val->mntns; event->pid = skb_val->pid_tgid >> 32; @@ -122,10 +96,9 @@ int ssh_detector(struct __sk_buff *skb) { event->dst_port = bpf_ntohs(tcph.dest); event->timestamp = bpf_ktime_get_boot_ns(); - } - - bpf_perf_event_output(skb, &events, BPF_F_CURRENT_CPU, &event, sizeof(struct event)); + __u64 skb_len = skb->len; + bpf_perf_event_output(skb, &events, skb_len << 32 | BPF_F_CURRENT_CPU, event, sizeof(struct event)); } return 0; diff --git a/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.h b/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.h index 7c075692..b574e7ec 100644 --- a/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.h +++ b/pkg/ebpf/gadgets/ssh/tracer/bpf/ssh.h @@ -17,7 +17,9 @@ #define ETH_HLEN 14 struct event { + // Keep netns at the top: networktracer depends on it __u32 netns; + gadget_timestamp timestamp; gadget_mntns_id mntns_id; __u32 pid; diff --git a/pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.go b/pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.go index 03e048aa..0172cc7a 100644 --- a/pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.go +++ b/pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.go @@ -75,11 +75,10 @@ type sshProgramSpecs struct { // // It can be passed ebpf.CollectionSpec.Assign. type sshMapSpecs struct { - EmptyEvent *ebpf.MapSpec `ebpf:"empty_event"` - Events *ebpf.MapSpec `ebpf:"events"` - GadgetHeap *ebpf.MapSpec `ebpf:"gadget_heap"` - GadgetMntnsFilterMap *ebpf.MapSpec `ebpf:"gadget_mntns_filter_map"` - GadgetSockets *ebpf.MapSpec `ebpf:"gadget_sockets"` + EmptyEvent *ebpf.MapSpec `ebpf:"empty_event"` + Events *ebpf.MapSpec `ebpf:"events"` + GadgetHeap *ebpf.MapSpec `ebpf:"gadget_heap"` + GadgetSockets *ebpf.MapSpec `ebpf:"gadget_sockets"` } // sshObjects contains all objects after they have been loaded into the kernel. @@ -101,11 +100,10 @@ func (o *sshObjects) Close() error { // // It can be passed to loadSshObjects or ebpf.CollectionSpec.LoadAndAssign. type sshMaps struct { - EmptyEvent *ebpf.Map `ebpf:"empty_event"` - Events *ebpf.Map `ebpf:"events"` - GadgetHeap *ebpf.Map `ebpf:"gadget_heap"` - GadgetMntnsFilterMap *ebpf.Map `ebpf:"gadget_mntns_filter_map"` - GadgetSockets *ebpf.Map `ebpf:"gadget_sockets"` + EmptyEvent *ebpf.Map `ebpf:"empty_event"` + Events *ebpf.Map `ebpf:"events"` + GadgetHeap *ebpf.Map `ebpf:"gadget_heap"` + GadgetSockets *ebpf.Map `ebpf:"gadget_sockets"` } func (m *sshMaps) Close() error { @@ -113,7 +111,6 @@ func (m *sshMaps) Close() error { m.EmptyEvent, m.Events, m.GadgetHeap, - m.GadgetMntnsFilterMap, m.GadgetSockets, ) } diff --git a/pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.o b/pkg/ebpf/gadgets/ssh/tracer/ssh_bpfel.o index e0afa5f2d170abe93a52caec099012d27196700e..44b9eff22a3d7df8d4cd1e1d9c0c18a7e7ad53c6 100644 GIT binary patch literal 18344 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 literal 20088 zcmd6v3viUzb;s{YU?iR*Mv1^U`S1e^EW`?g5V64vShk!{tN}Dpn#XEct$t1WkJAEqEV1?e;ADKExmp|q7%W92z`NoYKms5_PuQ6sd#OIe7vkvhu;o3l!N=IU! z0U2}Z;x}(F?u~ug+(G{A%a%_pGX_PNcOiZl@iN3LFRX7msPA$Ci3m5<$3`NI)LpP~ z_Su(g`DakLEgyea{u;?2mj7Z)v0G+wEW~In+*O7 zmyUYqxl^CI>=^!P0zY}LvwQMR7x&iwqB(lBds`FA6Ia;;`m+%d{YXDO4}UPEpZc6F z@A9*bm%;9HXgBkRf%&L9w|vUlH=5Hn`EtFpNeElXV&haQZWGcQ=3M!~IR7a0 z3+h`5OZo?5)j!KpHh=cwk6eARyUZ_tljHn;>nF|w+MnFu>I?n;V^#iJqk+ywp5kWA-tNjn{T+~eI~!v^y1{X8jQ#Ki+Y|Qt2VuV# zEqA*u*O!-xrOku3JocL9#w;&2TP=s>e?KfA=DT6Bc>C|M`A=Q?zVj;<1*W=LYSLC; z^&fkWb&c}ag0Q~pVSQ>|RsF=(U`+O+o44OUaa>;~H#iR0!yL+6Y(4ZE&MubY=3#Ga zxoyu)Ro%6ny*^B=YiBT##R~D)EN8Z4ekrQ zv9H|Ni8!3E|GV|!?g!!e;1pOU^*n>BzxAf_t-qyTYd+%Iul?=ExiR*S zuW}|<`5#`r)p^C+Ek3`RZqxsFI{(YOXpHH7XU)%OzTe^6^M5wq|LawH_*787^}qCM z@%jGy*6)q6=Wgxq2i*8A?(grp`d@Q@`B!Vl`@q8c%eP+T{nC%`>z|KTZ}P{_c7J*4 zrt)vc-%HKYHXk3>qI@cSGE<5MGNnwqlo?oV9?cfhslq^Z^hA_7of$1fgN58kG%{Km zEhe)A(fz~2W6SMho*Rt+hkqX(OASW{jy}5m@t#9$-tT=sW}jCn|1JicS1%usT&Urg z@czLFM-CHJ(B2O0shE0qMu1SYvw%M}?ha;=#Z~f1xmLxwCP&OVo@C!f?E+=-6u@&E z;w1^hJ8&^C15c7X@Ltk`qjPsLi_?qLo zI}y(~*`D%!KcTXId=muvo_QD`FNqNUGcGjChVS6-Qd|jK|BegWz4gg|Am(%JG_L=| zg|M1WUOpeh8h2u6()_VzoeXHi656mnIWC!{D1dST*J_c=kk`9{S96TRIyQnO*gnw* zw)|$NeXTzE`pPh8eRmAi@*}**O%~WQ*#f*7Gz@GoKN%S81(dagzFK= z9D_KnF3~3-OQ^XI!aJac&zB@1_d(_yVR*mD3CJfzEM|^A_VPfLO!z zC86d)FR8iW2GjX6f(_@2ubcJpZA$_d>+^GyZQ|RCXlh*-xRqYGPXTyUxxgO$diz{2wBcFuH_=nLT(h9jd-ib5#&~p6OcPZ z?uYycWX_8+b%`TQ+m`5rGBQ_nYaUvUf6ucsw1IbO|s3-bD4`*S}?^=CPC z60FT$0ByLpen-mMCk7JDvt$12VwYIJ&eutkHSRvW`T*phZ8crhe9Korev8;WDt7mR zVQ-i3V`58xH$%Tv^!}Yd1U=vHGjOketCft25H|<%wm@!0Ip~xv{~^({eUIQ`OkeEB z#X8uZj-WjU!3^x*KIjp+Eto@rY_g*zlT7YEa%BIzlSiNU`L5)#clUH9lhBRr++oH> zi`f&SnSqFA_LycyGdQ7{Qg$R$ETu;B#-G?sK09E>aGk(4P%I_$xkAYl3uz}%lg(R3 zl+KNem=mdi6PZ%7S`_N+Xs^~eoWt3Wf4pU-Dj2qPR1J$mtP0A`sxCd0DwxuEK4V5w zXOlQz7P6V5Ihh$Zr&GgY8Iu{wm&TK>OVB2Z*{3ot$%!_l=xZLzq|o*pkFdog3mHs} zWQWI1zK|>BjO(eSy5en9>5^oL{(xVSj7zEFNn4H?$P8yn+1zN*{cQepM{X3Q+O~^# zq*C7y=YqlPa4Azrj-XM=WbtJ3_}Ji}8P1HF{7LxJb-HlUoEpoFWoVVhDciL*of|f% zhf|};d?8bWR~(emS+BMIDP(hnY-t>m;{?ob9v{q(4rI<6AD7Zer$`?+L#g7Bfg(Me zDi%5PsZz@Rfzt=fXeyt@ao!X%Be_xrGoalJ=hCTRD^-*o6=jFFv_5sFR%EfskqpW% zxp{XcTgW8Y=M%+TvY0u6v7*3XbQaDLLubT(a5#5{Lsv8$f|JKhu{e};k0ZH)jIBAI zAKVUoE8^BP*G3dtH5W^Tv2-b__&nN*g0@8GOoWgfjGBSl_Ss1RM@O6XMkpvbRLYGO zo4by6Cwr1l^laHuv1TH2B!_dUf#mV=Ql`iPTB2+|nHwA|W=bv5=4^hbC1S5~gUyh) zY>6I@;!*^fS4w-^Xm2%N^k78e*6h%5v^8qq^3ZEmSuF>30;-h~bglW+xNWm*C2Fo_ zXVuKoI)KF6tbBfPEqGS6#kWLwaKxtcc=Ax!;jrIubP{tOiy%9SIWdwR$v5jfhxZ@r zJ<_F$*t-|Lli}i!aRBlK4D-q6dlq;|eedW!;yN1J9Wi<;Q^=viapzL%-P5ZZYOFPk zX71!zK8f``(!4p?X5d(64oc*V*<`b`QE1He*4FBO+0pdy*g$5x>;T(PcI%L-mpK@0 zMiFK;nsm<8Luj@8Gtf@O^zrw^-}g|oeS2gjY~$HdHZ_dtG!Pvhj|Q^Ee5#Zl$`oYA z*Jla^Gn@l zb+jwF|FPr~hr5nFgrxe-?1C+m_ksazzSw%ue5|r<`(&B1b;Ipkx~^ZobS#N^*=SeYEM&)yFgMYn zp8W^k*>x=0{p8VOTVTc}Ea0jR>noN~X`|#n+|_ri`_UuGV+VVpr=La%ui*sOUi5!7 zbGGFC&P>XYi@D_XN=N_OPy_q|=PppVciMw{qfYD?&2%8LaI{}@I-*?b0vrJ5PZ%id zeDSF=t4B+@p`!B|El@=PZ>1Jlyz_6tt}14scaRMOEjg|6aC3gqe?2a!j(Se@Fl8+@1_8vLXb@*8FU{5c4 zVbdz!@=KZT;OMar7?$k=CVfYH^{t%UR+2d4Y};o~BF=wfh0#cSB`1^P<4K#2E|_{L zYZKdh(*`N|^tlGjbgJwC+T>fqm@fANGn5+}9!O#r*XP~GJOIQ&YlJO%SV809*yCNq?j5QC@g5^ z9uUNAU{N#wKmkL}C%2yMfuookE2J|Ex4}P;+?;Cwk1-49sXUiRrjt}8`NVQOmn$W4 zq(eULXR_yG`)aRlUpri4us=tYEp~qY=`y>NLPuET>ZpJ5qxSm}wSOfWPTxQPM zlOM;m`nt3ATn~=K9+3@vo@V+Ou6q11-=la)_ywdtfEPpgfFA^x1CGEG0n`41@HLj- z9kcJaSfA%BlHQ6J7Tfw_wogna?*PvR>Gy-tMcZC)k1koxz`lMcW*Xoq=J(trJjL|0 zF#{8u?l~{~W2Qfi_6pz2ml7Y2nM*2tO86n9uRR|#R{~xGo)I2H`YOCMody3Y=3 zd>HK){%i2+_r}cifLDU+@Vn3VUXTWR0-NLs2a86Ia%fNlYetz@|KZEkNZmc!=fa|~$ z!gjtywdR8GEbLdonU?}y0e&H1rt`fl+aJRZWOZ=%RpEB<9qqMdR(KzH`J=VwTEKUJ zuM6Ap>aH~nD2wGKVPAi!)-(%$9K52Z*2Dw86Wkf_GH^GT>-!7LZ)(hW;Wy!jr5MLg zcpdlw%-2i8`@!|!z+RtG>5&kx~`LFQ1rEmjYa&E-@ zje_*Mk=`7nZ$NrnxF7cSUcq_|xCNXDxCz`X{4v-+_!9QxfSbU5!e2r9PR#%QfSbTW z!u(Nk73TkW;U4fNtdEI+?*d;CKErgZkI8`V0$&n-2I+76Zp^$8@ZI2P;g^|y4f{dB zcZ07A^9RUi4*NmCZv|fmb3SlAas4!5-I4A3>BD*uw(F-K>p?hy^41MuJp{ZK+%N3c z$64X`!M@=OSnmO^2A>z^e!%rVDeTw(jIdw-SB3rhpADGp=M9_l=M3swJB9HEhyCjj zX8*Qge;5jw`^AK?-!Co*`~6}vVD1-FLH@1q*Oh=*faip5dGOzLVedbF!pZiL=|BDu zPWB-Z_Wo-QnEvY&_WtV=_WnCB?EN<(?EQBkVEXT>u=mgPfa#wmtV6cX`zI3i{%H=F z{^=C9<-s3)0n|CELO{FxB;{<#n^{WB@-{qusb_s=z9@1Hqg@1N@d(?6Z~ zO^f~a{^<*t{y8h`=TBMK`{#VX^v{JLKj+U20n5o`scMmt-)rjLJQ8^{I!s0hcL7NV)!-ECPU_Lh74(2O|AX?*=gM0f0A-=teO{I zv4pdaY94?8XT3b*g<(?kJj;dQi=yvS`u9kC=5X%}!*7WFoNCX{`SNkE3d2Szhi9fR zjEa3i>Ax-d8KwVi(Ko5~^org-#0C}IC;B1Pp3jRuj(b!X_?-*=)}!?Iqx~E&|GV-1 zlIf>n-++5Z7*>jYN|m!t{J?u(7&eOi3rfFI?0K&W!=Tt-z@8q4zZHF_(tkwsyr+cW z711|iZwLNLM1NiB&xrnl((e*I_ZA&$F@F{cwMI>5 z7=0Mmo8TWGn(WsG#zau8HPy*XW;lwA`aVm}j^ZKFM+Af>7nu>nYlHUKW<&RzR=>=g z6}w2{A6R{LGJCf^XZ6eMw}~{3_y>XSme%sXC&A~;OGS*f;li=i21apQdHL=FTEOj0kcyek`j!Z@8ouA!7%=N^4w&V)2h8tk5&^S)hXQ7O z1Hv3Rwukp31~Th^TJdiM`T3ns3oZt>k8MuiV({GKu)_U{^NP!gClpUAo>Dxmct-K8 z;yJ~(6VUngH?UIyM~dT$6N-Bj_bbjTE-Ri;JgIm}@wDO@#j}d%6dSwW1pQYWDUK^n zDDF|*uQ;!`tQg0rdH$bNJf(PA@r>eG#dC`N7FHR5+0r~misOnCit$=zUieG#dC`7#7B#Le;X(S94U?~PAKkC+^;yVxU6_W@ucD@#nXys6wfN2 zQ|xasRC8(g{oHdT`?Y7jlVk9lP~4-qUvXY>S@DG8NySr&rxnjAo>e@j*x%ZHe;VXg z?>SN&SDaAXqqtvjUU6CRgyKoXQ;Me*&nTW%Jg3+nReXONawC_>r{fhm0!`D|<=@W`4 z6;COiRy?D4R`Hx-e+2aH@y|(~`Oc5Qb6jykagXAD#d*bL#S@Ar6;COiRy?D4R`Hx- z`;Sn9{x!(+m+vp%DKdDDD^4ixQQWUMuehvuLh+>HDaF%@XB5vWo>R%Zc-;)G)Uwi4#=SDaT| zRy?72Qt_1HX~i>&XBE#WHu9iY>A&Jgaa{49y-VKy=7%3iJhacWmhd?oK6l6rw;nk5 zSSum?BqI2#bHOhuY)`8agoKs>o;kFoJ?i900TDe<08m)-a zJ6%Gj#JePJ^KnIpi>70T%h4`zCq9XxUv^2{E^%91+kyj+PiW*D8T!4AU({H)#$CKy z;vEvVwZ+{MdqaP}hTQTfFt42D-42K4Imsu=W1~lVR;EslQx$!pBFz`LzQWK2xK)1t zKFQ~i+qOsh?^k#$OH?#Yn2~=_R2Ds7OR=9me}m}1j+phUZkj%MkLCZ}jF?+nFEhr^ z2Uf>-Fk$$()#TL1t6 diff --git a/pkg/ebpf/gadgets/ssh/tracer/tracer.go b/pkg/ebpf/gadgets/ssh/tracer/tracer.go index 67f5e005..0fb8e9a8 100644 --- a/pkg/ebpf/gadgets/ssh/tracer/tracer.go +++ b/pkg/ebpf/gadgets/ssh/tracer/tracer.go @@ -1,202 +1,109 @@ package tracer import ( + "context" "encoding/binary" - "errors" "fmt" - "net" - "os" - "syscall" + "net/netip" "unsafe" - "github.com/cilium/ebpf" - "github.com/cilium/ebpf/perf" - gadgetcontext "github.com/inspektor-gadget/inspektor-gadget/pkg/gadget-context" "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets" - "github.com/inspektor-gadget/inspektor-gadget/pkg/rawsock" + "github.com/inspektor-gadget/inspektor-gadget/pkg/networktracer" eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" - "github.com/kubescape/go-logger" - "github.com/kubescape/go-logger/helpers" "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" - "golang.org/x/sys/unix" ) //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -no-global-types -target bpfel -cc clang -cflags "-g -O2 -Wall" -type event ssh bpf/ssh.bpf.c -- -I./bpf/ -type Config struct { - MountnsMap *ebpf.Map -} - type Tracer struct { - config *Config - enricher gadgets.DataEnricherByMntNs - netEnricher gadgets.DataEnricherByNetNs - eventCallback func(*types.Event) - - objs sshObjects + *networktracer.Tracer[types.Event] - file int - reader *perf.Reader + cancel context.CancelFunc } -func NewTracer(config *Config, enricher gadgets.DataEnricherByMntNs, - eventCallback func(*types.Event), - netEnricher gadgets.DataEnricherByNetNs, -) (*Tracer, error) { - t := &Tracer{ - config: config, - enricher: enricher, - netEnricher: netEnricher, - eventCallback: eventCallback, - } +func NewTracer() (*Tracer, error) { + t := &Tracer{} if err := t.install(); err != nil { - t.close() - return nil, err + t.Close() + return nil, fmt.Errorf("installing tracer: %w", err) } - go t.run() - return t, nil } -func (t *Tracer) Stop() { - t.close() -} - -func (t *Tracer) close() { - if t.file != 0 { - syscall.Close(t.file) - t.file = 0 +func (t *Tracer) Close() { + if t.cancel != nil { + t.cancel() } - if t.reader != nil { - t.reader.Close() + if t.Tracer != nil { + t.Tracer.Close() } - - t.objs.Close() } func (t *Tracer) install() error { - spec, err := loadSsh() + networkTracer, err := networktracer.NewTracer[types.Event]() if err != nil { - return fmt.Errorf("loading ebpf program: %w", err) - } - - if err := gadgets.LoadeBPFSpec(t.config.MountnsMap, spec, nil, &t.objs); err != nil { - return fmt.Errorf("loading ebpf spec: %w", err) - } - - // Open raw socket - rawSockFd, err := rawsock.OpenRawSock(1) - if err != nil { - logger.L().Error("Error opening raw socket", helpers.Error(err)) - } - - // Attach BPF program to raw socket - if err := syscall.SetsockoptInt(rawSockFd, syscall.SOL_SOCKET, unix.SO_ATTACH_BPF, t.objs.SshDetector.FD()); err != nil { - logger.L().Error("Error attaching BPF program to raw socket", helpers.Error(err)) + return fmt.Errorf("creating network tracer: %w", err) } - - // Store the file for later cleanup - t.file = rawSockFd - - t.reader, err = perf.NewReader(t.objs.Events, gadgets.PerfBufferPages*os.Getpagesize()) - if err != nil { - return fmt.Errorf("creating perf ring buffer: %w", err) - } - + t.Tracer = networkTracer return nil } -func (t *Tracer) run() { - for { - record, err := t.reader.Read() - if err != nil { - if errors.Is(err, perf.ErrClosed) { - return - } - - msg := fmt.Sprintf("Error reading perf ring buffer: %s", err) - t.eventCallback(types.Base(eventtypes.Err(msg))) - return - } - - if record.LostSamples > 0 { - msg := fmt.Sprintf("lost %d samples", record.LostSamples) - t.eventCallback(types.Base(eventtypes.Warn(msg))) - continue - } - - bpfEvent := (*sshEvent)(unsafe.Pointer(&record.RawSample[0])) - - srcIP := make(net.IP, 4) - dstIP := make(net.IP, 4) - binary.BigEndian.PutUint32(srcIP, bpfEvent.SrcIp) - binary.BigEndian.PutUint32(dstIP, bpfEvent.DstIp) - - event := types.Event{ - Event: eventtypes.Event{ - Type: eventtypes.NORMAL, - Timestamp: gadgets.WallTimeFromBootTime(bpfEvent.Timestamp), - }, - WithMountNsID: eventtypes.WithMountNsID{MountNsID: bpfEvent.MntnsId}, - WithNetNsID: eventtypes.WithNetNsID{NetNsID: uint64(bpfEvent.Netns)}, - SrcIP: srcIP.String(), - DstIP: dstIP.String(), - SrcPort: bpfEvent.SrcPort, - DstPort: bpfEvent.DstPort, - Pid: bpfEvent.Pid, - Uid: bpfEvent.Uid, - Gid: bpfEvent.Gid, - Comm: gadgets.FromCString(bpfEvent.Comm[:]), - } - - if t.enricher != nil { - t.enricher.EnrichByMntNs(&event.CommonData, event.MountNsID) - t.netEnricher.EnrichByNetNs(&event.CommonData, event.NetNsID) - } - - logger.L().Info("SSH event", helpers.Interface("event", event)) - - t.eventCallback(&event) +func (t *Tracer) RunWorkaround() error { + if err := t.run(); err != nil { + t.Close() + return fmt.Errorf("running tracer: %w", err) } + return nil } -func (t *Tracer) Run(gadgetCtx gadgets.GadgetContext) error { - defer t.close() - if err := t.install(); err != nil { - return fmt.Errorf("installing tracer: %w", err) +func (t *Tracer) run() error { + spec, err := loadSsh() + if err != nil { + return fmt.Errorf("loading ebpf program: %w", err) } - go t.run() - gadgetcontext.WaitForTimeoutOrDone(gadgetCtx) + if err := t.Tracer.Run(spec, types.Base, t.parseSSH); err != nil { + return fmt.Errorf("setting network tracer spec: %w", err) + } return nil } -func (t *Tracer) SetMountNsMap(mountnsMap *ebpf.Map) { - t.config.MountnsMap = mountnsMap -} - -func (t *Tracer) SetEventHandler(handler any) { - nh, ok := handler.(func(ev *types.Event)) - if !ok { - panic("event handler invalid") +func (t *Tracer) parseSSH(rawSample []byte, netns uint64) (*types.Event, error) { + bpfEvent := (*sshEvent)(unsafe.Pointer(&rawSample[0])) + + srcIP := [4]byte{} + binary.BigEndian.PutUint32(srcIP[:], bpfEvent.SrcIp) + src := netip.AddrFrom4(srcIP).String() + dstIP := [4]byte{} + binary.BigEndian.PutUint32(dstIP[:], bpfEvent.DstIp) + dst := netip.AddrFrom4(dstIP).String() + event := types.Event{ + Event: eventtypes.Event{ + Type: eventtypes.NORMAL, + Timestamp: gadgets.WallTimeFromBootTime(bpfEvent.Timestamp), + }, + WithMountNsID: eventtypes.WithMountNsID{MountNsID: bpfEvent.MntnsId}, + WithNetNsID: eventtypes.WithNetNsID{NetNsID: netns}, + SrcIP: src, + DstIP: dst, + SrcPort: bpfEvent.SrcPort, + DstPort: bpfEvent.DstPort, + Pid: bpfEvent.Pid, + Uid: bpfEvent.Uid, + Gid: bpfEvent.Gid, + Comm: gadgets.FromCString(bpfEvent.Comm[:]), } - t.eventCallback = nh + + return &event, nil } type GadgetDesc struct{} func (g *GadgetDesc) NewInstance() (gadgets.Gadget, error) { - tracer := &Tracer{ - config: &Config{}, - } + tracer := &Tracer{} return tracer, nil } - -// // Helper function to convert host byte order to network byte order -// func htons(host uint16) uint16 { -// return (host&0xff)<<8 | (host&0xff00)>>8 -// } diff --git a/pkg/ebpf/gadgets/ssh/types/types.go b/pkg/ebpf/gadgets/ssh/types/types.go index 4cea4e68..65944120 100644 --- a/pkg/ebpf/gadgets/ssh/types/types.go +++ b/pkg/ebpf/gadgets/ssh/types/types.go @@ -10,17 +10,14 @@ type Event struct { eventtypes.WithMountNsID eventtypes.WithNetNsID - Pid uint32 `json:"pid,omitempty" column:"pid,template:pid"` - PPid uint32 `json:"ppid,omitempty" column:"ppid,template:ppid"` - Uid uint32 `json:"uid,omitempty" column:"uid,template:uid"` - Gid uint32 `json:"gid,omitempty" column:"gid,template:gid"` - UpperLayer bool `json:"upperlayer,omitempty" column:"upperlayer,template:upperlayer"` - Comm string `json:"comm,omitempty" column:"comm,template:comm"` - ExePath string `json:"exe_path,omitempty" column:"exe_path,template:exe_path"` - SrcPort uint16 `json:"src_port,omitempty" column:"src_port,template:src_port"` - DstPort uint16 `json:"dst_port,omitempty" column:"dst_port,template:dst_port"` - SrcIP string `json:"src_ip,omitempty" column:"src_ip,template:src_ip"` - DstIP string `json:"dst_ip,omitempty" column:"dst_ip,template:dst_ip"` + Pid uint32 `json:"pid,omitempty" column:"pid,template:pid"` + Uid uint32 `json:"uid,omitempty" column:"uid,template:uid"` + Gid uint32 `json:"gid,omitempty" column:"gid,template:gid"` + Comm string `json:"comm,omitempty" column:"comm,template:comm"` + SrcPort uint16 `json:"src_port,omitempty" column:"src_port,template:src_port"` + DstPort uint16 `json:"dst_port,omitempty" column:"dst_port,template:dst_port"` + SrcIP string `json:"src_ip,omitempty" column:"src_ip,template:src_ip"` + DstIP string `json:"dst_ip,omitempty" column:"dst_ip,template:dst_ip"` } func GetColumns() *columns.Columns[Event] { diff --git a/pkg/ruleengine/v1/r1003_malicious_ssh_connection.go b/pkg/ruleengine/v1/r1003_malicious_ssh_connection.go index b4ceb5ce..ae62a3ce 100644 --- a/pkg/ruleengine/v1/r1003_malicious_ssh_connection.go +++ b/pkg/ruleengine/v1/r1003_malicious_ssh_connection.go @@ -3,51 +3,24 @@ package ruleengine import ( "fmt" "slices" - "strings" - "time" + "github.com/goradd/maps" "github.com/kubescape/node-agent/pkg/objectcache" "github.com/kubescape/node-agent/pkg/ruleengine" "github.com/kubescape/node-agent/pkg/utils" apitypes "github.com/armosec/armoapi-go/armotypes" - tracernetworktype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/network/types" - - traceropentype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/open/types" "github.com/kubescape/go-logger" + + tracersshtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" ) const ( - R1003ID = "R1003" - R1003Name = "Malicious SSH Connection" - MaxTimeDiffInSeconds = 2 + R1003ID = "R1003" + R1003Name = "Malicious SSH Connection" ) -var SSHRelatedFiles = []string{ - "ssh_config", - "sshd_config", - "ssh_known_hosts", - "ssh_known_hosts2", - "ssh_config.d", - "sshd_config.d", - ".ssh", - "authorized_keys", - "authorized_keys2", - "known_hosts", - "known_hosts2", - "id_rsa", - "id_rsa.pub", - "id_dsa", - "id_dsa.pub", - "id_ecdsa", - "id_ecdsa.pub", - "id_ed25519", - "id_ed25519.pub", - "id_xmss", - "id_xmss.pub", -} - var R1003MaliciousSSHConnectionRuleDescriptor = RuleDescriptor{ ID: R1003ID, Name: R1003Name, @@ -55,7 +28,7 @@ var R1003MaliciousSSHConnectionRuleDescriptor = RuleDescriptor{ Tags: []string{"ssh", "connection", "port", "malicious"}, Priority: RulePriorityMed, Requirements: &RuleRequirements{ - EventTypes: []utils.EventType{utils.OpenEventType, utils.NetworkEventType}, + EventTypes: []utils.EventType{utils.SSHEventType}, }, RuleCreationFunc: func() ruleengine.RuleEvaluator { return CreateRuleR1003MaliciousSSHConnection() @@ -66,17 +39,13 @@ var _ ruleengine.RuleEvaluator = (*R1003MaliciousSSHConnection)(nil) type R1003MaliciousSSHConnection struct { BaseRule - accessRelatedFiles bool - sshInitiatorPid uint32 - configFileAccessTimeStamp int64 - allowedPorts []uint16 + allowedPorts []uint16 + requests maps.SafeMap[string, string] // Mapping of src IP to dst IP } func CreateRuleR1003MaliciousSSHConnection() *R1003MaliciousSSHConnection { - return &R1003MaliciousSSHConnection{accessRelatedFiles: false, - sshInitiatorPid: 0, - configFileAccessTimeStamp: 0, - allowedPorts: []uint16{22}, + return &R1003MaliciousSSHConnection{ + allowedPorts: []uint16{22}, } } func (rule *R1003MaliciousSSHConnection) Name() string { @@ -114,103 +83,50 @@ func (rule *R1003MaliciousSSHConnection) DeleteRule() { } func (rule *R1003MaliciousSSHConnection) ProcessEvent(eventType utils.EventType, event interface{}, objectCache objectcache.ObjectCache) ruleengine.RuleFailure { - if eventType != utils.OpenEventType && eventType != utils.NetworkEventType { + if eventType != utils.SSHEventType { return nil } - if eventType == utils.OpenEventType && !rule.accessRelatedFiles { - openEvent, ok := event.(*traceropentype.Event) - if !ok { - return nil - } else { - if IsSSHConfigFile(openEvent.FullPath) { - rule.accessRelatedFiles = true - rule.sshInitiatorPid = openEvent.Pid - rule.configFileAccessTimeStamp = int64(openEvent.Timestamp) - } - - return nil - } - } else if eventType == utils.NetworkEventType && rule.accessRelatedFiles { - networkEvent, ok := event.(*tracernetworktype.Event) - if !ok { - return nil - } - - nn := objectCache.NetworkNeighborhoodCache().GetNetworkNeighborhood(networkEvent.Runtime.ContainerID) - if nn == nil { - return nil - } + sshEvent := event.(*tracersshtype.Event) - timestampDiffInSeconds := calculateTimestampDiffInSeconds(int64(networkEvent.Timestamp), rule.configFileAccessTimeStamp) - if timestampDiffInSeconds > MaxTimeDiffInSeconds { - rule.accessRelatedFiles = false - rule.sshInitiatorPid = 0 - rule.configFileAccessTimeStamp = 0 + if !slices.Contains(rule.allowedPorts, sshEvent.DstPort) { + // Check if the event is a response to a request we have already seen. + if rule.requests.Has(sshEvent.DstIP) { return nil } - if networkEvent.Pid == rule.sshInitiatorPid && networkEvent.PktType == "OUTGOING" && networkEvent.Proto == "TCP" && !slices.Contains(rule.allowedPorts, networkEvent.Port) { - nnContainer, err := getContainerFromNetworkNeighborhood(nn, networkEvent.GetContainer()) - if err != nil { - return nil - } - for _, egress := range nnContainer.Egress { - for _, port := range egress.Ports { - if uint16(*port.Port) == networkEvent.Port { - return nil - } - } - } - rule.accessRelatedFiles = false - rule.sshInitiatorPid = 0 - rule.configFileAccessTimeStamp = 0 - - ruleFailure := GenericRuleFailure{ - BaseRuntimeAlert: apitypes.BaseRuntimeAlert{ - AlertName: rule.Name(), - InfectedPID: networkEvent.Pid, - FixSuggestions: "If this is a legitimate action, please add the port as a parameter to the binding of this rule", - Severity: R1003MaliciousSSHConnectionRuleDescriptor.Priority, - }, - RuntimeProcessDetails: apitypes.ProcessTree{ - ProcessTree: apitypes.Process{ - Comm: networkEvent.Comm, - Gid: &networkEvent.Gid, - PID: networkEvent.Pid, - Uid: &networkEvent.Uid, - }, - ContainerID: networkEvent.Runtime.ContainerID, - }, - TriggerEvent: networkEvent.Event, - RuleAlert: apitypes.RuleAlert{ - RuleDescription: fmt.Sprintf("SSH connection to disallowed port %d", networkEvent.Port), + rule.requests.Set(sshEvent.SrcIP, sshEvent.DstIP) + ruleFailure := GenericRuleFailure{ + BaseRuntimeAlert: apitypes.BaseRuntimeAlert{ + AlertName: rule.Name(), + InfectedPID: sshEvent.Pid, + FixSuggestions: "If this is a legitimate action, please add the port as a parameter to the binding of this rule", + Severity: R1003MaliciousSSHConnectionRuleDescriptor.Priority, + }, + RuntimeProcessDetails: apitypes.ProcessTree{ + ProcessTree: apitypes.Process{ + Comm: sshEvent.Comm, + Gid: &sshEvent.Gid, + PID: sshEvent.Pid, + Uid: &sshEvent.Uid, }, - RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ - PodName: networkEvent.GetPod(), - }, - RuleID: rule.ID(), - } - - return &ruleFailure + ContainerID: sshEvent.Runtime.ContainerID, + }, + TriggerEvent: sshEvent.Event, + RuleAlert: apitypes.RuleAlert{ + RuleDescription: fmt.Sprintf("SSH connection to disallowed port %s:%d", sshEvent.DstIP, sshEvent.DstPort), + }, + RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ + PodName: sshEvent.GetPod(), + }, + RuleID: rule.ID(), } + + return &ruleFailure } return nil } -func calculateTimestampDiffInSeconds(timestamp1 int64, timestamp2 int64) int64 { - return (timestamp1 - timestamp2) / int64(time.Second) -} - -func IsSSHConfigFile(path string) bool { - for _, sshFile := range SSHRelatedFiles { - if strings.Contains(path, sshFile) { - return true - } - } - return false -} - func (rule *R1003MaliciousSSHConnection) Requirements() ruleengine.RuleSpec { return &RuleRequirements{ EventTypes: R1003MaliciousSSHConnectionRuleDescriptor.Requirements.RequiredEventTypes(), From ead6d9bc77ef617e5385ec396066c9affb8c47d4 Mon Sep 17 00:00:00 2001 From: Amit Schendel Date: Tue, 13 Aug 2024 11:36:00 +0000 Subject: [PATCH 3/4] Fixing test Signed-off-by: Amit Schendel --- .../v1/r1003_malicious_ssh_connection_test.go | 95 +++---------------- 1 file changed, 13 insertions(+), 82 deletions(-) diff --git a/pkg/ruleengine/v1/r1003_malicious_ssh_connection_test.go b/pkg/ruleengine/v1/r1003_malicious_ssh_connection_test.go index b527b565..628687e3 100644 --- a/pkg/ruleengine/v1/r1003_malicious_ssh_connection_test.go +++ b/pkg/ruleengine/v1/r1003_malicious_ssh_connection_test.go @@ -3,35 +3,16 @@ package ruleengine import ( "testing" + tracersshtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types" "github.com/kubescape/node-agent/pkg/utils" - tracernetworktype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/network/types" - traceropentype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/open/types" eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" - "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" ) func TestR1003DisallowedSSHConnectionPort_ProcessEvent(t *testing.T) { rule := CreateRuleR1003MaliciousSSHConnection() - objCache := RuleObjectCacheMock{} - nn := objCache.NetworkNeighborhoodCache().GetNetworkNeighborhood("test") - if nn == nil { - nn = &v1beta1.NetworkNeighborhood{} - nn.Spec.Containers = append(nn.Spec.Containers, v1beta1.NetworkNeighborhoodContainer{ - Name: "test", - - Egress: []v1beta1.NetworkNeighbor{ - { - DNS: "test.com", - }, - }, - }) - - objCache.SetNetworkNeighborhood(nn) - } - // Test case 1: SSH connection to disallowed port - networkEvent := &tracernetworktype.Event{ + sshEvent := &tracersshtype.Event{ Event: eventtypes.Event{ Timestamp: 2, CommonData: eventtypes.CommonData{ @@ -48,71 +29,21 @@ func TestR1003DisallowedSSHConnectionPort_ProcessEvent(t *testing.T) { }, }, }, - PktType: "OUTGOING", - Proto: "TCP", - Port: 2222, - DstEndpoint: eventtypes.L3Endpoint{ - Addr: "1.1.1.1", - }, - Pid: 1, + SrcIP: "1.1.1.1", + DstIP: "2.2.2.2", + DstPort: 22, + SrcPort: 1234, } - openEvent := &traceropentype.Event{ - Event: eventtypes.Event{ - Timestamp: 1, - CommonData: eventtypes.CommonData{ - K8s: eventtypes.K8sMetadata{ - BasicK8sMetadata: eventtypes.BasicK8sMetadata{ - ContainerName: "test", - PodName: "test", - Namespace: "test", - }, - }, - Runtime: eventtypes.BasicRuntimeMetadata{ - ContainerID: "test", - ContainerName: "test", - }, - }, - }, - FullPath: "/etc/ssh/sshd_config", - Pid: 1, - } - rule.ProcessEvent(utils.OpenEventType, openEvent, &RuleObjectCacheMock{}) - failure := rule.ProcessEvent(utils.NetworkEventType, networkEvent, &objCache) - if failure == nil { - t.Errorf("Expected failure, but got nil") - } - - // Test case 2: SSH connection to allowed port - networkEvent.Port = 22 - failure = rule.ProcessEvent(utils.NetworkEventType, networkEvent, &objCache) + failure := rule.ProcessEvent(utils.SSHEventType, sshEvent, &RuleObjectCacheMock{}) if failure != nil { - t.Errorf("Expected failure to be nil, but got %v", failure) + t.Errorf("Expected nil since the SSH connection is to an allowed port, got %v", failure) } - // Test case 3: SSH connection to disallowed port, but not from SSH initiator - networkEvent.Port = 2222 - networkEvent.Pid = 2 - failure = rule.ProcessEvent(utils.NetworkEventType, networkEvent, &objCache) - if failure != nil { - t.Errorf("Expected failure to be nil, but got %v", failure) - } - - // Test case 4: SSH connection to disallowed port, but not from SSH initiator - networkEvent.Port = 2222 - networkEvent.Pid = 1 - networkEvent.Timestamp = 3 - failure = rule.ProcessEvent(utils.NetworkEventType, networkEvent, &objCache) - if failure != nil { - t.Errorf("Expected failure to be nil, but got %v", failure) - } - - // Test case 5: Time diff is greater than MaxTimeDiffInSeconds - networkEvent.Port = 2222 - networkEvent.Pid = 1 - networkEvent.Timestamp = 5 - failure = rule.ProcessEvent(utils.NetworkEventType, networkEvent, &objCache) - if failure != nil { - t.Errorf("Expected failure to be nil, but got %v", failure) + // Test disallowed port + sshEvent.DstPort = 1234 + failure = rule.ProcessEvent(utils.SSHEventType, sshEvent, &RuleObjectCacheMock{}) + if failure == nil { + t.Errorf("Expected failure since the SSH connection is to a disallowed port, got nil") } } From 9c592231ed3de9239fe7af036a955058443a66dc Mon Sep 17 00:00:00 2001 From: Amit Schendel Date: Tue, 13 Aug 2024 13:56:45 +0000 Subject: [PATCH 4/4] Adding profile check for links Signed-off-by: Amit Schendel --- ...010_symlink_created_over_sensitive_file.go | 27 +++++++++ ...ymlink_created_over_sensitive_file_test.go | 56 +++++++++++++++++- ...12_hardlink_created_over_sensitive_file.go | 27 +++++++++ ...rdlink_created_over_sensitive_file_test.go | 57 +++++++++++++++++-- 4 files changed, 160 insertions(+), 7 deletions(-) diff --git a/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file.go b/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file.go index 7ea2f7aa..8a94d12c 100644 --- a/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file.go +++ b/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file.go @@ -1,6 +1,7 @@ package ruleengine import ( + "errors" "fmt" "strings" @@ -87,6 +88,12 @@ func (rule *R1010SymlinkCreatedOverSensitiveFile) ProcessEvent(eventType utils.E return nil } + if allowed, err := isSymLinkAllowed(symlinkEvent, objCache); err != nil { + return nil + } else if allowed { + return nil + } + for _, path := range rule.additionalPaths { if strings.HasPrefix(symlinkEvent.OldPath, path) { return &GenericRuleFailure{ @@ -129,3 +136,23 @@ func (rule *R1010SymlinkCreatedOverSensitiveFile) Requirements() ruleengine.Rule EventTypes: R1010SymlinkCreatedOverSensitiveFileRuleDescriptor.Requirements.RequiredEventTypes(), } } + +func isSymLinkAllowed(symlinkEvent *tracersymlinktype.Event, objCache objectcache.ObjectCache) (bool, error) { + ap := objCache.ApplicationProfileCache().GetApplicationProfile(symlinkEvent.Runtime.ContainerID) + if ap == nil { + return true, errors.New("application profile not found") + } + + appProfileExecList, err := getContainerFromApplicationProfile(ap, symlinkEvent.GetContainer()) + if err != nil { + return true, err + } + + for _, exec := range appProfileExecList.Execs { + if exec.Path == symlinkEvent.Comm { + return true, nil + } + } + + return false, nil +} diff --git a/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file_test.go b/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file_test.go index 42cec9b1..61689b8c 100644 --- a/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file_test.go +++ b/pkg/ruleengine/v1/r1010_symlink_created_over_sensitive_file_test.go @@ -4,7 +4,9 @@ import ( "fmt" "testing" + eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" "github.com/kubescape/node-agent/pkg/utils" + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" tracersymlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/symlink/types" ) @@ -16,14 +18,50 @@ func TestR1010SymlinkCreatedOverSensitiveFile(t *testing.T) { t.Errorf("Expected r to not be nil") } + objCache := RuleObjectCacheMock{} + profile := objCache.ApplicationProfileCache().GetApplicationProfile("test") + if profile == nil { + profile = &v1beta1.ApplicationProfile{ + Spec: v1beta1.ApplicationProfileSpec{ + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "test", + Opens: []v1beta1.OpenCalls{ + { + Path: "/test", + Flags: []string{"O_RDONLY"}, + }, + }, + Execs: []v1beta1.ExecCalls{ + { + Path: "/usr/sbin/groupadd", + Args: []string{"test"}, + }, + }, + }, + }, + }, + } + objCache.SetApplicationProfile(profile) + } + // Create a symlink event e := &tracersymlinktype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test", + }, + }, + }, + }, Comm: "test", OldPath: "test", NewPath: "test", } - ruleResult := r.ProcessEvent(utils.SymlinkEventType, e, &RuleObjectCacheMock{}) + ruleResult := r.ProcessEvent(utils.SymlinkEventType, e, &objCache) if ruleResult != nil { fmt.Printf("ruleResult: %v\n", ruleResult) t.Errorf("Expected ruleResult to be nil since symlink path is not sensitive") @@ -34,7 +72,7 @@ func TestR1010SymlinkCreatedOverSensitiveFile(t *testing.T) { e.OldPath = "/etc/passwd" e.NewPath = "/etc/abc" - ruleResult = r.ProcessEvent(utils.SymlinkEventType, e, &RuleObjectCacheMock{}) + ruleResult = r.ProcessEvent(utils.SymlinkEventType, e, &objCache) if ruleResult == nil { fmt.Printf("ruleResult: %v\n", ruleResult) t.Errorf("Expected ruleResult to be Failure because of symlink is used over sensitive file") @@ -42,10 +80,22 @@ func TestR1010SymlinkCreatedOverSensitiveFile(t *testing.T) { } e.OldPath = "/etc/abc" - ruleResult = r.ProcessEvent(utils.SymlinkEventType, e, &RuleObjectCacheMock{}) + ruleResult = r.ProcessEvent(utils.SymlinkEventType, e, &objCache) if ruleResult != nil { fmt.Printf("ruleResult: %v\n", ruleResult) t.Errorf("Expected ruleResult to be nil since symlink is not used over sensitive file") return } + + // Test with whitelisted process + e.Comm = "/usr/sbin/groupadd" + e.OldPath = "/etc/passwd" + e.NewPath = "/etc/abc" + + ruleResult = r.ProcessEvent(utils.SymlinkEventType, e, &objCache) + if ruleResult != nil { + fmt.Printf("ruleResult: %v\n", ruleResult) + t.Errorf("Expected ruleResult to be nil since file is whitelisted and not sensitive") + return + } } diff --git a/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file.go b/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file.go index a6634365..17334224 100644 --- a/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file.go +++ b/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file.go @@ -1,6 +1,7 @@ package ruleengine import ( + "errors" "fmt" "strings" @@ -87,6 +88,12 @@ func (rule *R1012HardlinkCreatedOverSensitiveFile) ProcessEvent(eventType utils. return nil } + if allowed, err := isHardLinkAllowed(hardlinkEvent, objCache); err != nil { + return nil + } else if allowed { + return nil + } + for _, path := range rule.additionalPaths { if strings.HasPrefix(hardlinkEvent.OldPath, path) { return &GenericRuleFailure{ @@ -129,3 +136,23 @@ func (rule *R1012HardlinkCreatedOverSensitiveFile) Requirements() ruleengine.Rul EventTypes: R1012HardlinkCreatedOverSensitiveFileRuleDescriptor.Requirements.RequiredEventTypes(), } } + +func isHardLinkAllowed(hardlinkEvent *tracerhardlinktype.Event, objCache objectcache.ObjectCache) (bool, error) { + ap := objCache.ApplicationProfileCache().GetApplicationProfile(hardlinkEvent.Runtime.ContainerID) + if ap == nil { + return true, errors.New("application profile not found") + } + + appProfileExecList, err := getContainerFromApplicationProfile(ap, hardlinkEvent.GetContainer()) + if err != nil { + return true, err + } + + for _, exec := range appProfileExecList.Execs { + if exec.Path == hardlinkEvent.Comm { + return true, nil + } + } + + return false, nil +} diff --git a/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file_test.go b/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file_test.go index 27394ca5..b106f1c1 100644 --- a/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file_test.go +++ b/pkg/ruleengine/v1/r1012_hardlink_created_over_sensitive_file_test.go @@ -4,7 +4,9 @@ import ( "fmt" "testing" + eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types" "github.com/kubescape/node-agent/pkg/utils" + "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" tracerhardlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/hardlink/types" ) @@ -16,14 +18,50 @@ func TestR1012HardlinkCreatedOverSensitiveFile(t *testing.T) { t.Errorf("Expected r to not be nil") } + objCache := RuleObjectCacheMock{} + profile := objCache.ApplicationProfileCache().GetApplicationProfile("test") + if profile == nil { + profile = &v1beta1.ApplicationProfile{ + Spec: v1beta1.ApplicationProfileSpec{ + Containers: []v1beta1.ApplicationProfileContainer{ + { + Name: "test", + Opens: []v1beta1.OpenCalls{ + { + Path: "/test", + Flags: []string{"O_RDONLY"}, + }, + }, + Execs: []v1beta1.ExecCalls{ + { + Path: "/usr/sbin/groupadd", + Args: []string{"test"}, + }, + }, + }, + }, + }, + } + objCache.SetApplicationProfile(profile) + } + // Create a hardlink event e := &tracerhardlinktype.Event{ + Event: eventtypes.Event{ + CommonData: eventtypes.CommonData{ + K8s: eventtypes.K8sMetadata{ + BasicK8sMetadata: eventtypes.BasicK8sMetadata{ + ContainerName: "test", + }, + }, + }, + }, Comm: "test", OldPath: "test", NewPath: "test", } - ruleResult := r.ProcessEvent(utils.HardlinkEventType, e, &RuleObjectCacheMock{}) + ruleResult := r.ProcessEvent(utils.HardlinkEventType, e, &objCache) if ruleResult != nil { fmt.Printf("ruleResult: %v\n", ruleResult) t.Errorf("Expected ruleResult to be nil since hardlink path is not sensitive") @@ -33,8 +71,7 @@ func TestR1012HardlinkCreatedOverSensitiveFile(t *testing.T) { // Create a hardlink event with sensitive file path e.OldPath = "/etc/passwd" e.NewPath = "/etc/abc" - - ruleResult = r.ProcessEvent(utils.HardlinkEventType, e, &RuleObjectCacheMock{}) + ruleResult = r.ProcessEvent(utils.HardlinkEventType, e, &objCache) if ruleResult == nil { fmt.Printf("ruleResult: %v\n", ruleResult) t.Errorf("Expected ruleResult to be Failure because of hardlink is used over sensitive file") @@ -42,10 +79,22 @@ func TestR1012HardlinkCreatedOverSensitiveFile(t *testing.T) { } e.OldPath = "/etc/abc" - ruleResult = r.ProcessEvent(utils.HardlinkEventType, e, &RuleObjectCacheMock{}) + ruleResult = r.ProcessEvent(utils.HardlinkEventType, e, &objCache) if ruleResult != nil { fmt.Printf("ruleResult: %v\n", ruleResult) t.Errorf("Expected ruleResult to be nil since hardlink is not used over sensitive file") return } + + // Test with whitelisted process + e.Comm = "/usr/sbin/groupadd" + e.OldPath = "/etc/passwd" + e.NewPath = "/etc/abc" + + ruleResult = r.ProcessEvent(utils.HardlinkEventType, e, &objCache) + if ruleResult != nil { + fmt.Printf("ruleResult: %v\n", ruleResult) + t.Errorf("Expected ruleResult to be nil since file is whitelisted and not sensitive") + return + } }