Skip to content

Commit

Permalink
feat(filter): Callstack filter fields (#234)
Browse files Browse the repository at this point in the history
  • Loading branch information
rabbitstack authored Jan 11, 2024
1 parent 1ada2fa commit 0bf0761
Show file tree
Hide file tree
Showing 12 changed files with 526 additions and 94 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ require (
require (
github.com/rivo/uniseg v0.4.2 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
golang.org/x/arch v0.6.0 // indirect
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ 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=
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=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
Expand Down
115 changes: 113 additions & 2 deletions pkg/filter/accessor_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
psnap "github.com/rabbitstack/fibratus/pkg/ps"
"github.com/rabbitstack/fibratus/pkg/util/cmdline"
"github.com/rabbitstack/fibratus/pkg/util/loldrivers"
"golang.org/x/sys/windows"
"path/filepath"
"strconv"
"strings"
Expand Down Expand Up @@ -433,8 +434,12 @@ func (ps *psAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, e
}

const (
rootAncestor = "root" // represents the root ancestor
anyAncestor = "any" // represents any ancestor in the hierarchy
rootAncestor = "root" // represents the root ancestor
anyAncestor = "any" // represents any ancestor in the hierarchy
frameUEnd = "uend" // represents the last user space stack frame
frameUStart = "ustart" // represents the first user space stack frame
frameKEnd = "kend" // represents the last kernel space stack frame
frameKStart = "kstart" // represents the first kernel space stack frame
)

// ancestorFields recursively walks the process ancestors and extracts
Expand Down Expand Up @@ -566,6 +571,112 @@ func (t *threadAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value
return nil, nil
}
return kevt.GetParamAsString(kparams.NTStatus), nil
case fields.ThreadCallstackSummary:
return kevt.Callstack.Summary(), nil
case fields.ThreadCallstackDetail:
return kevt.Callstack.String(), nil
case fields.ThreadCallstackModules:
return kevt.Callstack.Modules(), nil
case fields.ThreadCallstackSymbols:
return kevt.Callstack.Symbols(), nil
case fields.ThreadCallstackAllocationSizes:
return kevt.Callstack.AllocationSizes(kevt.PID), nil
case fields.ThreadCallstackProtections:
return kevt.Callstack.Protections(kevt.PID), nil
case fields.ThreadCallstackCallsiteLeadingAssembly:
return kevt.Callstack.CallsiteInsns(kevt.PID, true), nil
case fields.ThreadCallstackCallsiteTrailingAssembly:
return kevt.Callstack.CallsiteInsns(kevt.PID, false), nil
case fields.ThreadCallstackIsUnbacked:
return kevt.Callstack.ContainsUnbacked(), nil
default:
if f.IsCallstackMap() {
return callstackFields(f.String(), kevt)
}
}
return nil, nil
}

// callstackFields is responsible for extracting
// the stack frame data for the specified frame
// index. The index 0 represents the least-recent
// frame, usually the base thread initialization
// frames.
func callstackFields(field string, kevt *kevent.Kevent) (kparams.Value, error) {
if kevt.Callstack.IsEmpty() {
return nil, nil
}
key, segment := captureInBrackets(field)
if key == "" || segment == "" {
return nil, nil
}
var i int
switch key {
case frameUStart:
i = 0
case frameUEnd:
for ; i < kevt.Callstack.Depth()-1 && !kevt.Callstack[i].Addr.InSystemRange(); i++ {
}
i--
case frameKStart:
for i = kevt.Callstack.Depth() - 1; i >= 0 && kevt.Callstack[i].Addr.InSystemRange(); i-- {
}
i++
case frameKEnd:
i = kevt.Callstack.Depth() - 1
default:
if strings.HasSuffix(key, ".dll") {
for n, frame := range kevt.Callstack {
if strings.EqualFold(filepath.Base(frame.Module), key) {
i = n
break
}
}
} else {
var err error
i, err = strconv.Atoi(key)
if err != nil {
return nil, err
}
}
}

if i > kevt.Callstack.Depth() || i < 0 {
i = 0
}
f := kevt.Callstack[i]

switch segment {
case fields.FrameAddress:
return f.Addr.String(), nil
case fields.FrameSymbolOffset:
return f.Offset, nil
case fields.FrameModule:
return f.Module, nil
case fields.FrameSymbol:
return f.Symbol, nil
case fields.FrameProtection, fields.FrameAllocationSize:
proc, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, kevt.PID)
if err != nil {
return nil, err
}
defer windows.Close(proc)
if segment == fields.FrameProtection {
return f.Protection(proc), nil
}
return f.AllocationSize(proc), nil
case fields.FrameCallsiteLeadingAssembly, fields.FrameCallsiteTrailingAssembly:
proc, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, false, kevt.PID)
if err != nil {
return nil, err
}
defer windows.Close(proc)
if segment == fields.FrameCallsiteLeadingAssembly {
return f.CallsiteAssembly(proc, true), nil
}
return f.CallsiteAssembly(proc, false), nil
case fields.FrameIsUnbacked:
return f.IsUnbacked(), nil
}
return nil, nil
}
Expand Down
148 changes: 102 additions & 46 deletions pkg/filter/fields/fields_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (

// pathRegexp splits the provided path into different components. The first capture
// contains the indexed field name. Next is the indexed key and, finally the segment.
var pathRegexp = regexp.MustCompile(`(pe.sections|pe.resources|ps.envs|ps.modules|ps.ancestor|kevt.arg)\[(.+\s*)].?(.*)`)
var pathRegexp = regexp.MustCompile(`(pe.sections|pe.resources|ps.envs|ps.modules|ps.ancestor|kevt.arg|thread.callstack)\[(.+\s*)].?(.*)`)

// Field represents the type alias for the field
type Field string
Expand Down Expand Up @@ -177,6 +177,26 @@ const (
ThreadAccessMaskNames Field = "thread.access.mask.names"
// ThreadAccessStatus represents the thread access status field
ThreadAccessStatus Field = "thread.access.status"
// ThreadCallstack represents the field that provides access to stack frames
ThreadCallstack Field = "thread.callstack"
// ThreadCallstackSummary represents the thread callstack summary field
ThreadCallstackSummary Field = "thread.callstack.summary"
// ThreadCallstackDetail represents the thread callstack detail field
ThreadCallstackDetail Field = "thread.callstack.detail"
// ThreadCallstackModules represents the callstack modules field
ThreadCallstackModules Field = "thread.callstack.modules"
// ThreadCallstackSymbols represents the callstack symbols field
ThreadCallstackSymbols Field = "thread.callstack.symbols"
// ThreadCallstackProtections represents the callstack region protections field
ThreadCallstackProtections Field = "thread.callstack.protections"
// ThreadCallstackAllocationSizes represents the private region page sizes field
ThreadCallstackAllocationSizes Field = "thread.callstack.allocation_sizes"
// ThreadCallstackCallsiteLeadingAssembly represents the callsite prelude opcodes field
ThreadCallstackCallsiteLeadingAssembly Field = "thread.callstack.callsite_leading_assembly"
// ThreadCallstackCallsiteTrailingAssembly represents the callsite postlude opcodes field
ThreadCallstackCallsiteTrailingAssembly Field = "thread.callstack.callsite_trailing_assembly"
// ThreadCallstackIsUnbacked represents the field that indicates if there is an unbacked stack frame
ThreadCallstackIsUnbacked Field = "thread.callstack.is_unbacked"

// PeNumSections represents the number of sections
PeNumSections Field = "pe.nsections"
Expand Down Expand Up @@ -506,14 +526,47 @@ const (
ProcessSID Segment = "sid"
// ProcessSessionID represents the session id bound to the process
ProcessSessionID Segment = "sessionid"

// FrameAddress represents the stack frame return address
FrameAddress Segment = "address"
// FrameSymbolOffset represents the symbol offset
FrameSymbolOffset Segment = "offset"
// FrameSymbol represents the symbol name
FrameSymbol Segment = "symbol"
// FrameModule represents the symbol module
FrameModule Segment = "module"
// FrameAllocationSize represents the frame region allocation size
FrameAllocationSize Segment = "allocation_size"
// FrameProtection represents the region page protection where the frame instruction lives
FrameProtection Segment = "protection"
// FrameIsUnbacked determines if the frame is unbacked
FrameIsUnbacked Segment = "is_unbacked"
// FrameCallsiteLeadingAssembly represents the leading callsite opcodes
FrameCallsiteLeadingAssembly = "callsite_leading_assembly"
// FrameCallsiteTrailingAssembly represents the trailing callsite opcodes
FrameCallsiteTrailingAssembly = "callsite_trailing_assembly"
)

func (s Segment) IsSection() bool {
return s == SectionEntropy || s == SectionSize || s == SectionMD5Hash
}
func (s Segment) IsModule() bool {
return s == ModuleChecksum || s == ModuleLocation || s == ModuleBaseAddress || s == ModuleDefaultAddress || s == ModuleSize
}
func (s Segment) IsProcess() bool {
return s == ProcessID || s == ProcessName || s == ProcessCmdline || s == ProcessExe || s == ProcessArgs || s == ProcessCwd || s == ProcessSID || s == ProcessSessionID
}
func (s Segment) IsCallstack() bool {
return s == FrameAddress || s == FrameSymbolOffset || s == FrameSymbol || s == FrameModule || s == FrameAllocationSize || s == FrameProtection || s == FrameIsUnbacked || s == FrameCallsiteLeadingAssembly || s == FrameCallsiteTrailingAssembly
}

func (f Field) IsEnvsMap() bool { return strings.HasPrefix(f.String(), "ps.envs[") }
func (f Field) IsModsMap() bool { return strings.HasPrefix(f.String(), "ps.modules[") }
func (f Field) IsAncestorMap() bool { return strings.HasPrefix(f.String(), "ps.ancestor[") }
func (f Field) IsPeSectionsMap() bool { return strings.HasPrefix(f.String(), "pe.sections[") }
func (f Field) IsPeResourcesMap() bool { return strings.HasPrefix(f.String(), "pe.resources[") }
func (f Field) IsKevtArgMap() bool { return strings.HasPrefix(f.String(), "kevt.arg[") }
func (f Field) IsCallstackMap() bool { return strings.HasPrefix(f.String(), "thread.callstack[") }

var fields = map[Field]FieldInfo{
KevtSeq: {KevtSeq, "event sequence number", kparams.Uint64, []string{"kevt.seq > 666"}, nil},
Expand Down Expand Up @@ -595,18 +648,27 @@ var fields = map[Field]FieldInfo{
PsParentUUID: {PsParentUUID, "unique parent process identifier", kparams.Uint64, []string{"ps.parent.uuid > 6000054355"}, nil},
PsChildUUID: {PsChildUUID, "unique child process identifier", kparams.Uint64, []string{"ps.child.uuid > 6000054355"}, nil},

ThreadBasePrio: {ThreadBasePrio, "scheduler priority of the thread", kparams.Int8, []string{"thread.prio = 5"}, nil},
ThreadIOPrio: {ThreadIOPrio, "I/O priority hint for scheduling I/O operations", kparams.Int8, []string{"thread.io.prio = 4"}, nil},
ThreadPagePrio: {ThreadPagePrio, "memory page priority hint for memory pages accessed by the thread", kparams.Int8, []string{"thread.page.prio = 12"}, nil},
ThreadKstackBase: {ThreadKstackBase, "base address of the thread's kernel space stack", kparams.Address, []string{"thread.kstack.base = 'a65d800000'"}, nil},
ThreadKstackLimit: {ThreadKstackLimit, "limit of the thread's kernel space stack", kparams.Address, []string{"thread.kstack.limit = 'a85d800000'"}, nil},
ThreadUstackBase: {ThreadUstackBase, "base address of the thread's user space stack", kparams.Address, []string{"thread.ustack.base = '7ffe0000'"}, nil},
ThreadUstackLimit: {ThreadUstackLimit, "limit of the thread's user space stack", kparams.Address, []string{"thread.ustack.limit = '8ffe0000'"}, nil},
ThreadEntrypoint: {ThreadEntrypoint, "starting address of the function to be executed by the thread", kparams.Address, []string{"thread.entrypoint = '7efe0000'"}, nil},
ThreadPID: {ThreadPID, "the process identifier where the thread is created", kparams.Uint32, []string{"kevt.pid != thread.pid"}, nil},
ThreadAccessMask: {ThreadAccessMask, "thread desired access rights", kparams.AnsiString, []string{"thread.access.mask = '0x1fffff'"}, nil},
ThreadAccessMaskNames: {ThreadAccessMaskNames, "thread desired access rights as a string list", kparams.Slice, []string{"thread.access.mask.names in ('IMPERSONATE')"}, nil},
ThreadAccessStatus: {ThreadAccessStatus, "thread access status", kparams.UnicodeString, []string{"thread.access.status = 'success'"}, nil},
ThreadBasePrio: {ThreadBasePrio, "scheduler priority of the thread", kparams.Int8, []string{"thread.prio = 5"}, nil},
ThreadIOPrio: {ThreadIOPrio, "I/O priority hint for scheduling I/O operations", kparams.Int8, []string{"thread.io.prio = 4"}, nil},
ThreadPagePrio: {ThreadPagePrio, "memory page priority hint for memory pages accessed by the thread", kparams.Int8, []string{"thread.page.prio = 12"}, nil},
ThreadKstackBase: {ThreadKstackBase, "base address of the thread's kernel space stack", kparams.Address, []string{"thread.kstack.base = 'a65d800000'"}, nil},
ThreadKstackLimit: {ThreadKstackLimit, "limit of the thread's kernel space stack", kparams.Address, []string{"thread.kstack.limit = 'a85d800000'"}, nil},
ThreadUstackBase: {ThreadUstackBase, "base address of the thread's user space stack", kparams.Address, []string{"thread.ustack.base = '7ffe0000'"}, nil},
ThreadUstackLimit: {ThreadUstackLimit, "limit of the thread's user space stack", kparams.Address, []string{"thread.ustack.limit = '8ffe0000'"}, nil},
ThreadEntrypoint: {ThreadEntrypoint, "starting address of the function to be executed by the thread", kparams.Address, []string{"thread.entrypoint = '7efe0000'"}, nil},
ThreadPID: {ThreadPID, "the process identifier where the thread is created", kparams.Uint32, []string{"kevt.pid != thread.pid"}, nil},
ThreadAccessMask: {ThreadAccessMask, "thread desired access rights", kparams.AnsiString, []string{"thread.access.mask = '0x1fffff'"}, nil},
ThreadAccessMaskNames: {ThreadAccessMaskNames, "thread desired access rights as a string list", kparams.Slice, []string{"thread.access.mask.names in ('IMPERSONATE')"}, nil},
ThreadAccessStatus: {ThreadAccessStatus, "thread access status", kparams.UnicodeString, []string{"thread.access.status = 'success'"}, nil},
ThreadCallstackSummary: {ThreadCallstackSummary, "callstack summary", kparams.UnicodeString, []string{"thread.callstack.summary contains 'ntdll.dll|KERNELBASE.dll'"}, nil},
ThreadCallstackDetail: {ThreadCallstackDetail, "detailed information of each stack frame", kparams.UnicodeString, []string{"thread.callstack.detail contains 'KERNELBASE.dll!CreateProcessW'"}, nil},
ThreadCallstackModules: {ThreadCallstackModules, "list of modules comprising the callstack", kparams.Slice, []string{"thread.callstack.modules in ('C:\\WINDOWS\\System32\\KERNELBASE.dll')"}, nil},
ThreadCallstackSymbols: {ThreadCallstackSymbols, "list of symbols comprising the callstack", kparams.Slice, []string{"thread.callstack.symbols in ('ntdll.dll!NtCreateProcess')"}, nil},
ThreadCallstackAllocationSizes: {ThreadCallstackAllocationSizes, "allocation sizes of private pages", kparams.Slice, []string{"thread.callstack.allocation_sizes > 10000"}, nil},
ThreadCallstackProtections: {ThreadCallstackProtections, "page protections masks of each frame", kparams.Slice, []string{"thread.callstack.protections in ('RWX', 'WX')"}, nil},
ThreadCallstackCallsiteLeadingAssembly: {ThreadCallstackCallsiteLeadingAssembly, "callsite leading assembly instructions", kparams.Slice, []string{"thread.callstack.callsite_leading_assembly in ('mov r10,rcx', 'syscall')"}, nil},
ThreadCallstackCallsiteTrailingAssembly: {ThreadCallstackCallsiteTrailingAssembly, "callsite trailing assembly instructions", kparams.Slice, []string{"thread.callstack.callsite_trailing_assembly in ('add esp, 0xab')"}, nil},
ThreadCallstackIsUnbacked: {ThreadCallstackIsUnbacked, "indicates if the callstack contains unbacked regions", kparams.Bool, []string{"thread.callstack.is_unbacked"}, nil},

ImageName: {ImageName, "full image name", kparams.UnicodeString, []string{"image.name contains 'advapi32.dll'"}, nil},
ImageBase: {ImageBase, "the base address of process in which the image is loaded", kparams.Address, []string{"image.base.address = 'a65d800000'"}, nil},
Expand Down Expand Up @@ -733,28 +795,14 @@ func Lookup(name string) Field {
if segment == "" {
return None
}
switch Segment(segment) {
case SectionEntropy:
return Field(name)
case SectionMD5Hash:
return Field(name)
case SectionSize:
if Segment(segment).IsSection() {
return Field(name)
}
case PsModules:
if segment == "" {
return None
}
switch Segment(segment) {
case ModuleSize:
return Field(name)
case ModuleChecksum:
return Field(name)
case ModuleDefaultAddress:
return Field(name)
case ModuleBaseAddress:
return Field(name)
case ModuleLocation:
if Segment(segment).IsModule() {
return Field(name)
}
case PsAncestor:
Expand All @@ -769,30 +817,38 @@ func Lookup(name string) Field {
// we can also get the `any` keyword
// that collects the fields of all
// ancestor processes
var keyRegexp = regexp.MustCompile(`[1-9]+|root|any`)
var keyRegexp = regexp.MustCompile(`^[1-9]+$|^root$|^any$`)
if !keyRegexp.MatchString(key) {
return None
}
switch Segment(segment) {
case ProcessName:
return Field(name)
case ProcessID:
return Field(name)
case ProcessArgs:
if Segment(segment).IsProcess() {
return Field(name)
case ProcessCmdline:
return Field(name)
case ProcessCwd:
return Field(name)
case ProcessExe:
return Field(name)
case ProcessSID:
}
case PeResources:
if key != "" && segment == "" {
return Field(name)
case ProcessSessionID:
}
case PsEnvs, KevtArg:
if key != "" {
return Field(name)
}
case PeResources, PsEnvs, KevtArg:
if key != "" && segment == "" {
case ThreadCallstack:
if segment == "" {
return None
}
// the key can be the stack frame
// index with 0 being the bottom
// userspace frame. u/k start/end
// keys identify the start/end
// user and kernel space frames.
// Lastly, it is possible to specify
// the name of the module from which
// the call was originated
var keyRegexp = regexp.MustCompile(`^[0-9]+$|^uend$|^ustart$|^kend$|^kstart$|^[a-zA-Z0-9]+\.dll$`)
if !keyRegexp.MatchString(key) {
return None
}
if Segment(segment).IsCallstack() {
return Field(name)
}
}
Expand Down
Loading

0 comments on commit 0bf0761

Please sign in to comment.