From bc42835b0b660eedbe6b37b3ef60dccfc019a2a4 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Thu, 13 Feb 2025 17:07:21 +0100 Subject: [PATCH] feat(actions): Isolate action With the isolate action it is possible to block all inbound/outbound traffic to the host except for the loopback address and the list of allowed IP addresses specified in the action parameter. --- go.mod | 8 + go.sum | 24 +- pkg/config/_fixtures/filters/default.yml | 4 + pkg/config/decoder.go | 28 ++- pkg/config/filters.go | 14 ++ pkg/config/filters_test.go | 5 + pkg/config/schema_windows.go | 29 ++- pkg/rules/action/isolate_windows.go | 265 +++++++++++++++++++++++ pkg/rules/engine.go | 10 +- pkg/symbolize/symbolizer_test.go | 64 +----- 10 files changed, 376 insertions(+), 75 deletions(-) create mode 100644 pkg/rules/action/isolate_windows.go diff --git a/go.mod b/go.mod index 0d8b05a38..9a9f0a59f 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/spf13/viper v1.6.2 github.com/streadway/amqp v1.0.0 github.com/stretchr/testify v1.8.1 + github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/gozstd v1.11.0 github.com/xeipuuv/gojsonschema v1.2.0 @@ -44,9 +45,16 @@ require ( ) require ( + github.com/BurntSushi/toml v0.4.1 // indirect github.com/rivo/uniseg v0.4.2 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d // indirect + go4.org/netipx v0.0.0-20220725152314-7e7bdc8411bf // indirect + golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + honnef.co/go/tools v0.3.2 // indirect ) require ( diff --git a/go.sum b/go.sum index e09b195d4..5fd250d1e 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= @@ -77,8 +78,9 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= @@ -230,6 +232,8 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= +github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -254,6 +258,12 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE= +go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA= +go4.org/netipx v0.0.0-20220725152314-7e7bdc8411bf h1:IdwJUzqoIo5lkr2EOyKoe5qipUaEjbOKKY5+fzPBZ3A= +go4.org/netipx v0.0.0-20220725152314-7e7bdc8411bf/go.mod h1:+QXzaoURFd0rGDIjDNpyIkv+F9R7EmeKorvlKRnhqgA= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc= golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -263,10 +273,14 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM= +golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -289,6 +303,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -330,6 +346,8 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -363,5 +381,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.3.2 h1:ytYb4rOqyp1TSa2EPvNVwtPQJctSELKaMyLfqNP4+34= +honnef.co/go/tools v0.3.2/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw= www.velocidex.com/golang/go-ntfs v0.2.1-0.20240818145200-04736de821dc h1:eeL+RUEGr6/lYL8hJEbvugrF88I6W4pBaVtFa1falj4= www.velocidex.com/golang/go-ntfs v0.2.1-0.20240818145200-04736de821dc/go.mod h1:itvbHQcnLdTVIDY6fI3lR0zeBwXwBYBdUFtswE0x1vc= diff --git a/pkg/config/_fixtures/filters/default.yml b/pkg/config/_fixtures/filters/default.yml index 66c649dc8..6232f1bc2 100644 --- a/pkg/config/_fixtures/filters/default.yml +++ b/pkg/config/_fixtures/filters/default.yml @@ -9,6 +9,10 @@ output: > `%ps.exe` attempted to reach out to `%net.sip` IP address action: - name: kill +- name: isolate + whitelist: + - 127.0.0.1 + - 8.8.8.8 min-engine-version: 2.0.0 tags: - TE diff --git a/pkg/config/decoder.go b/pkg/config/decoder.go index ec3c7d99f..c63bad2cb 100644 --- a/pkg/config/decoder.go +++ b/pkg/config/decoder.go @@ -18,7 +18,11 @@ package config -import "github.com/mitchellh/mapstructure" +import ( + "github.com/mitchellh/mapstructure" + "net" + "reflect" +) func decode(input, output interface{}) error { var decoderConfig = &mapstructure.DecoderConfig{ @@ -28,6 +32,7 @@ func decode(input, output interface{}) error { DecodeHook: mapstructure.ComposeDecodeHookFunc( mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToSliceHookFunc(","), + ipSliceDecodeHook(), ), } decoder, err := mapstructure.NewDecoder(decoderConfig) @@ -36,3 +41,24 @@ func decode(input, output interface{}) error { } return decoder.Decode(input) } + +func ipSliceDecodeHook() mapstructure.DecodeHookFunc { + return func(from reflect.Type, to reflect.Type, data interface{}) (interface{}, error) { + if to.Kind() == reflect.Slice && to.Elem() == reflect.TypeOf(net.IP(nil)) { + switch v := data.(type) { + case []interface{}: + var ips []net.IP + for _, s := range v { + ip, ok := s.(string) + if !ok { + continue + } + ips = append(ips, net.ParseIP(ip)) + } + return ips, nil + } + } + + return data, nil + } +} diff --git a/pkg/config/filters.go b/pkg/config/filters.go index caabea5be..5c9cac2d6 100644 --- a/pkg/config/filters.go +++ b/pkg/config/filters.go @@ -30,6 +30,7 @@ import ( "github.com/spf13/viper" "gopkg.in/yaml.v3" "io" + "net" "net/http" u "net/url" "os" @@ -64,6 +65,13 @@ type FilterAction any // indicates by the filter field expression. type KillAction struct{} +// IsolateAction defines an action for isolating the host +// via firewall rules. +type IsolateAction struct { + // Whitelist contains IP addresses that should remain accessible. + Whitelist []net.IP `mapstructure:"whitelist"` +} + // DecodeActions converts raw YAML map to // typed action structures. func (f FilterConfig) DecodeActions() ([]any, error) { @@ -89,8 +97,14 @@ func (f FilterConfig) DecodeActions() ([]any, error) { if err := dec(m, kill); err != nil { return nil, err } + case "isolate": + var isolate IsolateAction + if err := dec(m, isolate); err != nil { + return nil, err + } } } + return actions, nil } diff --git a/pkg/config/filters_test.go b/pkg/config/filters_test.go index 16de977c4..30a5cf2e2 100644 --- a/pkg/config/filters_test.go +++ b/pkg/config/filters_test.go @@ -62,6 +62,11 @@ func TestLoadRulesFromPaths(t *testing.T) { acts, err := f1.DecodeActions() require.NoError(t, err) require.IsType(t, KillAction{}, acts[0]) + require.IsType(t, IsolateAction{}, acts[1]) + + isolate := acts[1].(IsolateAction) + require.Len(t, isolate.Whitelist, 2) + require.Contains(t, isolate.Whitelist, net.ParseIP("127.0.0.1")) assert.Equal(t, "2.0.0", f1.MinEngineVersion) diff --git a/pkg/config/schema_windows.go b/pkg/config/schema_windows.go index d91d45d0c..1a1bb214c 100644 --- a/pkg/config/schema_windows.go +++ b/pkg/config/schema_windows.go @@ -508,12 +508,12 @@ var rulesSchema = ` "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { - "id": {"type": "string", "minLength": 36, "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"}, + "id": {"type": "string", "minLength": 36, "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"}, "version": {"type": "string", "minLength": 5, "pattern": "^([0-9]+.)([0-9]+.)([0-9]+)$"}, - "name": {"type": "string", "minLength": 3}, - "description": {"type": "string"}, + "name": {"type": "string", "minLength": 3}, + "description": {"type": "string"}, "output": {"type": "string", "minLength": 5}, - "notes": {"type": "string"}, + "notes": {"type": "string"}, "severity": {"type": "string", "enum": ["low", "medium", "high", "critical"]}, "min-engine-version": {"type": "string", "minLength": 5, "pattern": "^([0-9]+.)([0-9]+.)([0-9]+)$"}, "enabled": {"type": "boolean"}, @@ -522,17 +522,32 @@ var rulesSchema = ` "type": "object", "additionalProperties": { "type": "string"} }, - "tags": {"type": "array", "items": [{"type": "string", "minLength": 1}]}, + "tags": {"type": "array", "items": [{"type": "string", "minLength": 1}]}, "references": {"type": "array", "items": [{"type": "string", "minLength": 1}]}, "action": { "type": "array", "items": { "type": "object", + "additionalProperties": false, "properties": { - "name": {"type": "string", "enum": ["kill"]} + "name": {"type": "string", "enum": ["kill", "isolate"]}, + "whitelist": true }, "required": ["name"], - "additionalProperties": false + "if": { + "properties": {"name": {"const": "isolate"}} + }, + "then": { + "properties": { + "whitelist": {"type": "array", "minItems": 1, "items": {"type": "string", "format": "ipv4"}} + } + }, + "else": { + "properties": { + "name": {"type": "string", "enum": ["kill", "isolate"]} + }, + "additionalProperties": false + } } } }, diff --git a/pkg/rules/action/isolate_windows.go b/pkg/rules/action/isolate_windows.go new file mode 100644 index 000000000..1d1dd8b24 --- /dev/null +++ b/pkg/rules/action/isolate_windows.go @@ -0,0 +1,265 @@ +/* + * Copyright 2021-present by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package action + +import ( + "github.com/rabbitstack/fibratus/pkg/util/multierror" + "github.com/tailscale/wf" + "golang.org/x/sys/windows" + "net" + "net/netip" + "sync" +) + +var ( + inboundAllowRuleID = wf.RuleID(windows.GUID{Data1: 0xcc2d6ce, Data2: 0xe747, Data3: 0x4480, Data4: [8]byte{0x9b, 0xbd, 0x7f, 0xa8, 0x99, 0xb7, 0xb1, 0x9a}}) + outboundAllowRuleID = wf.RuleID(windows.GUID{Data1: 0x4480ae, Data2: 0x9b7f, Data3: 0xa899, Data4: [8]byte{0xbd, 0x9b, 0xa8, 0xb7, 0x7f, 0x99, 0xe7, 0x1a}}) + inboundDenyRuleID = wf.RuleID(windows.GUID{Data1: 0x7f9bbc, Data2: 0x99b7, Data3: 0xe747, Data4: [8]byte{0x9a, 0x9b, 0xa8, 0xbd, 0x44, 0x80, 0xcc, 0xc1}}) + outboundDenyRuleID = wf.RuleID(windows.GUID{Data1: 0xbd9bda, Data2: 0x7fa8, Data3: 0x99b7, Data4: [8]byte{0xcc, 0x44, 0x9a, 0x9b, 0xe7, 0x77, 0x47, 0xd1}}) +) + +var ( + // inboundAllowRuleName denotes the firewall rule name for allowed inbound traffic + inboundAllowRuleName = "Fibratus Allow (Inbound)" + // outboundAllowRuleName denotes the firewall rule name for allowed outbound traffic + outboundAllowRuleName = "Fibratus Allow (Outbound)" + // inboundDenyRuleName denotes the firewall rule name for isolated inbound traffic + inboundDenyRuleName = "Fibratus Isolate (Inbound)" + // outboundDenyRuleName denotes the firewall rule name for isolated outbound traffic + outboundDenyRuleName = "Fibratus Isolate (Outbound)" +) + +type firewall struct { + s *wf.Session + mu sync.Mutex + inbound *wf.Rule // rule for allowed inbound traffic + outbound *wf.Rule // rule for allowed outbound traffic +} + +func newFirewall() (*firewall, error) { + opts := &wf.Options{} + + sess, err := wf.New(opts) + if err != nil { + return nil, err + } + + return &firewall{s: sess}, nil +} + +func (f *firewall) allow(whitelist []net.IP) error { + f.mu.Lock() + defer f.mu.Unlock() + f.inbound = &wf.Rule{ + ID: inboundAllowRuleID, + Name: inboundAllowRuleName, + Layer: wf.LayerInboundIPPacketV4, + Action: wf.ActionPermit, + Conditions: []*wf.Match{ + {Field: wf.FieldIPLocalAddress, Op: wf.MatchTypeEqual, Value: netip.AddrFrom4([4]byte{127, 0, 0, 1})}, + {Field: wf.FieldIPRemoteAddress, Op: wf.MatchTypeEqual, Value: netip.AddrFrom4([4]byte{127, 0, 0, 1})}, + }, + } + + f.outbound = &wf.Rule{ + ID: outboundAllowRuleID, + Name: outboundAllowRuleName, + Layer: wf.LayerOutboundIPPacketV4, + Action: wf.ActionPermit, + Conditions: []*wf.Match{ + {Field: wf.FieldIPLocalAddress, Op: wf.MatchTypeEqual, Value: netip.AddrFrom4([4]byte{127, 0, 0, 1})}, + {Field: wf.FieldIPRemoteAddress, Op: wf.MatchTypeEqual, Value: netip.AddrFrom4([4]byte{127, 0, 0, 1})}, + }, + } + + for _, addr := range whitelist { + f.addIPCondition(addr) + } + + return multierror.Wrap(f.s.AddRule(f.inbound), f.s.AddRule(f.outbound)) +} + +func (f *firewall) deny() error { + f.mu.Lock() + defer f.mu.Unlock() + in := &wf.Rule{ + ID: inboundDenyRuleID, + Name: inboundDenyRuleName, + Layer: wf.LayerInboundIPPacketV4, + Action: wf.ActionBlock, + } + + out := &wf.Rule{ + ID: outboundDenyRuleID, + Name: outboundDenyRuleName, + Layer: wf.LayerOutboundIPPacketV4, + Action: wf.ActionBlock, + } + + return multierror.Wrap(f.s.AddRule(in), f.s.AddRule(out)) +} + +func (f *firewall) findAllowRules() error { + f.mu.Lock() + defer f.mu.Unlock() + + if f.inbound != nil && f.outbound != nil { + return nil + } + rules, err := f.s.Rules() + if err != nil { + return err + } + + for _, rule := range rules { + switch rule.ID { + case inboundAllowRuleID: + f.inbound = rule + case outboundAllowRuleID: + f.outbound = rule + } + if f.inbound != nil && f.outbound != nil { + break + } + } + + return nil +} + +func (f *firewall) removeAllowRules() error { + f.mu.Lock() + defer f.mu.Unlock() + return multierror.Wrap(f.s.DeleteRule(inboundAllowRuleID), f.s.DeleteRule(outboundAllowRuleID)) +} + +func (f *firewall) hasAllowRules() bool { + f.mu.Lock() + defer f.mu.Unlock() + return f.inbound != nil && f.outbound != nil +} + +func (f *firewall) addIPCondition(addr net.IP) { + f.mu.Lock() + defer f.mu.Unlock() + ip := netip.AddrFrom4([4]byte(addr)) + f.inbound.Conditions = append(f.inbound.Conditions, &wf.Match{Field: wf.FieldIPLocalAddress, Op: wf.MatchTypeEqual, Value: ip}) + f.inbound.Conditions = append(f.inbound.Conditions, &wf.Match{Field: wf.FieldIPRemoteAddress, Op: wf.MatchTypeEqual, Value: ip}) + f.outbound.Conditions = append(f.outbound.Conditions, &wf.Match{Field: wf.FieldIPLocalAddress, Op: wf.MatchTypeEqual, Value: ip}) + f.outbound.Conditions = append(f.outbound.Conditions, &wf.Match{Field: wf.FieldIPRemoteAddress, Op: wf.MatchTypeEqual, Value: ip}) +} + +func (f *firewall) hasIPCondition(addr net.IP) bool { + f.mu.Lock() + defer f.mu.Unlock() + for _, c := range f.inbound.Conditions { + if c.Field != wf.FieldIPLocalAddress && c.Field != wf.FieldIPRemoteAddress { + continue + } + + address, ok := c.Value.(netip.Addr) + if !ok { + continue + } + + if netip.AddrFrom4([4]byte(addr)) == address { + return true + } + } + + for _, c := range f.outbound.Conditions { + if c.Field != wf.FieldIPLocalAddress && c.Field != wf.FieldIPRemoteAddress { + continue + } + + address, ok := c.Value.(netip.Addr) + if !ok { + continue + } + + if netip.AddrFrom4([4]byte(addr)) == address { + return true + } + } + + return false +} + +var fw *firewall + +// Isolate talks to the WFP (Windows Filtering Platform) engine to +// set up firewall rules that result in complete host isolation. +// The traffic is allowed for the IP addresses specified in the +// permitted parameter. +// If the firewall rules already exist and the whitelist IP addresses +// are given, the rules are first removed and then recreated with the new +// allowed IP set. +func Isolate(whitelist []net.IP) error { + if fw == nil { + var err error + fw, err = newFirewall() + if err != nil { + return err + } + } + + if err := fw.findAllowRules(); err != nil { + return err + } + + switch { + case fw.hasAllowRules() && len(whitelist) > 0: + // rules were added and the whitelist + // is given in the action. Check if + // the given permitted addresses contain + // an item that is not already in the + // allowed rules conditions. + refresh := true + for _, addr := range whitelist { + if fw.hasIPCondition(addr) { + refresh = false + break + } else { + fw.addIPCondition(addr) + } + } + + if refresh { + if err := fw.removeAllowRules(); err != nil { + return err + } + return fw.allow(whitelist) + } + + return nil + case fw.hasAllowRules(): + // rules were added and no new permitted + // addresses are supplied in the action + return nil + default: + // rules were not added, so we set up + // the rule to allow localhost in/out + // traffic in addition to permitted + // IP address. + // Block the remaining in/out traffic + if err := fw.allow(whitelist); err != nil { + return err + } + return fw.deny() + } +} diff --git a/pkg/rules/engine.go b/pkg/rules/engine.go index 7e1190af2..ade331e54 100644 --- a/pkg/rules/engine.go +++ b/pkg/rules/engine.go @@ -304,7 +304,7 @@ func (e *Engine) ProcessEvent(evt *kevent.Kevent) (bool, error) { // Sending an alert is an implicit action // carried out each time there is a rule // match. Other actions are executed if -// defined in the rule definition. +// declared in the rule definition. func (e *Engine) processActions() error { defer e.clearMatches() e.mmu.Lock() @@ -324,15 +324,21 @@ func (e *Engine) processActions() error { } for _, act := range actions { - switch act.(type) { + switch t := act.(type) { case config.KillAction: log.Infof("executing kill action: pids=%v rule=%s", m.ctx.UniquePids(), f.Name) if err := action.Kill(m.ctx.UniquePids()); err != nil { return ErrRuleAction(f.Name, err) } + case config.IsolateAction: + log.Infof("executing isolate action: rule=%s", f.Name) + if err := action.Isolate(t.Whitelist); err != nil { + return ErrRuleAction(f.Name, err) + } } } } + return nil } diff --git a/pkg/symbolize/symbolizer_test.go b/pkg/symbolize/symbolizer_test.go index 6cdeb0173..5c670aac6 100644 --- a/pkg/symbolize/symbolizer_test.go +++ b/pkg/symbolize/symbolizer_test.go @@ -99,68 +99,6 @@ func TestLoadKernelModuleSymbolTables(t *testing.T) { r.AssertNumberOfCalls(t, "LoadModule", len(sys.EnumDevices())) } -func TestProcessCallstackFastMode(t *testing.T) { - r := new(MockResolver) - c := &config.Config{} - - psnap := new(ps.SnapshotterMock) - - r.On("UnloadModule", mock.Anything, mock.Anything) - - s := NewSymbolizer(r, psnap, c, false) - require.NotNil(t, s) - defer s.Close() - - proc := &pstypes.PS{ - Name: "notepad.exe", - PID: 23234, - Ppid: 2434, - Exe: `C:\Windows\notepad.exe`, - Cmdline: `C:\Windows\notepad.exe`, - SID: "S-1-1-18", - Cwd: `C:\Windows\`, - SessionID: 1, - Threads: map[uint32]pstypes.Thread{ - 3453: {Tid: 3453, StartAddress: va.Address(140729524944768), IOPrio: 2, PagePrio: 5, KstackBase: va.Address(18446677035730165760), KstackLimit: va.Address(18446677035730137088), UstackLimit: va.Address(86376448), UstackBase: va.Address(86372352)}, - 3455: {Tid: 3455, StartAddress: va.Address(140729524944768), IOPrio: 3, PagePrio: 5, KstackBase: va.Address(18446677035730165760), KstackLimit: va.Address(18446677035730137088), UstackLimit: va.Address(86376448), UstackBase: va.Address(86372352)}, - }, - Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, - Modules: []pstypes.Module{ - {Name: "ntdll.dll", Size: 32358, Checksum: 23123343, BaseAddress: va.Address(0x7ffb313833a3), DefaultBaseAddress: va.Address(0x7ffb313833a3)}, - {Name: "kernel32.dll", Size: 12354, Checksum: 23123343, BaseAddress: va.Address(0x7ffb5c1d0126), DefaultBaseAddress: va.Address(0x7ffb5c1d0126)}, - {Name: "user32.dll", Size: 212354, Checksum: 33123343, BaseAddress: va.Address(0x7ffb5d8e11c4), DefaultBaseAddress: va.Address(0x7ffb5d8e11c4)}, - }, - } - e := &kevent.Kevent{ - Type: ktypes.CreateFile, - Tid: 2484, - PID: 859, - CPU: 1, - Seq: 2, - Name: "CreateFile", - Timestamp: time.Now(), - Category: ktypes.File, - Host: "archrabbit", - Description: "Creates or opens a new file, directory, I/O device, pipe, console", - Kparams: kevent.Kparams{ - kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, - kparams.FilePath: {Name: kparams.FilePath, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, - kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, - kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.Enum, Value: uint32(1), Enum: fs.FileCreateDispositions}, - kparams.Callstack: {Name: kparams.Callstack, Type: kparams.Slice, Value: []va.Address{0x7ffb5c1d0396, 0x7ffb5d8e61f4, 0x7ffb3138592e, 0x7ffb313853b2, 0x2638e59e0a5}}, - }, - PS: proc, - } - - _, err := s.ProcessEvent(e) - require.NoError(t, err) - - assert.Len(t, e.Callstack, 5) - assert.Equal(t, "0x7ffb5c1d0396 kernel32.dll!?|0x7ffb5d8e61f4 user32.dll!?|0x7ffb3138592e ntdll.dll!?|0x7ffb313853b2 ntdll.dll!?|0x2638e59e0a5 unbacked!?", e.Callstack.String()) - assert.Equal(t, "kernel32.dll|user32.dll|ntdll.dll|unbacked", e.Callstack.Summary()) - assert.True(t, e.Callstack.ContainsUnbacked()) -} - func TestProcessCallstackPeExports(t *testing.T) { r := new(MockResolver) c := &config.Config{} @@ -303,7 +241,7 @@ func TestProcessCallstackPeExports(t *testing.T) { assert.Len(t, s.mods, 3) } -func TestProcessCallstackFullMode(t *testing.T) { +func TestProcessCallstack(t *testing.T) { r := new(MockResolver) c := &config.Config{}