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
}
+ //