Skip to content

Commit

Permalink
[windows][cws][wkint-469] Add permissions change notifications. (#24841)
Browse files Browse the repository at this point in the history
  • Loading branch information
derekwbrown authored May 31, 2024
1 parent bba6f6d commit 1b73957
Show file tree
Hide file tree
Showing 10 changed files with 431 additions and 30 deletions.
16 changes: 2 additions & 14 deletions comp/core/secrets/secretsimpl/check_rights_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion comp/core/secrets/secretsimpl/exec_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
20 changes: 15 additions & 5 deletions comp/etw/impl/etwSession.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
151 changes: 151 additions & 0 deletions pkg/security/probe/probe_auditing_windows.go
Original file line number Diff line number Diff line change
@@ -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
)

/*
<template tid="task_04670Args">
<data name="SubjectUserSid" inType="win:SID"/>
<data name="SubjectUserName" inType="win:UnicodeString"/>
<data name="SubjectDomainName" inType="win:UnicodeString"/>
<data name="SubjectLogonId" inType="win:HexInt64"/>
<data name="ObjectServer" inType="win:UnicodeString"/>
<data name="ObjectType" inType="win:UnicodeString"/>
<data name="ObjectName" inType="win:UnicodeString"/>
<data name="HandleId" inType="win:Pointer"/>
<data name="OldSd" inType="win:UnicodeString"/>
<data name="NewSd" inType="win:UnicodeString"/>
<data name="ProcessId" inType="win:Pointer"/>
<data name="ProcessName" inType="win:UnicodeString"/>
</template>
*/

// 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
}
145 changes: 145 additions & 0 deletions pkg/security/probe/probe_auditing_windows_test.go
Original file line number Diff line number Diff line change
@@ -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)
}

}
Loading

0 comments on commit 1b73957

Please sign in to comment.