-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[windows][cws][wkint-469] Add permissions change notifications. (#24841)
- Loading branch information
1 parent
bba6f6d
commit 1b73957
Showing
10 changed files
with
431 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
|
||
} |
Oops, something went wrong.