diff --git a/comp/core/secrets/secretsimpl/check_rights_windows.go b/comp/core/secrets/secretsimpl/check_rights_windows.go index dc027e0639d44..09c469e111b37 100644 --- a/comp/core/secrets/secretsimpl/check_rights_windows.go +++ b/comp/core/secrets/secretsimpl/check_rights_windows.go @@ -46,7 +46,7 @@ func checkRights(filename string, allowGroupExec bool) error { // create the sids that are acceptable to us (local system account and // administrators group) - localSystem, err := getLocalSystemSID() + localSystem, err := winutil.GetLocalSystemSID() if err != nil { return fmt.Errorf("could not query Local System SID: %s", err) } @@ -115,18 +115,6 @@ func getACL(filename string) (*winutil.ACL, error) { return fileDacl, err } -// getLocalSystemSID returns the SID of the Local System account -func getLocalSystemSID() (*windows.SID, error) { - var localSystem *windows.SID - err := windows.AllocateAndInitializeSid(&windows.SECURITY_NT_AUTHORITY, - 1, // local system has 1 valid subauth - windows.SECURITY_LOCAL_SYSTEM_RID, - 0, 0, 0, 0, 0, 0, 0, - &localSystem) - - return localSystem, err -} - // getAdministratorsSID returns the SID of the built-in Administrators group principal func getAdministratorsSID() (*windows.SID, error) { var administrators *windows.SID @@ -141,7 +129,7 @@ func getAdministratorsSID() (*windows.SID, error) { // getSecretUserSID returns the SID of the user running the secret backend func getSecretUserSID() (*windows.SID, error) { - localSystem, err := getLocalSystemSID() + localSystem, err := winutil.GetLocalSystemSID() if err != nil { return nil, fmt.Errorf("could not query Local System SID: %s", err) } diff --git a/comp/core/secrets/secretsimpl/exec_windows.go b/comp/core/secrets/secretsimpl/exec_windows.go index 820300228f22d..2bd1057af858a 100644 --- a/comp/core/secrets/secretsimpl/exec_windows.go +++ b/comp/core/secrets/secretsimpl/exec_windows.go @@ -25,7 +25,7 @@ const ddAgentServiceName = "datadogagent" func commandContext(ctx context.Context, name string, arg ...string) (*exec.Cmd, func(), error) { cmd := exec.CommandContext(ctx, name, arg...) done := func() {} - localSystem, err := getLocalSystemSID() + localSystem, err := winutil.GetLocalSystemSID() if err != nil { return nil, nil, fmt.Errorf("could not query Local System SID: %s", err) } diff --git a/comp/etw/impl/etwSession.go b/comp/etw/impl/etwSession.go index 2b689e3de9a63..ca39fac476dbf 100644 --- a/comp/etw/impl/etwSession.go +++ b/comp/etw/impl/etwSession.go @@ -166,11 +166,21 @@ func (e *etwSession) StopTracing() error { globalError = errors.Join(globalError, e.DisableProvider(guid)) } ptp := (C.PEVENT_TRACE_PROPERTIES)(unsafe.Pointer(&e.propertiesBuf[0])) - ret := windows.Errno(C.ControlTraceW( - e.hSession, - nil, - ptp, - C.EVENT_TRACE_CONTROL_STOP)) + var ret windows.Errno + if e.wellKnown { + if e.hTraceHandle == C.INVALID_PROCESSTRACE_HANDLE { + return windows.ERROR_INVALID_HANDLE + } + ret = windows.Errno(C.CloseTrace(e.hTraceHandle)) + + } else { + ret = windows.Errno(C.ControlTraceW( + e.hSession, + nil, + ptp, + C.EVENT_TRACE_CONTROL_STOP)) + } + if !(ret == windows.ERROR_MORE_DATA || ret == windows.ERROR_SUCCESS) { return errors.Join(ret, globalError) diff --git a/go.mod b/go.mod index bf7c8229b677e..b718b47ab60b7 100644 --- a/go.mod +++ b/go.mod @@ -207,7 +207,7 @@ require ( github.com/hashicorp/consul/api v1.29.1 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/golang-lru/v2 v2.0.7 - github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95 // indirect + github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95 github.com/iceber/iouring-go v0.0.0-20230403020409-002cfd2e2a90 github.com/imdario/mergo v0.3.16 github.com/invopop/jsonschema v0.12.0 diff --git a/pkg/security/probe/probe_auditing_windows.go b/pkg/security/probe/probe_auditing_windows.go new file mode 100644 index 0000000000000..4cc14012f9387 --- /dev/null +++ b/pkg/security/probe/probe_auditing_windows.go @@ -0,0 +1,151 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +// Package probe holds probe related files +package probe + +import ( + "strconv" + "strings" + "unsafe" + + "github.com/DataDog/datadog-agent/comp/etw" + etwimpl "github.com/DataDog/datadog-agent/comp/etw/impl" + + "golang.org/x/sys/windows" +) + +// the auditing manifest isn't nearly as complete as some of the others +// link https://github.com/repnz/etw-providers-docs/blob/master/Manifests-Win10-17134/Microsoft-Windows-Security-Auditing.xml + +// this site does an OK job of documenting the event logs, which are just translations of the ETW events +// https://www.ultimatewindowssecurity.com/securitylog/encyclopedia/ + +const ( + // unfortunately, in the manifest, the event ids don't have useful names the way they do for file/registry. + // so we'll make them up. + idObjectPermsChange = uint16(4670) // the ever helpful task_04670 +) + +/* + +*/ + +// we're going to try for a slightly more useful name +// +//revive:disable:var-naming +type objectPermsChange struct { + etw.DDEventHeader + subjectUserSid string + subjectUserName string + subjectDomainName string + subjectLogonId string + objectServer string + objectType string + objectName string + handleId fileObjectPointer + oldSd string + newSd string + processId fileObjectPointer + processName string +} + +func (wp *WindowsProbe) parseObjectPermsChange(e *etw.DDEventRecord) (*objectPermsChange, error) { + + pc := &objectPermsChange{ + DDEventHeader: e.EventHeader, + } + data := etwimpl.GetUserData(e) + + reader := stringparser{nextRead: 0} + pc.subjectUserSid = reader.GetSIDString(data) + pc.subjectUserName = reader.GetNextString(data) + pc.subjectDomainName = reader.GetNextString(data) + pc.subjectLogonId = strconv.FormatUint(reader.GetUint64(data), 16) + pc.objectServer = reader.GetNextString(data) + pc.objectType = reader.GetNextString(data) + pc.objectName = reader.GetNextString(data) + + pc.handleId = fileObjectPointer(reader.GetUint64(data)) + + pc.oldSd = reader.GetNextString(data) + pc.newSd = reader.GetNextString(data) + + pc.processId = fileObjectPointer(reader.GetUint64(data)) + + pc.processName = reader.GetNextString(data) + + // translate the registry path, if it's a registry path, into the more canonical form + pc.objectName = translateRegistryBasePath(pc.objectName) + return pc, nil +} + +func (pc *objectPermsChange) String() string { + var output strings.Builder + output.WriteString(" ObjectPermsChange name: " + pc.objectName + "\n") + output.WriteString(" oldsd: " + pc.oldSd + "\n") + output.WriteString(" newsd: " + pc.newSd + "\n") + + return output.String() +} + +type stringparser struct { + nextRead int +} + +func (sp *stringparser) GetNextString(data etw.UserData) string { + s, no, _, _ := data.ParseUnicodeString(sp.nextRead) + + if no == -1 { + sp.nextRead += 2 + } else { + sp.nextRead = no + } + return s +} + +func (sp *stringparser) GetSIDString(data etw.UserData) string { + l := data.Length() + b := data.Bytes(sp.nextRead, l-sp.nextRead) + sid := (*windows.SID)(unsafe.Pointer(&b[0])) + sidlen := windows.GetLengthSid(sid) + sp.nextRead += int(sidlen) + + var winstring *uint16 + err := windows.ConvertSidToStringSid(sid, &winstring) + if err != nil { + return "" + } + defer windows.LocalFree(windows.Handle(unsafe.Pointer(winstring))) + + return windows.UTF16PtrToString(winstring) + +} + +func (sp *stringparser) GetUint64(data etw.UserData) uint64 { + n := data.GetUint64(sp.nextRead) + sp.nextRead += 8 + return n +} +func (sp *stringparser) SetNextReadOffset(offset int) { + sp.nextRead = offset +} + +func (sp *stringparser) GetNextReadOffset() int { + return sp.nextRead +} diff --git a/pkg/security/probe/probe_auditing_windows_test.go b/pkg/security/probe/probe_auditing_windows_test.go new file mode 100644 index 0000000000000..dd876298b9c7a --- /dev/null +++ b/pkg/security/probe/probe_auditing_windows_test.go @@ -0,0 +1,145 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build windows && functionaltests + +// Package probe holds probe related files +package probe + +import ( + "os" + _ "os/exec" + _ "path/filepath" + "sync" + "testing" + "time" + + "github.com/DataDog/datadog-agent/pkg/ebpf/ebpftest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + winacls "github.com/hectane/go-acl" +) + +func processUntilAudit(t *testing.T, et *etwTester) { + + defer func() { + et.loopExited <- struct{}{} + }() + et.loopStarted <- struct{}{} + for { + select { + case <-et.stopLoop: + return + + case n := <-et.notify: + switch n.(type) { + case *objectPermsChange: + et.notifications = append(et.notifications, n) + return + } + } + } + +} + +func TestETWAuditNotifications(t *testing.T) { + t.Skip("Skipping test that requires admin privileges") + ebpftest.LogLevel(t, "info") + ex, err := os.Executable() + require.NoError(t, err, "could not get executable path") + testfilename := ex + ".testfile" + + wp, err := createTestProbe() + require.NoError(t, err) + require.NotNil(t, wp) + + // teardownTestProe calls the stop function on etw, which will + // in turn wait on wp.fimgw + defer teardownTestProbe(wp) + + et := createEtwTester(wp) + + wp.fimwg.Add(1) + go func() { + defer wp.fimwg.Done() + + var once sync.Once + mypid := os.Getpid() + + err := et.p.setupEtw(func(n interface{}, pid uint32) { + once.Do(func() { + close(et.etwStarted) + }) + if pid != uint32(mypid) { + return + } + select { + case et.notify <- n: + // message sent + default: + } + }) + assert.NoError(t, err) + }() + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + processUntilAudit(t, et) + }() + + // wait until we're sure that the ETW listener is up and running. + // as noted above, this _could_ cause an infinite deadlock if no notifications are received. + // but, since we're getting the notifications from the entire system, we should be getting + // a steady stream as soon as it's fired up. + <-et.etwStarted + <-et.loopStarted + + // create the test file + f, err := os.Create(testfilename) + assert.NoError(t, err) + f.Close() + + // set up auditing on this directory + /* + dirpath := filepath.Dir(testfilename) + + // enable auditing + + pscommand := `$acl = new-object System.Security.AccessControl.DirectorySecurity; + $accessrule = new-object System.Security.AccessControl.FileSystemAuditRule('everyone', 'modify', 'containerinherit, objectinherit', 'none', 'success'); + $acl.SetAuditRule($accessrule); + $acl | set-acl -path` + + pscommand += dirpath + ";" + + cmd := exec.Command("powershell", "-Command", pscommand) + assert.NoError(t, err) + err = cmd.Run() + assert.NoError(t, err) + */ + // this is kinda hokey. ETW (which is what FIM is based on) takes an indeterminant amount of time to start up. + // so wait around for it to start + time.Sleep(2 * time.Second) + err = winacls.Chmod(testfilename, 0600) + assert.NoError(t, err) + + assert.Eventually(t, func() bool { + select { + case <-et.loopExited: + return true + } + return false + }, 10*time.Second, 250*time.Millisecond, "did not get notification") + + stopLoop(et, &wg) + for _, n := range et.notifications { + t.Logf("notification: %s", n) + } + +} diff --git a/pkg/security/probe/probe_kernel_reg_windows.go b/pkg/security/probe/probe_kernel_reg_windows.go index c0b539c238435..32b984147122b 100644 --- a/pkg/security/probe/probe_kernel_reg_windows.go +++ b/pkg/security/probe/probe_kernel_reg_windows.go @@ -134,20 +134,24 @@ func (wp *WindowsProbe) parseCreateRegistryKey(e *etw.DDEventRecord) (*createKey return crc, nil } -func (cka *createKeyArgs) translateBasePaths() { - +func translateRegistryBasePath(s string) string { table := map[string]string{ "\\\\REGISTRY\\MACHINE": "HKEY_LOCAL_MACHINE", "\\REGISTRY\\MACHINE": "HKEY_LOCAL_MACHINE", "\\\\REGISTRY\\USER": "HKEY_USERS", "\\REGISTRY\\USER": "HKEY_USERS", } - for k, v := range table { - if strings.HasPrefix(strings.ToUpper(cka.relativeName), k) { - cka.relativeName = v + cka.relativeName[len(k):] + if strings.HasPrefix(strings.ToUpper(s), k) { + s = v + s[len(k):] } } + return s +} +func (cka *createKeyArgs) translateBasePaths() { + + cka.relativeName = translateRegistryBasePath(cka.relativeName) + } func (wp *WindowsProbe) parseOpenRegistryKey(e *etw.DDEventRecord) (*openKeyArgs, error) { cka, err := wp.parseCreateRegistryKey(e) diff --git a/pkg/security/probe/probe_windows.go b/pkg/security/probe/probe_windows.go index 8560b64f312e2..f48bfa9475d26 100644 --- a/pkg/security/probe/probe_windows.go +++ b/pkg/security/probe/probe_windows.go @@ -34,6 +34,7 @@ import ( "github.com/DataDog/datadog-agent/pkg/security/serializers" "github.com/DataDog/datadog-agent/pkg/util/log" "github.com/DataDog/datadog-agent/pkg/util/optional" + "github.com/DataDog/datadog-agent/pkg/util/winutil" "github.com/DataDog/datadog-agent/pkg/windowsdriver/procmon" "golang.org/x/sys/windows" @@ -66,12 +67,18 @@ type WindowsProbe struct { onETWNotification chan etwNotification // ETW component for FIM - fileguid windows.GUID - regguid windows.GUID + fileguid windows.GUID + regguid windows.GUID + auditguid windows.GUID + //etwcomp etw.Component fimSession etw.Session fimwg sync.WaitGroup + // the audit session needs a separate ETW session because it's using + // a well-known provider + auditSession etw.Session + // path caches filePathResolver *lru.Cache[fileObjectPointer, fileCache] regPathResolver *lru.Cache[regObjectPointer, string] @@ -202,6 +209,7 @@ func (p *WindowsProbe) initEtwFIM() error { // log at Warning right now because it's not expected to be enabled log.Warnf("Enabling FIM processing") etwSessionName := "SystemProbeFIM_ETW" + auditSessionName := "EventLog-Security" etwcomp, err := etwimpl.NewEtw() if err != nil { return err @@ -211,6 +219,23 @@ func (p *WindowsProbe) initEtwFIM() error { if err != nil { return err } + if ls, err := winutil.IsCurrentProcessLocalSystem(); err == nil && ls { + /* the well-known session requires being run as local system. It will initialize, + but no events will be sent. + */ + p.auditSession, err = etwcomp.NewWellKnownSession(auditSessionName, nil) + if err != nil { + return err + } + log.Info("Enabling the ETW auditing session") + } else { + if err != nil { + log.Warnf("Unable to determine if we're running as local system %v", err) + } else if !ls { + log.Warnf("Not running as LOCAL_SYSTEM; audit events won't be captured") + } + log.Warnf("Not enabling the ETW auditing session") + } // provider name="Microsoft-Windows-Kernel-File" guid="{edd08927-9cc4-4e65-b970-c2560fb5c289}" p.fileguid, err = windows.GUIDFromString("{edd08927-9cc4-4e65-b970-c2560fb5c289}") @@ -225,6 +250,12 @@ func (p *WindowsProbe) initEtwFIM() error { log.Errorf("Error converting guid %v", err) return err } + //